想象你是万人宴会的总策划。你请来十二家餐厅各做一道拿手菜,每家餐厅都交来一张“菜品交接单”:菜名叫什么、主厨是谁、配料放在几号托盘的哪一格。宴会开始前,你要把十二张单子合并成一张总菜单,为每道菜安排具体的桌号;凡是菜单上写着“隔壁那道菜”的模糊说法,都得改成实际桌号。 计算机中的程序,恰恰经历了同样的过程。编译器把 C 代码翻译成一个又一个目标文件(.o),这些文件里藏着两种关键信息:符号表(相当于菜品交接单)和重定位(相当于把“菜名”落成“桌号”)。链接器把它们合并、装配成最终的可执行文件。本文就带你翻开这沓交接单,从 ELF 格式出发,看懂符号表的结构、重定位的算法,以及 readelf、nm、objdump 这些“翻单工具”的用法。

岗位关联

掌握符号表与重定位,你就拿到了读懂任何二进制文件的钥匙。无论是排查链接报错、优化程序体积,还是做反汇编与安全分析,这套底层功底都是 AI 工程师处理代码与数据时的隐形优势。

目录
  1. 可视化演示
  2. 符号与地址:宴会菜单上的菜名
  3. ELF分段剖析:交接单的归档方式
  4. 符号表结构:交接单上的明细栏
  5. 重定位原理:把菜名替换成桌号
  6. 链接与装载:自带食材与共享厨房
  7. 工具链实战:三位帮厨的读单技巧
  8. 优化与陷阱:宴会事故与应急预案
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 3KB)。

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

符号与地址:宴会菜单上的菜名

你是万人宴会的总策划。每家餐厅交来的交接单,第一栏永远是“菜名”。“宫保鸡丁”“佛跳墙”——没有名字,后厨根本不知道哪道菜对应哪份原料。计算机程序也一样:CPU 只认地址,但人类写代码用的是函数名和变量名。编译器为了搭起“人类名字”与“机器地址”之间的桥,在每个目标文件里都藏了一张表,这张表就是符号表(symbol table)。

符号表是目标文件和可执行文件中的一张对照表,记录每个符号的名字、地址、大小和属性。所谓“符号”,就是程序里的函数名、全局变量名等一切需要跨代码块引用的名字。链接器拿着它回答一个核心问题:“main 函数到底在哪个地址?”它让“长得像文本的程序”和“只认数字的 CPU”顺利握手。

符号按可见性分为三类:全局符号(GLOBAL)对其他文件可见,能跨文件引用;局部符号(LOCAL)只在本目标文件内有效,通常来自 static 修饰的函数或变量;未定义符号(UNDEFINED)表示“这里用到了但没定义,请链接器帮我找”。符号表还附带绑定信息(binding)和类型信息(type),绑定决定可见范围,类型区分函数、数据还是节区。正是这些字段,让链接器能精准判断每个名字该去哪“认领”。

symtab: 符号名↦(地址, 大小, 绑定, 类型, 节号)\displaystyle \text{symtab}:\ \text{符号名} \mapsto (\text{地址},\ \text{大小},\ \text{绑定},\ \text{类型},\ \text{节号})

1. symtab:符号表对象,通常存放在 ELF 文件的 .symtab 节区中,是链接器解析引用的唯一依据。

symtab\text{symtab}

2. 符号名:程序里的函数名或变量名,人类可读,链接器通过它来匹配引用与定义。

符号名\text{符号名}

3. 地址与大小:地址是符号装载后的虚拟地址,大小是函数或变量占用的字节数。

地址, 大小\text{地址},\ \text{大小}

4. 绑定、类型与节号:绑定决定跨文件可见性,类型区分函数与数据,节号指明符号归属的段。

绑定, 类型, 节号\text{绑定},\ \text{类型},\ \text{节号}

看懂一张交接单的第一栏,你就能大致预判整场宴会的规模。但十二张单子堆在桌上,总得有个归档办法——接下来我们看看 ELF 文件是怎么把这一桌子纸整理成一本总册子的。

ELF分段剖析:交接单的归档方式

