You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何带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次?


额外测试现象

  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

已尝试的操作

  1. 在编译共享库时添加-Bsymbolic参数(作用为“创建共享库时,将全局符号引用绑定到库内的定义”),但无效果。
  2. 尝试将带constructor属性的函数声明为static,但无法实现。

问题分析与解答

这个问题的根源在于Linux动态链接器对全局符号的解析机制,以及GCC处理constructor属性函数的方式:

  1. 全局符号冲突与动态链接的符号解析规则
    用CAT(foo,)定义函数时,三个共享库生成的函数符号名都是foo——这是全局可见符号。Linux动态链接器(ld.so)遵循符号优先解析规则:当多个共享库提供同名全局符号时,动态链接器会选择第一个加载的库(此处为libx.so)中的符号定义,后续所有对该符号的引用都会绑定到这个最先加载的地址上。
    虽然每个共享库的.init_array段都记录了构造函数的执行入口,但GCC生成构造函数调用逻辑时,是通过符号引用关联的,而非直接写入绝对地址。动态链接器处理所有.init_array时,所有指向foo的引用都会被解析为libx.so中的foo,因此三次执行的都是同一个函数。

  2. 修改函数名后的正常逻辑
    改为CAT(foo,N)后,三个库的函数名分别为foox、fooy、fooz——这是三个独立的全局符号。动态链接器会为每个符号匹配对应库内的定义,.init_array中的每个构造函数引用都会绑定到各自库的函数,从而实现每个库的构造函数各执行一次。

  3. Cygwin环境的差异
    Cygwin基于Windows的PE/COFF格式,其动态链接器对同名符号的处理是每个库保留自身的符号定义,不会进行跨库符号覆盖,因此每个库的foo构造函数都会被独立执行。

  4. -Bsymbolic参数无效的原因
    -Bsymbolic仅作用于共享库内部的符号引用绑定,而构造函数的调用是动态链接器直接从.init_array读取并执行的,不经过库内部的符号解析逻辑,因此无法改变跨库的全局符号解析行为。

  5. static不可行的原因
    GCC不允许带constructor属性的函数为static,因为构造函数需要被动态链接器识别调用,而static符号是库内私有,动态链接器无法访问,会直接报错。

内容的提问来源于stack exchange,提问作者pmor

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 09:05:11