Linux下的汇编与Windows汇编最大的不同就是第一个操作数是原操作数,第二个是目的操作数。而Windows下却是相反。

1、 基本操作指令

简单的操作数类型说明。一般有三种。

(1)马上数操作数,也就是常数值。马上数的书写方式是“$”后面跟一个整数。比方$0x1F。这个会在后面的详细分析中见到非常多。

(2)寄存器操作数,它表示某个寄存器的内容。用符号Ea来表示随意寄存器a,用引用R[Ea]来表示它的值。这是将寄存器集合看成一个数组R,用寄存器表示符作为索引。

(3)操作数是存储器引用,它会依据计算出来的地址(通常称为有效地址)訪问某个存储器位置。用符号Mb[Addr]表示对存储在存储器中从地址Addr開始的b字节值的引用。

通常能够省略下标b。

略过,详细參考《深入理解计算机系统》

2. 最简C代码分析





    为简化问题。来分析一下最简的c代码生成的汇编代码:

    # vi test1.c

      

    int main()

    {

        return 0;

    }   

    

    编译该程序。产生二进制文件:

    # gcc test1.c -o test1

    # file test1  

    test1: ELF 32-bit LSB executable 80386 Version 1, dynamically linked, not stripped 





    test1是一个ELF格式32位小端(Little Endian)的可运行文件,动态链接而且符号表没有去除。

    这正是Unix/Linux平台典型的可运行文件格式。

    用mdb反汇编能够观察生成的汇编代码:





    # mdb test1

    Loading modules: [ libc.so.1 ]

    > main::dis                       ; 反汇编main函数,mdb的命令一般格式为  <地址>::dis

    main:          pushl   %ebp       ; ebp寄存器内容压栈。即保存main函数的上级调用函数的栈基地址

    main+1:        movl    %esp,%ebp  ; esp值赋给ebp,设置main函数的栈基址

    main+3:          subl    $8,%esp

    main+6:          andl    $0xf0,%esp

    main+9:          movl    $0,%eax

    main+0xe:        subl    %eax,%esp

    main+0x10:     movl    $0,%eax    ; 设置函数返回值0

    main+0x15:     leave              ; 将ebp值赋给esp,pop先前栈内的上级函数栈的基地址给ebp。恢复原栈基址

    main+0x16:     ret                ; main函数返回。回到上级调用

    > 





    注:这里得到的汇编语言语法格式与Intel的手冊有非常大不同,Unix/Linux採用AT&T汇编格式作为汇编语言的语法格式

         假设想了解AT&T汇编能够參考文章:Linux AT&T 汇编语言开发指南 





    问题:谁调用了 main函数?

     

     在C语言的层面来看,main函数是一个程序的起始入口点,而实际上,ELF可运行文件的入口点并非main而是_start。

     mdb也能够反汇编_start:

       

    > _start::dis                       ;从_start 的地址開始反汇编

    _start:              pushl   $0

    _start+2:            pushl   $0

    _start+4:            movl    %esp,%ebp

    _start+6:            pushl   %edx

    _start+7:            movl    $0x80504b0,%eax

    _start+0xc:          testl   %eax,%eax

    _start+0xe:          je      +0xf            <_start+0x1d>

    _start+0x10:         pushl   $0x80504b0

    _start+0x15:         call    -0x75           <atexit>

    _start+0x1a:         addl    $4,%esp

    _start+0x1d:         movl    $0x8060710,%eax

    _start+0x22:         testl   %eax,%eax

    _start+0x24:         je      +7              <_start+0x2b>

    _start+0x26:         call    -0x86           <atexit>

    _start+0x2b:         pushl   $0x80506cd

    _start+0x30:         call    -0x90           <atexit>

    _start+0x35:         movl    +8(%ebp),%eax

    _start+0x38:         leal    +0x10(%ebp,%eax,4),%edx

    _start+0x3c:         movl    %edx,0x8060804

    _start+0x42:         andl    $0xf0,%esp

    _start+0x45:         subl    $4,%esp

    _start+0x48:         pushl   %edx

    _start+0x49:         leal    +0xc(%ebp),%edx

    _start+0x4c:         pushl   %edx

    _start+0x4d:         pushl   %eax

    _start+0x4e:         call    +0x152          <_init>

    _start+0x53:         call    -0xa3           <__fpstart>

    _start+0x58:        call    +0xfb        <main>              ;在这里调用了main函数

    _start+0x5d:         addl    $0xc,%esp

    _start+0x60:         pushl   %eax

    _start+0x61:         call    -0xa1           <exit>

    _start+0x66:         pushl   $0

    _start+0x68:         movl    $1,%eax

    _start+0x6d:         lcall   $7,$0

    _start+0x74:         hlt

    > 





    问题:为什么用EAX寄存器保存函数返回值?

    实际上IA32并没有规定用哪个寄存器来保存返回值。但假设反汇编Solaris/Linux的二进制文件。就会发现,都用EAX保存函数返回值。

    这不是偶然现象,是操作系统的ABI(Application Binary Interface)来决定的。

