场景:
$ 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 &&
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH &&
gcc -L. -o main main.c -lx -ly -lz &&
./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 次,而不是从库 X、Y、Z 调用 1 次?
额外观察:
lib.c
从 CAT(foo,)
更改为 CAT(foo,N)
并重新运行 build_run.sh
,那么我们将看到:$ bash build_run.sh
fooz 0x7fc121dcc139
fooy 0x7fc121dd1139
foox 0x7fc121dd6139
barx
bary
barz
这是预期的行为。
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
我认为这是符号插入的一个例子。
加载共享库时,动态链接器默认全局解析符号(函数名、变量)。如果两个或多个共享库定义相同的符号(例如,
foo
),则该符号的第一个加载版本将用于所有后续引用。这称为“符号插入”。
潜在的修复: 编译每个共享库时,您可以使用
-Bsymbolic
链接器选项。
Cygwin 使用 DLL 库,而 Windows 有不同的机制。在 Windows 中,每个 DLL 的函数和变量都有不同的地址空间,这意味着符号名称在每个 DLL 内独立解析,而不是在所有加载的库中全局解析。