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系统调用退出程序。

  • 相关阅读:
    elasticsearch 中的Multi Match Query
    activiti 流程部署的各种方式
    elasticsearch 嵌套对象之嵌套类型
    elasticsearch Java High Level REST 相关操作封装
    elasticsearch 字段数据类型
    ubuntu 安装 docker
    elasticsearch 通过HTTP RESTful API 操作数据
    facenet 人脸识别(二)——创建人脸库搭建人脸识别系统
    POJ 3093 Margaritas(Kind of wine) on the River Walk (背包方案统计)
    墨卡托投影, GPS 坐标转像素, GPS 坐标转距离
  • 原文地址:https://www.cnblogs.com/biyeqingfeng/p/2244458.html
Copyright © 2011-2022 走看看