在Windows中,要调用DLL中的函数,该函数必须具有显式的导出声明。例如,__declspec(dllexport)
或.def
文件。
除了Windows之外,即使函数没有导出声明,我们也可以在.so
(共享对象文件)中调用函数。对于我来说,制作.so比使用.dll容易得多。
同时,我很好奇非Windows如何在没有显式导出声明的情况下由其他程序调用.so中定义的函数。我粗略猜测.so文件中的所有函数都会自动导出,但我不确定。
.so
文件通常是类Unix操作系统中的DSO(动态共享对象,a.k.a共享库)。您想知道如何使这样的文件中定义的符号对运行时加载程序可见,以便在执行时将DSO动态链接到某个程序的进程中。这就是你所说的“出口”。 “导出”是一个有点Windows / DLL-ish的术语,也容易与“外部”或“全局”混淆,所以我们会说动态可见。
我将解释如何在使用GNU工具链构建的DSO的上下文中控制符号的动态可见性 - 即使用GCC编译器(gcc
,g++
,gfortran
等)编译并与binutils链接器ld
(或兼容的替代方案)链接编译器和链接器)。我将用C代码说明。其他语言的机制相同。
目标文件中定义的符号是C源代码中的文件范围变量。即未在任何块内定义的变量。块范围变量:
{ int i; ... }
仅在执行封闭块时定义并且在目标文件中没有永久位置。
GCC生成的目标文件中定义的符号是本地的或全局的。
可以在定义它的目标文件中引用本地符号,但是目标文件根本不会显示它以进行链接。不适用于静态链接。不适用于动态链接。在C中,文件范围变量定义在默认情况下是全局的,如果使用static
存储类限定则是本地的。所以在这个源文件中:
foobar.c(1)
static int foo(void)
{
return 42;
}
int bar(void)
{
return foo();
}
foo
是当地的象征,bar
是全球性的。如果我们用-save-temps
编译这个文件:
$ gcc -save-temps -c -fPIC foobar.c
然后GCC将在foobar.s
中保存汇编列表,在那里我们可以看到生成的汇编代码如何记录bar
是全局的而且foo
不是:
foobar.s(1)
.file "foobar.c"
.text
.type foo, @function
foo:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $42, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size foo, .-foo
.globl bar
.type bar, @function
bar:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
call foo
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size bar, .-bar
.ident "GCC: (Ubuntu 8.2.0-7ubuntu1) 8.2.0"
.section .note.GNU-stack,"",@progbits
汇编程序指令.globl bar
意味着bar
是一个全局符号。没有.globl foo
;所以foo
是当地的。
如果我们检查目标文件中的符号,请使用
$ readelf -s foobar.o
Symbol table '.symtab' contains 10 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS foobar.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 2
4: 0000000000000000 0 SECTION LOCAL DEFAULT 3
5: 0000000000000000 11 FUNC LOCAL DEFAULT 1 foo
6: 0000000000000000 0 SECTION LOCAL DEFAULT 5
7: 0000000000000000 0 SECTION LOCAL DEFAULT 6
8: 0000000000000000 0 SECTION LOCAL DEFAULT 4
9: 000000000000000b 11 FUNC GLOBAL DEFAULT 1 bar
消息是一样的:
5: 0000000000000000 11 FUNC LOCAL DEFAULT 1 foo
...
9: 000000000000000b 11 FUNC GLOBAL DEFAULT 1 bar
对象文件中定义的全局符号以及仅全局符号可供静态链接器用于解析其他对象文件中的引用。实际上,本地符号只出现在文件的符号表中,以供调试器或其他一些目标文件探测工具使用。如果我们用最小的优化重做编译:
$ gcc -save-temps -O1 -c -fPIC foobar.c
$ readelf -s foobar.o
Symbol table '.symtab' contains 9 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS foobar.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 2
4: 0000000000000000 0 SECTION LOCAL DEFAULT 3
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 6
7: 0000000000000000 0 SECTION LOCAL DEFAULT 4
8: 0000000000000000 6 FUNC GLOBAL DEFAULT 1 bar
然后foo
从符号表中消失。
由于全局符号可用于静态链接器,因此我们可以将程序与从另一个目标文件调用foobar.o
的bar
链接:
main.c中
#include <stdio.h>
extern int foo(void);
int main(void)
{
printf("%d\n",bar());
return 0;
}
像这样:
$ gcc -c main.c
$ gcc -o prog main.o foobar.o
$ ./prog
42
但正如您所注意到的,我们不需要以任何方式更改foobar.o
以使bar
对加载器动态可见。我们可以将它链接到共享库中:
$ gcc -shared -o libbar.so foobar.o
然后动态链接相同的程序与该共享库:
$ gcc -o prog main.o libbar.so
这没关系:
$ ./prog
./prog: error while loading shared libraries: libbar.so: cannot open shared object file: No such file or directory
...哎呀。只要我们让加载器知道libbar.so
在哪里就可以了,因为我的工作目录不是默认情况下缓存的搜索目录之一:
$ export LD_LIBRARY_PATH=.
$ ./prog
42
目标文件foobar.o
在.symtab
部分中有一个符号表,包括(至少)静态链接器可用的全局符号。 DSO libbar.so
在其.symtab
部分也有一个符号表。但它也有一个动态符号表,在它的.dynsym
部分:
$ readelf -s libbar.so
Symbol table '.dynsym' contains 6 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __cxa_finalize
2: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
5: 00000000000010f5 6 FUNC GLOBAL DEFAULT 9 bar
Symbol table '.symtab' contains 45 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
...
...
21: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
22: 0000000000001040 0 FUNC LOCAL DEFAULT 9 deregister_tm_clones
23: 0000000000001070 0 FUNC LOCAL DEFAULT 9 register_tm_clones
24: 00000000000010b0 0 FUNC LOCAL DEFAULT 9 __do_global_dtors_aux
25: 0000000000004020 1 OBJECT LOCAL DEFAULT 19 completed.7930
26: 0000000000003e88 0 OBJECT LOCAL DEFAULT 14 __do_global_dtors_aux_fin
27: 00000000000010f0 0 FUNC LOCAL DEFAULT 9 frame_dummy
28: 0000000000003e80 0 OBJECT LOCAL DEFAULT 13 __frame_dummy_init_array_
29: 0000000000000000 0 FILE LOCAL DEFAULT ABS foobar.c
30: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
31: 0000000000002094 0 OBJECT LOCAL DEFAULT 12 __FRAME_END__
32: 0000000000000000 0 FILE LOCAL DEFAULT ABS
33: 0000000000003e90 0 OBJECT LOCAL DEFAULT 15 _DYNAMIC
34: 0000000000004020 0 OBJECT LOCAL DEFAULT 18 __TMC_END__
35: 0000000000004018 0 OBJECT LOCAL DEFAULT 18 __dso_handle
36: 0000000000001000 0 FUNC LOCAL DEFAULT 6 _init
37: 0000000000002000 0 NOTYPE LOCAL DEFAULT 11 __GNU_EH_FRAME_HDR
38: 00000000000010fc 0 FUNC LOCAL DEFAULT 10 _fini
39: 0000000000004000 0 OBJECT LOCAL DEFAULT 17 _GLOBAL_OFFSET_TABLE_
40: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __cxa_finalize
41: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
42: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
43: 00000000000010f5 6 FUNC GLOBAL DEFAULT 9 bar
动态符号表中的符号是动态可见的符号 - 可供运行时加载程序使用。你可以看到bar
出现在.symtab
和.dynsym
的libbar.so
中。在这两种情况下,符号在GLOBAL
(=绑定)列中具有bind
,在DEFAULT
(=可见性)列中具有vis
。
如果你想让readelf
只显示动态符号表,那么:
readelf --dyn-syms libbar.so
会这样做,但不适用于foobar.o
,因为目标文件没有动态符号表:
$ readelf --dyn-syms foobar.o; echo Done
Done
所以联系:
$ gcc -shared -o libbar.so foobar.o
创建libbar.so
的动态符号表,并用foobar.o
的全局符号表(以及GCC通过defauilt添加到链接的各种GCC样板文件)中的符号填充它。
这看起来像你的猜测:
我粗略猜测.so文件中的所有函数都会自动导出
是正确的。事实上它很接近,但不正确。
看看如果我像这样重新编译foobar.c
会发生什么:
$ gcc -save-temps -fvisibility=hidden -c -fPIC foobar.c
让我们再看看汇编列表:
foobar.s(2)
...
...
.globl bar
.hidden bar
.type bar, @function
bar:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
call foo
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
...
...
注意汇编程序指令:
.hidden bar
那之前没有。 .globl bar
仍在那里; bar
仍然是一个全球性的象征。我仍然可以在这个程序中静态链接foobar.o
:
$ gcc -o prog main.o foobar.o
$ ./prog
42
我仍然可以链接这个共享库:
$ gcc -shared -o libbar.so foobar.o
但我不能再动态链接这个程序:
$ gcc -o prog main.o libbar.so
/usr/bin/ld: main.o: in function `main':
main.c:(.text+0x5): undefined reference to `bar'
collect2: error: ld returned 1 exit status
在foobar.o
,bar
仍然在符号表中:
$ readelf -s foobar.o | grep bar
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS foobar.c
9: 000000000000000b 11 FUNC GLOBAL HIDDEN 1 bar
但它现在在输出的HIDDEN
(= vis
)列中标记为visibility
。
bar
仍然在libbar.so
的符号表中:
$ readelf -s libbar.so | grep bar
29: 0000000000000000 0 FILE LOCAL DEFAULT ABS foobar.c
41: 0000000000001100 11 FUNC LOCAL DEFAULT 9 bar
但这一次,它是一个LOCAL
符号。它不会被libbar.so
的静态链接器使用 - 正如我们刚才看到的,当我们的链接失败时。它根本不在动态符号表中:
$ readelf --dyn-syms libbar.so | grep bar; echo done
done
所以-fvisibility=hidden
在编译foobar.c
时的效果是让编译器在.globl
中将.hidden
符号注释为foobar.o
。然后,当foobar.o
链接到libbar.so
时,链接器会将每个全局隐藏符号转换为libbar.so
中的本地符号,这样只要libbar.so
与其他内容链接,它就无法用于解析引用。并且它不会将隐藏符号添加到libbar.so
的动态符号表中,因此运行时加载程序无法看到它们动态地解析引用。
到目前为止的故事:当链接器创建共享库时,它会在动态符号表中添加在输入对象文件中定义的所有全局符号,并且不会被编译器隐藏。这些成为共享库的动态可见符号。默认情况下不会隐藏全局符号,但我们可以使用编译器选项-fvisibility=hidden
隐藏它们。此选项引用的可见性是动态可见性。
现在,使用-fvisibility=hidden
从动态可见性中删除全局符号的能力看起来并不是很有用,因为我们使用该选项编译的任何目标文件似乎都不会为共享库提供动态可见的符号。
但实际上,我们可以单独控制目标文件中定义的哪些全局符号将是动态可见的,哪些不会。让我们改变foobar.c
如下:
foobar.c(2)
static int foo(void)
{
return 42;
}
int __attribute__((visibility("default"))) bar(void)
{
return foo();
}
您在此处看到的__attribute__
语法是GCC语言扩展,用于指定标准语言中无法表达的符号属性 - 例如动态可见性。 Microsoft的declspec(dllexport)
是Microsoft语言扩展,与GCC的__attribute__((visibility("default")))
具有相同的效果。但是对于GCC,在目标文件中定义的全局符号默认拥有__attribute__((visibility("default")))
,并且您必须使用-fvisibility=hidden
进行编译以覆盖它。
像上次一样重新编译:
$ gcc -fvisibility=hidden -c -fPIC foobar.c
现在foobar.o
的符号表:
$ readelf -s foobar.o | grep bar
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS foobar.c
9: 000000000000000b 11 FUNC GLOBAL DEFAULT 1 bar
尽管bar
,再次显示DEFAULT
与-fvisibility=hidden
能见度。如果我们重新连接libbar.so
:
$ gcc -shared -o libbar.so foobar.o
我们看到bar
回到动态符号表中:
$ readelf --dyn-syms libbar.so | grep bar
5: 0000000000001100 11 FUNC GLOBAL DEFAULT 9 bar
因此,-fvisibility=hidden
告诉编译器将全局符号标记为隐藏,除非在源代码中我们明确指定该符号的反补贴动态可见性。
这是从我们希望动态可见的目标文件中精确选择符号的一种方法:将-fvisibility=hidden
传递给编译器,并在源代码中单独指定__attribute__((visibility("default")))
,仅用于我们想要动态可见的符号。
另一种方法是不将-fvisibility=hidden
传递给编译器,并在源代码中单独指定__attribute__((visibility("hidden")))
,仅用于我们不希望动态可见的符号。所以,如果我们再次改变foobar.c
:
foobar.c(3)
static int foo(void)
{
return 42;
}
int __attribute__((visibility("hidden"))) bar(void)
{
return foo();
}
然后使用默认可见性重新编译:
$ gcc -c -fPIC foobar.c
bar
恢复隐藏在目标文件中:
$ readelf -s foobar.o | grep bar
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS foobar.c
9: 000000000000000b 11 FUNC GLOBAL HIDDEN 1 bar
在重新密封libbar.so
后,bar
再次缺席其动态符号表:
$ gcc -shared -o libbar.so foobar.o
$ readelf --dyn-syms libbar.so | grep bar; echo Done
Done
专业方法是将DSO的动态API最小化到指定的范围。使用我们讨论过的设备,这意味着使用-fvisibility=hidden
进行编译并使用__attribute__((visibility("default")))
来公开指定的API。动态API也可以使用一种名为version-script的链接器脚本与GNU链接器一起控制和版本化:这是一种更专业的方法。
进一步阅读: