当翻译单元被编译/组装成单个目标文件时,我知道它会为找到的每个变量/函数创建一个符号表。
这是大部分准确的:本地(又名堆栈,又名自动存储持续时间)变量永远不会放入符号表中(除非使用古老的调试格式,例如STABS)。
你不需要相信我的话:这很容易观察:
$ cat foo.c
int a_common_global;
int a_global = 42;
static int a_static = 43;
static int static_fn()
{
return 44;
}
int global_fn()
{
int a_local = static_fn();
static int a_function_static = 1;
return a_local + a_static + a_function_static;
}
$ gcc -c foo.c
$ readelf -Ws foo.o
Symbol table '.symtab' contains 14 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS foo.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000004 4 OBJECT LOCAL DEFAULT 3 a_static
6: 0000000000000000 11 FUNC LOCAL DEFAULT 1 static_fn
7: 0000000000000008 4 OBJECT LOCAL DEFAULT 3 a_function_static.1800
8: 0000000000000000 0 SECTION LOCAL DEFAULT 6
9: 0000000000000000 0 SECTION LOCAL DEFAULT 7
10: 0000000000000000 0 SECTION LOCAL DEFAULT 5
11: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM a_common_global
12: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 a_global
13: 000000000000000b 34 FUNC GLOBAL DEFAULT 1 global_fn
这里有几点值得注意:
-
a_local 没有出现在符号表中
-
a_function_static 在其名称后附加了“随机”数字。这样a_function_static 在不同的函数中不会发生冲突。
-
a_static 和 static_fn 有 LOCAL 链接
另请注意,虽然a_static 和static_fn 出现在符号表中,但这样做仅是为了帮助调试。本地符号不被后续链接使用,可以安全删除。
运行后strip --strip-unneeded foo.o:
$ readelf -Ws foo.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 SECTION LOCAL DEFAULT 1
2: 0000000000000000 0 SECTION LOCAL DEFAULT 3
3: 0000000000000000 0 SECTION LOCAL DEFAULT 4
4: 0000000000000000 0 SECTION LOCAL DEFAULT 5
5: 0000000000000000 0 SECTION LOCAL DEFAULT 6
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM a_common_global
8: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 a_global
9: 000000000000000b 34 FUNC GLOBAL DEFAULT 1 global_fn
当链接器生成最终的可执行文件时,是否有一个最终符号表,其中包含所有文件遇到的每个条目?
是的。像这样添加main.c:
$ cat main.c
extern int global_fn();
extern int a_global;
int a_common_global = 23;
int main()
{
return global_fn() + a_common_global + a_global;
}
$ gcc -c main.c foo.c
$ gcc main.o foo.o
$ readelf -Ws a.out
Symbol table '.symtab' contains 69 entries:
Num: Value Size Type Bind Vis Ndx Name
...我省略了不感兴趣的条目(有很多)。
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
34: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.c
35: 0000000000000000 0 FILE LOCAL DEFAULT ABS foo.c
36: 0000000000201030 4 OBJECT LOCAL DEFAULT 23 a_static
37: 000000000000061c 11 FUNC LOCAL DEFAULT 13 static_fn
38: 0000000000201034 4 OBJECT LOCAL DEFAULT 23 a_function_static.1800
50: 0000000000000627 34 FUNC GLOBAL DEFAULT 13 global_fn
63: 00000000000005fa 34 FUNC GLOBAL DEFAULT 13 main
64: 000000000020102c 4 OBJECT GLOBAL DEFAULT 23 a_global
我很困惑,因为如果我们将一个变量声明为静态意味着一个文件中的文件范围,那么当每次在可执行文件中遇到这个变量时,编译器是否必须引用最终符号表才能看到它的实际范围,还是它会为它生成特殊代码?
在链接阶段,编译器(通常)根本不被调用。并且链接器不会(不需要)关注LOCAL 符号。
一般来说,链接器只做两件事:
- 将未定义的引用(例如从
main.o 引用global_fn 和a_global)解析为它们的定义(在foo.o 中)和
- 应用重定位。
在foo.o 中为a_static 和a_function_static 应用重定位实际上并不需要它们的名字;只有它们在.data 部分中的偏移量,因为这个输出应该清楚:
$ objdump -dr foo.o
foo.o: file format elf64-x86-64
Disassembly of section .text:
...
000000000000000b <global_fn>:
b: 55 push %rbp
c: 48 89 e5 mov %rsp,%rbp
f: 48 83 ec 10 sub $0x10,%rsp
13: b8 00 00 00 00 mov $0x0,%eax
18: e8 e3 ff ff ff callq 0 <static_fn>
1d: 89 45 fc mov %eax,-0x4(%rbp)
20: 8b 15 00 00 00 00 mov 0x0(%rip),%edx # 26 <global_fn+0x1b>
22: R_X86_64_PC32 .data
26: 8b 45 fc mov -0x4(%rbp),%eax
29: 01 c2 add %eax,%edx
2b: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 31 <global_fn+0x26>
2d: R_X86_64_PC32 .data+0x4
31: 01 d0 add %edx,%eax
33: c9 leaveq
34: c3 retq
注意偏移量0x22 和0x2d 的重定位如何不说明名称(分别为a_static 和a_function_static.1800)。