Solaris/Linux操作系统的ABI就是Sytem V ABI。

概念:SFP (Stack Frame Pointer) 栈框架指针 





    正确理解SFP必须了解:

        IA32 的栈的概念

        CPU 中32位寄存器ESP/EBP的作用

        PUSH/POP 指令是怎样影响栈的

        CALL/RET/LEAVE 等指令是怎样影响栈的





    如我们所知:

    1)IA32的栈是用来存放暂时数据,并且是LIFO,即后进先出的。栈的增长方向是从高地址向低地址增长。按字节为单位编址。

    2) EBP是栈基址的指针,永远指向栈底(高地址),ESP是栈指针。永远指向栈顶(低地址)。

    3) PUSH一个long型数据时,以字节为单位将数据压入栈,从高到低按字节依次将数据存入ESP-1、ESP-2、ESP-3、ESP-4的地址单元。

4) POP一个long型数据。过程与PUSH相反,依次将ESP-4、ESP-3、ESP-2、ESP-1从栈内弹出,放入一个32位寄存器。

    5) CALL指令用来调用一个函数或过程,此时,下一条指令地址会被压入堆栈,以备返回时能恢复运行下条指令。

    6) RET指令用来从一个函数或过程返回。之前CALL保存的下条指令地址会从栈内弹出到EIP寄存器中,程序转到CALL之前下条指令处运行

    7) ENTER是建立当前函数的栈框架,即相当于下面两条指令:

        pushl   %ebp

        movl    %esp,%ebp

    8) LEAVE是释放当前函数或者过程的栈框架,即相当于下面两条指令:

        movl ebp esp

        popl  ebp





    假设反汇编一个函数。非常多时候会在函数进入和返回处,发现有类似例如以下形式的汇编语句: 

        

        pushl   %ebp            ; ebp寄存器内容压栈,即保存main函数的上级调用函数的栈基地址

        movl    %esp,%ebp       ; esp值赋给ebp。设置 main函数的栈基址

        ...........             ; 以上两条指令相当于 enter 0,0

        ...........

        leave                   ; 将ebp值赋给esp。pop先前栈内的上级函数栈的基地址给ebp,恢复原栈基址

        ret                     ; main函数返回,回到上级调用





    这些语句就是用来创建和释放一个函数或者过程的栈框架的。

原来编译器会自己主动在函数入口和出口处插入创建和释放栈框架的语句。