有经验的宴会总策划,绝不会把十二张交接单乱塞进抽屉。他会装订成一本总册子:封面写明宴会主题与规模,目录列出章节,正文按“冷菜、热菜、甜点”分区存放。ELF 文件(Executable and Linkable Format,可执行与可链接格式)就是计算机世界里这样一本总册子,它规定了二进制文件该如何“归档”。

ELF 是 Linux 和多数现代系统使用的二进制文件标准。它把内容分成三个层次:文件头(ELF header)像封面,记录文件类型、目标架构、入口点地址;节区(sections)像正文,按用途把内容切成一块块;节头表(section header table)像目录,逐条写明每个节区的名字、偏移、大小和对齐方式——哪个节在文件的哪个位置,一目了然。

最常见的三个节区:.text 存放机器指令,.data 存放已初始化的全局变量,.bss 存放未初始化变量(只占空间描述、不占文件体积)。符号表就存放在 .symtab 节区。这里有个新手常踩的坑:节区在文件中的偏移,与装载后的虚拟地址并不总是一致。链接器会把各节区装载到虚拟地址空间的不同位置,而符号的虚拟地址必须由“节区起始地址 + 节内偏移”计算出来。

Vsym=sh_addr+(Bsym−sh_offset)\displaystyle V_{sym} = \text{sh\_addr} + (B_{sym} - \text{sh\_offset})

1. sh_addr:该节区装载进内存后的起始虚拟地址,由链接器布局决定。

sh_addr\text{sh\_addr}

2. B_sym:符号在文件中的字节偏移,可直接从文件里读出。

BsymB_{sym}

3. sh_offset:节区在文件中的起始字节偏移,节头表里写明。

sh_offset\text{sh\_offset}

4. 做差再加:先用符号的文件偏移减去节区文件偏移,得到节内相对位置,再加上节区虚拟地址,就换算出符号的真实虚拟地址。

sh_addr+(Bsym−sh_offset)\text{sh\_addr} + (B_{sym} - \text{sh\_offset})

总册子的结构定好了,接下来就翻开它,看看交接单明细栏里到底写了哪些字段——这正是符号表条目的内部结构。

符号表结构:交接单上的明细栏

翻开总册子的交接单页,你会发现每道菜都占一行表格:菜名、主厨、分量、存放托盘号。ELF 的 .symtab 节区里同样如此,每个符号对应一条固定 24 字节的记录,C 语言中称之为 Elf64_Sym 结构。别看它短,五六个字段就把一个符号的“身份”描述得清清楚楚。

Elf64_Sym 有个巧妙设计:它不直接存字符串,而是存一个下标 st_name,指向 .strtab 字符串表中的某个位置。这样多个符号可以共享同一个字符串存储区,省下大量空间。真正让符号表“活”起来的,是 st_value 和 st_shndx:前者给出符号的值(地址),后者指明这个符号属于哪个节区——是代码段、数据段,还是“未定义”。

st_info 是结构里最“省”的字段:一个字节同时塞下绑定(binding)和类型(type)两种信息。绑定值 GLOBAL=1、LOCAL=0、WEAK=2,类型值 FUNC=2、OBJECT=1、SECTION=3。链接器读一个字节,就能同时知道“这个符号对外可见吗”和“它是个函数还是数据”。至于 st_shndx,值为 0(SHN_UNDEF)表示该符号未定义,值为 0xfff1(SHN_ABS)表示绝对值符号。

st_info=(bind≪4)  ∣  type\displaystyle \text{st\_info} = (\text{bind} \ll 4) \;|\; \text{type}

1. bind(绑定):标记符号的可见性,GLOBAL 全局、LOCAL 局部、WEAK 弱。

bind\text{bind}

2. 左移四位:把绑定值移到高 4 位,给低 4 位腾出空间给类型。

bind≪4\text{bind} \ll 4

3. type(类型):区分 FUNC(函数)、OBJECT(数据)、SECTION(节区)等。

type\text{type}

4. 按位或合并:两个半字节拼成一个字节,一次读出完整属性。

(bind≪4)  ∣  type(\text{bind} \ll 4) \;|\; \text{type}

交接单每行字段都有讲究,链接器正是逐行读这些明细,才知道哪道菜该上哪个桌。但光看交接单还不够——真正把菜端上桌的,是接下来这场“座位编排”:重定位。

