汇编代码的简单分析

  • 内容说明

    本次的内容,是一次 MOOC 课程的作业。具体的,是在 Linux 下对一段简单的 C 代码生成的汇编代码进行分析,进而了解计算机、CPU 的工作机制。

  • 作业声明

         qianyizhou17 + 原创作品转载请注明出处 + 《Linux 内核分析》MOOC 课程 
         http://mooc.study.163.com/course/USTC-1000029000 
    
  • 实验准备

    • 环境
      Linux
      需要介绍一下的是本次 MOOC 提供的实验楼的环境,可以直接访问 Linux 的环境,一系列操作也十分简单,十分赞!
    • 源码
      main.c
int func_a(int x)
{
  return x + 1000;
}

int func_b(int x)
{
  return func_a(x);
}

int main(void)
{
  return func_b(8) + 1;
}
  • 生成汇编指令
$ gcc –S –o main.s main.c -m32
  • 生成如下
    .file   "main.c"
    .text
    .globl  func_a
    .type   func_a, @function
func_a:
.LFB0:
    .cfi_startproc
    pushl   %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    movl    8(%ebp), %eax
    addl    $1000, %eax
    popl    %ebp
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
.LFE0:
    .size   func_a, .-func_a
    .globl  func_b
    .type   func_b, @function
func_b:
.LFB1:
    .cfi_startproc
    pushl   %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    subl    $4, %esp
    movl    8(%ebp), %eax
    movl    %eax, (%esp)
    call    func_a
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
.LFE1:
    .size   func_b, .-func_b
    .globl  main
    .type   main, @function
main:
.LFB2:
    .cfi_startproc
    pushl   %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    subl    $4, %esp
    movl    $8, (%esp)
    call    func_b
    addl    $1, %eax
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
.LFE2:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 4.8.2-19ubuntu1) 4.8.2"
    .section    .note.GNU-stack,"",@progbits


其中以 “.” 开头的为辅助信息,在分析代码时可以删除,整理后的汇编信息如下:

func_a:
    pushl   %ebp
    movl    %esp, %ebp
    movl    8(%ebp), %eax
    addl    $1000, %eax
    popl    %ebp
    ret
func_b:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $4, %esp
    movl    8(%ebp), %eax
    movl    %eax, (%esp)
    call    func_a
    leave
    ret
main:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $4, %esp
    movl    $8, (%esp)
    call    func_b
    addl    $1, %eax
    leave
    ret
  • 代码分析前提

    • 什么是栈
      在我的理解,栈是一片内存,用于暂时缓存程序的指令。那么为何不使用普通的连续结构,而使用 “栈” 结构?
      利用栈的特性(后入先出),与函数调用的性质一致(最近调用的最先返回)。
      栈底是高地址:即,逐渐压栈,会使得访问的栈地址逐渐变小,可能发生错误——栈溢出。
      我们使用 ebp 来标识栈底,esp 标识当前的栈指针。
    • 必要的汇编说明
      • push
        pushl X:
        subl $4, %esp
        movl X, (%esp)
      • pop
        popl Y:
        movl (%esp), Y
        addl %4, %esp
      • call
        call ADDR
        pushl %eip(*)
        movl ADDR, %eip(*)
      • enter
        pushl %ebp
        movl %esp, %ebp
      • leave
        movl %ebp, %esp
        popl %ebp
      • ret
        popl %eip(*)
    • 必要的寄存器说明
      • ebp
        标识栈底
      • esp
        标识当前的栈指针
      • eip
        指令寄存器:存储当前执行的指令地址
      • eax
        一般的,默认用于存储函数返回值
  • 代码分析
    首先代码从 17 main 开始执行

执行 18、19 行,即执行 call 指令,保存上一次的栈信息:将上次的栈底入栈,并将栈底、栈指针重新指向下一地址,完成当前函数栈的初始化:
执行前

         |         | 
ebp ->    --------- 5000
.        |         |
.
.
         |         | 
esp ->    --------- 4000

执行后

         |         | 
          --------- 5000
.        |         |
.
.
         |         | 
          --------- 4000
         | ebp 5000| 
esp/ebp-> --------- 3996

执行 20、21 行,将立即数 8 存储到 esp 所指向的空间中

         |         | 
          --------- 5000
.        |         |
.
.
         |         | 
          --------- 4000
         | ebp 5000| 
ebp->     --------- 3996
         |    8    | 
esp->     --------- 3992

执行 22 行:call func_b


         |         | 
          --------- 5000
.        |         |
.
.
         |         | 
          --------- 4000
         | ebp 5000| 
ebp->     --------- 3996
         |    8    | 
          --------- 3992
         | eip 23  | 
esp->     --------- 3988

同时 eip 指向了 func_b 的地址:8。继续执行时,将从 func_b 的下一个地址 9 开始执行。
执行第 9、10 行后:

         |         | 
          --------- 5000
.        |         |
.
.
         |         | 
          --------- 4000
         | ebp 5000|  
          --------- 3996
         |    8    | 
          --------- 3992
         | eip 23  | 
          --------- 3988
         | ebp 3996| 
