ELF 链接加载与 System V AMD64 ABI

共 79 题
📑 题目列表 79 题
#
★★★

1. ELF header(ELF64_Ehdr)中 e_type、e_machine、e_entry、e_phoff、e_shoff 等字段的含义

ELF header(ELF64_Ehdr)中 e_type、e_machine、e_entry、e_phoff、e_shoff 这几个字段分别表示什么?它们在文件定位与程序入口解析中各自承担什么作用?

  • ELF header 各字段的字节偏移与语义
  • e_type 对文件用途的分类作用
  • e_entry 与节/段表偏移对加载与解析的关键作用

ELF64_Ehdr 是 ELF 文件最开头的 64 字节结构,其中 e_type 表示文件类型(ET_REL、ET_EXEC、ET_DYN、ET_CORE),e_machine 指定目标机器架构(如 EM_X86_64、EM_AARCH64),e_entry 是程序入口的虚拟地址,e_phoff 指向程序头表(program header table)在文件中的偏移,e_shoff 指向节头表(section header table)的偏移。加载器通过 e_type 与 e_entry 判断是否可执行并确定入口,通过 e_phoff/e_shoff 定位段表与节表以完成映射与符号解析。

本题考查对 ELF 最外层索引结构的理解。e_phoff/e_shoff 是解析 ELF 的"根指针",而 e_entry 是程序执行的起点,三者共同构成系统加载 ELF 的入口路径。作答时需把字段语义与"加载/解析"两个环节关联起来,体现对 ELF 布局全局性的把握。

#
★★★

2. ELF(Executable and Linkable Format)的 relocatable(.o)、executable、shared object、core dump 四类文件

ELF 的四种类型 relocatable、executable、shared object、core dump 分别是什么?各类型在链接与运行阶段扮演什么角色?

  • 四种 ELF 类型的语义与适用场景
  • relocatable 与 executable 的目标差异
  • shared object 与 core dump 的用途

relocatable 文件(.o)是编译产物,含未解析符号与重定位信息,供链接器合并;executable 是已重定位的可执行文件,含完整加载信息;shared object(.so)是共享库,可被多个进程共享且支持动态重定位;core dump 是进程崩溃时内存与寄存器快照,用于调试。四种类型分别对应编译链接、运行、共享复用与故障诊断四个环节。

本题考察 ELF 类型与工具链生命周期的对应关系。作答时应按"产生阶段与用途"划分四类,并强调 relocatable 是链接输入、executable/shared object 是加载输出、core dump 是运行期产物,形成完整链条。

#
★★★

3. Program header(Elf64_Phdr)的 PT_LOAD、PT_INTERP、PT_DYNAMIC 段类型

Program header(Elf64_Phdr)中 PT_LOAD、PT_INTERP、PT_DYNAMIC 三种段类型分别代表什么?加载器如何利用它们完成内存映射与动态链接?

  • PT_LOAD 的映射含义与可执行段请求
  • PT_INTERP 指向解释器路径
  • PT_DYNAMIC 携带动态链接信息

PT_LOAD 描述需要映射到内存的段,包含可读、可写、可执行权限组合,加载器据其以页为单位建立映射;PT_INTERP 指定动态链接器(如 ld-linux-x86-64.so.2)的路径;PT_DYNAMIC 指向动态段(.dynamic),其中含 DT_NEEDED、DT_SYMTAB、DT_RELA 等动态链接所需信息。加载器先按 PT_LOAD 映射,再根据 PT_INTERP 加载解释器,最后经 PT_DYNAMIC 完成符号解析与重定位。

本题考察程序头表在加载过程中的核心作用。关键是把 PT_LOAD(映射)、PT_INTERP(解释器)、PT_DYNAMIC(动态信息)串联成一条完整的动态加载链,体现对"谁负责加载、加载后干什么"的理解。

#
★★★

4. Section header(Elf64_Shdr)的 .text、.data、.bss、.rodata 段

Section header(Elf64_Shdr)描述的 .text、.data、.bss、.rodata 段各存放什么内容?它们的权限与可执行文件布局有何差异?

  • .text 存放机器指令且可执行
  • .data/.rodata 存放可写/只读数据
  • .bss 存放未初始化数据且不占文件空间

.text 存放可执行机器指令,权限为可读可执行;.data 存放已初始化的全局/静态变量,可读写;.rodata 存放只读常量(字符串、const 数据),权限只读;.bss 存放未初始化的全局/静态变量,编译期仅记录大小而不在文件中占用空间,加载器将其清零。分段便于按权限映射与安全隔离,也便于链接器合并同类节。

本题考察 ELF 节(section)与加载段(segment)的区别。section 是链接期视角的语义单元,segment 是运行期映射单元。作答需强调权限差异与 .bss 不占空间的特性,并理解节与段由链接脚本(如 PHDRS 命令)映射的关系。

#
★★★

5. ld.so 的动态链接流程,从 DT_NEEDED 解析、符号查找到重定位的完整链路

ld.so 动态链接的完整流程是什么?DT_NEEDED 解析、符号查找与重定位各环节如何衔接?

  • DT_NEEDED 依赖的加载顺序
  • 符号查找的范围与 GOT 更新
  • 重定位的类型与执行时机

ld.so 先解析主程序的 DT_NEEDED 所列依赖库,按拓扑序递归加载每个库及其依赖;随后进入符号查找阶段,遍历各库的动态符号表(.dynsym)与 hash 表(.hash/.gnu.hash)查找符号;最后执行重定位,将绑定结果写入 GOT/PLT 或按 RELA 记录回填地址。动态链接器同时负责初始化(.init/.init_array)与构造器调用,并处理符号版本与 TSD 等细节。

本题考察动态链接宏观流程。核心是"依赖解析(加载)→ 符号查找(绑定)→ 重定位(写入)"三阶段顺序,并说明符号查找遵循全局符号表与依赖顺序,重定位依据 PT_DYNAMIC 中的 DT_RELA 等表项执行。

#
★★★

6. ELF 调试信息(DWARF)的 .debug_info、.debug_line、.debug_frame

DWARF 调试信息中的 .debug_info、.debug_line、.debug_frame 三个节分别存储什么?调试器如何利用它们实现断点与调用栈回溯?

  • .debug_info 的类型与变量描述
  • .debug_line 的行号映射
  • .debug_frame 的调用帧信息

.debug_info 以 DIE(Debugging Information Entry)描述类型、变量、函数等高层结构;.debug_line 记录源代码行号与地址的映射,用于断点与步进;.debug_frame 依据 DWARF CFI(Call Frame Information)描述栈帧恢复规则,用于 unwind 与 backtrace。调试器结合三者在运行时定位源码位置、恢复帧链并展开栈。

本题考察 DWARF 的层次划分。.debug_info 是"结构描述",.debug_line 是"行号映射",.debug_frame 是"帧恢复",三者分别回答"对象是什么、源码在哪、栈如何展开"。作答时按这三个维度组织即可。

#
★★★

7. readelf、objdump、nm、ldd、file 工具的实战

readelf、objdump、nm、ldd、file 这些工具各自最擅长分析 ELF 的哪个方面?在实战中如何组合使用?

  • readelf 解析 ELF 结构
  • objdump 反汇编与节内容
  • nm/ldd/file 的符号与依赖分析

readelf 以标准格式输出 ELF header、程序头、节头、动态段等结构信息;objdump 用于反汇编(-d)与查看节内容(-s);nm 列出符号表(-D 查看动态符号);ldd 显示动态库依赖;file 快速识别文件类型与架构。实战中先用 file 确认类型,再用 readelf 看段与依赖,用 objdump 反汇编关键函数,用 nm 定位符号,最后用 ldd 排查缺失库。

本题考察工具定位。按"结构、反汇编、符号、依赖、识别"归类五工具,并给出典型组合流程,体现工程排查思路而非单纯罗列命令。

#
★★★

8. 位置无关代码(PIC)与 PIE(Position Independent Executable)

位置无关代码(PIC)与 PIE(Position Independent Executable)有什么区别?它们在共享库与可执行文件中的定位机制各是什么?

  • PIC 与 PIE 的适用范围差异
  • GOT/PLT 与相对寻址
  • 安全与兼容性权衡

PIC 是生成位置无关代码的编译选项(-fPIC),用于共享库,使代码可在任意地址加载,通过 GOT/PC 相对寻址解引用全局数据;PIE 是位置无关的可执行文件(-fPIE -pie),将可执行文件也变成可重定位的,配合 ASLR 提升安全性。二者都依赖 GOT/PLT 与 PC 相对偏移,但 PIE 面向可执行文件,PIC 面向共享库,且 PIE 不能作为共享库使用。

本题考察 PIC 与 PIE 的本质区别。核心是"面向对象不同":PIC 服务共享库,PIE 服务可执行文件;共同点是都靠 GOT 与相对寻址实现位置无关。作答时强调安全收益(ASLR)与兼容性代价。

#
★★★

9. ELF TLS(Thread Local Storage)的 .tdata 与 .tbss 段

ELF 的 TLS(Thread Local Storage)中 .tdata 与 .tbss 段各存放什么?线程局部变量如何通过 TLS 模型实现隔离?

  • .tdata 存已初始化 TLS 变量
  • .tbss 存未初始化 TLS 变量
  • 线程控制块与 TLS 寻址

