一、符号表的定义
符号表是编译器和链接器用于记录和查找程序中各种符号信息的数据结构。一个符号可能表示变量、函数、全局数据、常量、段等内容。
一个典型的符号表项包含以下信息:
-
符号名(Symbol Name)
-
类型(如函数、变量、段等)
-
存储类别(本地、全局、弱符号等)
-
符号的地址或偏移量
-
所属段(如
.text、.data、.bss) -
定义或引用状态
在使用 ELF 格式的系统(如 Linux)中,每个目标文件通常会包含一个 .symtab 段,保存该目标文件中所有符号的详细信息。
二、链接过程概述
程序编译后会生成一个或多个目标文件(Object Files)。这些目标文件可能包含对其他文件中符号的引用。在最终生成可执行文件或共享库前,链接器(Linker)需要将这些目标文件组合起来,解决各个符号的引用关系,这一过程称为链接(Linking)。
链接分为两种:
-
静态链接(Static Linking):将所有目标文件和库合并成一个独立的可执行文件。
-
动态链接(Dynamic Linking):程序在运行时加载共享库,由运行时链接器处理符号解析。
三、没有符号表能完成链接吗?
不能。
链接器必须知道每个符号的定义位置和引用位置,这些信息全部依赖符号表。如果没有符号表,链接器将无法:
-
找出函数或变量的定义位置;
-
匹配函数调用或变量访问的引用;
-
执行重定位(即更新跳转或访问的地址)。
虽然在最终生成的可执行文件中可以删除符号表(比如使用 strip),但在链接阶段符号表是必须存在的。
四、静态链接的详细过程
静态链接将所有目标文件和库合并成一个完整的程序,链接器主要完成以下步骤:
静态链接流程图
┌──────────────────────────────┐
│ 编译生成目标文件 │
│ *.o, 其中包含 .symtab │
└────────────┬─────────────────┘
▼
┌──────────────────────────────┐
│ 链接器收集所有符号表 │
│ 构建全局符号表(Global Table)│
└────────────┬─────────────────┘
▼
┌──────────────────────────────┐
│ 符号解析 │
│ - 匹配引用与定义 │
│ - 检查冲突与未定义符号 │
└────────────┬─────────────────┘
▼
┌──────────────────────────────┐
│ 地址分配(段布局) │
│ 分配 .text、.data、.bss 等地址 │
└────────────┬─────────────────┘
▼
┌──────────────────────────────┐
│ 重定位处理 │
│ 修正代码中对符号的引用地址 │
└────────────┬─────────────────┘
▼
┌──────────────────────────────┐
│ 输出可执行文件或静态库 │
│ 符号已完全解析 │
└──────────────────────────────┘
五、符号表在链接过程中的作用
1. 符号解析
链接器使用符号表来解决“谁调用了谁”、“变量定义在哪”等问题。符号未解析或重复定义都会导致链接错误。
2. 重定位处理
目标文件内的地址是相对地址,链接器通过符号表获取符号的真实地址,并完成重定位。
3. 符号冲突处理
多个文件定义同一个全局变量会冲突。链接器使用符号表识别冲突,并根据规则(如强符号优先)解决。
六、动态链接中的符号表作用
动态链接在程序运行时解析符号,依赖以下数据结构:
-
.dynsym:动态符号表 -
.plt(Procedure Linkage Table):函数跳转 -
.got(Global Offset Table):记录函数实际地址 -
动态链接器(如
ld.so)负责解析外部库的函数和变量引用
即使延迟绑定,符号表仍然是必须的。
七、实操示例:查看符号表 & 链接错误
示例 1:查看 .symtab 内容
假设你有一个简单的目标文件 main.o,你可以使用以下命令查看符号表内容:
readelf -s main.o
部分输出示例:
Symbol table '.symtab' contains 5 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.c
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf
3: 0000000000000000 25 FUNC GLOBAL DEFAULT 1 main
说明:
-
main是定义在本文件中的函数(段索引为 1) -
printf是未定义的外部符号(段索引UND)
示例 2:链接时出现符号未定义错误
假设你有两个文件:
main.c
extern void foo();
int main() {
foo();
return 0;
}
gcc main.c -o main 会报错:
/usr/bin/ld: /tmp/ccXXXX.o: in function `main`:
main.c:(.text+0x5): undefined reference to `foo`
collect2: error: ld returned 1 exit status
这是因为:
-
main中调用了foo(); -
链接器在
.symtab中找到foo是一个UND(未定义)符号; -
但没有其他目标文件或库中提供
foo的定义,于是链接失败。
解决方法:提供 foo 的定义,或者链接包含 foo 定义的目标文件:
gcc main.c foo.c -o main
|
阶段 |
是否使用符号表 |
作用 |
|---|---|---|
|
编译 |
是 |
记录本地定义/引用符号 |
|
静态链接 |
是(必须) |
符号解析、重定位、地址分配 |
|
动态链接加载 |
是 |
运行时符号查找和绑定 |
|
程序运行 |
否(可剥离) |
如果不调试或需要反汇编,可无符号表 |
符号表是链接过程的核心数据结构。理解它不仅能帮助你解决常见链接错误,还能帮助你优化构建流程、理解程序结构,甚至进行反汇编与逆向工程。
1820

被折叠的 条评论
为什么被折叠?



