最近更新 · 33 篇文章按系列阅读
[链接器的世界-原理篇09] TLS:同一个变量,每个线程各有一份
thread_local 变量在每个线程里是同一个名字、不同的地址。这个地址由编译器、链接器和动态加载器合作算出:.tdata/.tbss 与 PT_TLS 是模板,%fs 和 TPIDR_EL0 指向每个线程自己的那一份,四种访问模型在通用和快之间取舍,链接器在知道更多信息时把慢序列原地改写成快序列。本文用 clang、ld.lld 和 GNU ld 的真实输出走一遍全过程,最后解释"cannot allocate memory in static TLS block"的根因。
阅读全文
[链接器的世界-原理篇08] 栈展开:沿调用栈找到回去的路
函数一层套一层地调用,返回地址散落在栈上。打印调用栈、抛出 C++ 异常时,运行时要一层层退回调用者,靠的是编译器和汇编器写进 .eh_frame 的一张"每条指令处怎么找回调用者"的表。本章用真实目标文件逐字节拆开 CIE/FDE,讲 CFI 状态机、personality 与 LSDA、两阶段展开,以及链接器为 .eh_frame 和 .eh_frame_hdr 要做的事。
阅读全文
[链接器的世界-原理篇07] 动态链接:把最后的地址留给运行时
共享库的加载地址要到运行时才知道,它引用的符号甚至可能被别的库替换。链接器在链接时能做的,是把所有"现在还填不了"的地址集中到少数几处,再给动态链接器 ld.so 写一份说明书。本文从 PIC、GOT、PLT 讲到几类动态重定位、惰性绑定、RELRO、符号插入、符号版本和 ld.so 的启动顺序,全部配真实的目标文件与链接输出。
阅读全文
[链接器的世界-实验篇06] 文件里的字节与内存里的字节:数据、BSS 与段权限
只有代码的程序几乎没有用。本实验加入只读常量、已初始化全局变量、BSS 和 COMMON,用三个权限不同的加载段描述它们,并让内核替我们检查:写只读数据、执行数据段都会收到 SIGSEGV。
阅读全文
[链接器的世界-原理篇06] 从入口到 main:程序启动之前发生了什么
链接器写完程序头表就退场了,剩下的事归加载器。本文先读 xv6 的 kexec,看一个最小的加载器只用哪几个字段、做哪三道检查;再读 Linux 的 load_elf_binary,看 PT_INTERP、PT_GNU_STACK、基址选择、.bss 的页尾清零与匿名页、按需调页和 brk;然后打印初始进程栈和辅助向量,跟着 musl 的源码从 _start 走到 main,手工复现驱动程序加的五个启动文件,构建一个 static-pie 看 rcrt1.o 怎样自己处理 R_X86_64_RELATIVE;最后在真实内核上改坏 ELF,看加载器怎样守住安全边界。
阅读全文
[链接器的世界-实验篇05] 跨越文件的调用:合并代码,再填写位移
上一阶段的可执行文件只能来自一个没有重定位的目标文件。本实验让汇编写的入口调用另一个文件里用 C 编译的函数:合并多个代码节,计算每个字段的 S+A-P,并保证失败时不留下半写的字节。
阅读全文
[链接器的世界-原理篇05] 布局与 GC:给每个节找位置,让无用代码退场
同一个目标文件,用不同的链接器、不同的选项链接,填进机器码的数字都不一样。这些数字取决于布局:输入节怎么并成输出节、输出节怎么分进段、段放在哪个地址、哪些节被垃圾回收删掉。本文用 GNU ld 和 lld 的真实输出,讲清楚程序头的每个字段、文件偏移与虚拟地址为什么要同余、-z separate-code、PIE 与基址、--gc-sections 的标记过程、__start_/__stop_ 符号,以及链接器脚本和映射文件。
阅读全文
[链接器的世界-实验篇04] 第一个能运行的 ELF:两张程序头与一个入口
前四个阶段只在读输入。本实验第一次写输出:规划一个最小的可执行映像,手工序列化 ELF 头和两个程序头,让 Linux 内核直接运行它并返回 42。
阅读全文