.tdata 存放已初始化的线程局部变量,在文件中有初始值;.tbss 存放未初始化的线程局部变量,不占文件空间。每个线程在启动时通过 TLS 控制块(TP,如 x86-64 的 fs 段基址)为 TLS 分配独立副本,并以 TLS 模型(initial-exec、local-exec、global-dynamic、local-dynamic)进行寻址。这样每个线程访问同一变量名时得到各自独立的存储实例。

本题考察 TLS 的内存布局。核心是 .tdata/.tbss 对应"初始化/未初始化"两类线程局部数据,以及线程通过 TP 寄存器(fs/gs)寻址实现隔离。作答时说明不同 TLS 模型在访问速度与灵活性上的取舍。

#
★★★

10. ELF note 段(.note.gnu.property、.note.gnu.build-id)的元数据

ELF 的 note 段(.note.gnu.property、.note.gnu.build-id)保存哪些元数据?它们在工具链与安全调试中有什么作用?

  • note 段的格式与组织
  • .note.gnu.build-id 的唯一标识作用
  • .note.gnu.property 的 ABI 特征

note 段以"名字+类型+描述"三元组存储元数据。.note.gnu.build-id 为每个二进制生成唯一哈希标识,供 debuginfod、gdb 与崩溃报告定位匹配对应调试文件;.note.gnu.property 记录程序依赖的处理器特性(如 x86 的 CET、IBT、SHSTK 等安全特性),内核与加载器据此验证或启用相应能力。二者都是不参与运行的元数据,但服务于构建标识与安全特性声明。

本题考察 note 段的两类典型用途。build-id 是"身份标识",用于调试信息匹配;property 是"特性声明",用于安全与 ABI 协商。作答时把 note 段格式与两类应用区分开即可。

#
★★★

11. ELF 版本脚本(version script)的符号可见性控制

ELF 版本脚本(version script)如何控制符号可见性?它在共享库 ABI 管理中有什么作用?

  • version script 的 global/local 规则
  • 符号版本化与导出控制
  • 与 -fvisibility 的配合

version script 通过 { global: ...; local: ...; } 块控制符号的导出与隐藏,local 符号不进入动态符号表,从而不会泄漏给外部链接。它还支持符号版本化(如 GLIBC_2.2.5),为导出符号绑定版本,便于同一库内多版本共存与向后兼容。它与 -fvisibility=hidden 配合,可精细控制共享库的公开 ABI 面。

本题考察共享库 ABI 控制。核心是 version script 的"导出白名单/黑名单"作用与符号版本化机制,既减少符号泄漏提高封装性,又通过版本标签实现 ABI 演进。作答时强调其对 ABI 稳定性的贡献。

#
★★★

12. binutils、lld、mold 三种链接器的性能差异

binutils(ld)、lld、mold 三种链接器在性能上有什么差异?它们各自的实现策略与适用场景是什么?

  • 三种链接器的并行化与布局策略
  • 链接速度与内存开销的权衡
  • 与编译器的兼容性

GNU ld 是传统链接器,串行处理、内存占用大、速度慢但兼容性最广;lld 是 LLVM 生态的链接器,采用多线程并行、更高效的数据结构,速度显著优于 GNU ld;mold 是"现代链接器",通过并行哈希、整体布局与高效 I/O 实现最快速度,常比 lld 快数倍。三者在 ELF 输出上语义兼容,工程上按构建规模与速度要求选择。

本题考察链接器性能背后的实现差异。核心是"并行化程度、数据结构、I/O 策略"决定速度差异,GNU ld 为兼容最大化放弃极致速度,lld/mold 以现代并行设计换取吞吐。作答时对比三者定位即可。

#
★★★

13. x86-64 与 AArch64 的 ABI 差异对比

x86-64(System V AMD64)与 AArch64(AAPCS)的 ABI 在传参、寄存器、栈布局上有哪些主要差异?

  • 整数/浮点参数寄存器差异
  • 栈对齐与帧布局
  • 返回与调用约定差异

x86-64 SysV 用 rdi、rsi、rdx、rcx、r8、r9 传整数参数,XMM0-7 传浮点,返回值在 rax/rdx;AArch64(AAPCS)用 x0-x7 传前 8 个整数参数,d0-d7(v0-v7)传浮点,返回值在 x0。栈对齐方面 x86-64 要求 16 字节对齐,AArch64 同样要求 16 字节对齐但通过 sp 与 x29 帧指针管理。相比 x86-64,AArch64 寄存器更多、参数寄存器映射更统一,栈帧也常以 x29 为基准。

本题考察跨架构 ABI 对比。核心是参数寄存器数量与命名、浮点寄存器、返回值寄存器的差异,以及栈对齐规则。作答时以"寄存器组、传参、返回、对齐"四方面对照即可。

#
★★★

14. 栈对齐(16 字节对齐)的 ABI 强制与崩溃边界

为什么 ABI 强制 16 字节栈对齐?违反对齐可能导致什么崩溃边界?

  • 16 字节对齐的 ABI 要求
  • 对齐对 SSE 指令的影响
  • 违反对齐的崩溃特征

System V ABI 要求函数入口时栈指针为 16 字节对齐,因为 SSE 指令(如 movaps)要求操作数为 16 字节对齐,若栈未对齐则会触发通用保护异常(GP fault)。编译器在调用时通过叶函数与调用者调整 rsp 保证对齐,尾调用与内联汇编若破坏对齐,会在执行 SIMD 指令或调用库函数时崩溃。浮点与结构体传参也依赖此对齐。

本题考察 ABI 对齐的硬件原因。核心是"SIMD 指令需对齐 → 栈必须保持 16 字节对齐 → 编译器与调用约定共同维护"。作答时说明对齐机制的实现与在汇编/尾调用中被破坏的后果。

#
★★★

15. 红区(red zone,128 字节)的工程语义

x86-64 System V ABI 中的红区(red zone,128 字节)是什么?它在叶函数优化中有什么工程语义?

  • 红区的定义与位置
  • 叶函数免栈优化
  • 与信号处理器的交互

红区是 rsp 下方 128 字节的保留区域,当函数是叶函数(不再调用其他函数)时,可放心使用该区域而不必调整 rsp,从而省去栈帧建立与销毁开销。由于信号处理器可能异步使用此区域,编译器必须保证在信号处理期间不破坏红区数据,因此非叶函数、含信号处理或调用的代码不能依赖红区。此优化对高频短函数有显著收益。

本题考察红区的使用条件。核心是"叶函数无调用 → 可安全用 rsp 下方 128 字节 → 免栈帧开销"。作答时强调红区的适用前提与信号处理约束,体现对 ABI 细节的理解。

#
★★★

16. 结构体返回的"内存参数"(sret)约定与寄存器返回的分界

结构体返回时,什么情况下使用"内存参数"(sret)约定,什么情况下用寄存器返回?分界依据是什么?

  • sret 隐藏指针参数
  • 大小与可对齐性决定返回方式
  • 编译器生成与调用方约定

当结构体大小超过两个 8 字节寄存器(16 字节)或不可按整数/浮点寄存器对齐返回时,编译器采用 sret:调用方在栈上分配空间并传入隐藏指针作为第一个参数,被调函数把结果写入该空间,返回时该指针由调用方持有。较小且可对齐的结构体(如 ≤16 字节)则用 rax/rdx 或 XMM 寄存器返回。sret 影响传参顺序与寄存器的分配,是 ABI 的重要约定。

本题考察结构体返回的 ABI 分界。核心是"大小/对齐决定走寄存器还是内存",sret 本质是隐藏的出参指针。作答时说明 16 字节阈值与寄存器返回的适用条件。

#
★★★

17. System V i386 ABI(32 位)的 cdecl ABI 传参规则

System V i386(32 位)的 cdecl ABI 的传参规则是什么?它的参数寄存器与清栈责任如何?

  • 参数全部经栈传递
  • 调用者负责清栈
  • 返回值在 eax/edx

cdecl ABI 中所有参数从右到左压入栈传递,不使用参数寄存器;调用结束后由调用者清除栈上参数(清栈责任在调用者);返回值存放在 eax(或 edx+eax 用于 64 位)。由于清栈责任在调用者,cdecl 支持可变参数。与 x86-64 大量使用寄存器传参相比,cdecl 依赖栈导致更多内存访问,但规则简单、可移植性好。

本题考察 32 位 cdecl 约定。核心是"栈传参、调用者清栈、eax 返回"三点,并联系可变参数支持。作答时对比 x86-64 的寄存器传参以突出差异。

#
★★★

18. x86-64 栈帧的 rbp 帧指针省略(-fomit-frame-pointer)

-fomit-frame-pointer 省略 rbp 帧指针有什么好处?代价是什么?

  • 省略 rbp 后的寄存器收益
  • 帧定位改为相对 rsp
  • 调试与回溯的代价

默认情况下函数用 rbp 保存栈帧基址,形成 rbp 链以便调试回溯。开启 -fomit-frame-pointer 后,编译器不再保存 rbp,而用 rsp 相对偏移访问局部变量,从而把 rbp 释放为可用的通用寄存器,减少指令与栈操作,提升性能。代价是调用链回溯(backtrace)失去了 rbp 链,需依赖 DWARF CFI 恢复帧,且某些调试场景信息减少。在性能敏感代码中常开启此选项。

