为何带constructor属性的同名函数从库X被调用3次而非三库各1次?
$ cat lib.c #include <stdio.h> #define STR_(x) #x #define STR(x) STR_(x) #define CAT_(x,y) x##y #define CAT(x,y) CAT_(x,y) __attribute__((constructor)) void CAT(foo,)(void) { printf("foo" STR(N) " %p\n", CAT(foo,)); } void CAT(bar,N)(void){ puts("bar" STR(N)); }
$ cat main.c void barx(void); void bary(void); void barz(void); int main(void) { barx(); bary(); barz(); }
$ cat build_run.sh gcc lib.c -DN=x -c -fPIC -o libx.o && gcc libx.o -o libx.so -shared && gcc lib.c -DN=y -c -fPIC -o liby.o && gcc liby.o -o liby.so -shared && gcc lib.c -DN=z -c -fPIC -o libz.o && gcc libz.o -o libz.so -shared && gcc -L. -o main main.c -lx -ly -lz && LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH ./main
$ bash build_run.sh foox 0x7f0bf002e139 foox 0x7f0bf002e139 foox 0x7f0bf002e139 barx bary barz
现象观察
- 所有
.so共享库都包含一个带constructor属性、名为foo的函数。 - 函数
foo仅从库X被调用3次(不符合预期),而非X、Y、Z三个库各调用1次(符合预期)。
核心疑问
按理解,带constructor属性的foo函数地址会被(直接或间接)放入.init_array段,函数名不应影响执行逻辑。那为何函数foo从库X被调用3次,而非三个库各调用1次?
额外测试现象
- 若将
lib.c中的CAT(foo,)改为CAT(foo,N)并重新执行build_run.sh,输出如下:
$ bash build_run.sh fooz 0x7fc121dcc139 fooy 0x7fc121dd1139 foox 0x7fc121dd6139 barx bary barz
此为符合预期的行为。
2. 在Cygwin环境下运行原示例(保留CAT(foo,)),函数foo会从X、Y、Z三个库各调用1次(符合预期)。
系统与软件信息
$ uname -a Linux xxx 5.15.0-79-generic #86-Ubuntu SMP Mon Jul 10 16:07:21 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux $ gcc --version gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
已尝试的操作
- 在编译共享库时添加
-Bsymbolic参数(作用为“创建共享库时,将全局符号引用绑定到库内的定义”),但无效果。 - 尝试将带
constructor属性的函数声明为static,但无法实现。
这个问题的根源在于Linux动态链接器对全局符号的解析机制,以及GCC处理constructor属性函数的方式:
全局符号冲突与动态链接的符号解析规则
用CAT(foo,)定义函数时,三个共享库生成的函数符号名都是foo——这是全局可见符号。Linux动态链接器(ld.so)遵循符号优先解析规则:当多个共享库提供同名全局符号时,动态链接器会选择第一个加载的库(此处为libx.so)中的符号定义,后续所有对该符号的引用都会绑定到这个最先加载的地址上。
虽然每个共享库的.init_array段都记录了构造函数的执行入口,但GCC生成构造函数调用逻辑时,是通过符号引用关联的,而非直接写入绝对地址。动态链接器处理所有.init_array时,所有指向foo的引用都会被解析为libx.so中的foo,因此三次执行的都是同一个函数。修改函数名后的正常逻辑
改为CAT(foo,N)后,三个库的函数名分别为foox、fooy、fooz——这是三个独立的全局符号。动态链接器会为每个符号匹配对应库内的定义,.init_array中的每个构造函数引用都会绑定到各自库的函数,从而实现每个库的构造函数各执行一次。Cygwin环境的差异
Cygwin基于Windows的PE/COFF格式,其动态链接器对同名符号的处理是每个库保留自身的符号定义,不会进行跨库符号覆盖,因此每个库的foo构造函数都会被独立执行。-Bsymbolic参数无效的原因
-Bsymbolic仅作用于共享库内部的符号引用绑定,而构造函数的调用是动态链接器直接从.init_array读取并执行的,不经过库内部的符号解析逻辑,因此无法改变跨库的全局符号解析行为。static不可行的原因
GCC不允许带constructor属性的函数为static,因为构造函数需要被动态链接器识别调用,而static符号是库内私有,动态链接器无法访问,会直接报错。
内容的提问来源于stack exchange,提问作者pmor