函数被调用时:

    1) EIP/EBP成为新函数栈的边界

    函数被调用时,返回时的EIP首先被压入堆栈。创建栈框架时,上级函数栈的EBP被压入堆栈。与EIP一道行成新函数栈框架的边界

    2) EBP成为栈框架指针SFP,用来指示新函数栈的边界

    栈框架建立后,EBP指向的栈的内容就是上一级函数栈的EBP,能够想象,通过EBP就能够把层层调用函数的栈都回朔遍历一遍,调试器就是利用这个特性实现 backtrace功能的

    3) ESP总是作为栈指针指向栈顶,用来分配栈空间

    栈分配空间给函数局部变量时的语句通常就是给ESP减去一个常数值,比如,分配一个整型数据就是 ESP-4

    4) 函数的參数传递和局部变量訪问能够通过SFP即EBP来实现 

    因为栈框架指针永远指向当前函数的栈基地址,參数和局部变量訪问通常为例如以下形式:

        +8+xx(%ebp)         ; 函数入口參数的的訪问

        -xx(%ebp)           ; 函数局部变量訪问

            

    假如函数A调用函数B。函数B调用函数C ,则函数栈框架及调用关系例如以下图所看到的:

    +-------------------------+----> 高地址

    | EIP (上级函数返回地址)    | 

    +-------------------------+ 

 +-->   | EBP (上级函数的EBP)      | --+ <------当前函数A的EBP (即SFP框架指针) 

 | +-------------------------+   +-->偏移量A 

 | | Local Variables         |   |

 | | ..........              | --+  <------ESP指向函数A新分配的局部变量,局部变量能够通过A的ebp-偏移量A訪问 

 | f +-------------------------+

 | r | Arg n(函数B的第n个參数)   | 

 | a +-------------------------+

 | m | Arg .(函数B的第.个參数)   |

 | e +-------------------------+

 | | Arg 1(函数B的第1个參数)   |

 | o +-------------------------+

 | f | Arg 0(函数B的第0个參数)   | --+ <------ B函数的參数能够由B的ebp+偏移量B訪问

 | +-------------------------+   +--> 偏移量B

 | A | EIP (A函数的返回地址)     |   | 

 | +-------------------------+ --+ 

 +--- | EBP (A函数的EBP)         |<--+ <------ 当前函数B的EBP (即SFP框架指针) 

    +-------------------------+   |

    | Local Variables         |   |

    | ..........              |   | <------ ESP指向函数B新分配的局部变量

    +-------------------------+   |

    | Arg n(函数C的第n个參数)   |   |

    +-------------------------+   |

    | Arg .(函数C的第.个參数)   |   |

    +-------------------------+   +--> frame of B

    | Arg 1(函数C的第1个參数)   |   |

    +-------------------------+   |

    | Arg 0(函数C的第0个參数)   |   |

    +-------------------------+   |

    | EIP (B函数的返回地址)     |   |

    +-------------------------+   |

 +-->   | EBP (B函数的EBP)         | --+ <------ 当前函数C的EBP (即SFP框架指针) 

 |      +-------------------------+

 | | Local Variables         |

 | | ..........              | <------ ESP指向函数C新分配的局部变量

 | +-------------------------+----> 低地址

frame of C



图 1-1 

       

    再分析test1反汇编结果中剩余部分语句的含义:

        

    # mdb test1

    Loading modules: [ libc.so.1 ]

    > main::dis                        ; 反汇编main函数

    main:          pushl   %ebp                            

    main+1:        movl    %esp,%ebp        ; 创建Stack Frame(栈框架)

    main+3:       subl    $8,%esp       ; 通过ESP-8来分配8字节堆栈空间

    main+6:       andl    $0xf0,%esp    ; 使栈地址16字节对齐

    main+9:       movl    $0,%eax       ; 无意义

    main+0xe:     subl    %eax,%esp     ; 无意义

    main+0x10:     movl    $0,%eax          ; 设置main函数返回值

    main+0x15:     leave                    ; 撤销Stack Frame(栈框架)

    main+0x16:     ret                      ; main 函数返回

    >





    下面两句似乎是没有意义的,果真是这样吗?

        movl    $0,%eax 

        subl     %eax,%esp

       

    用gcc的O2级优化来又一次编译test1.c:

    # gcc -O2 test1.c -o test1

    # mdb test1

    > main::dis

    main:         pushl   %ebp

    main+1:       movl    %esp,%ebp

    main+3:       subl    $8,%esp

    main+6:       andl    $0xf0,%esp

    main+9:       xorl    %eax,%eax      ; 设置main返回值,使用xorl异或指令来使eax为0

    main+0xb:     leave

    main+0xc:     ret

    > 

    新的反汇编结果比最初的结果要简洁一些。果然之前被觉得没用的语句被优化掉了,进一步验证了之前的推測。

    提示:编译器产生的某些语句可能在程序实际语义上没实用处。能够用优化选项去掉这些语句。