本题考察帧指针省略的取舍。核心是"省一个寄存器、少几条栈指令"换性能,代价是依赖 CFI 回溯。作答时说明缺省 rbp 与 rsp 相对寻址的关系及调试影响。

#
★★★

19. Itanium C++ ABI(itanium-cxx-abi)的 name mangling 规则

Itanium C++ ABI 的 name mangling 规则是什么?它如何编码命名空间、类与模板?

  • mangling 的编码前缀
  • 命名空间与类的编码
  • 模板类型的编码

Itanium C++ ABI 定义了 C++ 符号的 mangling 规则,以 _Z 开头,后跟编码的名称与类型。函数名以 _Z 开头,嵌套作用域(命名空间/类)用 N...E 包裹、各名称按长度前缀编码(如 _ZN3Foo3barEv 表示 Foo::bar());类成员并无特殊 C 前缀(C1/C2/C3 是构造函数变体,D1/D2 是析构函数变体);模板参数用 I...E 包裹,内建类型用单字母编码(int 为 i、long 为 l)。mangling 使函数重载与模板实例化在链接期产生唯一符号,也可用 c++filtabi::__cxa_demangle 还原。

本题考察 C++ 符号编码规则。核心是 _Z 前缀与作用域/类型编码层次,理解 mangling 是为了支持重载与模板的符号唯一性。作答时给出典型编码示例即可。

#
★★★

20. x86-64 SysV 的 EH frame 与 DWARF CFI 异常处理

x86-64 SysV 上 C++ 异常处理如何利用 .eh_frame 与 DWARF CFI 完成栈展开?异常展开与普通 unwind 有何关系?

  • .eh_frame 的 CFI 记录
  • 异常展开的栈帧回退
  • 与 backtrace 的复用

编译器为每个函数生成 .eh_frame 节,内含 DWARF 格式的 CFI(Call Frame Information)规则,描述如何在任意指令处恢复寄存器与栈帧。抛异常时,运行时(libgcc)沿 CFI 逐帧回退,找到匹配的 catch 处理器并调用析构函数。该 CFI 数据同时被 backtrace、libunwind 等复用,实现统一的栈展开。异常展开与普通 unwind 共用同一套 CFI,只是异常路径还需执行清理(cleanup)代码。

本题考察异常处理的展开机制。核心是 .eh_frame 中的 CFI 提供"逐帧恢复"能力,异常展开复用该机制并执行析构。作答时说明 CFI 与 backtrace 的关系。

#
★★★

21. COFF(Common Object File Format)的 section table 与 symbol table

COFF(Common Object File Format)的 section table 与 symbol table 如何组织?它们与 ELF 的节/符号表有何异同?

  • COFF section table 的字段
  • symbol table 与重定位
  • 与 ELF 的差异

COFF 文件的 section table 逐项描述各节(名称、大小、虚拟地址、指针、重定位/行号偏移、特性),symbol table 记录符号名、值、节索引与类型,符号名称以字符串表或短名存储。重定位表与 section 关联,记录每个需修正的位置。相比 ELF 的分区节头与独立节名,COFF 的节表与符号表更紧凑,且重定位直接挂在节上。Windows PE 是其扩展。

本题考察 COFF 的布局。核心是 section table 描述节、symbol table 记录符号、重定位按节组织。作答时与 ELF 对照,突出 COFF 的紧凑结构与符号隶属关系。

#
★★★

22. Mach-O 与 ELF 在段(segment)vs 节(section)层次上的差异

Mach-O 与 ELF 在段(segment)与节(section)的层次组织上有什么差异?为什么采用不同层次?

  • Mach-O 的 segment->section 两层
  • ELF 的 program/section 两层
  • 层次差异的工程意义

Mach-O 采用"segment 包含 section"的两层结构:LC_SEGMENT_64 命令定义 segment(如 __TEXT、__DATA、__LINKEDIT),segment 内再细分多个 section(如 __text、__data)。ELF 则用 program header(segment)与 section header(section)两套独立表,分别服务运行映射与链接语义。Mach-O 的层次更明确地把"运行权限域"与"语义区块"绑定,而 ELF 允许节与段灵活映射。二者都需区分"加载用"与"链接用"两套视图。

本题考察两种格式的层次设计。核心是 Mach-O 的 segment 内嵌 section 是强绑定,ELF 的 program/section 是弱耦合。作答时强调"权限域与语义单元"的关系差异。

#
★★★

23. Mach-O 中 __objc_methlist、__objc_classlist 等 Objective-C 元数据

Mach-O 的 __objc_methlist、__objc_classlist 等 Objective-C 元数据段保存什么?Objective-C 运行时如何利用它们实现类与方法查找?

  • __objc_data 与类元数据
  • __objc_methlist 的方法列表
  • 运行时注册与动态派发

Mach-O 的 __DATA 段含 __objc_classlist、__objc_methlist、__objc_selrefs 等节。__objc_classlist 列出所有类对象,__objc_methlist 保存类的方法列表,__objc_selrefs 记录选择器引用。dyld 加载时调用 ObjC 运行时注册这些类与协议,运行时据此在消息发送时按选择器查找方法实现,实现动态派发与 NSObject 消息机制。这些元数据使语言具备运行时反射能力。

本题考察 ObjC 运行时元数据。核心是"类表、方法表、选择器表"三组元数据支撑动态派发。作答时说明元数据段与 dyld 初始化、运行时查找的关系。

#
★★★

24. Mach-O 的 __TEXT、__DATA 段的"两段式"模型

Mach-O 的 __TEXT、__DATA 段"两段式"模型有什么意义?为什么把代码与数据分离?

  • __TEXT 只读可执行
  • __DATA 可读写
  • 权限隔离与共享

Mach-O 把可执行代码与常量放入 __TEXT 段(只读、可执行),把可变数据放入 __DATA 段(可读写)。两段式模型使代码段可被多个进程共享、可设只读权限防止篡改,并支持 W^X 安全策略(不可同时可写可执行)。同时 __TEXT 与 __DATA 的分离便于 DYLD 分别映射与签名验证,也符合现代操作系统的 NX 与 ASLR 要求。

本题考察 Mach-O 的权限分段。核心是"代码只读可执行、数据可读写"的分离带来共享与安全收益。作答时强调 W^X、共享与安全签名的意义。

#
★★

25. Mach-O 的 load commands,包括 LC_SEGMENT_64、LC_DYLD_INFO、LC_SYMTAB

Mach-O 的 LC_SEGMENT_64、LC_DYLD_INFO、LC_SYMTAB 等 load commands 分别描述什么?加载器如何解析它们?

  • LC_SEGMENT_64 的段映射
  • LC_DYLD_INFO 的动态链接信息
  • LC_SYMTAB 的符号表

Mach-O 的 load commands 是 header 后的命令列表,驱动加载器与 dyld 的行为。LC_SEGMENT_64 描述段的映射位置、大小与权限;LC_DYLD_INFO(或 LC_DYLD_INFO_ONLY)提供重定位、绑定、lazy 绑定等动态链接信息;LC_SYMTAB 指向符号表与字符串表。加载器依次读取这些命令,建立内存映射、解析依赖并完成符号绑定。

本题考察 Mach-O 加载命令体系。核心是"命令即加载说明",不同 LC_* 分别覆盖映射、动态链接、符号三方面。作答时把命令与加载环节对应起来。

#
★★

26. Mach-O 的 magic number,即 MH_MAGIC_64 与 MH_CIGAM_64(小端序)的区别

Mach-O 的 magic number MH_MAGIC_64 与 MH_CIGAM_64 有什么区别?字节序如何影响它们?

  • MH_MAGIC_64 与 MH_CIGAM_64 的值
  • 字节序与魔数交换
  • fat binary 的识别

MH_MAGIC_64 为 0xfeedfacf,是文件字节序与本机字节序一致(小端机器上读小端文件)时读取到的值;MH_CIGAM_64 为 0xcffaedfe,是字节交换后的值(CIGAM 即 MAGIC 字节序倒置)。系统通过魔数判断文件类型与字节序:若读到 MH_MAGIC_64 说明文件按本机字节序存储,若读到 MH_CIGAM_64 则需交换字节序读取。Universal/Fat binary 使用不同的魔数(0xcafebabe)标识多架构容器。

本题考察魔数与字节序识别。核心是 MAGIC/CIGAM 成对表示两种字节序,读取时据此决定是否交换。作答时说明魔数在文件识别与架构选择中的作用。

#
★★

27. dyld 的 Mach-O 动态链接流程与 dyld_shared_cache

dyld 如何加载 Mach-O 并完成动态链接?dyld_shared_cache 起什么作用?

  • dyld 的依赖解析与绑定
  • dyld_shared_cache 的共享库合并
  • 启动路径优化

dyld 读取主程序 load commands,按依赖递归加载共享库,进行 rebase、bind、lazy bind 等重定位,最后调用库初始化器与主程序入口。dyld_shared_cache 把系统共享库预合并为单一映射,减少页表与符号查找开销,加快启动并降低内存占用。dyld 还支持 closure 缓存与预绑定,把链接结果缓存以加速后续启动。

