csapp第三章

重要的前言

在学习这一章节的内容之前,可以通过这些linux命令来实现c code与assembly code的转换,看起来更直观。

可以使用如下终端命令编译生成machine code文件。

gcc -Og  sum.c -o sum

可以通过如下命令进行反汇编,从machine code反汇编得到assembly code

objdump -d sum > sum.d

或者是利用gdb

gdb sum

在gdb界面输入你要分析的函数名称(例如自定义的sum.c文件里面的void sumstore())

disassemble sumstore

这一点比起微机课程垃圾软件好看、好操作多了

1.回顾汇编旧知识

imm:立即数
reg:寄存器
mem:内存

movq:

(1)src为imm时,dst只能是reg或mem
(2)src为reg时,dst只能是reg或mem
(3)src为mem时,dst只能是reg
重点1
dst是reg时:

movq $0x4,$rax

对应c语言:

temp =0x4;

而dst是mem时:

movq $0x4,($rax)

对应c语言:

*p =0x4;

(寄存器名称)意味着使用该寄存器的地址来引用一些内存位置
如果括号之前有数字,说明要在该地址偏移对应的数字,才是真正的操作地址

重点2
函数的参数总是只会用某几个寄存器来实现。
第一个参数寄存器:%rdi
第二个参数寄存器:%rsi

leaq + salq:

重点3
leaq指令经常和salq指令配合,用于计算乘法
例如:

leaq (%rsi,%rsi,2),%rdx
salq $4,%rdx

相当于先将寄存器%rsi的值乘以3存到%rdx,再将%rdx的值左移4位(也就是乘以16)

cmpq,jmp,cmovle:

重点4:避免条件移动
对于C语言中的比较语句,可能你会想到使用汇编的jmp,但这不是一个好主意。
因为编译器会有个特性:执行当前语句的时候,实际上会提前执行之后的很多条语句。
如果遇到了条件语句,例如见到if,gcc会很想使用条件移动conditional move,但是这一类(例如jmp)的条件移动语句是很不好的。
条件移动实际上会使得程序把两个方向都做一遍,然后在判断的时候猜哪个方向是正确的。虽然大部分时间会猜对,但一旦猜错,会浪费很多很多时钟周期(最坏情况是40个时钟周期)去纠正。
更坏的情况是,其中一个方向做的操作是释放为空指针,如果做了这个方向,会认为是解除了引用,从而产生了。
你要做的就是尽量避免让gcc去做条件移动,也就是避免让gcc去“猜”。

可以放心去做条件移动的情况:两个方向都只是简单的算术运算,相对安全没有副作用

因此最好的解决方法就是,提前将then语句要做的处理和else语句要做的处理提到前面,在if判断时,修改成:如果if条件为真,则采用then语句得到的结果,否则采用else语句得到的结果
这个思想用在assembly code上非常重要,可以将jmp+goto的语句组合转化为只有cmp+cmovle一类的语句。关键:先做完两种情况的处理,在最后一时刻再来做选择

loop:

while(x)循环可以转化为do-while(x)循环前面再多加一次条件(x)测试
也就是修改成:

//while(x)
//rewrite in Goto Version
if(!x) goto done;
loop:
	//statements
	if(x) goto loop;
done:
	//out of loop

对于for循环,只需要将每一部分拆解开来变成while循环即可

for(init;test;update){
	body;
}
//for
//retrite in while version
init;
while(test){
	body;
	update;
}

//rewrite in goto version
init;
if(!test) goto done;
loop:
	body;
	update;
	if(test) goto loop;
done:
	//out of loop

实际上,-O1级别优化编译的情况下,上面的情况会修改成:将初始的条件判断test语句放到update和跳回loop的语句中间,也就是这样:

//-O1
init;
//if(!test) goto done;
loop:
	body;
	update;
	if(test) goto loop;
done:
	//out of loop

原因是大部分时间不需要这个初次test

switch

switch不好的地方在于,一是你可能漏掉了可能的情况case,二是你可能漏掉了应该有的break,这两点都使得switch容易出bug。
因此用的时候,最好在你故意没有添加break的地方注释“这里我是真的不需要用break,因为我要实现某个功能”。还要记得写default:

assembly code中对switch的实现方式是:

switch(x){
	//...
}
switch_eg:
	movq 	%rdx,%rcx
	cmpq 	$6,%rdi			#x:6
	ja		.L8				#use default
	jmp		*.L4(,%rdi,8)	#goto *JTab[x]

这段代码已经有不少的技巧。
技巧1:ja。使用无符号数比较,实现default。x大于6或者小于0都会跳到default
技巧2:jmp。跳转至实际的跳转表。break将转换为ret。

.section		.rodata
	.align 8
.L4:
	.quad		.L8	#x=0
	.quad		.L3	#x=1
	.quad		.L5	#x=2
	.quad		.L9	#x=3
	.quad		.L8	#x=4
	.quad		.L7	#x=5
	.quad		.L7	#x=6
	.quad		.L8	#x=0

.align的作用是

注:
1.如果第一个case的数字是负数或者是很大的数,编译器会帮你添加一个偏置,使得编译出来的第一个case值仍然是0
2.如果case用到的值很大而且分布比较稀疏、不是邻近的值,编译器不会傻傻的列出很多很多项.quad的表,而是会自动理解成if-else代码,并据此生成一个if-else tree,从而将时间复杂度从O(n)减少至O(log2(n))
3.switch在编译时会生成一个表,这一点实际上是会提升性能的,上面说的任何一种情况下使用switch总是优于if-else。当然前提是你要弄彻底明白switch在assembly code中的实现机理。

总结

C中的控制语句有:
if-then-else
do-while
while,for
switch

汇编中的控制有:
条件跳转
条件移动
使用跳转表(参考switch)
编译器生成一系列代码序列以实现更复杂的C控制语句

基本技巧:
loop转换为do-while或者是jump-to-middle的形式
switch使用跳转表
稀疏case的switch可能使用if-elseif-elseif-else的决策树形式实现

猜你喜欢

转载自blog.csdn.net/jieyannn/article/details/105199471