i386寄存器结构, gcc产生的汇编 GAS简介

i386的几个寄存器 :  

segment registers先暂时不管了, 弄清楚, 其作用是不是跟IA32, 或IA64有关

GAS的所有指命后面都带个字符后缀, 表操作数的大小, b, w, l分别是 8位, 16位, 32位,   l也表示8字节的双精度浮点数, 另外我在自己的电脑上生成.s时看到了movq,  pushq %rbx,  %rbx表示64位的寄存器引用,这是在Intel 64 architecture中才有的,q指的是(quadword registers)

通用寄存器就是整数寄存器, 用来存整数, 当然可以把它看作指针, 看怎么解析了  前6个比较通用, 后两个%ebp, %esp是用于指向栈中当前frame的起始,当在栈中堆积函数调用时,%ebp会不断被压入,%esp会随时指向栈顶

1.指令操作数

指令的操作数,就是一个操作中要引用的源数据值, 有立即数引用 $0x108,  寄存器引用 %eax, 存储器引用,存储器引用相对复杂, 比如4(%eax, %eax,2) ,  这个值是 4+(%eax)+(%eax)*2,  2是伸缩因子(scale factor)只能为1,2,4,8  存储器引用一般只是这种形式的特例,如(%eax), 4(%eax), 但这种通用的形式在引用数组元素时会用到, 伸缩因子其实是基本数据类型也就是数组中的元素所占的字节数,所以它一定是1,2,4,8这种

2.数据传送指令

