《深入理解计算机系统》AT&T x86汇编学习


前言:文章采用AT&T格式的汇编(也叫做ATT汇编,AT&T是运行贝尔实验室多年的公司),是GCC,OBJDUM等工具默认的汇编格式。ATT汇编与INTEL汇编的不同在于:1.Intel省略了大小指示 2.源操作数在目的操作数之前。

1 数据格式

        C语言中有多重数据格式,包括char, short, int, long long int, char *, float, doule, long double,不同字长的数据在ATT汇编中传输时,用到不同的传送指令。其中long double和具体的显示相关。

2 指令行为

数据访问:

        一个IA32的CPU包含有8个存储32位值的寄存器。在大多情况下,可以这8个被称通用寄存器,但在某些指令中规定了源寄存器和目的寄存器,这属于约定的规则。对于%eax  %ecx  %edx它们三个寄存器的保存、恢复规则不同于%ebx  %edi  %esi。最后两个寄存器保存着指向程序栈的位置,需要根据栈的管理规则来进行修改。


        指令中的操作数有三种类型:第一种类型是立即数,也就是常数。书写方式是以$开头后面跟一个C语言表示的整数,例如$0x1F.。第二种类型是寄存器,表示寄存器的某个内容,以%开头例如%eax。第三种类型是存储器引用,它会根据计算出来的地址访问存储器某个位置。而寻址模式可以分为以下情况:(带括号的都是寄存器寻址)


传送操作:

        将数据从一个位置复制到另一个位置的指令,是使用最频繁的指令。Mov指令将源操作数复制到目的目的操作数中,传送指令的两个操作数不能同时为存储器,将一个数据从存储器的一个位置传送到另外一个位置需要使用两条指令,第一条将源操作数复制到寄存器中,第二条将寄存器数据复制到存储器中。Movs和Movz都是将一个数据宽度较小的数据传送到较大的数据,高位使用符号扩展或者零扩展。对于pushl和popl指令来说操作的是栈,栈的基地址由%ebp来指定,栈顶元素由%esp来指定,%esp指向的就是栈顶元素。将一个双字压入栈中,首先要将%esp减4,然后将双字写入%esp指向的栈顶位置;弹出一个双字,首先要将%esp加4,然后将%esp指向的栈顶元素取出。

运算操作:


        这些操作分为四组:加载有效地址、一元操作、二元操作、移位。加载有效地址leal实际上是movl的变形,它的指令形式是从存储器到寄存器,但实际上是将存储器的地址读到了寄存器中。例如 %edx值为x,那么指令leal 7(%edx, %edx, 4) %eax的作用是将%eax的值变为 5x + 7。leal的目的操作数必须是一个寄存器。最后一组移位操作,先给出移位量,然后给出的是要移位的数,移位量采用单个字节编码并且只考虑低5位,即0-31。移位量可以试一个立即数,或者放在单字节寄存器%cl中。SAL和SHL都是在移位后右边填上零,而SAR是移位后左边添加符号位,SHL是移位后左边添加零。


逻辑控制:

      C语言中控制包括条件语句、分支语句、循环语句,从汇编来看,控制实际上是由条件判断、跳转来实现的。说白了就是状态测试、和jmp类的指令来完成的。

      状态:CPU中除了整数寄存器,还维护着一组单个位的条件码,也可以叫做状态。它们描述了最近的算术或逻辑操作的属性,通过检测这些属性来执行条件分支指令。最常见的状态码有:


        有两类指令它们只设置条件码而不改变任何其他寄存器,cmp指令根据操作数之差来设置条件码,除了设置条件码之外不改变寄存器的值。Test指令对操作数执行与运算,它们除了设置条件码之外不改变寄存器的值。


        条件码通常不会直接访问,使用的方法有三种:1.保存条件码的某个组合值到寄存器或者存储器  2.根据条件码做跳转  3.有条件的传送数据。

        对于第一种方法“保存条件码的组合值到寄存器或者存储器”,使用的是Set指令,不同的Set指令区别在于它们是不同条件码的某种组合值,后缀规定的是哪种组合。


         一条set指令的目的操作数是8个单字节寄存器元素之一,或是存储一个字节的存储器位置,将这个字节设置为0或者1.

        正常执行指令,指令会按照他们出现的顺序一条条地执行。跳转指令会导致执行切换到程序的一个全新的位置。在汇编代码中,这些跳转的目的位置通常是一个标号。


        jmp是无条件的跳转,它可以直接跳转, 也可以间接跳转,间接跳转时地址为寄存器或存储器中的值。表中其他的跳转都是有条件的跳转,他们根据条件码的某个组合,或跳转或执行下一条指令。这些跳转指令的名字以及跳转条件,与set指令对应的名字和条件是匹配的。虽然可以不关心机器代码格式的细节,但是理解跳转指令是如何编码的,对研究链接的过程是非常重要的。跳转指令有几种不同的编码,但最常见的都是PC相关的。它们会将目标指令地址和当前指令的地址之差作为编码(实际是jmp指令后的一条指令)。第二种编码方式是给出“绝对地址”。

