概述
文章目录
- 重要的前言
- 1.回顾汇编旧知识
- movq:
- leaq + salq:
- cmpq,jmp,cmovle:
- loop:
- switch
- 总结
重要的前言
在学习这一章节的内容之前,可以通过这些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的决策树形式实现
最后
以上就是大意金鱼为你收集整理的csapp第三章重要的前言1.回顾汇编旧知识的全部内容,希望文章能够帮你解决csapp第三章重要的前言1.回顾汇编旧知识所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复