本题考察 dyld 的加载与优化。核心是 dyld 完成依赖解析与绑定,dyld_shared_cache 是其性能优化手段。作答时说明 cache 如何减少小数文件映射的开销。

#
★★

28. COFF 与 ELF 的 ABI 兼容性挑战

COFF 与 ELF 的 ABI 兼容性挑战主要来自哪些方面?跨平台二进制为何难以直接复用?

  • 节/段命名与编号差异
  • 重定位与符号约定差异
  • 调用约定与调试格式差异

COFF(Windows)与 ELF(Unix)的 ABI 兼容性挑战来自多层面:节与段命名及组织方式不同,重定位类型与语义不同(如 Windows 的绝对/相对重定位与 ELF 的 PC-relative 族),符号约定不同(如 Windows 的 stdcall 修饰与 ELF 的 Itanium mangling),调用约定与寄存器规则不同,调试格式也各异(PDB vs DWARF)。因此同一 C 源码在不同平台需分别编译,无法直接复用二进制。

本题考察跨平台 ABI 差异。核心是"节、重定位、符号、调用约定、调试"五方面差异共同构成兼容性障碍。作答时说明为何不能直接复用二进制。

#
★★

29. Mach-O 的 LC_CODE_SIGNATURE 签名与 LC_DYLD_INFO_ONLY 重定位

Mach-O 的 LC_CODE_SIGNATURE 与 LC_DYLD_INFO_ONLY 分别起什么作用?它们在安全与重定位上有何意义?

  • LC_CODE_SIGNATURE 的代码签名
  • LC_DYLD_INFO_ONLY 的重定位信息
  • 安全验证与二进制约简

LC_CODE_SIGNATURE 指向代码签名数据块,用于验证插入的运行时代码与签名是否一致,防止篡改;LC_DYLD_INFO_ONLY 携带 rebase、bind 等重定位信息,专注动态链接所需数据。二者配合:签名保证二进制完整性,DYLD_INFO_ONLY 提供最优重定位数据。DYLD_INFO_ONLY 相比完整 DYLD_INFO 节省空间,因为只保留链接必需部分。

本题考察 Mach-O 的安全与重定位命令。核心是"签名验证完整性与重定位信息支撑动态绑定"的配合。作答时说明各自作用与空间优化。

#
★★

30. --as-needed、--no-undefined、-z now 的链接器标志语义

链接器标志 --as-needed、--no-undefined、-z now 分别控制什么行为?它们各自解决什么问题?

  • --as-needed 的按需链接
  • --no-undefined 的未定义符号检查
  • -z now 的立即绑定

--as-needed 只把实际用到的库写入 DT_NEEDED,减少不必要的依赖与启动开销;--no-undefined 要求链接时所有符号必须已定义,否则报错,防止运行时才暴露出缺失符号;-z now(BIND_NOW)把动态库的延迟绑定改为立即绑定,禁用 lazy binding,使 GOT 在加载时全部解析,提升安全性(配合 RELRO 使 GOT 只读)。三者分别针对依赖精简、符号完整性与安全绑定。

本题考察链接器标志的语义。核心是把三个标志各自对应"依赖精简、符号检查、安全绑定"目标。作答时说明各自解决的问题与工程收益。

#
★★

31. 动态加载 dlopen/dlsym/dlclose 的运行时链接

dlopen、dlsym、dlclose 如何实现运行时动态加载?它们在插件架构中有什么作用?

  • dlopen 的加载与依赖解析
  • dlsym 的符号查找
  • dlclose 的卸载与引用计数

dlopen 在运行时加载共享库并解析其依赖,返回句柄;dlsym 通过句柄在库中查找符号(函数或变量)返回地址;dlclose 卸载库,内核按引用计数决定是否真正释放。这三者构成插件机制:主程序在运行时按需加载模块、调用导出函数、卸载模块,无需在编译期链接。dlsym 的查找可限定在库内(RTLD_LOCAL)或全局(RTLD_GLOBAL)。

本题考察运行时动态链接。核心是 dlopen 加载、dlsym 查找、dlclose 卸载三步构成动态插件机制。作答时说明句柄与标志的作用。

#
★★

32. 动态链接的"延迟绑定"(lazy binding)vs"立即绑定"(now binding)的取舍

动态链接的"延迟绑定"(lazy binding)与"立即绑定"(now binding)各有什么取舍?工程上如何选择?

  • lazy binding 的首次调用开销
  • now binding 的安全与启动开销
  • RELRO 与 GOT 保护

lazy binding 在函数首次调用时才通过 PLT 跳转解析并回填 GOT,启动时只解析被调用的符号,减少启动时间但首次调用有延迟;now binding(BIND_NOW)在加载时一次性解析全部符号,启动稍慢但消除了首次调用延迟,且配合 Full RELRO 使 GOT 只读,防止 GOT 覆写攻击。安全敏感场景优先 now binding,追求启动速度时可用 lazy binding。

本题考察绑定策略的取舍。核心是"启动时间 vs 首个调用延迟与安全"。作答时说明两种策略的优缺点及应用场景。

#
★★

33. GCC 的 -fvisibility=hidden 与符号可见性控制

GCC 的 -fvisibility=hidden 如何控制符号可见性?它如何帮助规范共享库 ABI?

  • 默认隐藏与显式导出
  • attribute((visibility)) 配合
  • 减少符号表与内联优化

-fvisibility=hidden 让共享库中所有符号默认不可见,只有显式标注 attribute((visibility("default")))(或经导出宏)的符号才进入动态符号表。这使共享库只暴露少量公开 API,减少符号表体积、避免符号冲突,并允许编译器对未导出符号做更激进的内联优化。工程上配合 version script 可精确控制 ABI 面。

本题考察符号可见性默认策略。核心是"默认隐藏 + 显式导出"控制 ABI 面。作答时说明可见性对封装、符号表与优化的收益。

#
★★

34. 链接器"幽灵符号"(linker script symbol)与 VDSO

链接器"幽灵符号"(linker script symbol)与 VDSO 是什么?它们在链接与运行中如何交互?

  • linker script 定义的符号
  • VDSO 的虚拟动态共享对象
  • 系统调用加速

VDSO(Virtual Dynamic Shared Object)是内核映射到每个进程地址空间的一小段共享库,暴露如 gettimeofday、clock_gettime 等无需陷入内核即可执行的函数。链接器脚本可为这些符号定义地址或标记,使 VDSO 符号在链接/运行时可被解析。VDSO 通过用户态实现特定系统调用,避免上下文切换开销。链接器脚本符号与 VDSO 的关系体现了"符号可由脚本定义而非实体"的特性。

本题考察 linker script 符号与 VDSO。核心是 VDSO 是内核注入的用户态库,链接器脚本可定义符号。作答时说明 VDSO 加速系统调用的机制与脚本符号的灵活性。

#
★★

35. 动态库的"按需加载"vs"预加载"的工程取舍

动态库的"按需加载"(dlopen 时加载)与"预加载"(LD_PRELOAD/启动时加载)各有什么工程取舍?

  • 按需加载的启动收益
  • 预加载的符号覆盖
  • 依赖与初始化时机

按需加载(dlopen)在真正需要时才加载库,减少启动时间与初始内存占用,适合插件与可选功能;预加载(LD_PRELOAD 或启动时链接)在进程启动前加载库,适合符号覆盖(如拦截 malloc)与保证依赖就绪,但会占用启动时间并可能引入初始化顺序问题。工程上按功能是否必需、是否需要符号覆盖来选择。

本题考察加载时机的取舍。核心是"按需加载省启动、预加载做覆盖"。作答时说明两者在启动时间、功能可用性与符号覆盖上的差异。

#
★★

36. 动态链接的 GOT/PLT 攻击(return-to-PLT)

动态链接的 GOT/PLT 常被用作攻击面,return-to-PLT 攻击是什么?如何防御?

  • GOT/PLT 的跳板机制
  • return-to-PLT 的 ROP 利用
  • RELRO 与 CET 防御

return-to-PLT 是 ROP 攻击的一种:攻击者劫持返回地址使其指向 PLT 中的函数跳板,从而调用已解析的 libc 函数(如 system),绕过对普通指令地址的依赖。GOT 若可写则可能被覆写以劫持函数指针。防御手段包括:Full RELRO(-z now + -z relro)使 GOT 只读、ASLR 随机化地址、启用 CET(IBT/SHSTK)阻止非法跳转与返回、以及栈保护。这些措施大幅提高 return-to-PLT 的利用难度。

本题考察 GOT/PLT 攻击与防御。核心是 return-to-PLT 复用 PLT 跳板调用系统函数,防御靠 RELRO、ASLR、CET。作答时说明攻击原理与多层防御。

#
★★

37. Itanium C++ ABI 的"COMDAT"折叠与"弱符号"边界

Itanium C++ ABI 中的 COMDAT 折叠与弱符号如何工作?它们如何保证模板与 inline 函数的唯一性?

  • COMDAT 节的合并规则
  • 弱符号的重复定义处理
  • 模板实例化的唯一性