3 条件分支

        对于C语言中的条件分支if - else,if和else中的逻辑流只有一个会被执行。如果写成下面的形式:

        改写成汇编形式的语言:

4 循环

        C语言提供了多种循环结构,即for、while、do while。汇编中没有响应的的指令存在,可以利用条件测试和跳转组合来实现。大多数汇编器会根据一个do-while循环形式来产生循环代码。其他循环也会首先被转化为do-while形式,然后再被编译成机器代码。下面从do-while开始循序渐进的研究几种循环结构。

1.do-while结构:

do

       body-statement

while(test-expr);

翻译成汇编形式代码可以写成:

.loop

    body-statement

    cmp(test-expr)

    jg .loop

每次先进入循环体,执行body-statement,测试状态并选择跳转或不跳转。


2.while结构

与do-while不同,while结构需要先对test-expr做条件测试,改写成do-while形式如下:

改写成对应的汇编代码形式如下:


3.for结构

c语言标准说明,这个循环体与下面的循环代码行为基本一样:

改写成对应的汇编代码形式如下:

5 switch语句

        switch语句可以根据一个整数索引值进行多重分支,处理具有多种结果的测试时,这种语句特别有用。它们不仅提高了c代码的可读性,而且通过使用跳转表jt这种数据结构使得实现更加高效。跳转表实际上是一个数组,结果为数据的索引,索引对应的表项则为测试的跳转结果,也就是代码段的一个地址。即索引为结果状态,表项为跳转地址。和使用一组很长的if-else语句相比,使用跳转表的有点是执行开关语句的时间与开关情况的数量无关。

        下面是一个c语言switch的例子:

    return x;

}

转化为汇编形式:

 修改成为汇编代码:





6 过程

        一个过程的调用包括数据的传递、代码的转移、局部变量空间的分配。当然局部变量空间的回收、代码的恢复、数据的返回。每一个过程都有自己的栈,过程的调用和返回也都是通过操作栈来实现的。其中每个过程自己的栈,都被称为栈帧。IA32是通过栈来实现过程调用的。

       

        假设过程P调用过程Q, 则Q的参数放在P的栈帧中。另外,当P调用Q是,P中的返回地址压入栈帧的末尾。Q的栈帧从P的保存帧指针(P的栈底)开始, 后面是保存其他寄存器的值。

        call指令有一个目标,即指明被调用过程起始的指令地址。同跳转一样,调用可以是直接的,可以是间接的。在汇编代码中,直接调用的目标是一个标号,而间接调用的目标是*后面跟一个操作数指示符。

        call指令的效果是将返回地址入栈,并跳转到被调用过程的起始处。返回地址是在程序中紧跟在call后面的那条指令的地址。这样当被调用过程返回时,执行会从此处继续进行。ret指令从栈中弹出地址,并跳转到这个位置。用leave指令可以试栈做好返回准备。其等价于下面的代码:

movl  %ebp, %esp

