use*_*687 8 c gcc function-pointers x86-64 clang
typedef struct foo
{
void (*const t)(struct foo *f);
} foo;
void t(struct foo *f)
{
}
void (*const myt)(struct foo *f) = t;
foo f = {.t = t};
int main(void)
{
f.t(&f);
myt(&f);
return 0;
}
Run Code Online (Sandbox Code Playgroud)
当使用 x86-64 gcc 13.2 和 clang 16.0.0 编译上述代码时,会生成类似的汇编代码。下面显示的是 gcc 输出。
t:
ret
main:
sub rsp, 8
mov edi, OFFSET FLAT:f
call [QWORD PTR f[rip]]
xor eax, eax
add rsp, 8
ret
f:
.quad t
myt:
.quad t
Run Code Online (Sandbox Code Playgroud)
为什么两个编译器t
在通过函数调用时发出对函数的调用struct foo
,而在通过函数指针调用时则不发出函数调用myt
?为什么编译器无法看到t
函数指针struct foo
指向空函数并消除所有调用?由于f
是静态分配并在编译时初始化,因此其成员t
(一个在运行时无法更改的常量指针(不会调用未定义的行为))指向一个空函数t
。
Pet*_*des 10
GCC 和 Clang 都提供了在 before 之前运行任意代码的机制main
,因此这些编译器必须尊重这种可能性。(包括使用静态构造函数与 C++ 库或GNU C__attribute__((constructor))
链接。)
全局变量foo f
不是const
,因此 GCC 和 clang 假设它可能在执行到达之前已被修改f.t(&f);
。 更改为const foo f = ...;
让 GCC 和 clang 知道函数指针值是编译时常量,并内联它并优化掉空函数。(神箭)
那么修改后的代码会是什么样子f.t
呢?
显然它不能这样做,f.t = whatever
因为.t
成员是const
。GCC 和 clang 也拒绝编译f = (foo){NULL};
以替换整个结构的值,抱怨const
成员。(Clang 说错误:无法使用 const 限定数据成员 't' 分配给变量 'f'。GCC 使用与const foo f
, error: assignment of read-only variable 'f'相同的错误相同的错误。)
memcpy
但即使是在没有警告的情况下,他们也确实允许,而如果是-Wall -Wextra
,他们就会警告。(我用作占位符只是为了避免编写另一个有效函数。如果您可以将其更改为 NULL,则可以将其更改为在调用时会执行某些操作的函数。)f
const
NULL
#include <string.h>
// Code like this could hypothetically run before main in another compilation unit
void startup(void){
// f = (foo){NULL}; // illegal
foo tmp = {NULL};
memcpy(&f, &tmp, sizeof(f)); // legal, it seems, as long as foo f isn't const foo f
// (In which case this warns, and will segfault at run-time)
// actually clang emits no instructions for the memcpy in the UB case where it's trying to write const foo f.
}
Run Code Online (Sandbox Code Playgroud)
我还没有检查 ISO 标准,但假设 GCC/clang 的(缺少)警告与其优化器假设其他代码可以执行的操作一致,这解释了为什么在const
f
这种情况下 non- 不被视为具有已知值。
static foo f
帮助?制作它static foo f
(在没有定义startup()
修改它的函数的编译单元中)应该允许常量传播,因为编译单元外部的任何东西都看不到它,并且内部的任何东西都不会改变值。
但即使gcc -fwhole-program
是 GCC/clang -flto
(反汇编链接的二进制文件)也无法内联f.t(&f)
,即使这些选项确实让它们内联myt(&f)
为const
非非static
myt
。(神箭)。
将调用更改为f.t(0);
和myt(0)
允许 GCC 和 clang 优化这两个调用!(使用static foo f
and const myt
,或使用-fwhole-program
or -flto
。) 我认为&f
作为 arg 传递会击败转义分析,即使编译器理论上可以证明调用目标仍然始终是t()
在此编译单元中定义的,这实际上并没有让地址逃逸到编译单元之外。
// or non-const is the same if we're using -flto or -fwhole-program
void (*const myt)(const struct foo *f) = t;
static foo f = {.t = t};
int main(void)
{
f.t(0); // not f.t(&f) so it's clear to the optimizer the address of f doesn't escape
myt(0);
// f.t(0); myt(&f); // lets clang -flto optimize away both calls, but still not GCC -fwhole-program
}
Run Code Online (Sandbox Code Playgroud)
main
在 C 中,从程序内部再次调用并不是 UB 。因此,如果f.t(&f)
发生变化, (例如,从另一个编译单元中的函数)f.t
的下一次执行将是对不同函数的调用。这实际上是不可能的,但很容易想象编译器可能陷入了试图证明这一点的循环并放弃了。(通过此编译单元中的非函数获取其地址并将其传递给函数调用,这通常意味着地址转义。看到它转到不改变的位置,则需要在完成转义分析之前进行内联,但它不能通过函数指针内联,除非转义分析可以证明它知道正在调用哪个函数。)main
__attribute__((destructor))
f
static
t()
f
myt
另一方面确实如此,const
很容易优化掉。即使我们不使用const
or static
,使用-flto
or-fwhole-program
编译也很容易看到此编译单元(或任何具有 LTO 的编译单元)中的任何内容都不会更改它,并且&myt
不会作为函数 arg 传递,因此显然不会转义。(没有-flto
or const
/ static
,通过 global 的 constpropmyt
也不会发生。)
Godbolt - 使用main
dof.t(0);myt(0);
和*const myt
足以static foo f
让 GCC 和 clang 优化掉这两个调用,即使没有-flto
或-fwhole-program
。除非这个编译单元定义了一个类似startup()
to 的memcpy
函数&f
。
但是对于 non- static
foo f
,这种优化再次是不可能的,因为它甚至可以在第一次/唯一执行 之前被修改main
。
顺便说一句,我的 Godbolt 链接-fvisibility=hidden
只是为了确保不可能进行符号插入,这将是非变量static
的值与源中的初始值设定项不同的另一种方式。但这对于主可执行文件中的全局变量来说已经不可能了,只与-fPIC
编译对共享库安全的代码有关。
归档时间: |
|
查看次数: |
242 次 |
最近记录: |