COMDAT 节允许多个目标文件包含同名节(如模板实例化、inline 函数),链接器在合并时只保留一个副本,消除重复定义。弱符号(weak symbol)允许多个定义存在,链接器选择其中一个且不报错,用于 inline 函数与默认实现。COMDAT 与弱符号共同保证模板/内联函数在多 TU 编译后仍只有一个强定义,避免符号冲突与重复代码。

本题考察 COMDAT 与弱符号。核心是"合并同义副本 + 容忍多定义"保证唯一性。作答时说明两者在模板与 inline 代码中的协作。

#
★★

38. Java 的 JNI ABI 与 native 库的边界

Java 的 JNI ABI 如何定义 Java 与 native 代码的边界?JNI 函数的命名与调用约定是什么?

  • JNI 函数命名规则
  • JNIEnv 与函数表
  • 引用管理与异常处理

JNI 通过规范命名让 native 函数与 Java 方法对应:Java_包名_类名_方法名,把包名中的点替换为下划线。调用时 JNI 传入 JNIEnv 指针(内含函数表)与 jobject 引用,native 代码通过 JNIEnv 调用功能函数(如 FindClass、GetFieldID)。边界责任包括:通过 JNI 管理 GlobalRef 防止泄漏、检查异常、处理引用类型。JNI ABI 是平台相关的 C 接口,需符合目标平台的 C 调用约定。

本题考察 JNI 边界。核心是命名约定、JNIEnv 函数表与引用管理构成边界的契约。作答时说明 native 与 JVM 如何通过 JNI 交互。

#
★★

39. Rust ABI 的"显式"(extern "C")与"自定义"(Rust ABI 不稳定)差异

Rust 的默认可变 ABI 与显式 extern "C" 有什么差异?为什么 Rust ABI 不稳定?

  • Rust 默认 ABI 的不稳定性
  • extern "C" 的稳定 ABI
  • FFI 边界的选择

Rust 函数默认使用"Rust ABI",其布局与调用约定未固定、可能随版本变化,不适合跨语言互操作;显式标注 extern "C" 则采用稳定的 C 调用约定,保证与 C/C++ 及其他语言在 ABI 层兼容。Rust ABI 不稳定是因为语言仍可演进其内部表示,而 extern "C" 将 ABI 契约固定为平台 C 约定。FFI 边界必须使用 extern "C"(或 extern "system")以保证稳定。

本题考察 Rust ABI 选择。核心是"默认 Rust ABI 不稳定、extern C 稳定"决定 FFI 可行性。作答时说明为什么 FFI 需要显式 ABI。

#
★★

40. Go 的 ABI 在 1.17 之后的 register-based 改造

Go 在 1.17 之后引入 register-based ABI 改造了什么?它对性能与 ABI 稳定性有什么影响?

  • register-based ABI 的传参
  • 栈参数到寄存器参数
  • 性能收益与兼容性

Go 1.17 起引入 register-based ABI,把函数参数与返回值从栈上传送改为通过寄存器传递(amd64 上使用一组通用寄存器),减少栈访问与内存压力,显著提升函数调用性能。该 ABI 是内部实现,不对外暴露,Go 运行时与汇编通过 ABIInternal 兼容。改造后调用开销降低,但内联与逃逸分析等优化也随之调整。

本题考察 Go ABI 演进。核心是"寄存器传参替代栈传参"带来性能收益,且为内部实现不影响外部接口。作答时说明收益与 ABI 的稳定性。

#
★★

41. WASM(WebAssembly)的线性内存与 ABI 模型

WASM(WebAssembly)的线性内存模型与 ABI 有什么关系?其内存分配与函数调用如何组织?

  • 线性内存的单一地址空间
  • 内存分配与导出
  • 函数表与调用约定

WASM 使用线性内存(linear memory)作为唯一的连续地址空间,默认从 0 开始,通过 grow 指令扩展。内存通常由模块导出,宿主(如 JS)或模块内部通过 __heap_base 等符号管理分配。函数调用通过函数索引与导入/导出进行,参数与返回值按 WASM 的确定的类型化调用约定传递。WASM 的 ABI 由规范的验证与类型系统保证,无未定义行为,适合沙箱执行。

本题考察 WASM 内存与 ABI。核心是"线性内存单一地址空间 + 类型化调用约定"。作答时说明内存、导出符号与函数表的组织。

#
★★

42. 静态链接与动态链接在可执行体积、启动速度、内存共享与升级成本上如何取舍?

静态链接与动态链接在可执行体积、启动速度、内存共享与升级成本上如何取舍?

  • 体积与启动速度对比
  • 内存共享(共享库)
  • 升级与兼容性

静态链接把库代码并入可执行文件,体积更大、启动更快(无需解析依赖)、便于分发与离线部署,但无法共享内存且升级需重新编译整个程序;动态链接体积小、多个进程共享同一库代码节省内存、升级仅替换库文件即可,但需运行时解析依赖、启动稍慢,并存在 ABI 兼容与缺失库风险。工程上按部署环境与更新频率选择。

本题考察链接策略取舍。核心是"体积/启动 vs 共享/升级"的权衡。作答时从四个维度对比两种策略的优劣。

#
★★

43. LTO(Link Time Optimization)在链接期做跨模块内联与死代码删除的原理是什么,构建开销从何而来?

LTO(Link Time Optimization)在链接期做跨模块内联与死代码删除的原理是什么?构建开销从何而来?

  • LTO 的 IR 合并
  • 跨模块内联与 DCE
  • 构建时间与内存开销

LTO 在编译期把每个 TU 的中间表示(IR,如 GIMPLE/LLVM IR)写入目标文件,链接期把所有 IR 合并为单一单元,进行跨函数内联、常量传播与死代码删除(DCE),从而消除仅靠单文件无法发现的冗余。构建开销来自 IR 的体积、合并与全局优化阶段消耗的内存与时间,使链接明显变慢。权衡后对性能敏感的程序启用 LTO。

本题考察 LTO 原理与代价。核心是"链接期合并 IR 做全局优化",代价是构建时间与内存。作答时说明优化收益从何而来及开销成因。

#
★★

44. -ffunction-sections 配合 --gc-sections 如何剔除未引用函数,为何需结合 -fdata-sections?

-ffunction-sections 配合 --gc-sections 如何剔除未引用函数?为什么还需要结合 -fdata-sections?

  • -ffunction-sections 的函数级分段
  • --gc-sections 的未引用回收
  • -fdata-sections 的数据级回收

-ffunction-sections 让每个函数放入独立节,--gc-sections 在链接时按引用图回收未引用的节,从而剔除未被调用的函数。若只对函数分段而数据仍聚在一个节,未引用的全局变量无法被单独回收,因此需配合 -fdata-sections 让每个全局变量独立成节,才能一并回收未引用的数据。二者结合实现函数与数据级别的死代码/死数据删除。

本题考察链接期代码回收。核心是"函数/数据分节 + 引用图回收"。作答时说明为何 data-sections 是必要的配套。

#
★★

45. 强符号与弱符号的解析规则是什么,多个目标文件定义同名弱符号时链接器如何选择?

强符号与弱符号的解析规则是什么?多个目标文件定义同名弱符号时链接器如何选择?

  • 强符号的覆盖规则
  • 弱符号的多定义选择
  • 与 common symbol 的关系

一个符号可被强定义或弱定义(attribute((weak)))。链接规则:若一个强定义与多个弱定义同名,选择强定义;若多个弱定义而无强定义,链接器选择其中一个;若多个强定义则报重定义错误。弱符号用于提供默认实现,允许被覆盖。未初始化变量常作 common symbol,规则类似,-fno-common 会改变处理方式。

本题考察符号解析规则。核心是"强优先于弱、弱多选一、强多报错"。作答时说明弱符号默认实现与覆盖的语义。

#
★★

46. 链接器如何确定输出文件中各段的加载地址(LMA)与虚拟地址(VMA),嵌入式场景为何要分离二者?

链接器如何确定输出文件中各段的加载地址(LMA)与虚拟地址(VMA)?嵌入式场景为何要分离二者?

  • VMA 与 LMA 的定义
  • 链接脚本的 AT 指令
  • 嵌入式启动拷贝

VMA 是段运行时所在地址,LMA 是段在文件中加载的地址,二者由链接脚本(linker script)指定。通常 VMA 与 LMA 相同,但嵌入式场景常分离:代码存于 Flash(LMA)而运行于 RAM(VMA),启动代码需把数据从 Flash 拷贝到 RAM(或重定位 .data)。这种分离使只读代码可留在 Flash 执行,而可写数据在 RAM 中,兼顾存储与运行速度。

本题考察链接脚本的地址模型。核心是"VMA 运行地址、LMA 加载地址"及 Flash/RAM 分离的动机。作答时说明嵌入式启动拷贝的必要性。

#
★★

47. 静态链接 glibc 为何会带来 NSS、TLS 等运行期功能的兼容问题?

静态链接 glibc 为何会带来 NSS、TLS 等运行期功能的兼容问题?

  • NSS 的动态模块依赖
  • TLS 的运行时初始化
  • 静态链接的局限