ebp/esp-> --------- 3984

以此类推,调用 call func_a,执行 func_a,第 5 行 addl $1000, %eax 执行后,eax 中存储的值变为 1008。
执行 6 行,popl %ebp
执行前

eip      1
eax      1008
         |         | 
          --------- 5000
.        |         |
.
.
         |         | 
          --------- 4000
         | ebp 5000|  
          --------- 3996
         |    8    | 
          --------- 3992
         | eip 23  | 
          --------- 3988
         | ebp 3996| 
          --------- 3984
         |    8    | 
          --------- 3980
         | eip 15  | 
          --------- 3976
         | ebp 3984| 
ebp/esp-> --------- 3972

执行 popl %ebp 后

eip      1              
eax      1008           
         |         |    
          --------- 5000
.        |         |    
.                       
.                       
         |         |    
          --------- 4000
         | ebp 5000|    
          --------- 3996
         |    8    |    
          --------- 3992
         | eip 23  |    
          --------- 3988
         | ebp 3996|    
ebp    -> --------- 3984
         |    8    |    
          --------- 3980
         | eip 15  |    
esp    -> --------- 3976
         | ebp 3984|    
          --------- 3972

执行 ret 之后

eip      15 
eax      1008
         |         | 
          --------- 5000
.        |         |
.
.
         |         | 
          --------- 4000
         | ebp 5000|  
          --------- 3996
         |    8    | 
          --------- 3992
         | eip 23  | 
          --------- 3988
         | ebp 3996| 
ebp    -> --------- 3984
         |    8    | 
esp    -> --------- 3980
         | eip 15  | 
          --------- 3976
         | ebp 3984| 
          --------- 3972

注意,此时 eip 的值为 15,因此继续执行的指令为 func_b 的 leave 指令
执行后

eip      15             
eax      1008           
         |         |    
          --------- 5000
.        |         |    
.                       
.                       
         |         |    
          --------- 4000
         | ebp 5000|    
ebp    -> --------- 3996
         |    8    |    
          --------- 3992
         | eip 23  |    
esp    -> --------- 3988
         | ebp 3996|    
          --------- 3984
         |    8    |    
          --------- 3980
         | eip 15  |    
          --------- 3976
         | ebp 3984|    
          --------- 3972

继续执行 ret 后

eip      23             
eax      1008           
         |         |    
          --------- 5000
.        |         |    
.                       
.                       
         |         |    
          --------- 4000
         | ebp 5000|    
ebp    -> --------- 3996
         |    8    |    
esp    -> --------- 3992
         | eip 23  |    
          --------- 3988
         | ebp 3996|    
          --------- 3984
         |    8    |    
          --------- 3980
         | eip 15  |    
          --------- 3976
         | ebp 3984|    
          --------- 3972

addl $1, %eax 之后,eax 中存储 1009。

继续执行 leave,将当前的栈信息恢复。

before leave                    after leave, before ret 
eip      23                     eip      23             
eax      1009                   eax      1009           
         |         |                     |         |    
          --------- 5000        ebp    -> --------- 5000
.        |         |            .        |         |    
.                               .                       
.                               .                       
         |         |                     |         |    
          --------- 4000        esp    -> --------- 4000
         | ebp 5000|                     | ebp 5000|    
ebp    -> --------- 3996                  --------- 3996
         |    8    |                     |    8    |    
esp    -> --------- 3992                  --------- 3992
         | eip 23  |                     | eip 23  |    
          --------- 3988                  --------- 3988
         | ebp 3996|                     | ebp 3996|    
          --------- 3984                  --------- 3984
         |    8    |                     |    8    |    
          --------- 3980                  --------- 3980
         | eip 15  |                     | eip 15  |    
          --------- 3976                  --------- 3976
         | ebp 3984|                     | ebp 3984|    
          --------- 3972                  --------- 3972

调用 ret,恢复 eip:将 esp 所指向的指令值写入 eip,main 函数结束。

  • 总结
    • 进入函数时,一般首先pushl %ebp; movl %esp, %ebp来进行栈信息的保存,包括 1)将上次的栈底信息压入栈; 2)将栈底、栈指针重置为下一个栈地址。
    • 调用函数时,将 eip 的当前信息压入栈中,并更新 eip 使指向新的指令。
    • 函数返回的过程,即是从栈中读取 eip、ebp 的旧有信息并恢复上一个函数栈的过程
    • 其中,计算机可以使用机械的逻辑(依据 eip 读取下一条指令,并逐条eip所指向指令即可,周而复始),而通过精致的数据结构(栈)和基础的CPU指令,便能够实现复杂的逻辑!
    • 函数执行、并调用新的函数时,其栈信息基本如下:
 --------- 4000
| ebp DATA|    
 --------- XXXX
|         |    
|         |
|         |
 --------- XXXX
| eip DATA|    
 --------- 39XX

你可能感兴趣的:(C/C++,汇编,Linux)