27

C99GCC.

static inline在无法修改的标头中声明了一个函数。

该函数永远不会返回但未标记__attribute__((noreturn))

如何以告诉编译器它不会返回的方式调用该函数?

我从我自己的 noreturn 函数中调用它,部分想抑制“noreturn 函数返回”警告,但也想帮助优化器等。

我尝试包含一个带有属性的声明,但收到有关重复声明的警告。

我试过创建一个函数指针并将属性应用到那个,但它说函数属性不能应用于指向的函数。

4

2 回答 2

44

定义的函数以及调用外部函数的函数中,添加一个调用,__builtin_unreachable该调用至少内置于GCCClang编译器中并标记为noreturn. 事实上,这个函数没有做其他任何事情,也不应该被调用。它只是在这里,以便编译器可以推断程序执行将在此时停止。

static inline external_function() // lacks the noreturn attribute
{ /* does not return */ }

__attribute__((noreturn)) void your_function() {
    external_function();     // the compiler thinks execution may continue ...
    __builtin_unreachable(); // ... and now it knows it won't go beyond here
}

编辑:只是为了澄清评论中提出的几点,并通常提供一些背景信息:

  • 一个函数只有两种不返回的方式:永远循环,或者使通常的控制流短路(例如抛出异常、跳出函数、终止进程等)
  • 在某些情况下,编译器可能能够通过静态分析推断和证明函数不会返回。即使从理论上讲,这也不总是可能的,因为我们希望编译器速度很快,所以只能检测到明显/简单的情况。
  • __attribute__((noreturn))是一个注解(如const),它是程序员通知编译器他绝对确定函数不会返回的一种方式。遵循信任但验证原则,编译器试图证明该函数确实没有返回。如果它证明函数可以返回,则如果它可能会发出错误,或者如果它无法证明函数是否返回,则可能会发出警告。
  • __builtin_unreachable具有未定义的行为,因为它不应该被调用。它只是为了帮助编译器的静态分析。事实上,编译器知道这个函数不会返回,所以后面的任何代码都被证明是无法访问的(除非通过跳转)。

一旦编译器(通过它自己或在程序员的帮助下)确定某些代码是不可访问的,它可能会使用这些信息来进行如下优化:

  • 如果函数永远不会返回,则删除用于从函数返回给调用者的样板代码
  • 传播不可达信息,即如果一个代码点的唯一执行路径是通过不可达代码,那么这个点也是不可达的。例子:
    • 如果一个函数没有返回,任何跟随其调用并且不能通过跳转到达的代码也是无法到达的。示例:__builtin_unreachable()无法访问以下代码。
    • 特别是,函数返回的唯一途径是通过无法访问的代码,函数可以被标记noreturn。这就是发生的事情your_function
    • 不需要仅在无法访问的代码中使用的任何内存位置/变量,因此不需要设置/计算此类数据的内容。
    • 任何可能(1)不必要(前一个项目符号)和(2)没有副作用(例如pure函数)的计算都可以删除。

插图:

  • external_function无法删除对的调用,因为它可能有副作用。事实上,它可能至少有终止进程的副作用!
  • 的返回锅炉板your_function可以被移除

这是另一个示例,显示了如何删除无法到达的点之前的代码

int compute(int) __attribute((pure)) { return /* expensive compute */ }
if(condition) {
    int x = compute(input); // (1) no side effect => keep if x is used
                            // (8) x is not used  => remove
    printf("hello ");       // (2) reachable + side effect => keep
    your_function();        // (3) reachable + side effect => keep
                            // (4) unreachable beyond this point
    printf("word!\n");      // (5) unreachable => remove
    printf("%d\n", x);      // (6) unreachable => remove
                            // (7) mark 'x' as unused
} else {
                            // follows unreachable code, but can jump here
                            // from reachable code, so this is reachable
   do_stuff();              // keep
}
于 2014-08-20T15:39:25.413 回答
8

几种解决方案:

__attribute__

您应该尝试通过添加来修改其标题中的该函数__attribute__((noreturn))

您可以使用新属性重新声明一些函数,正如这个愚蠢的测试所展示的(向 中添加属性fopen):

 #include <stdio.h>

 extern FILE *fopen (const char *__restrict __filename,
            const char *__restrict __modes)
   __attribute__ ((warning ("fopen is used")));

 void
 show_map_without_care (void)
 {
   FILE *f = fopen ("/proc/self/maps", "r");
   do
     {
       char lin[64];
       fgets (lin, sizeof (lin), f);
       fputs (lin, stdout);
     }
   while (!feof (f));
   fclose (f);
 }