popl %ebp

       设过程Q为被调用过程,P为调用过程。Q的代码如下:

       Q:

               push %ebp

               movl %esp, %ebp

               body-ops

               leave                                ,恢复调用者P的栈帧,也可以采用movl %ebp, %esp 、popl %ebp

               ret

       P:

              call Q                          ,下一条语句的地址被压入P栈底部

              add $0x14, %esp        ,分配栈空间

        另外,如果函数要返回整数或指针的话,寄存器%eax可以用来保存返回值。

           程序寄存器组是唯一能被所有过程共享的资源, 虽然在给定时刻只能有一个过程是活动的,但是我们必须保证当一个过程P调用另一个过程Q时,被调用者Q不会覆盖调用者稍后会使用的寄存器的值。为此IA32采用了一组同意的寄存器使用惯例,所有的过程都必须遵守,包括程序库中的过程。

           根据惯例:寄存器%eax、%edx、%ecx是调用者P保存的寄存器,也就是说Q无论怎么使用这些寄存器, P过程都不会受到影响;而%ebx、%esi、%edi是被调用者Q保存的寄存器,意思是Q使用之前必须把他们保存到栈中。因此对于Q而言,前三个寄存器可以随意使用,后三个寄存器是找P借来用的待会要还。此外,根据这里描述的管理,必须保存寄存器%ebp和%esp。

7 递归
       哈哈,终于到了这里。程序员不了解栈帧结构,写代码会云里雾里。了解了递归的汇编代码,才能克服递归的低效率、资源占用、栈空间有限等这些问题 大笑 大笑 大笑好吧,递归的缺点怎么克服呢?效率低没办法,尽量采用优化办法,例如DP解决冗余问题;资源占用是必须的;栈空间有限可以考虑1.递归改为非递归 2.使用全局变量,这样传递参数就不需要占据空间 3.创建新线程时,貌似可以改变旧线程的栈空间大小(不确定)。
          因为每个调用在栈中都有自己的私有空间,多个未完成的过程的局部变量不会相互影响。此外,栈的原则很自然地提供了适当的策略,当过程被调用时分配拒不存储,当返回时释放存储。例如:
int rfact(int n) {
    int ret;
    if(n <=1)
          ret = 1;
    ret = n *rfact(n - 1);
    return ret;
}

这是一个求n的阶乘的函数,转化为汇编代码可以写成:

rfact:

    push %ebp,                                                 ,保存旧的%ebp

    movl  %esp, %ebp                                     ,%ebp设为新的帧指针

    pushl  %ebx                                                 ,保存调用者的%ebx

    subl    $4, %esp                                          ,栈上分配4字节

    movl    8(%ebp), %ebx                               ,获得n的值

    movl    $1, %eax                                          ,%eax = 1

    cmpl    $1, %ebx                                          ,比较%ebx与1的大小

    jle        .done                                                 ,若%ebx <= 1则进入done分支

    leal       -1(%ebx), %eax                              ,%eax = %ebx -1

    movl    %eax, (%esp)                                  ,%eax入栈

    call rfact                                                         ,存储imll的地址,并跳转到rfact

    imull    %ebx, %eax                                     ,%eax = %eax * n

.done:

    addl    $4, %esp                                           ,收回栈顶元素

    popl  %ebx                                                    ,恢复%ebx

    popl  %ebp                                                   ,恢复%ebp

    ret


该递归的帧的情况如下:



8 ++操作
x =  i++;
         i++的操作并不是原子操作,它分为4步:
        1.将变量i的值拷贝到一个寄存器A
        2.寄存器A赋值给X
        3.寄存器A值加1
        4.将寄存器A值写回到i对应存储器中
        这个上述的过程当中,若多个线程同时访问共享资源i,则存储器中的i值无法同步。A线程得到i值,若还没写回存储器,而B又得到i值,那么最后i被A、B操作的值都是i+1。

x =  ++i;

     1.将变量i的值拷贝到一个寄存器A

         2.寄存器A加1

         3.寄存器写回i对应存储器

         4.对i对应存储器到寄存器B,寄存器B写入到存储器X

         



9 数组和异质结构

感觉AT&T的汇编代码,比Intel格式的更好理解,设计者的习惯刚好和我对上胃口了,感觉学起来好理解多了,指令也好记。

1.数据的表示

2.寻址与传送

3.运算

4.控制

5.C语言的汇编实现

6.过程

7.数组、指针、结构

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值