文章目錄
  1. 1. 详情请见: http://heamon7.gitbooks.io/cscw2-newly-to-assembly/content/
  2. 2. 从汇编角度浅析C程序
    1. 2.0.1. 从以下5个部分来探究:
  • 3. 1 初探C程序的汇编级形式
    1. 3.1. 分为以下三个部分:
  • 4. 1.1 用gcc将C程序编译成汇编代码并查看
  • 5. 1.2 gcc的编译链接等过程
    1. 5.0.1. gcc代理的编译过程:
  • 6. 1.3 程序执行时的内存分配情况概览
  • 7. 2 汇编语言及其相关指令简介
    1. 7.0.1. 分为以下三个部分介绍:
  • 8. 2.1 汇编语言简介
  • 9. 2.2 8086,x86,x86-64简介
    1. 9.0.0.1. 8086
      1. 9.0.0.1.1. 寄存器
    2. 9.0.0.2. 80386
      1. 9.0.0.2.1. 寄存器
    3. 9.0.0.3. x86-64
      1. 9.0.0.3.1. 寄存器
  • 10. 2.3 AT&T风格x86-64汇编指令
    1. 10.1. 操作数指示符
      1. 10.1.0.0.1. 寻址模式
  • 10.2. 数据传送指令
  • 11. 4.6 函数调用栈总结分析
    1. 11.0.1. 程序启动时的Linux堆栈:
  • 12. 5 其他关于C程序的汇编话题
  • 详情请见: http://heamon7.gitbooks.io/cscw2-newly-to-assembly/content/

    从汇编角度浅析C程序

    从以下5个部分来探究:

    • 1.初探C程序的汇编级形式
    • 2.汇编语言及其相关指令简介
    • 3.用汇编分析C程序的工具介绍
    • 4.再探C程序的汇编级形式
    • 5.其他基于C程序的汇编话题

    1 初探C程序的汇编级形式

    分为以下三个部分:

    • 1.1用gcc将C程序编译成汇编代码并查看
    • 1.2C程序到可执行文件的编译链接过程
    • 1.3程序被执行时的内存分配情况概览

    1.1 用gcc将C程序编译成汇编代码并查看

    编写代码:code/1.1/ add.c :

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    int add(int a, int b) {
    int result;

    result = a + b;

    return result;
    }

    int main(int argc, char *argv[]) {
    int a,b,result;

    a = 1;
    b = 2;
    result = add(a,b);

    return 0;
    }

    执行shell命令得到汇编代码:

    1
    gcc -S add.c -o add.s

    生成代码: code/1.1/ add.s ,其中的关键部分:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    add:
    pushq %rbp
    movq %rsp, %rbp
    movl %edi, -20(%rbp)
    movl %esi, -24(%rbp)
    movl -24(%rbp), %eax
    movl -20(%rbp), %edx
    addl %edx, %eax
    movl %eax, -4(%rbp)
    movl -4(%rbp), %eax
    popq %rbp
    ret
    main:
    pushq %rbp
    movq %rsp, %rbp
    subq $32, %rsp
    movl %edi, -20(%rbp)
    movq %rsi, -32(%rbp)
    movl $1, -12(%rbp)
    movl $2, -8(%rbp)
    movl -8(%rbp), %edx
    movl -12(%rbp), %eax
    movl %edx, %esi
    movl %eax, %edi
    call add
    movl %eax, -4(%rbp)
    movl $0, %eax
    leave
    ret

    1.2 gcc的编译链接等过程

    gcc代理的编译过程:

    gcc代理的编译过程

    ref:
    http://7905648.blog.51cto.com/7895648/1297255
    http://tech.meituan.com/linker.html

    1.3 程序执行时的内存分配情况概览

    进程内存区域的分布:

    进程内存区域的分布

    ref:
    http://blog.sina.com.cn/s/blog_5420e0000101a0w1.html
    http://blog.csdn.net/chengyingzhilian/article/details/8045428
    http://www.kerneltravel.net/journal/v/mem.htm

    2 汇编语言及其相关指令简介

    分为以下三个部分介绍:

    • 2.1.汇编语言简介
    • 2.2 8086,x86,x86-64寄存器简介
    • 2.3 AT&T风格x86-64汇编指令

    2.1 汇编语言简介

    汇编语言采用了助记符(mnemonics)来代表特定低级机器语言的操作。特定的汇编目标指令集可能会包括特定的操作数。许多汇编程序可以识别代表地址和常量的标签(label)和符号(symbols),这样就可以用字符来代表操作数而无需采取写死的方式。普遍地说,特定的汇编语言和特定的机器语言指令集是一一对应的。

    汇编指令的两大风格分别是Intel汇编与AT&T汇编,分别被Microsoft Windows/Visual C++GNU/Gas采用(Gas也可使用Intel汇编风格)

    我们这里不介绍他们的使用区别,而直接介绍Linux下默认的AT&T汇编风格。

    ref:
    http://zh.wikipedia.org/zh/%E6%B1%87%E7%BC%96%E8%AF%AD%E8%A8%80

    2.2 8086,x86,x86-64简介

    8086

    8086是由英特爾公司於1976年开始設計,1978年年中发布的Intel第一款16位微處理器,同时也是x86架構之開端。

    所有的内部寄存器、内部及外部数据总线都是16位宽,因此是完全的16位微处理器。20位外部地址总线,因此物理寻址空间为1MB (即2^20
    = 1,048,576).由于内部寄存器都是16位,对1M地址空间寻址时采取了段寻址方式。8086的封装采用40引脚的双列直插(dual in-line),数据总线与地址总线复用了前16个引脚。16位的I/O地址,因此独立的I/O寻址空间为64KB (即2^16
    = 65,536).由于8086内部的地址寄存器是16 位宽,因而最大线性寻址空间为64 KB.使用超过64 KB内存空间的程序设计时,需要调整段寄存器(segment registers)。直到32位的80386出现之前,8086的这种段寻址相当不便.

    寄存器

    8086有8个16比特的寄存器,包括栈寄存器SP与BP,但不包括指令寄存器IP、控制寄存器FLAGS以及四个段寄存器。AX, BX, CX, DX,这四个寄存器可以按照字节访问;但BP, SI, DI, SP,这四个地址寄存器只能按照16位宽访问。

    Block diagram of Intel 8086:

    Block diagram of Intel 8086

    The 8086 registers:

    The 8086 registers

    ref:
    http://zh.wikipedia.org/zh/Intel_8086
    http://www.cnblogs.com/zhaoyl/archive/2012/05/15/2501972.html


    80386

    Intel 80386,是英特尔(Intel)公司的一款x86系列CPU,最初发布于1985年10月17日。
    80386处理器被广泛应用在1980年代中期到1990年代中期的IBM PC兼容机中。这些PC被称为“80386电脑”或“386电脑”,有时也简称“80386”或“386”。
    80386的重要特点是:

    寄存器

    x86通用寄存器:
    x86通用寄存器

    ref:
    http://blog.chinaunix.net/uid-23069658-id-3756930.html
    http://zh.wikibooks.org/wiki/X86%E7%B5%84%E5%90%88%E8%AA%9E%E8%A8%80/X86%E6%9E%B6%E6%9E%84%E5%8F%8A%E5%AF%84%E5%AD%98%E5%99%A8%E8%A7%A3%E9%87%8A


    x86-64

    x86-64(簡稱x64)是64位版本的x86指令集,向前相容於16位32位的x86架構。x64於1999年由AMD設計,AMD首次公開64位元集以擴充給x86,稱為「AMD64」。其後也為英特爾所採用,現時英特爾稱之為「Intel 64」,在之前曾使用過「Clackamas Technology」 (CT)、「IA-32e」及「EM64T」。
    Applerpm 以「x86-64」或「x86_64」稱呼此64位架構。太陽電腦(已被甲骨文公司收購)及 Microsoft 稱之為「x64」。BSD 家族及其他 Linux發行版則使用「amd64」,32位元版本則稱為「i386」(或 i486/586/686)。
    在x86-64出現以前,英特爾與惠普聯合設計出IA-64架構;惟IA-64並不與x86兼容,且市場反應較冷淡,同時受制於多個專利權,使其他廠商不能模仿。與x86兼容的AMD64架構便應運而生,其主要特點如名稱所述,既有支援64位通用暫存器、64位整數及邏輯運算、以及64位虛擬位址,設計人員又為架構作出不少改進,部份重大改變如下:

    寄存器

    x86-64寄存器

    ref:

    http://zh.wikipedia.org/zh/X86-64
    http://www.searchtb.com/2013/03/x86-64_register_and_function_frame.html

    (CPU中程序员唯一能够控制的就是寄存器)

    2.3 AT&T风格x86-64汇编指令

    操作数指示符

    操作数格式

    寻址模式
    1
    ADDRESS_OR_OFFSET(%BASE_OR_OFFSET,%INDEX,MULTIPLIER)

    它所表示的地址可以这样计算出来:

    1
    FINAL ADDRESS = ADDRESS_OR_OFFSET + BASE_OR_OFFSET + MULTIPLIER * INDEX

    其中ADDRESS_OR_OFFSET和MULTIPLIER必须是常数,BASE_OR_OFFSET和INDEX必须是寄存器。在有些寻址方式中会省略这4项中的某些项,相当于这些项是0。

    • 直接寻址(Direct Addressing Mode)。只使用ADDRESS_OR_OFFSET寻址,例如movl ADDRESS, %eax
      把ADDRESS地址处的32位数传送到eax
      寄存器。
    • 变址寻址(Indexed Addressing Mode) 。上一节的movl data_items(,%edi,4), %eax
      就属于这种寻址方式,用于访问数组元素比较方便。
    • 间接寻址(Indirect Addressing Mode)。只使用BASE_OR_OFFSET寻址,例如movl (%eax), %ebx,把eax寄存器的值看作地址,把这个地址处的32位数传送到ebx寄存器。注意和movl %eax, %ebx区分开。
    • 基址寻址(Base Pointer Addressing Mode)。只使用ADDRESS_OR_OFFSET和BASE_OR_OFFSET寻址,例如movl 4(%eax), %ebx ,用于访问结构体成员比较方便,例如一个结构体的基地址保存在eax 寄存器中,其中一个成员在结构体内的偏移量是4字节,要把这个成员读上来就可以用这条指令。
    • 立即数寻址(Immediate Mode)。就是指令中有一个操作数是立即数,例如movl $12, %eax 中的$12 ,这其实跟寻址没什么关系,但也算作一种寻址方式。
    • 寄存器寻址(Register Addressing Mode)。就是指令中有一个操作数是寄存器,例如movl $12, %eax 中的%eax ,这跟内存寻址没什么关系,但也算作一种寻址方式。在汇编程序中寄存器用助记符来表示,在机器指令中则要用几个Bit表示寄存器的编号,这几个Bit也可以看作寄存器的地址,但是和内存地址不在一个地址空间。

    ref:
    http://docs.linuxtone.org/ebooks/C&CPP/c/ch18s04.html

    数据传送指令

    数据传送指令:

    数据传送指令

    常用的三条指令:

    • S,D
      1
      ```
      - 
      ```push S
    • D
      1
      ```
      
      ## 算术和逻辑运算指令
      
      算术和逻辑运算指令:
      
      ![算术和逻辑运算指令](http://picture-repository-of-heamon7.qiniudn.com/assembly算术和逻辑运算指令.png)
      
      常用的三条指令:
      
      > - 
      ```inc D
      ``` 
      - 
      ```add S,D
    • S,D
      1
      ```
      
      
      ## 过程调用指令
      过程调用指令:
      
      ![过程调用指令](http://picture-repository-of-heamon7.qiniudn.com/assembly过程调用指令.png)
      
      常用的三条指令:
      
      > - 
      ```call Lable   
      ``` 
      - 
      ```leave
    • 1
      ```
      
      上面这三条指令每一条都相当于几条指令。  
      
      call指令的效果是将返回地址入栈,并跳转到被调用过程的起始处。
      
      leave指令使栈做好返回的准备,等价于下面的代码序列:

    mov %rbp,%rsp
    pop %rbp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    ret指令从栈中弹出地址,并跳转到这个位置。


    3 用汇编分析C程序的工具介绍
    ========
    ### 分为以下两个部分介绍:
    > - 3.1 gcc
    - 3.2 gdb&&ddd


    3.1 gcc
    =======

    ### C 编程中相关文件后缀:

    >.a 静态库 (archive)
    .c C源代码(需要编译预处理)
    .h C源代码头文件
    .i C源代码(不需编译预处理)
    .o 对象文件
    .s 汇编语言代码
    .so 动态库


    ### 常用编译选项:
    >- -o:指定生成的输出文件;
    - -E:仅执行编译预处理;
    - -S:将C代码转换为汇编代码;
    - -Wall:显示警告信息;
    - -c:仅执行编译操作,不进行连接操作。
    - -g:表示在生成的目标文件中带调试信息



    ref:
    http://man.linuxde.net/gcc
    http://wiki.ubuntu.org.cn/Gcchowto
    http://wiki.ubuntu.org.cn/Compiling_C

    3.2 gdb&&ddd
    =========


    ### 使用GDB
    一般来说GDB主要调试的是C/C++的程序。要调试C/C++的程序,首先在编译时,我们必须要把调试信息加到可执行文件中。使用编译器(cc/gcc/g++)的 -g 参数可以做到这一点。如:

    $gcc -g -Wall hello.c -o hello
    $g++ -g -Wall hello.cpp -o hello

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32

    ### 常用命令
    >- gdb <program> //启动gdb
    - break <function>/break <linenum> //设置断点
    - continue //恢复程序的运行直到程序结束,或下一个断点到来。
    - step <count> //单步跟踪,如果有函数调用,他会进入该函数。
    - next <count>
    - stepi 或 si
    - nexti 或 ni
    - ........


    ref:
    http://wiki.ubuntu.org.cn/%E7%94%A8GDB%E8%B0%83%E8%AF%95%E7%A8%8B%E5%BA%8F

    http://wiki.ubuntu.org.cn/index.php?title=Insight%E7%9A%84%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95%EF%BC%88gdb%E7%9A%84%E6%9C%80%E4%BC%98%E5%89%8D%E7%AB%AF%EF%BC%89&variant=zh-hans

    (objdump)


    4 再探C程序的汇编级形式:
    ===========

    > - 查看反汇编代码
    - 进行常规调试
    - 进行机器级代码调试
    - 用objdump查看反汇编代码
    - 逐段分析反汇编代码
    - 函数调用栈总结分析

    4.1 查看反汇编代码
    ======
    gcc编译链接得到目标文件:

    gcc -g -Wall add.c -o add

    1
    2

    利用gdb查看反汇编代码:

    gdb -q add

    1
    2

    得到:

    heamon7@ubuntu:~/Project/test$ gdb -q add
    Reading symbols from add…done.
    (gdb) l 1,20
    1 int add(int a, int b) {
    2 int result;
    3
    4 result = a + b;
    5
    6 return result;
    7 }
    8
    9 int main(int argc, char *argv[]) {
    10 int a,b,result;
    11
    12 a = 1;
    13 b = 2;
    14 result = add(a,b);
    15
    16 return 0;
    17 }
    (gdb) disass add
    Dump of assembler code for function add:
    0x00000000004004ed <+0>: push %rbp
    0x00000000004004ee <+1>: mov %rsp,%rbp
    0x00000000004004f1 <+4>: mov %edi,-0x14(%rbp)
    0x00000000004004f4 <+7>: mov %esi,-0x18(%rbp)
    0x00000000004004f7 <+10>: mov -0x18(%rbp),%eax
    0x00000000004004fa <+13>: mov -0x14(%rbp),%edx
    0x00000000004004fd <+16>: add %edx,%eax
    0x00000000004004ff <+18>: mov %eax,-0x4(%rbp)
    0x0000000000400502 <+21>: mov -0x4(%rbp),%eax
    0x0000000000400505 <+24>: pop %rbp
    0x0000000000400506 <+25>: retq
    End of assembler dump.
    (gdb) disass main
    Dump of assembler code for function main:
    0x0000000000400507 <+0>: push %rbp
    0x0000000000400508 <+1>: mov %rsp,%rbp
    0x000000000040050b <+4>: sub $0x20,%rsp
    0x000000000040050f <+8>: mov %edi,-0x14(%rbp)
    0x0000000000400512 <+11>: mov %rsi,-0x20(%rbp)
    0x0000000000400516 <+15>: movl $0x1,-0xc(%rbp)
    0x000000000040051d <+22>: movl $0x2,-0x8(%rbp)
    0x0000000000400524 <+29>: mov -0x8(%rbp),%edx
    0x0000000000400527 <+32>: mov -0xc(%rbp),%eax
    0x000000000040052a <+35>: mov %edx,%esi
    0x000000000040052c <+37>: mov %eax,%edi
    0x000000000040052e <+39>: callq 0x4004ed
    0x0000000000400533 <+44>: mov %eax,-0x4(%rbp)
    0x0000000000400536 <+47>: mov $0x0,%eax
    0x000000000040053b <+52>: leaveq
    0x000000000040053c <+53>: retq
    End of assembler dump.
    (gdb)

    1
    2
    3
    4
    5
    6


    4.2 进行常规调试
    =======


    接着打断点,进行常规调试,查看程序的运行的情况:

    (gdb) b 14
    Breakpoint 1 at 0x400524: file add.c, line 14.
    (gdb) r
    Starting program: /home/heamon7/Project/test/add

    Breakpoint 1, main (argc=1, argv=0x7fffffffe658) at add.c:14
    14 result = add(a,b);
    (gdb) p a
    $1 = 1
    (gdb) p b
    $2 = 2
    (gdb) p result
    $3 = 0
    (gdb) s
    add (a=1, b=2) at add.c:4
    4 result = a + b;
    (gdb) s
    6 return result;
    (gdb) s
    7 }
    (gdb) p result
    $4 = 3
    (gdb) c
    Continuing.
    [Inferior 1 (process 23833) exited normally]
    (gdb)

    1
    2
    3
    4

    4.3 进行机器级代码调试
    =======

    接着我们看一下从机器级层面对代码进行调试,查看程序的运行情况:

    (gdb) b 12
    Breakpoint 1 at 0x400516: file add.c, line 12.
    (gdb) r
    Starting program: /home/heamon7/Project/test/add

    Breakpoint 1, main (argc=1, argv=0x7fffffffe658) at add.c:12
    12 a = 1;
    (gdb) si
    13 b = 2;
    (gdb)
    14 result = add(a,b);
    (gdb)
    0x0000000000400527 14 result = add(a,b);
    (gdb)
    0x000000000040052a 14 result = add(a,b);
    (gdb)
    0x000000000040052c 14 result = add(a,b);
    (gdb)
    0x000000000040052e 14 result = add(a,b);
    (gdb)
    add (a=0, b=0) at add.c:1
    1 int add(int a, int b) {
    (gdb)
    0x00000000004004ee 1 int add(int a, int b) {
    (gdb)
    0x00000000004004f1 1 int add(int a, int b) {
    (gdb)
    0x00000000004004f4 1 int add(int a, int b) {
    (gdb)
    4 result = a + b;
    (gdb)
    0x00000000004004fa 4 result = a + b;
    (gdb)
    0x00000000004004fd 4 result = a + b;
    (gdb)
    0x00000000004004ff 4 result = a + b;
    (gdb)
    6 return result;
    (gdb)
    7 }
    (gdb)
    0x0000000000400506 7 }
    (gdb)
    0x0000000000400533 in main (argc=1, argv=0x7fffffffe658) at add.c:14
    14 result = add(a,b);
    (gdb)
    16 return 0;
    (gdb)
    17 }
    (gdb)
    0x000000000040053c 17 }
    (gdb)
    __libc_start_main (main=0x400507

    , argc=1, argv=0x7fffffffe658,
    init=, fini=, rtld_fini=,
    stack_end=0x7fffffffe648) at libc-start.c:321
    321 libc-start.c: No such file or directory.
    (gdb) c
    Continuing.
    [Inferior 1 (process 23945) exited normally]
    (gdb)
    1
    2
    3
    4
    5
    6
    7
    8


    4.4 用objdump查看反汇编代码
    =======


    这里的运行情况和我们常规调试看到的并不一样,我们回到之前的汇编代码,分析一下每一步在干什么。
    为了方便查看源C代码和汇编代码的对应关系,我们利用反汇编工具objdump得到的反汇编代码,来分析程序执行过程。
    从目标文件反汇编得到的汇编代码:

    gcc -g -Wall add.c -o add
    objdump -S add

    1
    2

    得到:

    00000000004004ed :
    int add(int a, int b) {
    4004ed: 55 push %rbp
    4004ee: 48 89 e5 mov %rsp,%rbp
    4004f1: 89 7d ec mov %edi,-0x14(%rbp)
    4004f4: 89 75 e8 mov %esi,-0x18(%rbp)
    int result;

    result = a + b;
    

    4004f7: 8b 45 e8 mov -0x18(%rbp),%eax
    4004fa: 8b 55 ec mov -0x14(%rbp),%edx
    4004fd: 01 d0 add %edx,%eax
    4004ff: 89 45 fc mov %eax,-0x4(%rbp)

    return result;
    

    400502: 8b 45 fc mov -0x4(%rbp),%eax
    }
    400505: 5d pop %rbp
    400506: c3 retq

    0000000000400507

    :

    int main(int argc, char *argv[]) {
    400507: 55 push %rbp
    400508: 48 89 e5 mov %rsp,%rbp
    40050b: 48 83 ec 20 sub $0x20,%rsp //为main函数分配栈帧空间,此处为32个字节
    40050f: 89 7d ec mov %edi,-0x14(%rbp) //将系统传给main函数的第一个参数复制到自己的栈帧(可以查看值吗?)
    400512: 48 89 75 e0 mov %rsi,-0x20(%rbp) //将系统传给main函数的第二个参数复制到自己的栈帧,,并存放在栈帧的最顶部(可以查看值吗?)

    int a,b,result;
    
    a = 1;
    

    400516: c7 45 f4 01 00 00 00 movl $0x1,-0xc(%rbp) //此处为给main函数的局部变量a赋值过程,将该值复制到main函数的栈帧中;使用movl表明这个变量是4个字节的,打在C代码第12行的断点,在汇编代码里实际断点在这里
    b = 2;
    40051d: c7 45 f8 02 00 00 00 movl $0x2,-0x8(%rbp) //此处为给main函数的局部变量b赋值过程,将该值复制到main函数的栈帧中;可以看出局部变量的地址是从低向高分配的
    result = add(a,b);
    400524: 8b 55 f8 mov -0x8(%rbp),%edx
    400527: 8b 45 f4 mov -0xc(%rbp),%eax
    40052a: 89 d6 mov %edx,%esi
    40052c: 89 c7 mov %eax,%edi
    40052e: e8 ba ff ff ff callq 4004ed
    400533: 89 45 fc mov %eax,-0x4(%rbp)

    return 0;
    

    400536: b8 00 00 00 00 mov $0x0,%eax
    }
    40053b: c9 leaveq
    40053c: c3 retq
    40053d: 0f 1f 00 nopl (%rax)

    1
    2
    3
    4
    5
    6
    7



    4.5 逐段分析反汇编代码:
    ========


    接下来,我们对以上代码逐段分析:

    int main(int argc, char *argv[]) {
    400507: 55 push %rbp //建立main函数的栈帧
    400508: 48 89 e5 mov %rsp,%rbp //建立main函数的栈帧
    40050b: 48 83 ec 20 sub $0x20,%rsp //为main函数分配栈帧空间,此处为32个字节
    40050f: 89 7d ec mov %edi,-0x14(%rbp) //将系统传给main函数的第一个参数复制到自己的栈帧(可以查看值吗?)
    400512: 48 89 75 e0 mov %rsi,-0x20(%rbp) //将系统传给main函数的第二个参数复制到自己的栈帧,,并存放在栈帧的最顶部(可以查看值吗?)

    1
    2

    此时通过:

    (gdb) x/1xg $rbp-0x20
    0x7fffffffe550: 0x00007fffffffe658
    (gdb) x/1xw $rbp-0x14
    0x7fffffffe55c: 0x00000001

    1
    2
    3
    4
    5
    6

    我们发现传递给main函数的第一个参数的值是1,而第二个参数的值貌似是一个内存地址(值的引用/指针)。并且可以看出函数的实参列表(Arglist)在栈帧中的地址是从高向低分配的。
    同时我们认为main函数的栈帧是从此时的$rbp+0x10开始的,也就是在main函数栈帧的底部里还保存了rbp和rip。
    (很奇怪的是两个参数之间存的值竟然是_start函数的的4字节地址,这里是考虑了8个字节的对齐吗?)

    接着看:

    int a,b,result;
    
    a = 1;
    

    400516: c7 45 f4 01 00 00 00 movl $0x1,-0xc(%rbp) //此处为给main函数的局部变量a赋值过程,将该值复制到main函数的栈帧中;使用movl表明这个变量是4个字节的
    b = 2;
    40051d: c7 45 f8 02 00 00 00 movl $0x2,-0x8(%rbp) //此处为给main函数的局部变量b赋值过程,将该值复制到main函数的栈帧中

    1
    2
    3
    4

    我们发现,打在C程序代码第12行的断点,在汇编代码实际断点在这里,也可以看出函数局部变量(Locals)的地址是从低向高分配的。而且一个有趣的现象是ArglistLocals的起始地址都是-0x10(%rbp)。

    接着看:

    result = add(a,b);
    

    400524: 8b 55 f8 mov -0x8(%rbp),%edx //这里把传递给add函数的第二个参数的值复制到寄存器edx
    400527: 8b 45 f4 mov -0xc(%rbp),%eax //这里把传递给add函数的第一个参数的值复制到寄存器eax
    40052a: 89 d6 mov %edx,%esi //接着把传递给add函数的第二个参数值从edx寄存器复制到esi寄存器
    40052c: 89 c7 mov %eax,%edi //接着把传递给add函数的第一个参数值从eax寄存器复制到edi寄存器
    40052e: e8 ba ff ff ff callq 4004ed //这里call命令让整个程序的执行流跳转到地址(地址长度是一个字节)0x4004ed(也就是add函数的地址)处,
    400533: 89 45 fc mov %eax,-0x4(%rbp) //(暂停main函数的分析,进而分析add函数;注意这条指令的地址在该函数的上一条指令被执行前,存入eip寄存器了)

    1
    2
    3
    4
    5
    6
    7
    8
    9

    这里实际的情况印证了之前讲到的,rdirsirdxrcxr8dr9d6个寄存器会依次暂存主调函数传给被调函数的参数。而之所以中间还要转存到edxeax是因为,cpu从寄存器中读取数据的速度远远大于从内存中读取数据的速度;为了提高性能,一旦内存中一个参数被使用,那么先会被暂存到一个空余的寄存器中,以后再使用时,就不用从内存中读取了。

    我们也可以看出在存取传递给函数的参数时,是从右向左读取的。
    eip的工作原理是,cpu读取当前eip指向的指令,存入指令缓冲器(指令队列)中,然后eip根据被读取指令的长度,增加相应的字节数,指向下一条指令,然后cpu执行指令队列中刚刚读取的指令。

    call这个跳转指令在执行时,实际分为两步,一个是先pop该指令执行时eip的值(即主调函数的调用发生时的下一条指令地址)到当前函数的栈帧中(当前栈帧增长,esp的值会减小8个字节),然后程序的执行流跳转到相应的地址处,即eip的值等于相应的地址(此处即为add函数的地址处0x00000000004004ed)。

    接着看add函数的内部:

    00000000004004ed :
    int add(int a, int b) {
    4004ed: 55 push %rbp //首先把主调函数的栈基址入栈(栈增长,esp的值减小8个字节)
    4004ee: 48 89 e5 mov %rsp,%rbp // 让当前基址指针指向主调函数的栈顶
    4004f1: 89 7d ec mov %edi,-0x14(%rbp) //将主调函数传给add函数的第一个实参复制到add函数的栈帧
    4004f4: 89 75 e8 mov %esi,-0x18(%rbp) //将主调函数传给add函数的第二个实参复制到add函数的栈帧

    1
    2
    3
    4
    5
    6

    系统并没有像在main函数里的那样,显式地给add函数分配栈帧空间,原因是add函数内并不调用其他函数,因此没有必要让esp的值再发生变化。所以实际上add函数的栈帧的顶部和其主调函数的栈顶重合。
    同时我们认为add函数的栈帧开始于此时的$rbp+0x10,
    (这里为什么要保留16个字节的空间没有使用呢?)

    接着:

    int result;
    
    result = a + b;
    

    4004f7: 8b 45 e8 mov -0x18(%rbp),%eax //将加法运算的第二个操作数的值从栈中复制到eax寄存器
    4004fa: 8b 55 ec mov -0x14(%rbp),%edx //将加法运算的第一个操作数的值从栈中复制到edx寄存器
    4004fd: 01 d0 add %edx,%eax //执行加法运算,并将值保存在eax寄存器中
    4004ff: 89 45 fc mov %eax,-0x4(%rbp) //将eax寄存器中的值(得到的和)复制到add函数的栈帧中(这个地址就是add函数的局部变量result的地址)

    1
    2
    3
    4

    这里验证了之前讲到的,在使用内存中定义的一个值时,会先把它复制到一个寄存器中暂存起来。

    接下来:
    return result;
    

    400502: 8b 45 fc mov -0x4(%rbp),%eax //将返回值result复制到寄存器eax中
    }
    400505: 5d pop %rbp //将add函数栈帧的栈顶值(上一个函数的栈基址)弹出到rbp寄存器中
    400506: c3 retq //ret指令从栈中弹出地址,并跳转到这个地址,这里相当于把值弹出到eip寄存器中。

    1
    2
    3
    4

    这里验证了前面讲的eax寄存器经常存储被调函数的返回值。执行到这里后,由于之前栈中压入的eip,跳转到 # callq 400ed <add> #指令的下一条指令。

    然后:

    400533: 89 45 fc mov %eax,-0x4(%rbp) //将被调函数的返回值复制到main函数栈帧中(局部变量result)

    return 0;
    

    400536: b8 00 00 00 00 mov $0x0,%eax //main函数向它的主调函数返回值0
    }
    40053b: c9 leaveq //leave指令可以使栈做好返回准备,
    40053c: c3 retq //同之前介绍的一样
    40053d: 0f 1f 00 nopl (%rax)

    1
    2

    leave指令相当于以下两条指令:

    mov %rbp,%rsp
    pop %rbp
    ```

    4.6 函数调用栈总结分析

    程序启动时的Linux堆栈:

    程序启动时的Linux堆栈

    5 其他关于C程序的汇编话题

    • 递归函数
    • 数组的表示
    • 结构体的表示
    • 条件跳转指令
    • 符号寄存器
    • ……
    文章目錄
    1. 1. 详情请见: http://heamon7.gitbooks.io/cscw2-newly-to-assembly/content/
    2. 2. 从汇编角度浅析C程序
      1. 2.0.1. 从以下5个部分来探究:
  • 3. 1 初探C程序的汇编级形式
    1. 3.1. 分为以下三个部分:
  • 4. 1.1 用gcc将C程序编译成汇编代码并查看
  • 5. 1.2 gcc的编译链接等过程
    1. 5.0.1. gcc代理的编译过程:
  • 6. 1.3 程序执行时的内存分配情况概览
  • 7. 2 汇编语言及其相关指令简介
    1. 7.0.1. 分为以下三个部分介绍:
  • 8. 2.1 汇编语言简介
  • 9. 2.2 8086,x86,x86-64简介
    1. 9.0.0.1. 8086
      1. 9.0.0.1.1. 寄存器
    2. 9.0.0.2. 80386
      1. 9.0.0.2.1. 寄存器
    3. 9.0.0.3. x86-64
      1. 9.0.0.3.1. 寄存器
  • 10. 2.3 AT&T风格x86-64汇编指令
    1. 10.1. 操作数指示符
      1. 10.1.0.0.1. 寻址模式
  • 10.2. 数据传送指令
  • 11. 4.6 函数调用栈总结分析
    1. 11.0.1. 程序启动时的Linux堆栈:
  • 12. 5 其他关于C程序的汇编话题