CMU 15-213 CSAPP (Ch1~Ch3)
CMU 15-213 CSAPP (Ch5~Ch7)
CMU 15-213 CSAPP (Ch8)
CMU 15-213 CSAPP (Ch9)
CMU 15-213 CSAPP (Ch10)
视频链接
课件链接
该视频课程使用 64位 编译器!
Ch5.Optimizing Program Performance
There is more to performance than asymptotic complexity1.
- Constant factors,静态的、代码层面的
Code writting、Algorithm、Data representations、Procedures and loops … - System Level understanding,动态的、系统原理层面的
Compiler、Execute、Processors、Memory、Operation …
5.1 Generally Useful Optimizations
优化要考虑平台多样性,过度优化反而使程序受限于硬件的实现,不具备泛化性。
5.1.1 Optimizing Compilers
- 从编译器角度 可以 优化的方向
- register allocation
- code selection and ordering
- dead code elimination
- eliminating minor inefficiencies
- 从编译器角度 不能 优化的方向
- select best overall algritm
- potential memory aliasing
- potential procedure side-effects
- 编译器必须选择保守(conservative)的策略
- 只做静态优化
- 只对同一文件中多个函数(间)进行优化
上古编译器优化能力差,但时过境迁,汇编no.1的说法已然落伍,根据编译结果,重构代码,more compiler friendly 才是万能的方法。
5.1.2 Code Ordering
例如,循环不变式外提,gcc -O1级别优化
for(int j=0;j<COL;j++)
arr[i*n+j]=VAL; // 将 i*n 移到循环之前保存为局部变量
5.1.3 Strength Reduction
例如,乘/除法(低效操作)替换为 加减法或位移(Intel Nehalem 整型乘法将花费3个CPU周期)
for(int i=0;i<ROW;i++)
{
int ni=n*i; // ni = n*i 优化为 ni += n
for(int j=0;j<COL;j++)
a[ni+j]=VAL;
}
5.1.4 Sharing of Common Subexpressions
例如,图片数组均值化,gcc -O1级别优化
int avg(int i,int j,int n)
{
int up = val[(i-1)*n + j];
int down = val[(i+1)*n + j];
int left = val[i*n + j-1];
int right = val[i+n + j+1]; //未进行优化,使用了至少三次乘法
return (up + down + left + right)/4;
}
#g++ -S -O1 .\tc.cpp
avg:
leal -1(%rcx), %eax # %eax = i-1
imull %r8d, %eax # %eax = n(i-1)
leal (%rax,%r8,2), %r11d # %r11d = n(i-1)+2n = n(i+1)
leaq val(%rip), %r10 # %r10 = &val
addl %edx, %eax # %eax = n(i-1)+j
movslq %eax, %r9 # %r9 = SignExtend(%eax)
leal (%r11,%rdx), %eax # %eax = n(i+1)+j
cltq
movl (%r10,%rax,4), %eax # %eax = *{ &val + 4[n(i+1)+j] } = down
addl (%r10,%r9,4), %eax # %eax = up + down
subl %r8d, %r11d # %r11d = n(i+1)-n = n*i
leal -1(%rdx,%r11), %r9d # %r9d = n*i+j-1
movslq %r9d, %r9
addl (%r10,%r9,4), %eax # %eax = %eax + left
addl %r8d, %ecx # %ecx = n
leal 1(%rdx,%rcx), %edx # %edx = n*i+j+1
movslq %edx, %rdx
addl (%r10,%rdx,4), %eax # %eax = %eax + right
leal 3(%rax), %edx # %edx = %rax + 3 //这里没懂
cmovs %edx, %eax # 是在判断有没有整型溢出?为什么是+3?
sarl $2, %eax # %eax = %eax / 4
ret
实验结果,gcc -O1 并没有使用共享子式,手动优化后逻辑如下
int avg(int i,int j,int n)
{
int addr = n*i+j; // 只使用一次乘法
int up = val[addr-n];
int down = val[addr+n];
int left = val[addr-1];
int right = val[addr+1];
return (up + down + left + right)/4;
}
5.2 Optimization Blockers
5.2.1 Procedure Calls
for(size_t i=0;i<strlen(s);i++)
{
if(s[i] >= 'A' && s[i] <= 'Z')
s[i] -= ('A' - 'a');
}
Time cost grows nonlinearly as the string length does. It’s quadratic.
每次循环都会调用strlen,strlen都会遍历一次字符串,出于保守策略,编译器也没有将strlen移到循环外:
- 不能确定循环体内s是否被修改,循环过程中strlen(s)的结果是否有变化;
- strlen的版本在linking阶段才确定,不一定就是标准库的strlen;
总之,编译器对函数调用的优化十分有限,只能人为调整。
5.2.2 Memory References
5.3 Instruction Level Parallelism
这里只简述部分原理,详细内容参考ECE 447 Introduction to Computer Architecture

5.3.1 Superscalar Out Of Order Execution
超标量乱序执行
程序视为线性的指令序列,一次性读入尽可能多的指令,将无依赖关系的指令拆分,同时执行复数条指令,让计算单元尽可能的被利用。比如,前后两条指令分别是 mov a, b 和 add c, d ,两条指令相互独立,可同时进入执行阶段。
1993年英特尔 Pentium 就已经可以同时执行两条指令,之后推出的 Pentium Pro 更是现代处理器的基础。
5.3.2 Pipelined Functional Units
B站弹幕举了个很有意思的例子:
只有两个锅,一个饼一个面要煎一分钟,煎三个饼最快要多长时间?
花费了4分钟,问题出在最后会空锅浪费时间。换个方法:
之前的空锅,是因为一个饼不能同时煎正反两面,所有的饼又都一样,正面一分钟,反面一分钟,通过C的正面置换B的反面,使得step3时,B的反面没煎,C的反面也没煎,最后一步可以将两个锅都利用起来。
核心思想,就是将一个动作分解为更细的阶段动作,调整先后顺序,用阶段动作填补空闲。
假设每个乘法指令被分解为三个阶段,每个阶段又对应有专用的硬件设施(dedicated hardware):
long l1 = a*b;
long l2 = a*c;
long res = l1*l2;
| Stages | Cycle 1 | Cycle 2 | Cycle 3 | Cycle 4 | Cycle 5 | Cycle 6 | Cycle 7 |
|---|---|---|---|---|---|---|---|
| 1 | a * b | a * c | l1 * l2 | ||||
| 2 | a * b | a * c | l1 * l2 | ||||
| 3 | a * b | a * c | l1 * l2 | ||||
| 原本需要9个周期才能执行完的任务,通过对不同阶段的硬件设施的充分利用,只需要7个周期便可完成 ! |
5.3.3 Haswell CPU
loops unrolling 这一块有点糊,之后整理
5.4 Dealing with Conditionals
分支预测2
Ch6.The Memory Hierarchy
6.1 Storage technologies and trends
- Random-Access Memory (RAM)
- 基本存储单元为 cell (1 bit 信息)
- 多块 RAM chips 组成通常所说的“内存”(workhorse memory)
- RAM 有两种,通过 存储单元的实现方式 区分3
| Varieties | Trans/cell | Access time | Refresh | EDC | Cost | Applications |
|---|---|---|---|---|---|---|
| Static RAM | 4 or 6 | 1 X | No need | Maybe | 100X | Cache memories |
| Dynamic RAM | 1 | 10 X | Need | Yes | 1X | Main memories frame buffers |
- Nonvolatile Memory
- 存储 Firmware(BIOS、磁盘、网卡、显卡、图形加速器、安全系统 …)
- 固态硬盘(U盘、智能手机、mp3播放器、平板 tablets、笔记本)
- Disk Cache ???
| Varieties | Characteristics |
|---|---|
| ROM (Read-only memory) | 生产时编程 |
| PROM (Programmable ROM) | 能且只能编一次 |
| EPROM (Eraseable PROM) | 大部分可擦除再编(UV,X-Ray) |
| EEPROM (Electrically eraseable PROM) | electronic erase capability |
| Flash memory | “整块”擦除,约十万次后 bricked |
- Memory Bus
- Memory operations 约 50 ~ 100 纳秒,Registers operations 短于 1 纳秒

