zoukankan      html  css  js  c++  java
  • Linux下的AT&T语法(即GNU as 汇编语法)入门(转载)

       

    注:本文系转载,用于学习。

    版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明
    http://littlewhitetwo.blogbus.com/logs/40534078.html

    本文来自:http://blog.chinaunix.net/u/16292/showart_395234.html

    工作这么长时间,一直在C语言这一层面上钻研和打拼,日积月累,很多关于C的疑惑在书本和资料中都难以找到答案。程序员是追求完美的一个种群,其头脑中哪怕是存在一点点的思维黑洞都会让其坐卧不宁。不久前在itput论坛上偶得《Computer Systems A Programmer's Perspective》(以下称CS.APP)这本经典好书,遂连夜拜读以求解惑。虽说书中没有能正面的回答我的一些疑惑,但是它却为我指明了一条通向 “无惑”之路 -- 这就是打开汇编之门。

    汇编语言是一门非常接近机器语言的语言,其语句与机器指令之间的对应关系更加简单和清晰。打开汇编之门不仅仅能解除高级语言给你带来的疑惑,它更能让你更加的理解现代计算机的运行体系,还有一点更加重要的是它给你带来的是一种自信的感觉,减少了你在高处摇摇欲坠的恐惧,响应了侯捷老师的“勿在浮沙筑高台”的号召。现在学习汇编的目的已与以前大大不同了。正如CS.APP中所说那样“程序员学习汇编的需求随着时间的推移也发生了变化,开始时是要求程序员能直接用汇编编写程序,现在则是要求能够阅读和理解优化编译器产生的代码”。能阅读和理解,这也恰恰是我的需求和目标。

    在大学时接触过汇编,主要是Microsoft MASM宏汇编,不过那时的认识高度不够加上态度不端正,错失了一个很好的学习机会。现在绝大部分时间是使用GCC在Unix系列平台上工作,选择汇编语言当然是GNU汇编了,恰好CS.APP中使用的也是GNU的汇编语法。由于学习汇编的主要目的还是“解惑”,所以形式上多是以C代码和汇编代码的比较。

    1、汇编让你看到更多
    随着你使用的语言的层次的提高,你眼中的计算机将会越来越模糊,你的关注点也越来越远离语言本身而靠近另一端“问题域”,比如通过JAVA,你更多看到的是其虚拟机,而看不到真实的计算机;通过C,你看到的也仅仅是内存一层;到了汇编语言,你就可以深入到寄存器一层自由发挥了。汇编程序员眼里的“独特风景” 包括:
    a) “程序计数器(%eip)” -- 一个特殊寄存器,其中永远存储下一条将要执行的指令的地址;
    b) 整数寄存器 -- 共8个,分别是%eax、%ebx、%ecx、%edx、%esi、%ebi、%esp和%ebp,它们可以存整数数据,可以存地址,也可以记录程序状态等。早期每个寄存器都有其特殊的用途,现在由于像linux这样的平台多采用“平面寻址[1]”,寄存器的特殊性已经不那么明显了。
    c) 条件标志寄存器 -- 保存最近执行的算术指令的状态信息,用来实现控制流中的条件变化。
    d) 浮点数寄存器 -- 顾名思义,用来存放浮点数。
    虽说寄存器的特殊性程度已经弱化,但是实际上每个编译器在使用这些寄存器时还是遵循一定的规则的,以后再说。

    2、初窥汇编
    下面是一个简单的C函数:
    void dummy() {
    int a = 1234;
    int b = a;
    }
    我们使用gcc加-S选项将之转换成汇编代码如下(省略部分内容):
    movl $1234, -4(%ebp)
    movl -4(%ebp), %eax
    movl %eax, -8(%ebp)
    看了一眼又一眼,还是看不懂,只是发现些熟悉的内容,因为上面提过如%ebp、%eax等。这只是个引子,让我们感性的认识一下汇编的“容貌”。我们一点点地来看。咋看一眼汇编代码长得似乎很相似,没错,汇编代码就是一条一条的“指令+操作数”的语句的集合。汇编指令是固定的,每条指令都有其固定的用途,而操作数表示则有多种类型。

    1) 操作数表示
    大部分汇编指令都有一个或多个操作数,包括指令操作中的源和目的。一条标准的指令格式大 致是这样的:“指令 + 源操作数 + 目的操作数”,其中源操作数可以是立即数、从寄存器中读出的数或从内存中读出的数;而目的操作数则可以是寄存器或内存。按这么一分类,操作数就大致有三种:
    a) 立即数表示法 -- 如“movl $1234, -4(%ebp)”中的“$1234”,就是一个立即数作为操作数,按照GNU汇编语法,立即数表示为“$+整数”。立即数常用来表示代码中的一些常数,如上例中的“$1234”。注意一点的是立即数不能作为目的操作数。
    b) 寄存器表示法 -- 这种比较简单,它就是表示寄存器之内容。如上面的“movl -4(%ebp), %eax”中的%eax就是使用寄存器表示法作源操作数,而“movl %eax, -8(%ebp)”中的%eax则是使用寄存器表示法作目的操作数。
    c) 内存引用表示法 -- 计算出的该操作数的值表示的是相应的内存地址。汇编指令根据这个内存地址访问相应的内存位置。如上例“movl -4(%ebp), %eax”中的“-4(%ebp)”,其表示的内存地址为(%ebp寄存器中的内容-4)得到的值。

    2) 数据传送指令
    汇编语言中最最常用的指令 -- 数据传送指令,也是我们接触的第一种类别的汇编指令。其指令的格式为:“mov 源操作数, 目的操作数”。
    mov 系列支持从最小一个字节到最大双字的访问与传送。其中movb用来传送一字节信息,movw用来传送二字节,即一个字的信息,movl用来传送双字信息。 这些不详说了。除此以外mov系列还提供两个带位扩展的指令movsbl和movzbl

    ==============================================================

    汇编语言作为一种高效的,而且紧密结合硬件平台的编程语言,在操作系统,嵌入式开发等领域都有着十分重要的作用。正因为汇编依赖于硬件结构(CPU 指令码),因此不同体系结构上的汇编语言也大相径庭。本文简单介绍了Linux下的AT&T语法(即GNU as 汇编语法),以及在Linux下汇编的基本方法。

    AT&T语法起源于AT&T贝尔实验室,是在当时用于实现Unix系统的处理器操作码语法之上而形成的,AT&T语法和Intel语法主要区别如下:
    AT&T使用$表示立即数,Intel不用,因此表示十进制2时,AT&T为$2,而Intel就是2
    AT&T在寄存器前加%,比如eax寄存器表示为%eax
    AT&T 处理操作数的顺序和Intel相反,比如,movl %eax, %ebx是将eax中的值传递给ebx,而Intel是这样的mov ebx, eax
    AT&T在助记符的后面加上一个单独字符表示操作中数据的长度,比如movl $foo, %eax等同于Intel的mov eax, word ptr foo
    长跳转和调用的格式不同,AT&T为ljmp $section, $offset,而Intel为jmp section:offset
    主要的区别就是这些,其他的细节还有很多,下面给出一个具体的例子来说明

    #cpuid.s Sample program

    .section .data

    output:
    .ascii "The processor Vendor ID is 'xxxxxxxxxxxx'\n"

    .section .text
    .globl _start

    _start:

    movl $0, %eax

    cpuid

    movl $output, %edi

    movl %ebx, 28(%edi)

    movl %edx, 32(%edi)

    movl %ecx, 36(%edi)

    movl $4, %eax

    movl $1, %ebx

    movl $output, %ecx

    movl $42, %edx

    int $0x80

    movl $1, %eax

    movl $0, %ebx

    int $0x80


    这个程序的作用是查询CPU的厂商ID,其中:

    ,ascii定义字符串(和Intel格式完全不同).section是声明段的语句,.data和.text是段名,分别为数据段和代码段, _start是gas(GNU汇编器)的默认入口标签,表示程序从这里开始执行。.globl将_start声明成了外部程序访问的标签。cpuid为指令请求CPU的指定信息,该指令用eax作为输入,ebx,edx,ecx作为输出,这里将0作为cpuid的输入指令,请求返回CPU的厂商ID字符串。返回的结果,一个12字节的字符串,分别存储在三个寄存器中,其中ebx存放低4位,edx中间4位,ecx高4位(注意顺序!)。接下来定义一个指针edi,edi指向output的开始地址,然后接着的3条语句将output里的x替换为厂商信息。28(%edi)中的28表示偏移量,即整个地址为%edi里的地址加上28个字节,这个地址正好是output里第一个x的地址。再接下来就是打印结果了,这里用到了Linux的一个系统调用(int 0x80),该系统调用的参数分别为:eax 系统调用号,ebx 要写入的文件描述符,ecx 字符串首地址,edx 字符串长度,程序里这些个参数的值分别为4,1(标准输出),output的地址和42。最后再次调用1号系统调用-退出函数,返回shell,这次 ebx中的值是返回给shell的退出代码,0表示无异常

    然后汇编连接运行程序:
    [root@zieckey-laptop src]# as -o cpuid.o cpuid.s 
    [root@zieckey-laptop src]# ld cpuid.o -o cpuid
    [root@zieckey-laptop src]# ./cpuid
    The processor Vendor ID is 'GenuineIntel'
    [root@zieckey-laptop src]# 

    本人的电脑是Pentium M的CPU所以返回的结果是GenuineIntel。

    几点说明:

    1)Linux的标准汇编环境为as,ld,gdb,gprof,objdump等GNU开发调试工具,除了gdb外,其他全部随binutils包发布。其中as使用的是AT&T语法。在Linux下也可以使用Nasm来进行Intel格式的汇编程序编写

    2)Linux下汇编的系统调用为int 0x80,和DOS下的int 21h大同小异,只不过传递参数不同

    3)段声明语句.section不需要像Intel格式那样在段结尾的时候加上段结束标志(SEGMENT/ENDS),下一个段的开始自动标志着上个段的结束

    4)简单程序的入口标签不是必须要定义的,ld会自己判断入口,但是会给出警告

    ===========================================例子2

    例 2. 求一组数的最大值的汇编程序

    #PURPOSE: This program finds the maximum number of a
    #   set of data items.
    #
    #VARIABLES: The registers have the following uses:
    #
    # %edi - Holds the index of the data item being examined
    # %ebx - Largest data item found
    # %eax - Current data item
    #
    # The following memory locations are used:
    #
    # data_items - contains the item data. A 0 is used
    # to terminate the data
    #
     .section .data
    data_items:   #These are the data items
     .long 3,67,34,222,45,75,54,34,44,33,22,11,66,0
    
     .section .text
     .globl _start
    _start:
     movl $0, %edi   # move 0 into the index register
     movl data_items(,%edi,4), %eax # load the first byte of data
     movl %eax, %ebx  # since this is the first item, %eax is
       # the biggest
    
    start_loop:   # start loop
     cmpl $0, %eax   # check to see if we've hit the end
     je loop_exit
     incl %edi   # load next value
     movl data_items(,%edi,4), %eax
     cmpl %ebx, %eax  # compare values
     jle start_loop  # jump to loop beginning if the new
        # one isn't bigger
     movl %eax, %ebx  # move the value as the largest
     jmp start_loop  # jump to loop beginning
    
    loop_exit:
     # %ebx is the status code for the exit system call
     # and it already has the maximum number
     movl $1, %eax   #1 is the exit() syscall
     int $0x80

    汇编、链接、执行:

    $ as max.s -o max.o
    $ ld max.o -o max
    $ ./max
    $ echo $?

    这个程序在一组数中找到一个最大的数,并把它作为程序的退出状态。这组数在.data段给出:

    data_items:
     .long 3,67,34,222,45,75,54,34,44,33,22,11,66,0

    .long指示声明一组数,每个数占32位,相当于C语言中的数组。这个数组开头有一个标号data_items,汇编器会把数组的首地址作为data_items符号所代表的地址,data_items类似于C语言中的数组名。data_items这个标号没有用.globl声明,因为它只在这个汇编程序内部使用,链接器不需要知道这个名字的存在。除了.long之外,常用的数据声明还有:

    • .byte,也是声明一组数,每个数占8位

    • .ascii,例如.ascii "Hello world",声明了11个数,取值为相应字符的ASCII码。注意,和C语言不同,这样声明的字符串末尾是没有'\0'字符的,如果需要以'\0'结尾可以声明为.ascii "Hello world\0"

    data_items数组的最后一个数是0,我们在一个循环中依次比较每个数,碰到0的时候让循环终止。在这个循环中:

    • edi寄存器保存数组中的当前位置,每次比较完一个数就把edi的值加1,指向数组中的下一个数。

    • ebx寄存器保存到目前为止找到的最大值,如果发现有更大的数就更新ebx的值。

    • eax寄存器保存当前要比较的数,每次更新edi之后,就把下一个数读到eax中。

    _start:
     movl $0, %edi

    初始化edi,指向数组的第0个元素。

    movl data_items(,%edi,4), %eax

    这条指令把数组的第0个元素传送到eax寄存器中。data_items是数组的首地址,edi的值是数组的下标,4表示数组的每个元素占4字节,那么数组中第edi个元素的地址应该是data_items + edi * 4,从这个地址读数据,写成指令就是上面那样,这种地址的表示方式在下一节还会详细解释。

    movl %eax, %ebx

    ebx的初始值也是数组的第0个元素。下面我们进入一个循环,在循环的开头用标号start_loop表示,循环的末尾之后用标号loop_exit表示。

    start_loop:
     cmpl $0, %eax
     je loop_exit

    比较eax的值是不是0,如果是0就说明到达数组末尾了,就要跳出循环。cmpl指令将两个操作数相减,但计算结果并不保存,只是根据计算结果改变eflags寄存器中的标志位。如果两个操作数相等,则计算结果为0,eflags中的ZF位置1。je是一个条件跳转指令,它检查eflags中的ZF位,ZF位为1则发生跳转,ZF位为0则不跳转,继续执行下一条指令。可见条件跳转指令和比较指令是配合使用的,前者改变标志位,后者根据标志位做判断,如果参与比较的两数相等则跳转,je的e就表示equal。

    incl %edi
     movl data_items(,%edi,4), %eax

    edi的值加1,把数组中的下一个数传送到eax寄存器中。

    cmpl %ebx, %eax
     jle start_loop

    把当前数组元素eax和目前为止找到的最大值ebx做比较,如果前者小于等于后者,则最大值没有变,跳转到循环开头比较下一个数,否则继续执行下一条指令。jle也是一个条件跳转指令,le表示less than or equal。

    movl %eax, %ebx
     jmp start_loop

    更新了最大值ebx然后跳转到循环开头比较下一个数。jmp是一个无条件跳转指令,什么条件也不判断,直接跳转。loop_exit标号后面的指令用exit系统调用退出程序。

  • 相关阅读:
    js小程序
    事务的概念
    为期一个月培训的总结
    软件测试培训总结篇2
    软件测试培训总结篇1
    软件测试培训第30天
    软件测试培训第29天
    软件测试培训第28天
    软件测试培训第26天
    软件测试培训第27天
  • 原文地址:https://www.cnblogs.com/biyeqingfeng/p/2244458.html
Copyright © 2011-2022 走看看