C语言 告诉 gcc 函数调用不会返回



我在GCC下使用C99

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

该函数从不返回,但不标记为__attribute__((noreturn))

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

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

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

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

定义的函数中,调用外部函数,添加对__builtin_unreachable的调用,该调用至少内置于 GCC 和 Clang 编译器中,并标记为 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(,这是程序员通知编译器他绝对确定函数不会返回的一种方式。遵循信任但验证原则,编译器试图证明该函数确实不会返回。如果 if 证明函数可能返回,则可能会发出错误,如果无法证明函数是否返回,则发出警告。
  • __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("%dn", 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
}

几种解决方案:

使用 __attribute__ 重新声明函数

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

您可以使用 new 属性重新声明一些函数,正如这个愚蠢的测试所演示的那样(向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(编译时,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 诊断程序来有选择地禁用警告。


使用 MELT 定制 GCC

最后,您可以使用 MELT 插件自定义最近的gcc,并对简单的扩展(使用 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

换句话说,您只需在MakefileCFLAGS中添加一些-fplugin-*标志!

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

使用 MELT 扩展,

您不会收到任何额外的警告,因为 MELT 扩展会即时更改声明函数的内部 GCC AST(GCC (!

使用 MELT 定制 GCC 可能是最可靠的解决方案,因为它正在修改 GCC 内部 AST。当然,这可能是最昂贵的解决方案(它是特定于 GCC 的,并且在 GCC 不断发展时可能需要进行小的更改,例如在使用下一个版本的 GCC 时(,但正如我试图展示的那样,在您的情况下它很容易。

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

最新更新