CMU 15-213 CSAPP (Ch5~Ch7)

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站弹幕举了个很有意思的例子:
只有两个锅,一个饼一个面要煎一分钟,煎三个饼最快要多长时间?

step 1
饼A正面
饼B正面
step 2
饼A反面
饼B反面
step 3
空锅
饼C正面
step 4
空锅
饼C反面

花费了4分钟,问题出在最后会空锅浪费时间。换个方法:

step 1
饼A正面
饼B正面
step 2
饼A反面
饼C正面
step 3
饼C反面
饼B反面

之前的空锅,是因为一个饼不能同时煎正反两面,所有的饼又都一样,正面一分钟,反面一分钟,通过C的正面置换B的反面,使得step3时,B的反面没煎,C的反面也没煎,最后一步可以将两个锅都利用起来。
核心思想,就是将一个动作分解为更细的阶段动作,调整先后顺序,用阶段动作填补空闲
假设每个乘法指令被分解为三个阶段,每个阶段又对应有专用的硬件设施(dedicated hardware):

long l1 = a*b;
long l2 = a*c;
long res = l1*l2;
StagesCycle 1Cycle 2Cycle 3Cycle 4Cycle 5Cycle 6Cycle 7
1a * ba * cl1 * l2
2a * ba * cl1 * l2
3a * ba * cl1 * 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
VarietiesTrans/cellAccess timeRefreshEDCCostApplications
Static RAM4 or 61 XNo needMaybe100XCache memories
Dynamic RAM110 XNeedYes1XMain memories frame buffers
  • Nonvolatile Memory
    • 存储 Firmware(BIOS、磁盘、网卡、显卡、图形加速器、安全系统 …)
    • 固态硬盘(U盘、智能手机、mp3播放器、平板 tablets、笔记本)
    • Disk Cache ???
VarietiesCharacteristics
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 纳秒
      Cache结构
  • Disk Geometry
    Cache结构
    • 厂商述硬盘容量单位 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
    • 现代硬盘 将 多个physical sectors 组成 logical blocks
      • Disk controller 负责将 blocks 映射成(surface,track,sector)三元组
      • Disk controller 为每个Zone预留一些空 Cylinder 作为坏道的替补
        格式化后,硬盘可用容量 < 标定最大容量
        Cache结构

“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控制器等,部分设备只在总线上留有接口,允许自行扩展,如网卡,独立显卡等
      Cache结构
    • 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,等
      Cache结构
  • 2003年,Intell开发了800 W W W功耗处理器,需要 4 i n c h 2 inch^2 inch2大小的 heatsink 散热,功耗随时钟频率增大,摩尔定律失效,时钟频率无法再有指数级的提升,时钟周期稳定在0.1纳秒附近;Intell另谋出路,增加 chip 的 processor core 个数,通过并行提升效率

Cache结构
  • 内存限制了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 TypeContentIn whereLatency(cycles)Managed by
costliersmallerfaster
Registers4-8 Bytes Words retrieved from the L1 cacheCPU core0Compiler
TLBAddress translationsOn-Chip TLB0Hardware MMU
L1 cache64-byte blocksOn-Chip L14Hardware
L2 cache64-byte blocksOn-Chip L210Hardware
Virtual Memory4-KB pagesMain memory100Hardware + OS
Buffer cacheParts of filesMain memory100OS
Disk cacheDisk sectorsDisk controller100,000Disk firmware
Network buffer cacheParts of filesLocal disk10,000,000NFS client
Browser cacheWeb pagesLocal disk10,000,000Web browser
Web cacheWeb pagesRemote server disks1,000,000,000Web proxy server
cheaperlargerslower
  • 整个设计结构的核心思想 ⇒ 缓存
    A smaller, faster storage device that acts as a staging area for a subset of the data in a larger, slower device.
Data is transfered in same block size units
Data is transfered in address form
CPU
rax
rbx
rcx
Cache
8
14
9
Memory
0
1
2
3
4
5
6
7
...
  • 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 Chip.
System Bus
ALU
Register file
%rax
%rbx
...
Cache Memory
Bus Interface
I/O Bridge

CPU 缓存内部的 查找逻辑 必须 简单 高效,所以完全由硬件实现,内部结构如下;
Cache结构

valid bit 指出当前 line 中的数据是否正被使用,是否有效
CPU 将目标数据的 地址 送往 Cache,Cache 将地址分为 三个域:

t bitss bitsb 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

Cache结构

递归查找数据,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,第二次才是测量值。
Cache结构

  • 随着 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 次;
    Cache结构
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 次;
    Cache结构
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次;
    Cache结构
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 个元素:
Cache结构

  • 未采取分块策略,每次内循环 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 次。
Cache结构
  • 采取分块策略后,每个 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} B2n8B2=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 < 3B3< 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在其中充当 翻译 和 链接的角色
Cache结构

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 );
  • Executable and Linkable Format
    • obj 文件的 二进制标准格式 ( Standard binary format ),.o,a.out,.so 文件都会遵循;
    • ELF 文件结构如下
SectionsContents
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 sectionCode 代码段
.rodata sectionRead only data,包含 jump tables 等
.data section包含 初始化 的全局变量
static variables
.bss section
( Block started by symbol,IBM )
包含 未初始化 过的全局变量
不占用.o文件的空间
程序加载时候需要这些变量,才分配空间
.symtab sectionSymbol resolution 阶段生成
包含以下符号的入口 ( entry ),如
procedure
global variables
static 修饰的任何东西
.rel.text section包含 代码段 重定位信息
汇编器 输出
告知 Linker 加载到 memory 时需要重定位的符号
.rel.data section包含 数据段 重定位信息
汇编器 输出
告知 Linker 加载到 memory 时需要重定位的符号
.debug sectionSymbolic 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
      定义 与 引用 仅出现在同一个模块中;

注意
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;
Cache结构
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

链接后的可执行文件

sectionContent
ELF headerStart Addr = 0x00
Program header table
Necessary for executables !
.init section
.text section
.rodata sectionjump tables, etc
.data section
.bss section
.symtab section
.debug section.rel.data 和 .rel.text
只在"链接"时发挥功能
链接后已经被去掉
.line
.strtabHighest address
载入内存后的 layout
ContentAddress
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)
原封不动的拷贝到内存
用户程序实际被加载到的地址
Unused0x00~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

atoi.c
compiler
printf.c
random.c
atoi.o
printf.o
random.o
Archiver
libc.a

标准数学库 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 编译链接成可执行程序的大致过程如下:

main.c + sum.h
gcc -c
sum.c
main.o
sum.o
Linker / ld
Archiver
libc.a
printf.o
other necessary func
libsum.a
a.out

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;
}

  1. 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 nlimf(n)n2 ↩︎

  2. CSAPP英文原版P250,中文第三版P145 ↩︎

  3. 浅聊SRAM和DRAM的区别 ↩︎

  4. https://www.micron.com/-/media/client/global/documents/products/technical-note/nand-flash/tn2942_nand_wear_leveling.pdf ↩︎

  5. Weak Symbol ↩︎

  6. More detail in Virtual Memory ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值