- Memory operations 约 50 ~ 100 纳秒,Registers operations 短于 1 纳秒
- Disk Geometry
- 厂商述硬盘容量单位 1GB = 1 0 9 10^9 109,而非 2 30 2^{30} 230 Bytes;
- 容量 取决于 磁盘表面 信息压缩(squeeze)的程度:
- 扇区密度 Sector density = s e c t o r s o n e _ t r a c k \frac{sectors}{one\_track} one_tracksectors
- 记录密度 Recording density = b i t s i n c h _ t r a c k \frac{bits}{inch\_track} inch_trackbits
- 轨道密度 Track density = t r a c k s i n c h _ r a d i u s \frac{tracks}{inch\_radius} inch_radiustracks
- 面密度 Areal density = 记录密度 × \times × 轨道密度 = b i t s i n c h 2 \frac{bits}{inch^2} inch2bits
- 磁道扇区总数 随圆周 增大而增大,为便于读取数据,将相邻磁道划为一组 记录区(Recording Zone),要求记录区内各个磁道 扇区密度 相同;
- 三个影响读写速率的因素(较SRAM慢数千倍,DRAM慢数百倍)
- Seek time 磁头移动到目标柱面的时间:
Tavg_seek = 3~9 ms - Rotational latency 目标柱面上等待目标扇区到达磁头的时间:
Tavg_rotation = 1 2 × 60 s R o u n d _ P e r _ M i n u t e \frac{1}{2} \times \frac{60s}{Round\_Per\_Minute} 21×Round_Per_Minute60s,约 4ms - Transfer Time 磁头 扫过一个扇区的时间:
Tavg_transfer = 60 s R o u n d _ P e r _ M i n u t e × 1 A v g ( S e c t o r s _ P e r _ T r a c k ) \frac{60s}{Round\_Per\_Minute} \times \frac{1}{Avg(Sectors\_Per\_Track)} Round_Per_Minute60s×Avg(Sectors_Per_Track)1,约 0.02ms
- Seek time 磁头移动到目标柱面的时间:
- 现代硬盘 将 多个physical sectors 组成 logical blocks:
- Disk controller 负责将 blocks 映射成(surface,track,sector)三元组
- Disk controller 为每个Zone预留一些空 Cylinder 作为坏道的替补
格式化后,硬盘可用容量 < 标定最大容量

“The most interesting ideas in computer science are involve some form of indirection.”
- I/O Bus
- 下图为 (2010年) PCI (Peripheral Component Interconnect) IO总线系统,PCI是广播总线 (broadccast bus),所有设备抢占同一总线,同一时间最多一个设备能够占用总线
- 现代操作系统更加复杂,使用PCIE (PCI Express) IO总线结构,点对点通信,开关切换使用权 (arbitrated by switch),但速度更快
- 部分设备直接焊接在主板上,如磁盘控制器,部分显卡,USB控制器等,部分设备只在总线上留有接口,允许自行扩展,如网卡,独立显卡等

- Disk Read 分三步,磁盘IO太慢,让CPU空等数据传输是巨大浪费,因此引入 DMA
- CPU 向 Disk Controller 发送(command,logical block num,memory address)三元组
- Disk Controller 读取 blocks 映射的扇区,通过 I/O 总线⇒I/O Bridge⇒内存总线,直接将数据拷贝到内存指定地址,没有经过CPU
- Controller 使用 中断 (asserts a pin on CPU chip) 通知 CPU,拷贝完成
-
Solid State Disk
- 介于 机械磁盘 和 DRAM 之间,兼具 容量 和 速度,以闪存作为存储介质
- Page是读写操作的最小单位(512B~4KB),一组Page按序组成Block
- Page只能写入空Block,数据的写入 必然伴随目标Block中除目标Page外的其他Pages,拷贝到空Block的过程,因此 “写” 比 “读” 慢
- 每个Block寿命最多擦写10万次
- Flash translation layer 提供很多方法来延长SSD的寿命,如缓存,均衡磨损4,等