glibc 的 NSS(Name Service Switch)通过 dlopen 动态加载模块(如解析 /etc/nsswitch.conf 中的 hosts、passwd),静态链接时这些模块无法动态加载,导致名字解析等功能异常;TLS 与 locale 等初始化依赖动态加载的子系统与可执行文件交互,静态链接无法完整复制运行时行为。因此 glibc 通常建议动态链接,静态链接会牺牲部分运行期功能。

本题考察静态链接 glibc 的局限。核心是 NSS 的动态模块与 TLS 初始化依赖运行期机制。作答时说明为何静态链接破坏这些功能。

#
★★

48. 链接器如何处理未定义符号与公共符号(common symbol),-fno-common 改变了什么行为?

链接器如何处理未定义符号与公共符号(common symbol)?-fno-common 改变了什么行为?

  • 未定义符号的解析
  • common symbol 的合并
  • -fno-common 的差异

未定义符号(undefined symbol)在链接时需从其他目标文件或库中找到定义,否则报错;common symbol 是未初始化的全局变量,多个 TU 的声明可合并为一个大小的公共符号,链接器取最大者。默认(-fcommon)允许这种合并,-fno-common 则把每个定义视为独立强定义,多个定义会报重定义错误,迫使变量显式定义或初始化,从而发现潜在错误并提升可预测性。

本题考察 common symbol 处理。核心是"common 合并 vs 独立强定义"。作答时说明 -fno-common 如何改变多定义行为。

#
★★

49. Mach-O 的"通用二进制"(Universal/Fat binary)的多架构支持

Mach-O 的"通用二进制"(Universal/Fat binary)如何支持多架构?其结构与优缺点是什么?

  • Fat header 的架构列表
  • 多架构切片选择
  • 体积与兼容性

Universal/Fat binary 用一个 fat header 封装多个架构的 Mach-O 切片(如 x86_64、arm64),每个切片包含架构标识、偏移与大小。加载器按当前 CPU 架构选择对应切片加载。优点是单文件支持多架构、便于分发;缺点是体积随架构数成倍增大。fat binary 也用于同时支持旧版与新版平台。

本题考察 Universal binary。核心是"fat header + 多切片 + 按架构选择"。作答时说明结构、选择机制与体积代价。

#
★★

50. 源码、ABI、API、行为兼容性构成的 ABI 兼容性"层"

源码、ABI、API、行为兼容性这几种"层"的兼容性有什么区别?它们如何影响软件升级?

  • 源码兼容与 ABI 兼容
  • API 兼容与行为兼容
  • 兼容性层级的作用

源码兼容指源码不加修改即可重新编译通过;ABI 兼容指已编译的二进制无需重新编译即可运行(函数签名、布局、调用约定不变);API 兼容指接口(函数名、参数)不变;行为兼容指语义行为不变。它们渐次严格:源码兼容不保证 ABI 兼容,ABI 兼容隐含 API 兼容。升级库时保持 ABI 兼容可避免强制重新编译客户端,行为兼容则保证运行结果一致。

本题考察兼容性层级。核心是区分"源码可重编译、二进制可运行、接口不变、行为一致"。作答时说明各层含义与升级的影响。

#
★★

51. ABI 稳定性的边界,新增函数 vs 修改函数签名 vs 修改 struct 布局如何取舍

ABI 稳定性的边界是什么?新增函数、修改函数签名、修改 struct 布局分别对 ABI 有何影响?

  • 新增函数不破坏 ABI
  • 修改签名破坏 ABI
  • 修改 struct 布局破坏 ABI

ABI 稳定性要求已编译二进制兼容。新增函数通常不破坏 ABI(旧二进制不需新函数);修改函数签名(参数、返回类型)会破坏 ABI,因为调用约定与符号 mangling 改变;修改 struct 布局(字段顺序、大小、union)会破坏 ABI,因为二进制按旧布局访问字段。因此维护 ABI 稳定须避免修改签名与布局,只能新增或保留旧符号。

本题考察 ABI 稳定边界。核心是"新增不破坏、改签名/布局破坏"。作答时说明哪些改动是 ABI 安全的。

#
★★

52. Node.js 的 N-API 与 NAN 的 ABI 稳定性

Node.js 的 N-API 与 NAN 在 ABI 稳定性上有什么差异?为什么 N-API 更稳定?

  • N-API 的稳定 ABI
  • NAN 的宏抽象
  • 跨版本兼容性

NAN(Native Abstractions for Node.js)通过宏把 V8 API 差异抽象掉,但依赖 V8 内部头文件,需随 Node 主版本重新编译;N-API 是 Node 提供的稳定 C 接口,通过不依赖 V8 内部实现的函数表(napi_env)维护 ABI 稳定,编译后的原生模块可在支持 N-API 的 Node 版本间直接复用,无需重编译。N-API 承诺 ABI 稳定,是推荐选择。

本题考察 N-API 与 NAN。核心是"N-API 稳定 ABI 免重编译、NAN 需重编译"。作答时说明两者对 V8 依赖的差异。

#
★★

53. Python 的 C API(CPython)的稳定 vs 不稳定边界

Python 的 C API(CPython)的稳定与不稳定边界是什么?什么情况下需要重编译扩展?

  • 稳定 API 与不稳定 API
  • ABI 的限定
  • 扩展重编译场景

CPython 提供稳定 ABI(stable ABI,通过 Py_LIMITED_API 使用),承诺跨小版本兼容,用稳定 ABI 编译的扩展可在多个 Python 版本复用;而完整的 C API 依赖内部结构(如 PyObject 布局、PyThreadState),随版本变化,使用完整 API 的扩展需针对每次发布重编译。内部布局与私有接口是不稳定边界,稳定 ABI 是刻意维护的公开契约。

本题考察 CPython C API 边界。核心是"稳定 ABI 免重编译 vs 完整 API 依赖内部需重编译"。作答时说明两者适用场景。

#
★★

54. PLT(Procedure Linkage Table)与 GOT(Global Offset Table)的 lazy binding 机制

PLT(Procedure Linkage Table)与 GOT(Global Offset Table)的 lazy binding 机制是如何工作的?

  • PLT 跳板与 GOT 表项
  • 首次调用的解析流程
  • GOT 回填与后续调用

调用共享库函数时,代码先跳到 PLT 的对应项,PLT 项第一跳转到 GOT 中该函数的槽位。lazy binding 下,首次调用时 GOT 槽位指向 PLT 的解析代码,解析器确定函数地址并回填 GOT 槽位后跳转;后续调用 GOT 槽位已为真实地址,直接跳转。这样未调用函数不解析,减少启动开销,但 GOT 需可写以支持回填(牺牲部分 RELRO 保护)。

本题考察 lazy binding 机制。核心是"PLT 跳板 + GOT 槽位 + 首次解析回填"。作答时说明首次与后续调用的差异。

#
★★

55. 动态段的 DT_NEEDED、DT_SYMTAB、DT_STRTAB、DT_RELA 等动态表项

动态段中的 DT_NEEDED、DT_SYMTAB、DT_STRTAB、DT_RELA 等表项分别是什么?动态链接器如何利用它们?

  • DT_NEEDED 的依赖表
  • DT_SYMTAB/DT_STRTAB 的符号表
  • DT_RELA 的重定位表

.dynamic 段的表项成对给出标签与值。DT_NEEDED 列出依赖库名;DT_SYMTAB 指向动态符号表(.dynsym);DT_STRTAB 指向字符串表(含符号名);DT_RELA/DT_REL 指向重定位表。动态链接器据此遍历依赖、在符号表中查找符号、按重定位表修正地址。这些表项共同构成动态链接的数据入口。

本题考察动态表项。核心是把各 DT_* 与"依赖、符号、字符串、重定位"对应。作答时说明链接器如何消费这些表项。

#
★★

56. 符号表(.symtab)与动态符号表(.dynsym)的差异

符号表(.symtab)与动态符号表(.dynsym)有什么差异?各自在什么场景使用?

  • .symtab 的完整符号
  • .dynsym 的导出符号
  • strip 与动态链接

.symtab 是完整符号表,含局部符号、调试信息所需的全部符号,strip 后通常被移除;.dynsym 是动态符号表,只含动态链接所需的导出/导入符号,是动态链接器在运行时实际使用的表。动态链接器通过 .dynsym 与 hash 表解析符号,而 .symtab 供调试与 nm 使用。因此剥离 .symtab 不影响运行,但影响调试。

本题考察两种符号表。核心是".symtab 完整可剥离、.dynsym 运行必需"。作答时说明各自用途与 strip 的影响。

#
★★

57. 重定位表(.rela.dyn、.rela.plt)的 RELA 与 REL 格式

重定位表(.rela.dyn、.rela.plt)的 RELA 与 REL 格式有什么区别?

  • REL 与 RELA 的结构差异
  • RELA 的 addend 字段
  • 平台选择

REL 重定位项为 r_offset + r_info 两个字段,addend 存于被重定位位置;RELA 重定位项为 r_offset + r_info + r_addend 三个字段,addend 显式存储。RELA 因 addend 独立而更灵活,x86-64 使用 RELA(.rela.dyn、.rela.plt);某些架构(如 x86 32 位)使用 REL。RELA 便于处理需要复合加数的重定位,且不依赖初始内容。

本题考察重定位格式。核心是"REL 无独立 addend、RELA 有"。作答时说明字段差异与 x86-64 的选择。

