从源码到进程
点一个框看它做什么、在哪篇讲过;选一条路径,一步一步看一个 .cpp 怎么变成 .o、链接时报的错从哪来、./a.out 之后发生了什么。蓝字可以直接点开对应的文章或地图。
进程地图
速查页
静态图
一个 .cpp 变成 .o
链接:拼成可执行文件
静态库和动态库
./a.out 之后
第一次调 printf
undefined reference 从哪来
看原尺寸
从源码到进程:编译、链接、加载
上面是编译:每个 .cpp 各自变成一个 .o。中间是链接:所有 .o 和库拼成一个可执行文件。下面是运行:内核和动态链接器把它变成进程地图上的那几段。
橙线 = 代码拷进输出文件;蓝色虚线 = 只记一个名字,运行时再找;绿色虚线 = 加载时 mmap 进地址空间;灰线 = 下一步。
蓝字 = 在哪里讲过,进程地图会打开对应的那张图。
编译:一次一个翻译单元
链接:把所有 .o 和库拼成一个文件
加载和运行
源文件和头文件
a.cpp、b.cpp:各自单独编译
a.h:放声明,被 #include 进去
一个 .cpp 加上它 include 的
全部内容 = 一个翻译单元
编译器一次只看一个翻译单元,
不知道别的 .cpp 里写了什么
① 预处理:g++ -E
#include:把头文件原样粘进来
#define:宏按文本替换
#if、#ifdef:删掉不要的分支
产物 a.ii:纯 C++ 文本,
常常有几万行
② 编译:g++ -S
语法、类型检查:编译错误多在这
模板在这里按具体类型实例化
优化:内联、常量传播……
名字修饰:f(int) → _Z1fi
产物 a.s:汇编文本
③ 汇编:g++ -c
汇编指令 → 机器码
产物 a.o:可重定位目标文件
地址还没定:调别的文件里的
函数,地址先填 0,
旁边记一条重定位项
a.o 里有什么(nm、readelf)
段:.text 机器码,.data,.bss,.rodata
符号表:T f = 这里定义了 f
U g = 用了 g,定义不在这里
重定位表:.text+0x1c 处要填 g 的地址
每个 .o 都从地址 0 排起,互相不知道对方
进程地图:这些段最后落在哪
静态库 libx.a
一堆 .o 打成的包(ar)
链接器只挑用得上的 .o,
把代码拷进可执行文件
动态库 libc.so.6
链接时只检查符号在不在,
记下“运行时要 libc.so.6”,
代码不拷进来
④ 链接:ld(g++ 最后一步替你调)
1. 符号解析:每个 U 都要找到唯一的定义
一个都没有 → undefined reference
有两个 → multiple definition
2. 合并:各个 .o 的 .text 拼成一个 .text,
.data、.bss 同理;定下每个段、
每个符号的最终地址
3. 重定位:按重定位项把地址填进去
产物:可执行文件,或者一个 .so
可执行文件 a.out(readelf -l)
ELF 头:入口地址 _start
程序头:加载时每一段 mmap 到哪
LOAD r-x:.text
LOAD r--:.rodata、.eh_frame
LOAD rw-:.data、.bss(.bss 只记大小)
INTERP:/lib64/ld-linux-x86-64.so.2
DYNAMIC:需要 libc.so.6……
.plt、.got:调 .so 里函数的跳板
进程地图:.text、.data、.bss
链接报错,多半是这几种
undefined reference to f:
· 只声明没定义,或那个 .cpp
没参与链接
· 少了 -l,或 -l 写在了前面
· 模板的定义写在 .cpp 里
· C 函数没加 extern "C"
multiple definition of x:
· 头文件里定义了普通函数或变量
可以多处定义:inline、模板
落到进程地图上
.text → 代码段(r-x)
.rodata → 只读数据(r--)
.data、.bss → 读写数据(rw-)
libc.so、ld.so → mmap 区
栈:内核放好 argv、环境变量
进程地图
⑥ 动态链接器 ld.so
按 DT_NEEDED 找 .so:rpath、
LD_LIBRARY_PATH、/etc/ld.so.cache
找到的库 mmap 进来,做重定位
跑全局对象的构造函数
最后跳到 _start,由它调 main
⑤ execve("./a.out")
内核读 ELF 头和程序头
按 LOAD 段 mmap,建好栈
有 INTERP:把 ld.so 也映射进来,
先从 ld.so 开始跑
静态链接的:直接从 _start 开始
进程和线程:fork 和 exec
⑦ 调 .so 里的函数:PLT 和 GOT
call printf@plt:先跳到 PLT 的跳板
跳板去 GOT 里取真实地址,再跳过去
延迟绑定:第一次取到的是解析函数,
解析完把 printf 的地址写回 GOT
-z now:启动时全填好,再设只读
所有 .o 一起交给链接器
拷进来
只记名字
./a.out
运行时调库函数
跳过去
mmap
运行时 mmap 进来,进程间共享
图随文章更新:讲到模板实例化、内联和链接时优化(LTO)时再往上补。