数据传送指令是最频繁使用的指令,首先是movl (movb, movw, movq), mov是从左操作数到右操作数,不能都是存储器引用.  还有pushl和popl, 想像一下这两个指令的数据传输过程: pushl   S  :    S  ->   M[R[%esp]]  ;  R[%esp] -4 -> R[%esp], 对于S的地址在这里是不是-4之前的%esp,先不用纠结,这里应该只是一个图解而以,popl %eax 与 movl (%esp), %eax    addl 4, %esp  是等价的。  简单说说大端和小端(big endian, little endian), 首先若一个对象在内存中占用了连续的多个字节,那几乎在所有的机器上,对象的地址为字节序列中最小的地址,也就是上面S的地址应该是最后%esp的值,而大端表示和小端表示指的是把最高有效位放在低地址还是最低有效位,前者就是大端,后者就是小端(大端小端参加这篇随笔http://www.cnblogs.com/livingintruth/archive/2012/07/10/2585144.html)

3.算术和逻辑操作

加载有效地址lea : Load Effective Address。 leal就是movl的变形,它的第一个操作数一定是存储器引用,但它实际上把 这个存储器引用的地址移到右操作数,即目的操作数,目的操作数必须是寄存器。在C中使用取地址操作符如 &n, 对应的gas指令就会是lea,   另一个例子leal   7(%edx, %edx, 4) , %eax,  假如说%edx中的值是x , 这个指令把  5x+7放在 %eax中,  所以这个指令还常用来算普通的算术操作, 当然也有leaq,在我自己的机子上就出现了,目标操作数就是%rax

其它的整数算术操作,是非常标准的一元或二元操作,一元的有incl, decl, negl, notl后面两个是取负和取补, 二元的有addl, subl, imull, xorl, orl, andl前三个是算术+,-,*后三个是逻辑^(异或),|,&。然后是移位操作,第一个操作数是移位量,第二个操作数是待移位的值,左移sall,shll效量是一样的,右边填0, 右移分逻辑和算术右移,算术右移填上符号位,逻辑右移填0

 

4。 看一个例子,移位操作的

1 int move(int i){
3 i <<= 4; 4 i >>= 6; 5 return i; 6 }

对应的gas汇编代码

 1 _Z4movei:
 2 .LFB0:
 3     .cfi_startproc
 4     pushq    %rbp
 5     .cfi_def_cfa_offset 16
 6     .cfi_offset 6, -16
 7     movq    %rsp, %rbp
 8     .cfi_def_cfa_register 610     movl    %edi, -4(%rbp)
11     sall    $4, -4(%rbp)
12     sarl    $6, -4(%rbp)
13     movl    -4(%rbp), %eax
14     popq    %rbp
15     .cfi_def_cfa 7, 8
16     ret
17     .cfi_endproc

以.开头的指令都是指导汇编器和链接器的,这里把它列出来,以后写例子我都把它去掉了,有时间会专门研究下这些指令的作用,但是好像没有找到任何文档,连computer system书上都说没有关于这些指令的作用和它们与源代码关系的说明

可以看到,任何一个函数调用开始的两行(4,7)都是 pushq  %ebp ; moveq  %rsp, %rbp。最后两行都是popq %rbp ; ret 。可以动态的想像一下这个过程,这两步刚好是一来一往的状态。move的参数i之前被放到%edi中了,可能是因为参数少,所以在调用move时并没有什么参数进栈的操作,10到12行就是关健的三条指令了,最后把操作结果放到%eax中用作返回,看来%edi用来放参数,%eax用来放返回值应该是比较惯用的方式。 另外值得注意的是整个函数过程中%rsp都没有变动,所有对栈中当前函数frame局部变量的操作都是直接针对%rbp直接硬编码地址,最后当函数返回时,这些局部变量也就相当于失效了

 

5。一个算术逻辑运算的例子

 1 int test(int a, int b, int c){
 2     a = -a;
 3     b=~b;
 4     int e = a + b;
 5     e  = a - b;
 6     e = a * b;
 7     int e4 = a & b;
 8     int e3 = a | b;
 9     int e2 = a ^ b;
10     return e3;
11 }

 

对应的gas的代码是,去掉以.开头的命令

 1 _Z4testiii:
 2     pushq    %rbp
 3     movq    %rsp, %rbp
 4     movl    %edi, -20(%rbp)
 5     movl    %esi, -24(%rbp)
 6     movl    %edx, -28(%rbp)
 7     negl    -20(%rbp)
 8     notl    -24(%rbp)
 9     movl    -24(%rbp), %eax
10     movl    -20(%rbp), %edx
11     addl    %edx, %eax
12     movl    %eax, -16(%rbp)
13     movl    -24(%rbp), %eax
14     movl    -20(%rbp), %edx
15     movl    %edx, %ecx
16     subl    %eax, %ecx
17     movl    %ecx, %eax
18     movl    %eax, -16(%rbp)
19     movl    -20(%rbp), %eax
20     imull    -24(%rbp), %eax
21     movl    %eax, -16(%rbp)
22     movl    -24(%rbp), %eax
23     movl    -20(%rbp), %edx
24     andl    %edx, %eax
25     movl    %eax, -12(%rbp)
26     movl    -24(%rbp), %eax
27     movl    -20(%rbp), %edx
28     orl    %edx, %eax
29     movl    %eax, -8(%rbp)
30     movl    -24(%rbp), %eax
31     movl    -20(%rbp), %edx
32     xorl    %edx, %eax
33     movl    %eax, -4(%rbp)
34     movl    -8(%rbp), %eax
35     popq    %rbp
36     ret

 

把每条命令都认真看懂,4~6行是把三个寄存器%edi, %esi ,%edx中的内容放到栈中,注意这三个寄存器在最上面那张图上的位置,这个可能也没什么关系,这三个寄存器中存放的是调用这个函数的函数放置的参数。注意它们在栈中放置的地址前面预留了16个字节,根据的我测试,参数占据的frame的地址前面会预留的地址是以16字节为单位的,而这些预留的地址是给函数中的局部变量的,上面这个例子中,刚好有4个int型的局部变量,占用16个字节,所以刚刚好,如果再加上一个e5,有5个局部变量的话,就会给局部变量预留32个字节。7,8行直接对a和b求负和求补(补没明白),  注意接下来的几组二元操作,除了imull之外,其它的都是把操作数移到寄存器之后再操作,并且除了 subl是[%eax, %ecx]之外,其它的二元操作都是 [%edx, %eax], 并且所有运算的结果(包括前面的subl)都是从%eax移到栈中的,每个局部变量在栈中的地址都是固定好的, 而最后要return的值是e3, e3在 -8(%ebp)中,就把这个mov到%eax中,然后退栈

注意到上面从c到gas的映射中,函数名是不一样的,其实这不是c到gas的映身,是C++到gas的映射,因为的我源代码的扩展名用的是.cpp, 上面的move对应的是_Z4movei, test对应的是_Z4testiii, 如果扩展名用.c的话,函数名就不会变,虽然我都是用gcc编译的,以后的例子一般我就用.c了。在 inside object model 中第4章有提到 member function 的 name mangling,当然和这个应该不是一回事

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

导航