-
2003年,Intell开发了800 W W W功耗处理器,需要 4 i n c h 2 inch^2 inch2大小的 heatsink 散热,功耗随时钟频率增大,摩尔定律失效,时钟频率无法再有指数级的提升,时钟周期稳定在0.1纳秒附近;Intell另谋出路,增加 chip 的 processor core 个数,通过并行提升效率
- 内存限制了CPU读取数据的速度,拖慢了程序的运行效率,Locality特性是解决问题的关键
6.2 Locality of refrence
"【Principle of Locality】Programs tend to use data and instructions whose addresses near or equal to those they have used recently. "
- 访问局部性,应用程序在访问内存的时候,倾向于访问内存中附近的值:
- 【Temporal locality】时间局部性,被引用过的内存地址容易再次被引用;
- 【Spatial locality】空间局部性,被引用过的内存地址其附近的内容容易再次被使用;
- 顺序局部性,除转移指令外,程序的指令多是顺序执行
int sum = 0; // sum is referenced each iteration, Temporal locality
// Instructions in sequence, Spatial locality
for(int i=0;i<n;i++) // Instructions repeated each Iteration, Temporal locality
sum += a[i]; // nearby array elements are referenced, Spacial locality
return sum;
6.3 Caching in the memory hierarchy
“These fundamental properties complement each other beautifully. And they suggest an approach for organizing memory and storage systems known as a memory hierarchy.”
| Cache Type | Content | In where | Latency(cycles) | Managed by |
|---|---|---|---|---|
| costlier | smaller | … | faster | … |
| Registers | 4-8 Bytes Words retrieved from the L1 cache | CPU core | 0 | Compiler |
| TLB | Address translations | On-Chip TLB | 0 | Hardware MMU |
| L1 cache | 64-byte blocks | On-Chip L1 | 4 | Hardware |
| L2 cache | 64-byte blocks | On-Chip L2 | 10 | Hardware |
| Virtual Memory | 4-KB pages | Main memory | 100 | Hardware + OS |
| Buffer cache | Parts of files | Main memory | 100 | OS |
| Disk cache | Disk sectors | Disk controller | 100,000 | Disk firmware |
| Network buffer cache | Parts of files | Local disk | 10,000,000 | NFS client |
| Browser cache | Web pages | Local disk | 10,000,000 | Web browser |
| Web cache | Web pages | Remote server disks | 1,000,000,000 | Web proxy server |
| cheaper | larger | … | slower | … |
- 整个设计结构的核心思想 ⇒ 缓存
A smaller, faster storage device that acts as a staging area for a subset of the data in a larger, slower device.
- CPU访问的数据块在cache中称作 Hit,反之为 Miss,处理Miss最简单粗暴的方式,从DRAM中取出,覆盖cache中最早读入的块,即FIFO;
- Miss 分为三种:
- Cold(compulsory)miss,cache is empty and every needed block is in DRAM
- Conflict miss,算法导致两个k+1层的 block,set 和 tag 冲突,原k层 block 被替换
- Capacity miss,working set(工作集,active cache blocks)size exceeds(hardware)cache size,存不下导致
6.4 Cache Memories
CPU 缓存内部的 查找逻辑 必须 简单 高效,所以完全由硬件实现,内部结构如下;

valid bit 指出当前 line 中的数据是否正被使用,是否有效
CPU 将目标数据的 地址 送往 Cache,Cache 将地址分为 三个域:
| t bits | s bits | b bits |
|---|---|---|
| tag 标签位 | set index 集合索引 | block offset 块内偏移 |
- 【Step 1】比对 set index ,是否命中 set
- 【Step 2】set 命中,对比 set 中的 tag,是否命中 line,命中后判断 valid bit 是否有效
- 【Step 3】根据 offset 从 block 中取出数据
空间局部性 决定 CPU 前后几次访问的地址大概率相邻,在 set index 表示的地址范围内变化,因此先用 set 索引,再用 tag 索引,效率更高;作为索引,set 总数 = 2 S e t 地址占有位数 2^{Set 地址占有位数} 2Set地址占有位数;
6.4.1 Cache Read
- 直接映射缓存 ( Direct Mapped Cache ),每个 set 只有一个 line;

这里可以看到 直接映射缓存(E=1)的缺点,最高位 tag 总是在冲突,与内存 block 交换,再冲突,再换内存… 没能很好实现 缓存 的目标:
E-way Set Associative Cache
以 2-路组相联高速缓存( E = 2 )为例;
为了与 直接映射缓存 进行公平的比较,Cache 的 block 总数不变,每组 set 中的 Line 数量翻倍,因此 set 的总数需要减半:
Fully Associative Cache
set 只剩一个就成为 ”全相联高速缓存“;
tag 值的对比 依赖于 特殊硬件,关联性提升是以 硬件复杂度提升 作为代价的,“全相连” 成本过高,通常只会以软件的形式出现(Virtual memory)。
截止2015年,相联性最强 的芯片 来自 Intel 16路组相联 L3 高速缓存。
如何选定缓存组数?
Intell:
【step 1】选定 Block Size = 64 Bytes
【step 2】选定 Cache 大小
【step 3】选定每组路数 ( Line )
【step 4】组数 (Set)
如果 line 中 block 被修改,那么该 block 被覆写前,应当写回 Memory,如何选择缓存中被覆写的块 ( LRU, least recently used ),等内容 详见 Virtual memory。
6.4.2 Cache Write
What to do on a write-hit ?
- Write-through,立即写回下级 (k+1) 存储,Cache 始终 mirrors Memory,但过于耗时
- Write-back,直到 conflict miss 出现要求替换 line 时,才将数据写回 下级 (k+1) 存储
- 需要一个标志位 (dirty bit),表明 Cache 中的 block 有修改
What to do on a write-miss ?
- Write-allocate,从下级 (k+1) 存储拷贝到 Cache 的 line 中,更新 line 中的 block
- No-write-allocate,直接写到 memory,不经过拷贝 block 到 Cache 中的过程
6.4.3 Writing Cache Friendly Code
递归查找数据,L1 ⇒ L2 ⇒ L3 ⇒ Memory (DRAM) ,但这些 Cache 的 block 大小均为 64 Bytes
- 衡量Cache性能的指标 (metrics)
- Miss Rate,未命中率, m i s s e s a c c e s s e s \frac{misses}{accesses} accessesmisses,1 - hit rate;
- Hit Time,查找set等确认hit花费的时间,Intell中L1花费4个时钟周期,L2花费10个,还有向Registers 返回值花费的时间;
- Miss Penalty,未命中需额外花费的时间,从 Memory 拿数据的50~200个时钟周期;
由此可以看出,系统为一次 miss 付出的代价十分巨大,99%的命中率的系统 比 97%命中率的系统 在性能表现上好两倍,比如 hit time = 1 cycle,miss penalty = 100 cycles:
- 97% hit rate,Average Access Time = 1 * 97% + 100 * 3% = 4 cycles
- 99% hit rate,Average Access Time = 1 * 99% + 100 * 1% = 2 cycles
查找、命中、缓存、等等这些机制,都是有硬件实现自动执行的,没有显式操作的方法,我们能做的只有写出 Cache friendly 的代码,保证更高的命中率。
代码中"循环"出现的频率很高,而嵌套循环的最里层循环出现次数最多,因此以里层循环举例:
- 重复引用同一个局部变量 是友好的,访问寄存器 (Temporal locality)
- 引用 全局变量 不友好,生命周期很长,需要访问内存才能同步
- ++=1 单步自增 是友好的,块大小限制下,命中率高 (Spacial locality )
The Memory Mountain :教科书封面
- Read throughput ( read bandwidth ),每秒能够从内存中读取的字节数,MB/s
- Memory Mountain,plot ( Read throughput ,spacial locality,temporal locality ) 表征系统性能
long data[MAXLEN]; //global vectors will be in memory
int test(int elems, int stride)
{
long i,sx2=stride*2,sx3=stride*3,sx4=stride*4;
long acc0=0, acc1=0, acc2=0, acc3=0;
long length = elems,limit = length - sx4;
// long占4个字节, 步进=4,地址增加16字节
for (i=0; i<limit; i += sx4)
{
// 这里展开成acc0 .. acc4是为了充分利用Intell core的并行能力(超标量乱序执行)
acc0 = acc0 + data[i];
acc1 = acc1 + data[i+stride];
acc2 = acc0 + data[i+sx2];
acc3 = acc0 + data[i+sx3];
}
//最后小于sx4一步的其他元素累计到acc0
for (;i<length;i++)
acc0 = acc0 + data[i];
return ( acc0 + acc1 + acc2 + acc3);
}
每组 test ( elems, stride ) 需要调用 test 两次,第一次 warm up 避免 Cold Miss,第二次才是测量值。