重定位原理:把菜名替换成桌号

宴会座位表出炉后,总策划要做一件繁琐却关键的事:把菜单里所有“隔壁那道菜”的含糊说法,改成实际桌号。比如“宫保鸡丁”最终定在 3 号桌 5 号位,那菜单上凡是提到它的地方都要改成“3 号桌 5 号位”。这一步,在编译世界里叫重定位(relocation)。

为什么需要重定位?因为目标文件里的代码在编译时是“悬空”的:编译器知道 foo 这个名字,却不知道 foo 最终排在虚拟地址的哪个位置。链接器合并所有目标文件、确定整体布局后,就要逐一修改这些悬空引用。每个待修改的位置都对应一条重定位条目(relocation entry),记载三个关键信息:r_offset(待修改的位置)、r_info(引用的符号下标 + 重定位类型)、r_addend(编译器附加的修正量)。

x86-64 上最常见的两种类型:R_X86_64_PC32 用于生成 PC 相对跳转,计算方式为 S + A - P;R_X86_64_PLT32 用于调用可能被动态链接的函数,会走 PLT 表(过程链接表)间接跳转。无论哪种,最终动作都一样:把计算结果写回指令的机器码,让运行时无需再做任何地址修正。

R=S+A−P\displaystyle R = S + A - P

1. S(目标地址):被引用符号在链接后的最终地址,决定“这道菜最终在哪”。

SS

2. A(加数):编译器写入的偏移修正量,用于对付指令长度等细节偏差。

AA

3. P(重定位点):当前被修改指令的位置,也就是“菜单上写着菜名的那个位置”。

PP

4. 三步计算:用目标地址减去当前位置,再加上修正量,得到的就是跳转指令需要的相对偏移。

S+A−PS + A - P

把“隔壁那桌”精确换成“3 号桌 5 号位”,客人们落座后才不会东张西望。但不同宴会的组织方式,决定了这份座位表是开场前定死,还是开席后再调——这就引出链接与装载的两种玩法。

链接与装载:自带食材与共享厨房

办宴会,有两种省钱策略。一种:每家餐厅自带全部食材和厨师,宴席完全独立自给——这是静态链接;另一种:租一个中央厨房,蒸锅、烤箱共享,谁要用就去取——这是动态链接。两者对“座位表是否提前定死”的确定性,截然不同。

静态链接把多个目标文件和静态库一次性合并成一个完整可执行文件,链接完成后所有地址全部固定。它的标准流程是三步:先进行地址空间分配,为每个节区安排虚拟地址;再完成符号解析,把每个引用与对应的定义匹配上;最后执行重定位,写死所有地址。好处是独立部署、启动快,坏处是体积大——每个程序都自带一份 libc。

动态链接则让可执行文件只记录“我需要 libc.so.6”这类依赖,真正的符号解析推迟到装载阶段,由动态链接器完成。这里有两个核心结构:GOT(全局偏移表)存放符号真实地址,装载时由动态链接器填写;PLT(过程链接表)做跳板,第一次调用触发懒绑定(lazy binding)解析地址,后续调用直接命中缓存。这也是地址绑定的三种方式:编译期绑定、加载期绑定、首次调用绑定。

addr(sym)=GOT[i]\displaystyle \text{addr}(sym) = \text{GOT}[i]

1. GOT:全局偏移表,本质是一个地址数组,每项存一个符号的真正地址。

GOT\text{GOT}

2. i(槽位下标):符号在 GOT 中的下标,由重定位条目预先指定。

ii

3. addr(sym):符号在运行时的真实地址,装载前未知、装载后确定。

addr(sym)\text{addr}(sym)

4. 间接寻址:代码通过查 GOT 获得地址,实现位置无关代码(PIC),共享库才能在任意地址装载。

GOT[i]\text{GOT}[i]

共享厨房让每个程序轻装上阵,但取菜总得多走两步。理解了动静两种玩法,我们进入实战环节——看总策划的三大帮厨如何“翻交接单”。

工具链实战:三位帮厨的读单技巧

总策划手边有三件趁手工具:想快速核对菜名,找 nm;想细看每道菜的制作流程,用 objdump;想完整查阅交接单档案,拿 readelf。三者都直接解析 ELF 文件,只是视角不同,配合起来能把二进制文件的底细翻个底朝天。