#
★★

58. strip 与符号去除的工程意义

strip 与符号去除的工程意义是什么?它有什么利与弊?

  • 减小文件体积
  • 隐藏符号信息
  • 调试能力的代价

strip 移除 .symtab、调试信息等符号表,显著减小可执行文件体积,便于分发与部署,也减少可被逆向的符号信息。代价是失去符号化调试与 backtrace 的可读性,需依赖 Debuginfod 或保留单独调试文件。生产环境常 strip 以减小体积,同时用 build-id 关联调试文件,兼顾体积与可调试性。

本题考察 strip 的取舍。核心是"减体积、藏符号 vs 丢调试信息"。作答时说明 strip 与 build-id/调试文件的配合。

#
★★

59. x86-64 SysV 的 PLT 与 lazy binding 的栈布局

x86-64 SysV 中 PLT 与 lazy binding 的栈布局如何?解析过程中栈如何传递?

  • PLT 项的指令序列
  • 解析器参数的栈传递
  • GOT 槽位回填

x86-64 的 PLT 项首条指令是 jmp *GOT[n],lazy binding 下 GOT 槽位指向 PLT 的后续指令,该指令把解析所需的符号序号(reloc index)压栈,再跳转到 PLT 0 的公共解析例程,解析例程通过 GOT[1] 的 link_map 与 GOT[2] 的解析函数(_dl_runtime_resolve)完成解析并回填 GOT 槽位,最后跳转目标函数。栈布局借此传递 link_map 与重定位索引。

本题考察 PLT 栈布局。核心是"GOT 槽位、符号索引压栈、_dl_runtime_resolve"。作答时说明首次解析如何在栈上传递参数。

#
★★

60. System V AMD64 ABI 中 rdi、rsi、rdx、rcx、r8、r9 这 6 个整数参数寄存器

System V AMD64 ABI 的 6 个整数参数寄存器 rdi、rsi、rdx、rcx、r8、r9 的传参顺序是什么?超过 6 个参数如何处理?

  • 6 个整数参数寄存器顺序
  • 溢出参数走栈
  • 寄存器数量与调用优化

System V AMD64 ABI 规定前 6 个整数/指针参数依次用 rdi、rsi、rdx、rcx、r8、r9 传递,第 7 个及之后参数从右到左压入栈。浮点参数用 XMM0-7。调用者负责清理栈上参数。寄存器传参减少内存访问、提升调用性能,是 64 位 ABI 相比 32 位的关键改进。

本题考察 SysV 传参。核心是"6 寄存器 + 栈溢出"的规则。作答时说明寄存器顺序与溢出参数处理。

#
★★

61. System V AMD64 中 rax 与 rdx 承载返回值(128 位返回值)的规则

System V AMD64 的返回值如何使用 rax 与 rdx?128 位返回值如何存放?

  • rax 存普通返回值
  • 128 位返回值用 rax+rdx
  • 浮点返回用 XMM0

System V AMD64 中整数/指针返回值存放在 rax,128 位返回值(如 __int128、两个整数组成的结构体)使用 rax+rdx 拼接;浮点返回值在 XMM0。返回寄存器的选择与函数返回类型相关,编译器据此生成序言。小于 16 字节的结构体也可能用寄存器返回,超过则走 sret。

本题考察返回值约定。核心是"rax 普通、rax+rdx 128 位、XMM0 浮点"。作答时说明返回寄存器的分配。

#
★★

62. 可变参数(variadic)的 AL 寄存器(浮点个数)传递

可变参数(variadic)函数中 AL 寄存器传递什么?为什么需要它?

  • AL 存浮点参数个数
  • 可变参数的类型推断
  • 与寄存器值的配合

System V AMD64 规定可变参数函数调用时,AL 寄存器保存调用者传入的向量寄存器(XMM)参数个数,供被调函数用 va_arg 确定浮点参数来源。因为可变参数函数在编译期不确定参数类型,需在运行期通过 AL 与寄存器保存区(register save area)推断参数布局。AL 的低 8 位表示浮点参数数量。

本题考察 variadic 的 AL。核心是"AL 记录浮点参数个数供 va_arg 使用"。作答时说明其与寄存器保存区的关系。

#
★★

63. 浮点参数通过 XMM0-XMM7 寄存器传递的规则

System V AMD64 中浮点参数如何用 XMM0-XMM7 传递?与整数参数如何区分?

  • XMM0-7 传浮点参数
  • 参数类型分区
  • 与 AL 个数的配合

System V AMD64 用 XMM0-XMM7 传递前 8 个浮点/向量参数,与整数参数(rdi-r9)分区独立,同一参数位置按类型选择寄存器类别。被调函数通过 XMM 寄存器与寄存器保存区访问浮点参数。可变参数函数用 AL 报告浮点参数个数,以正确区分整数与浮点来源。这种分区让浮点与整数传参互不干扰。

本题考察浮点传参。核心是"XMM0-7 独立传浮点 + AL 计数"。作答时说明浮点与整数参数的分区。

#

64. AAPCS(AArch64 Procedure Call Standard)与 SysV 的对照

AAPCS(AArch64 Procedure Call Standard)与 x86-64 SysV 在调用约定上有哪些对照差异?

  • AArch64 的 x0-x7 参数
  • 浮点 v0-v7 参数
  • 栈与返回约定

AAPCS 用 x0-x7 传前 8 个整数参数,v0-v7(d0-d7)传浮点参数,比 SysV 的 6 个整数寄存器更多;返回值在 x0(浮点在 v0)。AArch64 要求栈 16 字节对齐,且符合 AAPCS 的寄存器函数为 x29 帧指针(可选)。相比 SysV,AArch64 参数寄存器更多、规则更统一,调用约定也更规整。

本题考察 AAPCS 与 SysV 对照。核心是"参数寄存器数量、命名、返回"的差异。作答时按传参、返回、对齐对照。

#

65. PE 的 section,包括 .text、.data、.rsrc、.reloc 与 DLL 导入表

PE 的 .text、.data、.rsrc、.reloc 等 section 各存放什么?DLL 导入表如何组织?

  • PE 各 section 的用途
  • .reloc 的重定位
  • 导入表与导入地址表

PE 的 .text 存代码,.data 存可写数据,.rsrc 存资源(图标、字符串、对话框),.reloc 存基址重定位表(让 DLL 在被加载任意基址时修正地址)。DLL 导入表(Import Table)列出导入的 DLL 与函数,配合导入地址表(IAT)在实际调用时填入函数地址。这些 section 与导入机制共同支撑 Windows 动态链接。

本题考察 PE section 与导入表。核心是"各 section 用途 + 导入表/导入地址表"。作答时说明重定位与导入机制。

#

66. PE(Portable Executable,Windows COFF 扩展)的 DOS stub 与 PE header

PE(Portable Executable,Windows COFF 扩展)的 DOS stub 与 PE header 各是什么?为什么 PE 以 DOS stub 开头?

  • DOS stub 的兼容性
  • PE header 的签名与结构
  • 从 DOS 到 PE 的演进

PE 文件以 DOS stub 开头(MZ 头 + 一段小的 DOS 程序),其作用是让旧 DOS 系统能识别并提示"无法在 DOS 下运行",同时通过 e_lfanew 字段指向真正的 PE header。PE header 含 "PE\0\0" 签名、COFF 头与可选头(含入口点、镜像基址、section 表偏移)。这种设计使 PE 向后兼容 DOS 时代,是 Windows 可执行格式的历史延续。

本题考察 PE 的组成部分。核心是"DOS stub 兼容旧系统 + PE header 承载新格式"。作答时说明 e_lfanew 定位与演进。

#

67. COFF 的 PE32+(64 位 PE)格式

COFF 的 PE32+(64 位 PE)格式相比 PE32 有什么差异?它如何支持 64 位地址?

  • PE32+ 的可选头字段
  • 64 位地址与基址
  • 与 PE32 的兼容

PE32+ 是 64 位 PE 格式,可选头中 ImageBase、字段等扩展为 64 位,entry point 与地址空间使用 64 位。它支持更大的镜像与 64 位虚拟地址,magic 字段为 0x20b(PE32 为 0x10b)。PE32+ 与 PE32 结构基本一致,仅可选头字段宽度不同,Windows 加载器据 magic 区分。.reloc 重定位等机制同样适用。

本题考察 PE32+。核心是"64 位可选头字段 + 大地址空间"。作答时说明与 PE32 的字段差异。

#

68. Windows DLL 的导入表(import table)与导出表(export table)

Windows DLL 的导入表(import table)与导出表(export table)分别是什么?它们如何配合完成符号解析?

  • 导入表列出的依赖
  • 导出表提供的符号
  • 导入/导出匹配

DLL 的导出表(Export Table)列出 DLL 对外提供的函数与数据符号(含名称与序号),导入表(Import Table)列出依赖的 DLL 及其所需函数。加载器解析时,把导入项与导出表匹配,把函数地址填入导入地址表(IAT)。这样主模块与 DLL 通过导入/导出表完成符号绑定,构成 Windows 动态链接的基础。

本题考察 DLL 导入/导出。核心是"导出表供符号、导入表求符号、IAT 填地址"。作答时说明二者配合的解析过程。