- 随着 stride 的增大,程序的空间局部性变差,吞吐量下降;
- 当 stride × \times × sizeof(type) > block,将无法从 Spacial Locality 中获益,每次访问都会 miss;
- 随着 elems 的增大,会出现更多的 Capacity Miss,吞吐量下降;
- s1=1 时,L2 到 L3 没有出现 Ridge,可能是因为 L2 相关硬件识别出 step=1,提前从 L3 中取出 L2 miss 的 block;
Spacial Locality Improvement
- 假设 Block = 32 Bytes = 4 × \times × sizeof ( double ),且 n 极大,同前,重点关注内部循环
- 循行访问矩阵a,4次访问1次miss为命中率25%,循列访问矩阵b,次次miss未命中率100%,单次内循环平均miss 1.25 次;

for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
{
double sum=0; // a,b元素为double
for(int k=0;k<n;k++)
sum += a[i][k] * b[k][j];
c[i][j]=sum;
}
- 循行访问矩阵b,命中率25%,循行访问矩阵c,命中率25%,单次内循环平均miss 0.5 次;

for(int k=0;k<n;k++)
for(int i=0;i<n;i++)
{
double r = a[i][k];
for(int j=0;j<n;j++)
c[i][j] += r * b[k][j];
}
- 循列访问矩阵a,命中率100%,循列访问矩阵c,命中率100%,单次内循环平均miss 2次;

for(int j=0;j<n;j++)
for(int k=0;k<n;k++)
{
double r = b[k][j];
for(int i=0;i<n;i++)
c[i][j] += a[i][k] * r;
}
总之,“行”扫描模式的空间局部性好,命中率高,而计算结果又要保存到 c 中,因此 inner loop 只能是 c 和 {a/b} 的“行”扫描访问。
“写” 比 “读” 数据处理起来要简单得多,"读"如果miss,除了等待什么事也干不了。
Temporal Locality Improvement
还是 n
×
\times
× n 矩阵乘法,当 n >> block size 时,可以利用分块的思想减少 Cache block 置换的次数。
假设 Cache Block 为 64 Bytes,矩阵元素为 double 类型 8 Bytes,则每个Block可放 8 个元素:

- 未采取分块策略,每次内循环 A矩阵 “行” 遍历共 miss n 8 \frac{n}{8} 8n 次,B矩阵 “列” 遍历 (每次都 miss) 共 miss n 次,总计 9 n 8 \frac{9n}{8} 89n 次,矩阵 A 与 B 相乘 共 miss 9 8 n 3 \frac{9}{8}n^3 89n3 次。
- 采取分块策略后,每个 sub_block 大小 B * B 个元素,A的 sub_block 中 “行” 遍历每 8 次 miss 1次 共计
B
8
\frac{B}{8}
8B,B的 sub_block 中 “列” 遍历 (每次都 miss) 共 miss B 次,因此 sub_block 中总计 miss
B
2
8
\frac{B^2}{8}
8B2 次;
以 sub_block 为元素遍历相乘,A 行 miss n B \frac{n}{B} Bn 次,B 列 miss / f r a c n B /frac{n}{B} /fracnB,共计 miss 2 n B \frac{2n}{B} B2n 次,行 与 列 sub_block 相乘共计 miss 2 n B ⋅ B 2 8 = n B 4 \frac{2n}{B} \cdot \frac{B^2}{8} = \frac{nB}{4} B2n⋅8B2=4nB 次;
A 与 B 相乘得到 C 共计 miss n B 4 ⋅ ( n B ) 2 = n 3 4 B \frac{nB}{4} \cdot (\frac{n}{B})^2 = \frac{n^3}{4B} 4nB⋅(Bn)2=4Bn3 次; - 可见 B 越大 miss 越少,但因为要将 A、B、C 的 sub_block 同时塞进 Cache,因此要求
3
⋅
B
3
<
3\cdot B^3 <
3⋅B3< Cache Size,想想多级 Cahe,block 后一次 Memory 存取就计算完 Block大小的元素;
Ch7.Linking
7.1 Why and how
Why Linkers ?
- Reason 1: Modularity
- 程序源码可拆解,而不是一个“傻大块” ( monolithic )
- 可用来构建通用库
- Reason 2: Efficiency
- 节约时间,因为可拆解,每个文件单独编译,修改时只用编译一个文件
- 节约空间,库函数被聚合 (aggregated) 到一个源文件,但只有真正被调用到的函数才会被编译链接进 exe 中
linux> gcc -Og -o prog main.c sum.c
linux> ./prog
GCC在其中充当 翻译 和 链接的角色