问题:为什么用xorl来设置eax的值?

    注意到优化后的代码中,eax返回值的设置由 movl $0,%eax 变为 xorl %eax,%eax ,这是由于IA32指令中,xorl比movl有更高的执行速度。





    概念:Stack aligned 栈对齐

    那么,下面语句究竟是和作用呢?

        subl    $8,%esp

       andl    $0xf0,%esp     ; 通过andl使低4位为0,保证栈地址16字节对齐

       

    表面来看,这条语句最直接的后果是使ESP的地址后4位为0,即16字节对齐,那么为什么这么做呢?

    原来,IA32 系列CPU的一些指令分别在4、8、16字节对齐时会有更快的执行速度,因此gcc编译器为提高生成代码在IA32上的执行速度。默认对产生的代码进行16字节对齐





        andl $0xf0,%esp 的意义非常明显,那么 subl $8,%esp 呢,是必须的吗?

    这里如果在进入main函数之前。栈是16字节对齐的话,那么,进入main函数后,EIP和EBP被压入堆栈后,栈地址最末4位二进制位必然是1000,esp -8则恰好使后4位地址二进制位为0000。看来,这也是为保证栈16字节对齐的。





    假设查一下gcc的手冊,就会发现关于栈对齐的參数设置:

    -mpreferred-stack-boundary=n    ; 希望栈依照2的n次的字节边界对齐, n的取值范围是2-12





    默认情况下,n是等于4的。也就是说。默认情况下,gcc是16字节对齐,以适应IA32大多数指令的要求。





    让我们利用-mpreferred-stack-boundary=2来去除栈对齐指令:

      

    # gcc -mpreferred-stack-boundary=2 test1.c -o test1

       

    > main::dis

    main:       pushl   %ebp

    main+1:     movl    %esp,%ebp

    main+3:     movl    $0,%eax

    main+8:     leave

    main+9:     ret

    > 





    能够看到。栈对齐指令没有了。由于。IA32的栈本身就是4字节对齐的,不须要用额外指令进行对齐。

    那么,栈框架指针SFP是不是必须的呢?

    # gcc -mpreferred-stack-boundary=2 -fomit-frame-pointer test1.c -o test

    > main::dis

    main:       movl    $0,%eax

    main+5:     ret

    > 





    由此可知,-fomit-frame-pointer 能够去除SFP。

问题:去除SFP后有什么缺点呢?

       

    1)添加调式难度

        因为SFP在调试器backtrace的指令中被使用到。因此没有SFP该调试指令就无法使用。

    2)减少汇编代码可读性

        函数參数和局部变量的訪问,在没有ebp的情况下,都仅仅能通过+xx(esp)的方式訪问,而非常难区分两种方式,减少了程序的可读性。

       

    问题:去除SFP有什么长处呢?

       

    1)节省栈空间

    2)降低建立和撤销栈框架的指令后,简化了代码

    3)使ebp空暇出来,使之作为通用寄存器使用,添加通用寄存器的数量

    4)以上3点使得程序执行速度更快





    概念:Calling Convention  调用约定和 ABI (Application Binary Interface) 应用程序二进制接口

         

        函数怎样找到它的參数?

        函数怎样返回结果?

        函数在哪里存放局部变量?

        那一个硬件寄存器是起始空间?

        那一个硬件寄存器必须预先保留?





    Calling Convention  调用约定对以上问题作出了规定。Calling Convention也是ABI的一部分。

因此,遵守同样ABI规范的操作系统。使其相互间实现二进制代码的互操作成为了可能。

    比如:因为Solaris、Linux都遵守System V的ABI。Solaris 10就提供了直接执行Linux二进制程序的功能。

    详见文章:关注: Solaris 10的10大新变化 

             

3. 小结

    本文通过最简的C程序。引入下面概念:

        SFP 栈框架指针

        Stack aligned 栈对齐

        Calling Convention  调用约定 和 ABI (Application Binary Interface) 应用程序二进制接口

    今后。将通过进一步的实验,来深入了解这些概念。通过掌握这些概念,使在汇编级调试程序产生的core dump、掌握C语言高级调试技巧成为了可能。

