GAS中流程控制的实现,for, while, if, switch

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
上面是用了一个叫做跳转表(jump table)的数据结构来实现,优点是到任意一个case块的时间都为常量,第6行的ja是无符号的大于,应该说在这种形式中肯定都会用到ja, 在汇编代码的准备阶段4~5行,也就是在ja之前,会先把跳转的数i先通过算术限定在0~n之间(也就对每个case后面的数都做整体移动),然后用这个数来做索引,14~22行就是当i为0至8时,应该对应的跳转,对0~8中不属于case的数,都跳到default。这种实现只在用数跳转的常量在一个较小的范围内,如果像之前的500,gcc就没有使用这种形式,因为jump table里面得500个指针了, 我自己的试验,当这个数为50时,gcc还是使用了jump table的形式。  在跳转表的那一块代码中我写了一点注释,大概就应该是这样的理解。8行是获取要跳转的地址,可以看到这里怎么去获取到一个数组的元素,索引就是处理后的switch数,在%rax中,伸缩因子(scale factor)是8,即在我的系统上,地址是占用8字节来存。   我想修改上面的GAS代码把.L8的值打出来,这个事情留着以后

posted on 2012-07-11 12:08  小宇2  阅读(1612)  评论(0编辑  收藏  举报

导航