What Do Linkers Do ?
- Step 1: Symbol resolution
- 符号的定义 ( define ) 和 引用 ( reference ),包括 全局变量 和 函数(临时变量 在 栈上);
- Assembler 将 符号的定义 存储在 object 文件中,以 结构体数组的 形式组成 符号表 ( Symtab ),每个结构体由 { 符号名称,大小,地址 } 等信息组成;
- Linker 将 符号引用 与 符号定义 相关联( 如果符号重名,负责决定 后续引用哪一个定义 );
- Step 2: Relocation
- 将多个文件中分散的 code、data、等 section 合并 ( merges ) 到同一文件中 (.exe) 组成segment;
- 合并前,函数存放在 obj 文件中的 偏移地址 ( relative locations ) 上;合并时 给符号分配 内存绝对地址 ( absolute memory locations );
- 合并后,将 所有对符号的引用 替换为 ( bind to ) 对应的 “新地址” ;
7.2 Link and load
7.2.1 Object Files
- 三类 obj 文件
- Relocatable Object File ( .o )
汇编器产出,One .o file <==> One .c file,二进制形式的 代码 及 数据,无法直接加载到内存中执行; - Executable object file ( a.out )
可以直接被加载到内存并执行,历史上 Unix 开发人员对 可执行文件 的缺省命名 是 a.out,所以沿用至今; - Shared object file ( .so )
可以在程序 加载 或 运行 过程中被动态加载内存,并进行链接 的特殊 Relocatable obj 文件,这种技术主要被用于构建共享库,在 Windows 上又被称为 DLLs ( Dynamic Link Libraries );
- Relocatable Object File ( .o )
- Executable and Linkable Format
- obj 文件的 二进制标准格式 ( Standard binary format ),.o,a.out,.so 文件都会遵循;
- ELF 文件结构如下
| Sections | Contents |
|---|---|
| ELF header | 包含 Word size,byte ordering, file type (.o,exec,.so),machine type,etc |
| ( Segment header tables ) | 只存在于 exec 文件中,包含 Page size, virtual addresses memory segments ( 各个段落在内存中的位置 ),segment sizes 等 Linker 将obj文件中功能相同的section组合成segment |
| .text section | Code 代码段 |
| .rodata section | Read only data,包含 jump tables 等 |
| .data section | 包含 初始化 的全局变量 static variables |
| .bss section ( Block started by symbol,IBM ) | 包含 未初始化 过的全局变量 不占用.o文件的空间 程序加载时候需要这些变量,才分配空间 |
| .symtab section | Symbol resolution 阶段生成 包含以下符号的入口 ( entry ),如 procedure global variables static 修饰的任何东西 |
| .rel.text section | 包含 代码段 重定位信息 汇编器 输出 告知 Linker 加载到 memory 时需要重定位的符号 |
| .rel.data section | 包含 数据段 重定位信息 汇编器 输出 告知 Linker 加载到 memory 时需要重定位的符号 |
| .debug section | Symbolic debugging info 包含 源代码 到 机器码 的行数关联 gcc 编译时 -g 参数开启调试 |
| Section header table | 表明上述 Section 的起始位置 |
待求证
初始化过的 global variables 在 程序载入内存时 被赋予 (.data 中存储的) 初始值;
未被初始化的 global variables,仅在.bss中留有符号说明,并不实际占用空间,在载入过程中被赋 0
//main.c
#include <stdio.h>
int sum(int *a, int n);
int array[2] = {1, 2};
int main()
{
int val = sum(array, 2);
return val;
}
//sum.c
int sum(int *a, int n)
{
int i, s = 0;
for (i = 0; i < n; i++)
{
s += a[i];
}
return s;
}
[root@VM-4-10-centos] gcc -c -g main.c
[root@VM-4-10-centos lighthouse]> readelf main.o -h
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 784 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 12
Section header string table index: 11
[root@VM-4-10-centos lighthouse]> readelf main.o -s
Symbol table '.symtab' contains 13 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 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000004 4 OBJECT LOCAL DEFAULT 4 a
6: 0000000000000000 0 SECTION LOCAL DEFAULT 6
7: 0000000000000000 0 SECTION LOCAL DEFAULT 7
8: 0000000000000000 0 SECTION LOCAL DEFAULT 5
9: 0000000000000000 8 OBJECT GLOBAL DEFAULT 3 array
10: 0000000000000000 4 OBJECT GLOBAL DEFAULT 4 b
11: 0000000000000000 47 FUNC GLOBAL DEFAULT 1 main
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND sum
- Symbols
- Global Symbols
在一个模块 ( .c ) 中定义,其他模块中 ( .c ) 引用;
没有被static声明的 ( non-static ) 函数 或 全局变量 - External symbols
在一个模块中被引用,其他模块中被定义; - Local symbols
定义 与 引用 仅出现在同一个模块中;
- Global Symbols
注意
Local symbols 指的是 static 全局变量 和 static 函数 ( private function ),而不是由 Compiler 在栈上操作的 局部变量 ( local program variables );
所以其实 static 的主要功能之一是私有化全局变量和函数,限制 scope 为函数内 or 模块内,实现模块的封装;
7.2.2 Symbol resolution
int f()
{
static int x = 0; // static属性,初始化过,存.data节
return x; // 作用域 限制在函数内
}
int g()
{
static int x; // static属性,没有初始化,存.bss节
return x;
}
How Linkers Resolves Duplicate Symbol Definitions
- 【Strong Symbols】函数 和 初始化 的全局变量
- 【Weak Symbols5】没有初始化 的全局变量
Linker’s Symbol Rules
- Multiple strong symbols is not allowed !
- All reference go to the strong symbol !
- Mutiple weak symbols,choose an arbitrary one !
gcc 的 -fno-common 选项可指定 链接时 不允许多个 同名 弱符号 存在
- 类型相同,编译可以通过,运行时引用同一个 x
//main.c
int x;
int p(){};
//sum.c
int x;
int q(){};
- 类型不同,链接时若选择 sum 中对 x 的定义,x 与 y 分配的地址相差 sizeof(int),sum.c 中对 x 的double的写操作指令会践踏 y;
待 实验验证 ???
Linker Puzzles are perplexing and confounding
//main.c
int x;
int y;
int p(){};
//sum.c
double x;
int q(){};
- 有强选强,直接选 main.c 中的 x
//main.c
int x=7;
int p(){};
//sum.c
double x;
int q(){};
Global Variables
- Avoid to use global variables if you can;
- 用 static 将作用域限制在模块(.c 或 .o)内;
- 初始化 global variables,促使 Linker 进行 强符号 连接检查;
- 用 extern 声明 global variables 是一个外部变量;
7.2.3 Relocation
所有程序都由 lib.c 中的 startup code 执行初始化,传递argc、argv 开始 run;main 执行结束后,return 到 lib.c 中的 startup code;

