1, cmp, jmp指令, 先看几个C中的循环
1 int sum(int* p, int size){ 2 int sum = 0; 3 for (int i = 0; i < size; i++){ 4 sum += *(p+i); 5 } 6 return sum; 7 } 8 int sum1(int* p, int size){ 9 int sum = 0; 10 int i = 0; 11 do{ 12 sum += *(p+i); 13 i++; 14 }while(i < size); 15 return sum; 16 } 17 int sum2(int* p, int size){ 18 int sum = 0; 19 int i = 0; 20 while(i < size){ 21 sum += *(p+i); 22 i++; 23 } 24 return sum; 25 }
以上3个函数是等价的实现,都是求和,用了for, do-while, while三种C中的循环,看看对应的GAS代码
1 sum: 2 pushq %rbp 3 movq %rsp, %rbp 4 movq %rdi, -24(%rbp) # 形参 p 5 movl %esi, -28(%rbp) # size 6 movl $0, -8(%rbp) # 局部变量 sum 7 movl $0, -4(%rbp) # 局部变量 i 8 jmp .L2 9 .L3: # loop body 和 update-expr(这一块就是实现下面两行) 10 movl -4(%rbp), %eax # sum+=*(p+i); 11 cltq # i++; 12 salq $2, %rax # 得到i*4, %rax用来存放地址, 4是sizeof(int) 13 addq -24(%rbp), %rax # 得到地址,即c中的(p+i) 14 movl (%rax), %eax # *(p+i)=>%eax 15 addl %eax, -8(%rbp) # sum+=*(p+i) 16 addl $1, -4(%rbp) # i++ 17 .L2: # test-expr(和跳转) 18 movl -4(%rbp), %eax # i=>%eax 19 cmpl -28(%rbp), %eax # 比较i, size 20 jl .L3 21 movl -8(%rbp), %eax # return sum 22 popq %rbp 23 ret 24 25 sum1: 26 pushq %rbp 27 movq %rsp, %rbp 28 movq %rdi, -24(%rbp) 29 movl %esi, -28(%rbp) 30 movl $0, -8(%rbp) 31 movl $0, -4(%rbp) 32 .L5: 33 movl -4(%rbp), %eax 34 cltq 35 salq $2, %rax 36 addq -24(%rbp), %rax 37 movl (%rax), %eax 38 addl %eax, -8(%rbp) 39 addl $1, -4(%rbp) 40 movl -4(%rbp), %eax 41 cmpl -28(%rbp), %eax 42 jl .L5 43 movl -8(%rbp), %eax 44 popq %rbp 45 ret 46 47 sum2: 48 pushq %rbp 49 movq %rsp, %rbp 50 movq %rdi, -24(%rbp) 51 movl %esi, -28(%rbp) 52 movl $0, -8(%rbp) 53 movl $0, -4(%rbp) 54 jmp .L7 55 .L8: 56 movl -4(%rbp), %eax 57 cltq 58 salq $2, %rax 59 addq -24(%rbp), %rax 60 movl (%rax), %eax 61 addl %eax, -8(%rbp) 62 addl $1, -4(%rbp) 63 .L7: 64 movl -4(%rbp), %eax 65 cmpl -28(%rbp), %eax 66 jl .L8 67 movl -8(%rbp), %eax 68 popq %rbp 69 ret
对sum做了一些注释,可以看到它做的几部份事,先对在栈中给参数分配地方,形式参数也是局部变量的,然后是循环的初始条件,当然普遍来说这里应该都是局部变量的初始化, 循环的初始化之后就是循环体了,然后就是检测循环条件了, 后面两个例子包括前面一个,其实我不去仔细看,看不出个什么,先理解到这里吧。 注意上面每个循环的实现都用了cmpl指令,cmp和test是两个只设置条件码而不改变其它寄存器的指令。 第19行,cmpl -28(%rbp) %eax 前者是size,后者是i, 可以看到后面的那个跳转语句是jl ,所以可以看到是根据i和size的关系来设置的条件码,也就是条件码反应的是后者操作数和前者操作数的逻辑关系(大小,等于)。而jmp, set指令集指定的条件也是指后者操作数和前者操作数是否满足相应关系,满足则跳转。上面的三个例子都有cmpl和jl的组合。
2。set指令
下面看一个使用set指令的例子,还是一个函数,与上面三个函数功能等价
1 int sum3(int* p, int size){ 2 int sum = 0; 3 int i = 0; 4 while(i++ < size){ 5 sum += *(p+i-1); 6 } 7 return sum; 8 }
对应的GAS代码
1 sum3: 2 pushq %rbp 3 movq %rsp, %rbp 4 movq %rdi, -24(%rbp) 5 movl %esi, -28(%rbp) 6 movl $0, -8(%rbp) 7 movl $0, -4(%rbp) 8 jmp .L10 9 .L11: 10 movl -4(%rbp), %eax # i=>%eax 11 cltq 12 subq $1, %rax # i-1=>%eax 13 salq $2, %rax 14 addq -24(%rbp), %rax 15 movl (%rax), %eax 16 addl %eax, -8(%rbp) 17 .L10: 18 movl -4(%rbp), %eax 19 cmpl -28(%rbp), %eax 20 setl %al #~~~~~~~~~ 21 addl $1, -4(%rbp) # i++ 22 testb %al, %al 23 jne .L11 #~~~~~~~~~ 24 movl -8(%rbp), %eax 25 popq %rbp 26 ret
可以看到和sum2例子的区别在于, sum2把update-expr即i++放到了loop body的最后,而sum3则放在test-expr后马上i++, 看看GAS代码的区别,sum3中19~23行,在cmpl之后,jmp之前来做了i++的操作,而且jmp的依据必须是cmpl的结果,也就是这之间做的操作i++不能影响jmp的条件,用的就是set和test来实现的。 set指令和jmp的指令集完全是对应的,比如有setne,jne。setl, jl等等,jmp是满足这个比较条件则跳转,而set是满足这个比较条件则把操作数置1,否则置0, set的操作数可以是8个单字节寄存器之一,如%al, %ah, %bl, %bh,也可以是存储器中一个字节。也就是说set跟在cmp之后就把cmp的结果保存在了这个字节当中,然后就可以进行其它的操作了,如这里的i++, 然后用testb来检测刚才保存的结果,由于被testb设置的字节只可能是0或者1,1就是符合set的那个条件,0就是不符合,所以这里如果之前set使用的条件就是我们跳转想要用的条件,那在test之后就用jne就行了,也就是是1就跳转,若是相反的条件就用je, 只用这两个就足够了, 因为test一个set过的字节,只可能设置 ZF位 (这里说得有点罗嗦)
在cmp之后jmp或在cmp之后set是两种最常使用条件码的方式
3. 一个if语句的例子
没有找到一个好的例子,写了一个为测试而测试的例子,下个例子中,如果a<=0, ++b不会被执行到
1 int cond(int a, int b){ 2 if (((a>0) && (++b>0))){ 3 return a + b; 4 } 5 return a-b; 6 }
对应的GAS
1 cond: 2 pushq %rbp 3 movq %rsp, %rbp 4 movl %edi, -4(%rbp) 5 movl %esi, -8(%rbp) 6 cmpl $0, -4(%rbp) # test a>0 7 jle .L2 8 addl $1, -8(%rbp) # test ++b>0 9 cmpl $0, -8(%rbp) 10 jle .L2 11 movl -8(%rbp), %eax 12 movl -4(%rbp), %edx 13 addl %edx, %eax # a+b=>%eax 14 jmp .L3 15 .L2: 16 movl -8(%rbp), %eax # a-b=>%eax 17 movl -4(%rbp), %edx 18 movl %edx, %ecx 19 subl %eax, %ecx 20 movl %ecx, %eax 21 .L3: 22 popq %rbp 23 ret #return %eax
可以看到在c中的第二行 if((a>0) && (++b>0))条件判断时,出现了&&(||也是同样的)时,从左向右如果已经做出的判断能得到结果了,就不会再去执行后面的表达式了,从相应的GAS中可以看出来, 即如果a<=0, ++b是不会被执行到的
4, switch语句,switch语句提供了一个根据整数索引值进行多重分支的能力,注意编译器要求一定是整数,GAS有两种方式来实现,我自己试了一下,先看个c程序
1 int wy(int n){ 2 int t; 3 switch (n){ 4 case 1 : t = 1; break; 5 case 2 : t = 5; break; 6 case 500 : t = 9; break; 7 case 4 : t = 12; break; 8 case 5 : t = 16; break; 9 default : t = 0; 10 } 11 return t; 12 }
这个switch语句跳转的分支比较多,注意第6行用了一个500,比较特别,刻意用来测试的,gcc生成的GAS代码如下
1 wy: 2 pushq %rbp 3 movq %rsp, %rbp 4 movl %edi, -20(%rbp) # n 5 movl -20(%rbp), %eax 6 cmpl $4, %eax 7 je .L5 # n==4 8 cmpl $4, %eax 9 jg .L8 # n>4 10 cmpl $1, %eax 11 je .L3 # n==1 12 cmpl $2, %eax 13 je .L4 # n==2 14 jmp .L2 # default 15 .L8: 16 cmpl $5, %eax 17 je .L6 # n==5 18 cmpl $500, %eax 19 je .L7 # n==500 20 jmp .L2 21 .L3: 22 movl $1, -4(%rbp) 23 jmp .L9 # break; 24 .L4: 25 movl $5, -4(%rbp) 26 jmp .L9 27 .L7: 28 movl $9, -4(%rbp) 29 jmp .L9 30 .L5: 31 movl $12, -4(%rbp) 32 jmp .L9 33 .L6: 34 movl $16, -4(%rbp) 35 jmp .L9 36 .L2: # default 37 movl $0, -4(%rbp) 38 .L9: # switch之后,break将要跳转到的 39 movl -4(%rbp), %eax 40 popq %rbp 41 ret
可以看到基本上就是把n移到%eax中,然后一个一个比较,估计因为switch只能针对整数跳转,所以都是放在%eax中来再判断, 在上面最上面的流程中,大于4的两个数,5和500,被放在了一起,下面大于4的那块,这应该算一个优化吧,减少了平均的找寻次数。每个case块最终都会对应GAS中的一个.L块,而这些.L块的顺序一定是和c中switch下case包括default的顺序一致,因为如果没有break语句,流程是顺序向下执行的,上面这个例子中break; 对应 jmp .L9 。从.L3到.L2对应的是case 1:到default :顺序也是完全一致,而之前的.L8和再之前判断都是准备工作,为了定义到执行下面的这些哪一块。
现在把c代码中的第6行的500改成8, gcc生成的GAS变成了如下的形式
1 wy: 2 pushq %rbp 3 movq %rsp, %rbp 4 movl %edi, -20(%rbp) # n 5 cmpl $8, -20(%rbp) # 大于case里面的跳转条件,直接跳过switch语句 6 ja .L2 # 无符号的> 7 movl -20(%rbp), %eax 8 movq .L8(,%rax,8), %rax # 根据处理过后的那个数在跳转表中找到那个项,也就是应该跳转的地址 9 jmp *%rax # 间接跳转 10 .section .rodata # 注意,这里是一个section的声明了,这个section应该是整个进程空间的一块,如这里是放只读读据read-only data 11 .align 8 12 .align 4 13 .L8: # 这里的意思应该是从.L8这个地址开始,依次放了这么多个数,每个数.quad表明8个字节,放的都是地址,从.L2,.L3,..,.L7 14 .quad .L2 15 .quad .L3 16 .quad .L4 17 .quad .L2 18 .quad .L5 19 .quad .L6 20 .quad .L2 21 .quad .L2 22 .quad .L7 23 .text 24 .L3: # case 1 : 25 movl $1, -4(%rbp) 26 jmp .L9 27 .L4: # case 2 : 28 movl $5, -4(%rbp) 29 jmp .L9 30 .L7: # case 8 : 31 movl $9, -4(%rbp) 32 jmp .L9 33 .L5: # case 4 : 34 movl $12, -4(%rbp) 35 jmp .L9 36 .L6: # case 5 : 37 movl $16, -4(%rbp) 38 jmp .L9 39 .L2: # default : 40 movl $0, -4(%rbp) 41 .L9: 42 movl -4(%rbp), %eax 43 popq %rbp 44 ret