我在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
换句话说,您只需在Makefile
的CFLAGS
中添加一些-fplugin-*
标志!
顺便说一句,我只是在 MELT 监视器中编码(在 github 上:https://github.com/bstarynk/melt-monitor...,文件meltmom-process.melt
非常相似的东西。
您不会收到任何额外的警告,因为 MELT 扩展会即时更改声明函数的内部 GCC AST(GCC 树(!
使用 MELT 定制 GCC 可能是最可靠的解决方案,因为它正在修改 GCC 内部 AST。当然,这可能是最昂贵的解决方案(它是特定于 GCC 的,并且在 GCC 不断发展时可能需要进行小的更改,例如在使用下一个版本的 GCC 时(,但正如我试图展示的那样,在您的情况下它很容易。
附言。2019年,GCC MELT是一个废弃的项目。如果你想自定义GCC(对于任何最新版本的GCC,例如GCC 7、8或9(,你需要用C++编写自己的GCC插件。