用宏覆盖

最后,您可以定义一个宏,例如

#define func(A) {func(A); __builtin_unreachable();}

(这使用了在宏内部,宏名称不是宏扩展的事实)。

如果您的永不返回func声明为返回,例如int,您将使用如下语句表达式

#define func(A) ({func(A); __builtin_unreachable(); (int)0; })

像上面这样的基于宏的解决方案并不总是有效,例如,如果func作为函数指针传递,或者只是一些(func)(1)合法但丑陋的人代码。


noreturn 使用属性重新声明静态内联

下面的例子:

 // file ex.c
 // declare exit without any standard header
 void exit (int);

 // define myexit as a static inline
 static inline void
 myexit (int c)
 {
   exit (c);
 }

 // redeclare it as notreturn
 static inline void myexit (int c) __attribute__ ((noreturn));

 int
 foo (int *p)
 {
   if (!p)
     myexit (1);
   if (p)
     return *p + 2;
   return 0;
 }

当使用 GCC 4.9 (来自 Debian/Sid/x86-64) as 编译时,gcc -S -fverbose-asm -O2 ex.c会给出一个包含预期优化的程序集文件:

         .type   foo, @function
 foo:
 .LFB1:
    .cfi_startproc
    testq   %rdi, %rdi      # p
    je      .L5     #,
    movl    (%rdi), %eax    # *p_2(D), *p_2(D)
    addl    $2, %eax        #, D.1768
    ret
.L5:
    pushq   %rax    #
    .cfi_def_cfa_offset 16
    movb    $1, %dil        #,
    call    exit    #
    .cfi_endproc
 .LFE1:
    .size   foo, .-foo

您可以使用#pragma GCC diagnostic来选择性地禁用警告。


使用MELT自定义GCC

gcc最后,您可以使用MELT插件自定义您最近的应用程序并编码您的简单扩展(使用MELT域特定语言)以noreturn在遇到所需功能时添加属性。它可能是十几行 MELT 行,使用register_finish_decl_first和匹配函数名称。

因为我是MELT(自由软件 GPLv3+)的主要作者,如果你问,我什至可以为你编写代码,例如在这里或最好在gcc-melt@googlegroups.com;给出你的永不返回函数的具体名称。

可能 MELT 代码如下所示:

  ;;file your_melt_mode.melt
  (module_is_gpl_compatible "GPLv3+")
  (defun my_finish_decl (decl)
     (let ( (tdecl (unbox :tree decl))
       )
     (match tdecl
        (?(tree_function_decl_named
            ?(tree_identifier ?(cstring_same "your_function_name")))
          ;;; code to add the noreturn attribute
          ;;; ....
        ))))
  (register_finish_decl_first my_finish_decl)

真正的 MELT 代码稍微复杂一些。你想在your_adding_attr_mode那里定义。问我更多。

一旦您your_melt_mode.melt 根据需要编写了 MELT 扩展(并将 MELT 扩展编译your_melt_mode.quicklybuilt.so为MELT 教程中记录的内容),您将使用

  gcc -fplugin=melt \
      -fplugin-arg-melt-extra=your_melt_mode.quicklybuilt \
      -fplugin-arg-melt-mode=your_adding_attr_mode \
      -O2 -I/your/include -c yourfile.c

换句话说,您-fplugin-*只需CFLAGSMakefile!

顺便说一句,我只是在 MELT 监视器中编码(在 github 上:https ://github.com/bstarynk/melt-monitor ...,文件meltmom-process.melt非常相似。

使用 MELT 扩展,您不会收到任何额外的警告,因为 MELT 扩展会动态更改声明函数的内部 GCC AST(一个 GCC树)!

使用 MELT 定制 GCC 可能是最安全的解决方案,因为它正在修改 GCC 内部 AST。当然,它可能是最昂贵的解决方案(它是 GCC 特定的,并且可能需要在 GCC 发展时进行小改动,例如在使用 GCC 的下一个版本时),但正如我试图展示的那样,它在你的情况。

PS。2019 年,GCC MELT 是一个废弃的项目。如果您想定制 GCC(对于任何最新版本的 GCC,例如 GCC 7、8 或 9),您需要用 C++ 编写自己的GCC 插件

于 2014-08-20T15:44:16.370 回答