LINUX下GDB反汇编和调试的更多相关文章

  1. Linux下gdb线程的调试

    多线程的调试命令 1.info threads: 这条命令显示的是当前可调试的所有线程,GDB会给每一个线程都分配一个ID.前面有*的线程是当前正在调试的线程. 2.thread ID: 切换到当前调 ...

  2. [转] linux下的c/c++调试器gdb

    PS:1. 断点C++类函数,用b 命名空间::类名::方法名 2. 编译参数一定要加-g,才可断点调试 http://www.cnblogs.com/xd502djj/archive/2012/08 ...

  3. linux下的c/c++调试器gdb

    Reference:  http://www.cnblogs.com/xd502djj/archive/2012/08/30/2663960.html linux下的c/c++调试器gdb gdbLi ...

  4. Linux知识(5)----LINUX下GDB调试

    命令 解释 示例   file 加载被调试的可执行程序文件.因为一般都在被调试程序所在目录下执行GDB,因而文本名不需要带路径. (gdb) file gdb-sample     r c Run的简 ...

  5. 一文入门Linux下gdb调试(二)

    作者:良知犹存 转载授权以及围观:欢迎添加微信号:Conscience_Remains 总述     今天我们介绍一下core dump文件,Core dump叫做核心转储,它是进程运行时在突然崩溃的 ...

  6. linux中gdb的可视化调试

    今天get到一个在linux下gdb调试程序的技巧和大家分享一下!平时我们利用gcc进行编程,进行程序调试时,观察程序的跳转等不是这么直观.都是入下的界面! 但是如果我们在编译连接时上加了-g命令生成 ...

  7. linux下gdb如何处理coredump错误

    linux下gdb如何处理coredump错误 在编写C++程序中,我们经常会遇到一种错误,segment fault, 这种coredump错误 会导致程序运行时异常退出或者终止,这种错误没有明显错 ...

  8. Linux下C/C++程序调试基础(GCC,G++,GDB,CGDB,DDD)

    在写程序的时候,经常会遇到一些问题,比如某些变量计算结果不是我们预期的那样,这时我们需要对程序进行调试.本文主要介绍调试C/C++在Linux操作系统下主要的调试工具. 在Linux下写程序,C/C+ ...

  9. Linux下C语言的调试 - gdb

    调试是每个程序员都会面临的问题. 如何提高程序员的调试效率, 更好更快地定位程序中的问题从而加快程序开发的进度, 是大家共同面对的问题. 可能Windows用户顺口就会说出:用VC呗 :-) , 它提 ...

随机推荐

  1. webdriver高级应用- 启动FireFox的同时打开Firebug

    1. 首先本机Firefox浏览器需要安装一下firebug插件,具体怎么安装这里不赘述,网上教程很多. 2. 具体自动化实现的代码如下: #encoding=utf-8 from selenium ...

  2. TensorFlow——深入MNIST

    程序(有些不甚明白的地方改日修订): # _*_coding:utf-8_*_ import inputdata mnist = inputdata.read_data_sets('MNIST_dat ...

  3. [python学习篇][python工具使用篇][1] 编辑,设置等

    1 添加sublime到环境变量 win +r ,输入sysdm.cpl, 在弹出的界面选择高级,选择环境变量,编辑path,添加sublime的安装目录(这是sublime的一种安装方式,另外一种安 ...

  4. 矩阵快速幂在ACM中的应用

    矩阵快速幂在ACM中的应用 16计算机2黄睿博 首发于个人博客http://www.cnblogs.com/BobHuang/ 作为一个acmer,矩阵在这个算法竞赛中还是蛮多的,一个优秀的算法可以影 ...

  5. DS作业06-图

    1.本周学习总结(0--2分) 1.1思维导图 1.2谈谈你对图结构的认识及学习体会. 图这一章的学习,是经过树学习后,难得一章重新寻找到感觉的学习.因为这一章比较少用递归,使用的是结构体,很多东西我 ...

  6. 【转】深入理解JVM—JVM内存模型

    http://www.cnblogs.com/dingyingsi/p/3760447.html#3497199 我们知道,计算机CPU和内存的交互是最频繁的,内存是我们的高速缓存区,用户磁盘和CPU ...

  7. JDBC 学习笔记(四)—— JDBC 加载数据库驱动,获取数据库连接

    1. 加载数据库驱动 通常来说,JDBC 使用 Class 类的 forName() 静态方法来加载驱动,需要输入数据库驱动代表的字符串. 例如: 加载 MySQL 驱动: Class.forName ...

  8. JDBC 学习笔记(三)—— JDBC 常用接口和类,JDBC 编程步骤

    1. JDBC 常用接口和类 DriverManager 负责管理 JDBC 驱动的服务类,程序中主要的功能是获取连接数据库的 Connection 对象. Connection 代表一个数据库连接对 ...

  9. 【bzoj4247】挂饰 背包dp

    题目描述 JOI君有N个装在手机上的挂饰,编号为1...N. JOI君可以将其中的一些装在手机上. JOI君的挂饰有一些与众不同——其中的一些挂饰附有可以挂其他挂件的挂钩.每个挂件要么直接挂在手机上, ...

  10. 慕课 python 操作数据库2 银行转账实例

    CREATE TABLE `account` ( `acctid` ) DEFAULT NULL COMMENT '账户ID', `) DEFAULT NULL COMMENT '余额' ) ENGI ...