readelf 是最底层的档案管理员,能打印文件头、节头表、符号表和重定位表;objdump 偏向反汇编,把机器码还原成人能读的汇编指令;nm 是最轻量的符号浏览器,一行一个符号,干净利落。实际工作中,排查“链接报错符号找不到”,先用 nm 扫一眼目标文件,再用 readelf -s 确认符号表细节;想理解一段行为异常的代码,就用 objdump -d 反汇编慢慢看。

最常用的命令有三个:readelf -s 查看符号表,readelf -r 查看重定位条目;objdump -d 反汇编 .text 节区,objdump -t 列出符号表。而 nm 的输出用单字母表示符号类型:T 是已定义全局函数、D 是已定义全局数据、U 是未定义引用;小写 t、d 则对应局部符号。

type={T全局函数定义D全局数据定义U未定义引用\displaystyle \text{type} = \begin{cases} T & \text{全局函数定义} \\ D & \text{全局数据定义} \\ U & \text{未定义引用} \end{cases}

1. T:nm 输出的大写 T,表示符号定义在代码段(.text)中,全局可见。

TT

2. D:大写 D 表示符号定义在数据段(.data)中,已初始化且全局可见。

DD

3. U:U 表示当前文件只引用、未定义,符号定义在其他目标文件或库中。

UU

4. 大小写规律:大写代表全局符号,小写(t、d)代表局部符号,是 nm 输出的通用约定。

t, dt,\ d

三件工具齐上阵,交接单上的秘密无处遁形。但宴会办得多了,总策划也踩过不少坑——最后来看看那些经典事故与应急预案。

优化与陷阱:宴会事故与应急预案

有的宴会,两家餐厅都叫“佛跳墙”,宾客当场懵了;有的菜单赫然写着“鱼香肉丝”,却没有任何一个厨房在做这道菜。链接器每天处理的,正是这些“重名”和“失踪”事故。

符号冲突分两种情况:强符号与强符号同名,链接器直接报错 duplicate symbol;强符号与弱符号同名,则按规则选择强符号,不报错。另一方面,undefined reference 是符号只有引用却找不到定义,常见原因有三个:没链接包含定义的库、库的链接顺序不对、符号被 static 隐藏导致外部不可见。排查思路就是顺着引用反向找:哪个文件引用的?哪个文件该定义?链接命令里到底有没有包含它。

工程优化上,strip 命令删除 .symtab 静态符号表和调试段,可显著减小二进制体积,但 .dynsym(动态符号表)必须保留——动态链接器靠它解析导出符号,删了程序会直接崩溃。链接器还常用垃圾收集(--gc-sections)和 ICF(Identical Code Folding)来合并冗余代码,进一步瘦身。

winner={强符号强弱冲突任意弱符号双弱冲突链接错误双强冲突\displaystyle \text{winner} = \begin{cases} \text{强符号} & \text{强弱冲突} \\ \text{任意弱符号} & \text{双弱冲突} \\ \text{链接错误} & \text{双强冲突} \end{cases}

1. 强符号:C 中普通定义的函数与全局变量,默认拥有最高优先级。

强符号\text{强符号}

2. 弱符号:用 __attribute__((weak)) 显式标记的符号,用于提供可覆盖的默认实现。

弱符号\text{弱符号}

3. 双强冲突:两个强符号同名,链接器无法裁决,直接报错终止。

链接错误\text{链接错误}

4. 裁决规则:弱让强、双弱随便选、双强迫死,理解这条优先级就能定位大多数重名事故。

winner\text{winner}

掌握这些事故预案,你的项目就能像一场秩序井然的宴会一样平稳开场。那么问题来了:这套“读交接单”的功夫,在 AI 时代还能派上哪些新用场?

AI 应用场景

恶意 ELF 样本的符号级特征提取

阶段:特征工程

解决的问题:恶意软件常通过加壳、混淆来对抗静态检测,但符号表和重定位表是最难完全抹除的结构痕迹。传统杀软基于字节签名,面对变种容易失效;若能从 ELF 中提取符号频率、重定位类型分布、未定义符号比例等特征,AI 模型就能在“看不清代码”的情况下识别恶意行为。

具体案例:安全团队扫描一批 IoT 固件样本,用 readelf -s 提取每个 ELF 的符号表,统计 system、execve 等敏感 API 的出现次数,再结合重定位条目中 PLT32 类型的占比,喂给随机森林做恶意/正常二分类,对未知变种的检出率显著高于传统签名方案。

AI 辅助二进制理解与代码生成

阶段:模型推理与生成

解决的问题:逆向工程师阅读反汇编代码耗时巨大,而 LLM 无法直接“看”二进制。符号表正好充当了“翻译词典”:把函数名、全局变量名、调用关系传给模型,它就能快速理解程序结构,生成可读的 C 代码骨架。

具体案例:一个 AI 编程助手接入 objdump -d 的反汇编输出和 nm 的符号清单,对固件中的某加密函数自动生成伪代码,并标注其调用了哪些动态链接库函数。工程师只需校对重定位表指向的调用关系,就能还原完整逻辑,整体效率提升一倍。

供应链漏洞扫描与 SBOM 生成

阶段:数据分析与匹配

解决的问题:现代软件大量依赖第三方库,但可执行文件经过链接后,库的“身份”变得模糊。通过读取动态符号表(.dynsym)和共享库依赖,可以自动生成软件物料清单(SBOM),再与漏洞库进行 AI 语义匹配,及时发现“哪个函数来自哪个有漏洞的库”。

具体案例:DevSecOps 流水线在每次构建后自动运行解析脚本,提取所有二进制文件的动态符号导出集合,与 CVE 数据库中的漏洞函数签名做向量匹配,成功在某个内部工具的依赖中定位到存在 Log4Shell 风险的库版本并触发告警。

Python 演示

python56 行
# -*- coding: utf-8 -*-"""简化版符号解析与重定位模拟器"""  class Symbol:    def __init__(self, name, addr, defined):        self.name = name        self.addr = addr        self.defined = defined     def __repr__(self):        tag = 'D' if self.defined else 'U'        return '%s %s @0x%x' % (tag, self.name, self.addr)  def merge_symbols(objects):    """合并多个目标文件的符号表,解析同名符号"""    table = {}    for obj in objects:        for name, sym in obj.items():            old = table.get(name)            if old is None:                table[name] = sym            elif old.defined and sym.defined:                raise ValueError('duplicate symbol: %s' % name)            elif sym.defined:                table[name] = sym    return table  def relocate(refer_site, sym, addend=0):    """重定位计算: R = S + A - P,这里简化为 S + A"""    return sym.addr + addend  def main():    foo_o = {        'main': Symbol('main', 0x400100, True),        'foo': Symbol('foo', 0x400140, True),    }    bar_o = {        'foo': Symbol('foo', 0, False),        'bar': Symbol('bar', 0x400200, True),    }    table = merge_symbols([foo_o, bar_o])    print('== merged symbol table ==')    for name in sorted(table):        print('%5s -> %s' % (name, table[name]))    site = 0x400210    target = relocate(site, table['foo'])    print('== relocation ==')    print('patch site 0x%x with foo addr 0x%x' % (site, target))  if __name__ == '__main__':    main()
目的:用纯 Python 模拟链接器的核心两步——合并符号表、对引用进行地址修正,帮助理解符号解析与重定位的最小闭环。
  1. class Symbol定义符号数据结构,包含名字、地址和“是否已定义”标记,对应 ELF 中 Elf64_Sym 的三个核心字段。
  2. def merge_symbols(objects)模拟链接器的符号解析:把多个目标文件的符号表合并成一张全局表。
  3. old = table.get(name)查同名符号,判断是否发生冲突,就像翻交接单找重名菜品。
  4. raise ValueError('duplicate symbol: %s' % name)两个强符号同名时抛出错误,对应链接器的 duplicate symbol。
  5. elif sym.defined若旧符号未定义、新符号已定义,用定义覆盖引用,对应强弱符号冲突时选强。
  6. def relocate(refer_site, sym, addend=0)简化版重定位,即 S + A 计算出要写入的地址。
  7. table['foo']从合并后的符号表查找 foo 的定义地址,模拟“按名字寻址”。
  8. print('patch site ...')打印被修改位置和写入值,直观展示重定位结果。

常见误区

误区我一直以为符号表只是给调试器用的,发布前用 strip 删掉就好,反正程序照常跑,删了还能减小体积。

正解符号表分两层。.symtab 静态符号表主要用于链接和调试,strip 删除后不影响运行;但 .dynsym 动态符号表是运行必需的——动态链接器要靠它解析共享库符号、完成装载重定位。如果强行删除 .dynsym,程序在启动时就会崩溃。所以“符号表没影响”只对静态符号表成立。

为何常见:很多人接触的 strip 场景恰好只删 .symtab,运行完全不受影响,容易以偏概全,把“静态符号表可删”推广成“所有符号表都可删”。

误区我以为重定位就是把引用处的地址值整个替换成符号的实际地址,替换完就大功告成。

正解重定位的结果绝大多数不是绝对地址,而是相对偏移。R_X86_64_PC32 的 S+A-P 计算中,P 是当前位置,结果是相对下一条指令的差值;R_X86_64_PLT32 还要经过 PLT 跳板间接跳转。x86-64 上的重定位类型有几十种,符号类型、节区对齐、指令编码都会影响最终写回的值。

为何常见:初学者接触的第一个例子往往是绝对寻址的简化模型,把特例当成了通例,忽略了 PC 相对寻址在 x86-64 中的统治地位。

误区静态链接把所有代码都打进一个可执行文件,动态链接还要额外加载 `.so`,所以静态链接的文件一定更小。

正解恰恰相反。静态链接把用到的库函数全部复制进可执行文件,一个"Hello World"静态编译可达 800KB;动态链接只存一个 GOT 表和依赖列表,同样程序仅 16KB。而且多个程序运行时还能共享内存中的同一份 libc.so,磁盘和内存都更省。

为何常见:把“一个文件”误等同于“体积小”,忽略了共享库的复用收益,也忽略了静态库把代码复制进文件的实际开销。

误区strip 会删除符号表,所以 strip 之后的 ELF 文件里一个符号都没有,程序自然无法运行。

正解可执行文件运行需要的是动态符号表 .dynsym,链接触发器默认会保留它;strip 删除的是静态符号表 .symtab 和调试段。对于目标文件(.o)而言,如果强行删除 .symtab,它就无法再作为链接输入。工程上常用 strip --strip-debug 只删调试信息、保留 .symtab,兼顾体积与可重链接性。

为何常见:把“可执行文件”和“目标文件”混为一谈,忽略了二者对符号表的依赖方式不同,也忽略了 strip 工具本身有多档参数。

误区static 变量不是全局符号吗?反正变量名全局唯一,我另写一个文件用 extern 声明,应该能访问吧。

正解static 修饰的全局变量和函数在符号表中确实是 LOCAL 绑定,链接器根本不会把它暴露给其他编译单元。即使你用 extern 声明,链接阶段解析也会失败——因为局部符号不在全局查找范围内。GLOBAL 只授予非 static 的定义,这是 C 语言作用域与链接器可见性两个层面的共同约束。

为何常见:人们容易把 C 语言的作用域规则与链接器的可见性规则混为一谈,以为“我能看到”就等于“链接器能看到”。

面试题

Q1. 符号表最主要的作用是什么?

单选题 · easy

  • A.存储程序运行时数据
  • B.建立符号名到地址的映射← 正确答案
  • C.优化指令执行速度
  • D.管理物理内存分配

答案:B。符号表的核心是建立名字到地址的映射,就像宴会的菜品交接单把菜名对应到桌号。A 是数据段的职责,C 是编译器优化,D 是操作系统的职责,都与符号表无关。

Q2. ELF 文件中哪个节区专门存放符号表?

单选题 · easy

  • A..text 节区
  • B..data 节区
  • C..symtab 节区← 正确答案
  • D..bss 节区

答案:C。ELF 各节区分工明确:.text 放代码、.data 放已初始化数据、.bss 放未初始化数据,只有 .symtab 专门存放符号表条目。就像总册子里专门有一页记录菜名清单,其他页各司其职。

Q3. 重定位公式 R = S + A - P 中,P 代表什么?

单选题 · mid

  • A.符号的最终地址
  • B.重定位的加数
  • C.重定位点的位置← 正确答案
  • D.程序的入口地址

答案:C。S 是目标符号地址,A 是加数,P 是重定位点——也就是当前被修改指令所在的位置。三者缺一不可:用目标地址减去当前地址,才能算出 PC 相对跳转需要的偏移,相当于“从这桌到那桌的直线距离”。

Q4. 全局符号与局部符号的主要区别是什么?

单选题 · easy

  • A.名字长度不同
  • B.可见范围不同← 正确答案
  • C.存储位置不同
  • D.符号大小不同

答案:B。全局符号对整个程序可见,局部符号只在本编译单元内有效,区别由符号表里的绑定属性(GLOBAL/LOCAL)决定。这与名字长短、存放位置、占用空间大小都无关,纯粹是“能不能被别人看见”的可见性差异。

Q5. 以下哪些工具可以查看 ELF 文件的符号表?

多选题 · mid

  • A.readelf← 正确答案
  • B.objdump← 正确答案
  • C.nm← 正确答案
  • D.gcc

答案:ABC。readelf -s、objdump -t、nm 都能查看符号表,只是输出格式和侧重不同:readelf 最详细,objdump 偏重反汇编,nm 最简洁。gcc 是编译器,负责把源码翻译成目标文件,本身不提供查看符号表的命令。

Q6. R_X86_64_PC32 重定位的结果表示什么?

单选题 · mid

  • A.绝对目标地址
  • B.相对指令的偏移量← 正确答案
  • C.文件中的偏移
  • D.栈区的偏移量

答案:B。R_X86_64_PC32 的 S+A-P 算出来是相对偏移:目标地址减当前位置。x86 的 call 指令按下一条指令地址加偏移量寻址,所以结果是相对下一条指令的偏移,而不是绝对地址或文件偏移。就像告诉客人“往前走三步右转”,而不是“商场三楼”。

Q7. 链接时报 undefined reference 最常见的原因是什么?

单选题 · mid

  • A.缺少符号定义库← 正确答案
  • B.编译器版本过旧
  • C.符号名字符过长
  • D.系统内存严重不足

答案:A。最常见原因是链接命令没有包含符号定义所在的目标文件或库,或者库的排列顺序不对。编译器版本、符号名长度、内存不足都与链接错误无关。这就像菜单上写了菜名,但后厨根本没有这道菜——只有把做菜的厨房请进来才能解决。

Q8. 关于 strip 命令的说法,哪些是正确的?

多选题 · hard

  • A.删除静态符号表← 正确答案
  • B.删除调试信息← 正确答案
  • C.导致程序无法运行
  • D.减小文件体积← 正确答案

答案:ABD。strip 会删除静态符号表 .symtab 和调试信息,从而减小文件体积;但可执行文件仍能正常运行,因为运行依赖的动态符号表 .dynsym 会被保留。C 说“导致程序无法运行”是错误认知,它混淆了静态符号表与动态符号表的不同命运。

Q9. 强符号与弱符号同名冲突时,链接器选择哪个?

单选题 · hard

  • A.弱符号
  • B.强符号← 正确答案
  • C.全部保留
  • D.随机选择

答案:B。普通定义的函数和全局变量是强符号,__attribute__((weak)) 标记的是弱符号。碰撞时强符号获胜、弱符号被丢弃;两个强符号同名则直接报错。这就像两家餐厅都叫“佛跳墙”,总策划会优先选中那家评分更高的,而两家都自称“招牌”时只能二选一或取消一道。

Q10. 动态链接中 GOT(全局偏移表)的作用包括哪些?

多选题 · extreme

  • A.存放符号真实地址← 正确答案
  • B.实现位置无关代码← 正确答案
  • C.存放源代码文本
  • D.支持共享库懒绑定← 正确答案

答案:ABD。GOT 存放符号在运行时的真实地址,代码通过查 GOT 间接访问符号,从而实现位置无关代码;配合 PLT,第一次调用时完成懒绑定解析,后续直接命中地址。GOT 不存放源代码文本,那是 .text 节区的职责。共享厨房里 GOT 就是那张“取菜位置表”,谁要用都去查一眼。