Linker 需要安排 这些 symbols 在 executable obj 中具体的存放位置;
那么,哪些 Symbols 需要被安排?
Compiler 创建 relocation entry 来提示 Linker 哪些 symbols 是需要 relocation 的,并将这些 Relocation Entry 存放在 Relocaion section 中;
[root@VM-4-10-centos lighthouse]> readelf main.o -r
Relocation section '.rela.text' at offset 0x238 contains 4 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000000e 00090000000a R_X86_64_32 0000000000000000 array + 0
000000000013 000c00000004 R_X86_64_PLT32 0000000000000000 sum - 4
00000000001c 000400000002 R_X86_64_PC32 0000000000000000 .bss + 0
000000000027 000a00000002 R_X86_64_PC32 0000000000000000 b - 4
Relocation section '.rela.eh_frame' at offset 0x298 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
[root@VM-4-10-centos lighthouse]> objdump -d main.o
main.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: be 02 00 00 00 mov $0x2,%esi
d: bf 00 00 00 00 mov $0x0,%edi
12: e8 00 00 00 00 callq 17 <main+0x17>
17: 89 45 fc mov %eax,-0x4(%rbp)
1a: 8b 15 00 00 00 00 mov 0x0(%rip),%edx
20: 8b 45 fc mov -0x4(%rbp),%eax
23: 01 c2 add %eax,%edx
25: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
2b: 01 d0 add %edx,%eax
2d: c9 leaveq
2e: c3 retq
- main.s 中 调用sum前,相对地址 0x0d上是一条 mov指令,用于向 %rdi 传入 array 地址,由于还没有 linking,所以用 0x0 表示;
- main.o 中 ‘.rela.text’ 提示 Linker,相对地址 0x0e 上,是 mov 指令的 Src操作数,需要替换为一个 32位的地址,即 array 在 linking 后的绝对地址;
- 同理,Compiler 现阶段不知道 sum 函数的地址,在相对地址 0x13 上用 0x0 表示 sum 的地址,并在 ‘.rela.text’ 中告知 Linker 有一个 32 位 PC ( Program Counter ) 相对地址 需要替换;(为什么是 sum - 4 ?)
关于 PC 和 IP 寄存器
Intell 使用 CS:IP,即{ Code Segment << 4 + Instruction Pointer } 两个寄存器存储、表示 下一条指令的地址,非Intell厂商 及 CS类教材 使用 PC (Program Counter) 存储 下一条指令的地址,本质上功相同
[root@VM-4-10-centos lighthouse]> gcc main.o sum.o
[root@VM-4-10-centos lighthouse]> objdump -d a.out > obj.txt
[root@VM-4-10-centos lighthouse]> cat obj.txt
0000000000400536 <main>:
400536: 55 push %rbp
400537: 48 89 e5 mov %rsp,%rbp
40053a: 48 83 ec 10 sub $0x10,%rsp
40053e: be 02 00 00 00 mov $0x2,%esi
400543: bf 20 10 60 00 mov $0x601020,%edi
400548: e8 18 00 00 00 callq 400565 <sum>
40054d: 89 45 fc mov %eax,-0x4(%rbp)
400550: 8b 15 da 0a 20 00 mov 0x200ada(%rip),%edx # 601030 <a>
400556: 8b 45 fc mov -0x4(%rbp),%eax
400559: 01 c2 add %eax,%edx
40055b: 8b 05 cb 0a 20 00 mov 0x200acb(%rip),%eax # 60102c <b>
400561: 01 d0 add %edx,%eax
400563: c9 leaveq
400564: c3 retq
0000000000400565 <sum>:
400565: 55 push %rbp
400566: 48 89 e5 mov %rsp,%rbp
400569: 48 89 7d e8 mov %rdi,-0x18(%rbp)
40056d: 89 75 e4 mov %esi,-0x1c(%rbp)
400570: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%rbp)
400577: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
40057e: eb 1d jmp 40059d <sum+0x38>
400580: 8b 45 fc mov -0x4(%rbp),%eax
400583: 48 98 cltq
400585: 48 8d 14 85 00 00 00 lea 0x0(,%rax,4),%rdx
40058c: 00
40058d: 48 8b 45 e8 mov -0x18(%rbp),%rax
400591: 48 01 d0 add %rdx,%rax
400594: 8b 00 mov (%rax),%eax
400596: 01 45 f8 add %eax,-0x8(%rbp)
400599: 83 45 fc 01 addl $0x1,-0x4(%rbp)
40059d: 8b 45 fc mov -0x4(%rbp),%eax
4005a0: 3b 45 e4 cmp -0x1c(%rbp),%eax
4005a3: 7c db jl 400580 <sum+0x1b>
4005a5: 8b 45 f8 mov -0x8(%rbp),%eax
4005a8: 5d pop %rbp
4005a9: c3 retq
4005aa: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)
链接后生成 a.out,使用 objdump -d 反汇编可得:
- 地址 0x400544 上,array 的地址由 Linker 分配为 0x601020;
- PC (Program Counter) 始终指向下一条指令的地址,因此执行 “callq” 指令(0x400548)时,PC = 0x40054d,所以在链接阶段,Linker 将地址 0x400549 上的4字节,即 “callq” 的操作数,替换为 0x18,使得执行 “callq” 后,程序跳转到 PC + 0x18 = 0x400565 地址上执行sum函数;
- 0x18 是 补码 ( two’s complement ),意味着作为 “callq” 指令的操作数,地址偏移量 可负(向前跳转)可正(向后跳转);
为什么链接前 main.o 的 '.rela.text' 中,sum 的临时地址填为 "sum - 4" ?(1) 由上可知,Callq 指令会跳转到 { PC寄存器值 + 操作数Src } 的地址上,Src 是 R_X86_64_PC/PLT32类型 的 symbols;
(2) Linker 只 "傻乎乎" 的按照指示计算替换这些 symbols 的地址,计算都是 Compiler 做的;
(3) 为了使 Linker 在链接阶段,正确计算并替换 "callq" 这类PC相对寻址,Compiler 明确告知 Linker,操作数 Src 应当被替换为 &sum - &Src - sizeof(Src),即jmp的 偏移地址 还需要减去 偏移地址 本身的大小 = 四字节;
7.2.4 Memory Layout
链接后的可执行文件
| section | Content |
|---|---|
| ELF header | Start Addr = 0x00 |
| Program header table Necessary for executables ! | |
| .init section | |
| .text section | |
| .rodata section | jump tables, etc |
| .data section | |
| .bss section | |
| .symtab section | |
| .debug section | .rel.data 和 .rel.text 只在"链接"时发挥功能 链接后已经被去掉 |
| .line | |
| .strtab | Highest address |
| Content | Address |
|---|---|
| Kernel virtual memory | 用户态不可见,仅限内核使用 访问即造成 Segment fault |
| User stack (created at runtime) | 向下生长 |
| “大堆栈” ( for huge object ) | 在请求超大块空间时出现 推测 malloc 不同大小块时的 分配策略 不同 |
| (%rsp stack pointer) | |
| Memory-mapped region for shared libraries | .so 文件 |
| 堆顶 被 全局变量 “brk” ,break 指着,由内核维护 | |
| Run-time heap (create by malloc) | 向上生长 |
| Read/write data segment (.data, .bss) | 变量被初始化为 symtab 中记录的值 |
| Read-only code segment (.init,.text,.rodata) | 原封不动的拷贝到内存 用户程序实际被加载到的地址 |
| Unused | 0x00~0x3FFFFF Linux程序保留4M大小的地址 |
7.3 Library
7.3.1 Packaging
程序员总是在抽象,因此需要将功能封装成库,定义API,提供给用户(其他程序员)使用;
- 【Option.1.】所有函数放一个 Src 文件,生成一个巨大的 obj,浪费 时间 与 空间(编译链接加载未用函数);
- 【Option.2.】一个函数一个 obj,explicitly 链接用到的 obj,会给使用者造成巨大负担(哪个被用到?一长串的 gcc 命令)
7.3.2 Static Libraries
老掉牙的解决方案,来自 Unix 开发 stuff,采取Option.2.的策略:
A concatenated collection of obj files,called "archive(归档)files ";
Linkers 通过头部 的 内容表 查找到对应 obj 文件块 的偏移地址,选择性链接;
[Unix]$ ar --help
commands:
d - delete file(s) from the archive
m[ab] - move file(s) in the archive
p - print file(s) found in the archive
q[f] - quick append file(s) to the archive
r[ab][f][u] - replace existing or insert new file(s) into the archive
s - act as ranlib
t - display contents of archive
x[o] - extract file(s) from the archive
[Unix]$ ar rs libsum.a sum.c
ar: creating libsum.a
[Unix]$ ls
a.out libsum.a main.c main.o obj.txt sum.c sum.o
C的标准库 libc.a 约有超过1500个 obj 文件,4.6MB,包括 I/O,memory allocation,signal handling,string handling,data and time,random numbers,integer math
标准数学库 libm.a 同理,444个 obj 文件,2MB,包括 floating point math (sin,cos,tan,log,sqrt…)
如果需要更新其中某个 函数的代码,如 printf 的实现,就需要 重新compile,重新archive;
//只将sum.c直接归入libsum.a,剔除sum.o,则编译会直接报错
[Unix]$ ar q libsum.a sum.c
[Unix]$ ar -d libsum.a sum.o
[Unix]$ ar -t libsum.a
sum.c
[Unix]$ gcc main.c -L. -lsum
./libsum.a: error adding symbols: Archive has no index; run ranlib to add onecollect2: error: ld returned 1 exit status
举例,将 7.2.1 中的 sum.c 编译成 libsum.a 后,将 main.c 编译链接成可执行程序的大致过程如下:
Linker’s algorithm for resolving external reference
- Scan (*.o)、(*.a) files in command line order;( ⇒ command line order 很关键 )
- During the scan, keep a list of the current unresolved references;
- Everytime new (*.o)、(*.a) is encountered,try ro resolve each unresolved reference in the list;
- At end of scan, any entries in unresolved list causes error. ( ⇒ 库 朝后放)
[Unix]$ gcc -L. -lsum main.o
main.o: In function `main':
main.c:(.text+0x13): undefined reference to `sum'
collect2: error: ld returned 1 exit status
Disadvantages of static libraries
- 一些基础的 库函数(libc) 会被重复拷贝到每一个 exe 中;
- exe 加载到内存中执行时也会包含重复的内容,吃 系统内存;
- 库的一个小改动 将导致所有依赖它的 可执行文件 需要重新链接;
7.3.2 Shared Libraries
更 modern 的解决方案,也可以称为 dynamically link libraries( Windows 上则是 DLLs)
- 所有系统上运行的 exe 共享6同一个加载到内存中的 库 (*.so) 实例 ( Instance )
- 动态库 的 data 和 code 并不是在 exe 编译 和 静态链接 时合入,而是 “Dynamic linking”
load-time linking
- 首次载入内存时链接,Linux 上较为常见,函数调用交由 dynamic linker(ld-linux.so)自动处理;
[Unix]$ gcc -shared -o libsum.so sum.c
[Unix]$ gcc -c -o main.o -I. main.c
[Unix]$ ls
libsum.so main.c main.o sum.c
[Unix]$ gcc libsum.so main.o
[Unix]$ ./a.out
./a.out: error while loading shared libraries: libsum.so: cannot open shared object file: No such file or directory # 记得 libsum.so 的路径添加到 环境变量 $LD_LIBRARY_PATH 中
[Unix]$ export LD_LIBRARY_PATH=./:$LD_LIBRARY_PATH
[Unix]$ ./a.out #成功执行
[Unix]$
[Unix]$ readelf -r a.out #看看sum的动态链接标记
...
Relocation section '.rela.plt' at offset 0x4c8 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
000000601018 000400000007 R_X86_64_JUMP_SLO 0000000000000000 sum + 0
手动 调用链接器能否完成链接 ?
[Unix]$ export LIBRARY_PATH=./:$LIBRARY_PATH [Unix]$ ld main.o libsum.so ld: warning: cannot find entry symbol _start; defaulting to 0000000000400310 [Unix]$ ld main.o libsum.so --entry=main -lc [Unix]$ ./a.out -bash: ./a.out: No such file or directory并不能成功,为什么? _start 其实在crt0.o中
flowchart TD
A[main.c + sum.h] --gcc -c -o main.o -I. main.c--> E[main.o <br>]
B[sum.c] --gcc -shared -o libsum.so sum.c--> F[libsum.so <br>]
E --> L[Linker / ld ]
G[libc.so] --> L
F --Relocation and symtab info--> L
L --Partially linked executable--> H[a.out <br>]
H --> I[Loader <br>sys call -- execve]
I --call--> J[Dynamic linker <br>ld-linux.so]
F --loaded by linker--> J
G --loaded by linker--> J
J --Fully linked executable<br>Relocation done-->K[a.out]
#include <*.h> 由 preprocessor 处理,解释 (interpret) 并 扩展 (expand)
[Unix]$ gcc -o main.i -E main.c
run-time linking
- 运行过程中链接,通过 dlopen 接口载入动态库,常见于 Distributing Software、High-performance web server、和 Runtime Library Iterpositioning;
#include <stdlib.h>
#include <stdio.h>
#include <dlfcn.h>
int array[2] = {1, 2};
int main()
{
void *handle;
int (*sum)(int *a, int n);
char *error;
handle=dlopen("./libsum.so", RTLD_LAZY);
if(!handle)
{
fprintf(stderr,"%s\n",dlerror());
exit(-1);
}
sum=dlsym(handle,"sum");
if(!sum)
{
printf("%s\n",dlerror());
exit(-1);
}
int val = sum(array, 2);
printf("sum=[%d]\n",val);
dlclose(handle);
return val;
}
[Unix]$ gcc main.c -ldl
[Unix]$ ./a.out
./libsum.so: cannot open shared object file: No such file or directory
[Unix]$ cp ../lib/libsum.so ./
[Unix]$ ./a.out
sum=[3]
7.2.4 Library Interpositioning
Goal: Intercept function calls from libraries (截获?个人感觉有点像 “顶包” 或 "替考“ ?)
Key: Create wrappers
Usage:
- Security
- Confinement (sandboxing)
- Behind the scenes encryption
- Monitoring and Profiling
- Count number of calls to function
- Characterize call sizes and arguments to functions
- Malloc tracing
- Detecting memory leaks
- Generating address traces
Implement:
- Compile time
//====== test.c ==========
#include <stdio.h>
#include <malloc.h>
int main()
{
int *p = malloc(32);
free(p);
return(0);
}
//====== mymalloc.c ==========
#include <stdio.h>
#include <malloc.h>
/* malloc wrapper function */
void *mymalloc(size_t size)
{
void *ptr = malloc(size);
printf("malloc(%d)=%p\n",
(int)size, ptr);
return ptr;
}
/* free wrapper function */
void myfree(void *ptr)
{
free(ptr);
printf("free(%p)\n", ptr);
}
//====== malloc.h ==========
#ifdef COMPILETIME
#define malloc(size) mymalloc(size)
#define free(ptr) myfree(ptr)
void *mymalloc(size_t size);
void myfree(void *ptr);
#endif
gcc 参数 -D 很关键
[Unix]$ ls
malloc.h mymalloc.c test.c
[Unix]$ gcc -Wall test.c mymalloc.c
[Unix]$ ./a.out # 默认使用C库的 malloc
[Unix]$ gcc -Wall -DCOMPILETIME test.c mymalloc.c # 开启开关,使用魔改 mymalloc
[Unix]$ ./a.out
malloc(32)=0xbb32a0
free(0xbb32a0)
- Link time
//====== mymalloc.c ==========
#include <stdio.h>
void *__real_malloc(size_t size);
void __real_free(void *ptr);
/* malloc wrapper function */
void *__wrap_malloc(size_t size)
{
void *ptr = __real_malloc(size); /* Call libc malloc */
printf("malloc(%d) = %p\n", (int)size, ptr);
return ptr;
}
/* free wrapper function */
void __wrap_free(void *ptr)
{
__real_free(ptr); /* Call libc free */
printf("free(%p)\n", ptr);
}
gcc 参数 -Wl 表示,将其后紧跟的参数中的逗号 “,” 替换为 空格,传递给 Linker;
Linker 收到 --wrap func 参数后,会将 对 “func()” 的调用,替换为对符号 “__wrap_func()” 的调用
同时,将 func.o 中对 “__real_func()” 的调用,替换为对符号 "func()"的调用
本质和 compile 原理一样,只是 “偷换数名“ 这个动作被 Linker 默认实现了
[Unix]$ gcc -c test.c
[Unix]$ gcc test.o
[Unix]$ ./a.out #默认使用C库 malloc
[Unix]$ gcc -c mymalloc.c
[Unix]$ gcc test.o mymalloc.o # 因为 __real_malloc 未定义而使用不了
mymalloc.o: In function `__wrap_malloc':
mymalloc.c:(.text+0x14): undefined reference to `__real_malloc'
mymalloc.o: In function `__wrap_free':
mymalloc.c:(.text+0x54): undefined reference to `__real_free'
collect2: error: ld returned 1 exit status
[Unix]$ gcc -Wall -Wl,--wrap,malloc -Wl,--wrap,free test.o mymalloc.o # 关键参数 -Wl
[Unix]$ ./a.out
malloc(32) = 0x24712a0
free(0x24712a0)
“-Wl,–wrap,* " 参数名 是 固定的,不能换名,”__real_func" 同理 !!
#include <stdio.h> void *__real_malloc(size_t size); void __real_free(void *ptr); /* 把 wrap 换成 my */ void *__my_malloc(size_t size) {...} void __my_free(void *ptr) {...}gcc -Wall -Wl,--my,malloc -Wl,--my,free test.o mymalloc.o /usr/bin/ld: unrecognized option '--my' /usr/bin/ld: use the --help option for usage information collect2: error: ld returned 1 exit status
- Load / Run time
//====== mymalloc.c ==========
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
void *malloc(size_t size)
{
void *(*mallocp)(size_t size);
char *error;
mallocp = dlsym(RTLD_NEXT, "malloc"); /* Get addr of libc malloc */
if ((error = dlerror()) != NULL) {
fputs(error, stderr);
exit(1);
}
char *ptr = mallocp(size); /* Call libc malloc */
printf("malloc(%d) = %p\n", (int)size, ptr);
return ptr;
}
void free(void *ptr)
{
void (*freep)(void *) = NULL;
char *error;
if (!ptr)
return;
freep = dlsym(RTLD_NEXT, "free"); /* Get address of libc free */
if ((error = dlerror()) != NULL) {
fputs(error, stderr);
exit(1);
}
freep(ptr); /* Call libc free */
printf("free(%p)\n", ptr);
}
linux> make intr
gcc -Wall -DRUNTIME -shared -fpic -o mymalloc.so mymalloc.c -ldl
gcc -Wall -o intr int.c
linux> make runr
(LD_PRELOAD="./mymalloc.so" ./intr)
malloc(32) = 0xe60010
free(0xe60010)
linux>
#ifdef TRACE
#define _GNU_SOURCE //must be the first declaration
#include <dlfcn.h>
#define MALLOC_TRACE
#endif
#include <stdio.h>
#include <stdlib.h>
#ifdef TRACE
typedef void *HMALLOC(size_t size);
typedef void HFREE(void *ptr);
HMALLOC *libc_malloc = NULL;
HFREE *libc_free = NULL;
void *malloc(size_t size)
{
void *ptr = libc_malloc(size);
/*do whatever you want to trace malloc*/
void *caller_address = __builtin_return_address(0);
fprintf(stderr,"malloc(%u) caller [%s][%p]\n",size,__FILE__,caller_address);
/*end of your trace*/
return ptr;
}
void free(void *ptr)
{
/*do whatever you want to trace free*/
void *caller_address = __builtin_return_address(0);
fprintf(stderr,"free caller [%s][%p]\n",__FILE__,caller_address);
/*end of your trace*/
libc_free(ptr);
return;
}
void allocation_init(void)
{
libc_malloc = dlsym(RTLD_NEXT,"malloc");
if(NULL == libc_malloc)
{
printf("malloc init fail\n");
exit(1);
}
libc_free = dlsym(RTLD_NEXT,"free");
if(NULL == libc_free)
{
printf("free init fail\n");
exit(1);
}
return;
}
#endif
int main(int argc, char** argv)
{
#ifdef TRACE
allocation_init();
#endif
/*USER CODE*/
char *ptr=malloc(10);
free(ptr);
return 0;
}
Asymptotics, 渐进分析,example: f ( n ) = n 2 + 3 n f(n) = n^2 + 3n f(n)=n2+3n , lim n → ∞ f ( n ) ≈ n 2 \lim\limits_{n\to \infin} f(n) \approx n^2 n→∞limf(n)≈n2 ↩︎
CSAPP英文原版P250,中文第三版P145 ↩︎
https://www.micron.com/-/media/client/global/documents/products/technical-note/nand-flash/tn2942_nand_wear_leveling.pdf ↩︎
More detail in Virtual Memory ↩︎
533

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