#

69. DT_RPATH 与 DT_RUNPATH 的差异,即 RUNPATH 不传递给子进程

DT_RPATH 与 DT_RUNPATH 有什么差异?为什么 RUNPATH 不传递给子进程?

  • DT_RPATH 的递归传递
  • DT_RUNPATH 的局部性
  • 搜索路径优先级

DT_RPATH 和 DT_RUNPATH 都指定库搜索路径,但 DT_RPATH 会传递给子进程(其依赖链也继承此路径),且优先级高;DT_RUNPATH 只作用于当前对象,不传递给子进程,优先级低于 LD_LIBRARY_PATH。因 RUNPATH 局部性强、更安全,现代工具链(-Wl,-rpath 默认)倾向生成 DT_RUNPATH 而非 DT_RPATH。

本题考察 RPATH/RUNPATH。核心是"传播性、优先级、安全性"差异。作答时说明 RUNPATH 局部性的工程意义。

#

70. RPATH、RUNPATH、LD_LIBRARY_PATH 的库搜索路径优先级

RPATH、RUNPATH、LD_LIBRARY_PATH 的库搜索路径优先级是如何排列的?

  • 默认路径与 LD_LIBRARY_PATH
  • RUNPATH 与 RPATH 的位置
  • 搜索顺序的规则

动态链接器搜索库的顺序大致为:DT_RPATH(若存在且无 RUNPATH)、LD_LIBRARY_PATH、DT_RUNPATH、默认路径(/lib、/usr/lib 等)、缓存(ld.so.cache)。已剥离的 DT_RPATH 优先级高于 LD_LIBRARY_PATH,而 DT_RUNPATH 优先级低于 LD_LIBRARY_PATH。理解此顺序对排查"库被加载到错误版本"问题至关重要。

本题考察搜索优先级。核心是"RPATH > 环境变量 > RUNPATH > 默认路径"的层次。作答时说明各优先级及其对排查的意义。

#

71. dlmopen 的命名空间隔离

dlmopen 的命名空间隔离是什么?它解决什么问题?

  • dlmopen 的 namespace 参数
  • 同名符号隔离
  • 多实例加载

dlmopen 与 dlopen 类似,但允许把库加载到不同的命名空间(namespace),每个命名空间拥有独立的符号查找表,从而隔离同名符号,避免不同库实例间的符号冲突。它常用于在同一进程内加载多个版本或独立插件,保证互不干扰。缺点是不同命名空间间符号不共享,需注意资源传递。

本题考察 dlmopen。核心是"命名空间隔离同名符号"。作答时说明其与 dlopen 的差异及适用场景。

#

72. 版本脚本与符号版本(symbol versioning)的 ABI 兼容性

版本脚本与符号版本(symbol versioning)如何支撑 ABI 兼容性?

  • 符号版本标签
  • 多版本共存
  • 向后兼容

符号版本化通过为导出的符号附加版本标签(如 GLIBC_2.2.5),使同一符号可对应多个版本,形成版本节点链。旧二进制引用旧版本,新二进制引用新版本,互不冲突,从而在同一库内承载多版本 ABI。版本脚本(version script)定义版本的分配与继承关系,是管理符号版本的主要手段,支撑长期向后兼容。

本题考察符号版本化。核心是"版本标签让多版本共存"。作答时说明版本脚本如何支撑 ABI 演进。

#

73. 动态库的 SONAME 与版本号管理

动态库的 SONAME 与版本号管理是什么?它如何支撑 ABI 兼容与升级?

  • SONAME 的记录
  • 主/次版本号
  • 升级与兼容策略

SONAME(如 libfoo.so.1)是链接器记录在 DT_SONAME 中的库标识,二进制引用它而非文件名,加载器据此定位。动态库通常用主版本号(ABI 不兼容时递增)与次版本号(兼容时递增)组织,如 libfoo.so.1.2.3,SONAME 反映主版本。升级时若 ABI 兼容只需替换实现文件,破坏 ABI 则递增主版本并更新 SONAME,避免客户端误用。

本题考察 SONAME 与版本管理。核心是"SONAME 标识 ABI 版本 + 主/次版本控制升级"。作答时说明版本号与兼容策略。

#

74. ABI(Application Binary Interface)的范围,涵盖传参规则、数据布局、名称修饰与调用堆栈布局

ABI(Application Binary Interface)的范围包括哪些?传参规则、数据布局、名称修饰、调用堆栈布局各起什么作用?

  • ABI 的定义范围
  • 各组成部分的作用
  • 跨语言互操作的前提

ABI 定义程序在二进制层的接口契约,范围包括:调用约定(传参寄存器、返回寄存器、栈布局)、数据布局(结构体对齐、大小、字段偏移)、名称修饰(mangling 规则)、异常处理与 TLS 等。这些约定使分别编译的对象能正确互操作,也是跨语言 FFI 的前提。ABI 不变则二进制兼容,改变则需重新编译。

本题考察 ABI 范围。核心是"调用约定、数据布局、名称修饰、栈布局"构成 ABI。作答时说明各组成部分对互操作的作用。

#

75. C 标准的版本(C89、C99、C11、C17、C23)的兼容性

C 标准的版本(C89、C99、C11、C17、C23)之间的兼容性如何?各版本引入了哪些变化?

  • 各标准版本的关键特性
  • 语言 vs ABI 兼容
  • 向后兼容策略

C89、C99、C11、C17、C23 是 C 标准的不同版本,C89 是基础,C99 引入可变长数组、restrict、stdint 等,C11 增加原子操作、线程、_Generic,C17 主要是修正,C23 增加 _BitInt、标准化的属性等。新标准整体向后兼容,但编译器可能因新语义对旧代码产生警告。C 语言兼容性指源码可编译,与 ABI 兼容相对独立;只要 ABI 不变,二进制仍可互操作。

本题考察 C 标准版本。核心是"各版本特性 + 源码兼容与 ABI 兼容区分"。作答时说明标准演进与 ABI 的关系。

#

76. C++ ABI 的对象布局(vptr、vtable)与 RTTI 的稳定性

C++ ABI 的对象布局(vptr、vtable)与 RTTI 的稳定性如何影响继承与多态?

  • vptr/vtable 的布局
  • RTTI 的类型信息
  • 布局稳定性的作用

C++ ABI 定义有虚函数的类在对象开头放置 vptr(指向 vtable),vtable 含虚函数地址与 RTTI 信息;RTTI 用 typeinfo 对象描述类型名称与继承关系。对象布局(字段顺序、vptr 位置、虚函数槽位)由 ABI 固定,跨编译器/版本保持稳定,才能保证多态派发与 dynamic_cast 正确。布局或 vtable 顺序变化会破坏 ABI 兼容。

本题考察 C++ 对象布局。核心是"vptr/vtable 布局 + RTTI 由 ABI 固定"。作答时说明布局稳定对多态与 RTTI 的作用。

#

77. libstdc++ 与 libc++ 的 ABI 不兼容问题

libstdc++ 与 libc++ 的 ABI 不兼容问题体现在哪里?为什么同一程序不能混用?

  • 两个标准库的差异
  • 符号与类型布局差异
  • 混用的后果

libstdc++(GCC)与 libc++(LLVM)是不同实现的标准库,其内部类型布局(如 std::string、std::vector 的内部结构)、符号名与 ABI 约定可能不同。若程序与库使用不同标准库,跨边界传递标准库对象时可能因布局不匹配而崩溃。因此同一二进制应统一使用同一标准库实现,混用是 ABI 不兼容的典型来源。

本题考察标准库 ABI 差异。核心是"不同实现内部布局与符号不同导致混用崩溃"。作答时说明统一标准库的必要性。

#

78. abi-compliance-checker、abidiff 等 ABI 兼容性测试工具

abi-compliance-checker、abidiff 等 ABI 兼容性测试工具如何工作?它们对库演进有什么价值?

  • 工具的比较对象
  • 检测的破坏类型
  • 在 CI 中的作用

abi-compliance-checker 与 abidiff 比较两个版本的库(或 DSO),通过解析符号表、类型布局、调用约定等,检测新增/删除符号、函数签名变化、结构体布局变化等 ABI 破坏点。它们能定位"从这个版本升级后二进制是否兼容",在 CI 中作为回归检测,防止误改破坏 ABI,支撑库的长期演进。

本题考察 ABI 测试工具。核心是"比较版本检测 ABI 破坏"。作答时说明工具在库演进与 CI 中的价值。

#

79. ABI 版本化(symbol versioning)的 Soname 演进

ABI 版本化(symbol versioning)与 Soname 演进如何配合管理库的版本?二者的作用分别是什么?

  • symbol versioning 的符号级版本
  • Soname 的库级版本
  • 演进策略

symbol versioning 在符号粒度提供版本(同一库内多版本共存),Soname 在库粒度标识 ABI 主版本(破坏时递增)。演进时:兼容性改动通常只更新实现,SONAME 不变,符号版本可新增;破坏性改动则递增 SONAME 主版本并可能保留旧 Soname 兼容。二者配合让库在持续演进时保持旧二进制可用。

本题考察库版本管理。核心是"符号级版本 vs 库级 Soname"的分工。作答时说明两者配合支撑长期演进。