基于x86的Hello World汇编代码分析(AT&T风格汇编)

基于x86的Hello World汇编代码分析

(AT&T汇编风格)

    本文通过对由gcc对简单C语言代码编译生成的汇编码进行逐句分析解读,来学习x86的汇编结构和堆栈机制。文章涉及细节较多,难免出错,望读者不吝赐教!

一、代码

C语言代码:
/* file: hello.c */
 1 #include <stdio.h>
 2
 3 int add( int a, int b){
 4      return (a+b);
 5 }
 6
 7 int main( int argc, char **argv){
 8      int a, b, c;
 9     a = 3;
10     b = 4;
11     c = add(a, b);
12     printf( "a+b= %d /n ", c);
13     printf( "Hello World! /n ");
14      return  0;
15 }
16


gcc -S -ohello.s hello.c输出文件:
/* file: hello.s */
 1      .file  " hello. c"
 2      .text
 3 .globl  add
 4      .type   add, @ function
 5 add:
 6      pushl  % ebp
 7      movl   % esp, % ebp
 8      movl   12(% ebp), % edx
 9      movl   8(% ebp), % eax
10      addl   % edx, % eax
11      popl   % ebp
12      ret
13      .size   add, .- add
14      .section   .rodata
15 .LC0:
16      .string    " a+ b=% d/ n"
17 .LC 1:
18      .string    " Hello  World!"
19      .text
20 .globl  main
21      .type   main, @ function
22 main:
23      leal   4(% esp), % ecx
24      andl   $- 16, % esp
25      pushl  - 4(% ecx)
26      pushl  % ebp
27      movl   % esp, % ebp
28      pushl  % ecx
29      subl   $ 36, % esp
30      movl   $ 3, - 8(% ebp)
31      movl   $ 4, - 12(% ebp)
32      movl   - 12(% ebp), % eax
33      movl   % eax, 4(% esp)
34      movl   - 8(% ebp), % eax
35      movl   % eax, (% esp)
36      call   add
37      movl   % eax, - 16(% ebp)
38      movl   - 16(% ebp), % eax
39      movl   % eax, 4(% esp)
40      movl   $ .LC 0, (% esp)
41      call   printf
42      movl   $ .LC 1, (% esp)
43      call   puts
44      movl   $ 0, % eax
45      addl   $ 36, % esp
46      popl   % ecx
47      popl   % ebp
48      leal   - 4(% ecx), % esp
49      ret
50      .size   main, .- main
51      .ident " GCC: ( Ubuntu  4. 3. 2- 1 ubuntu12) 4. 3. 2"
52      .section   .note.GNU- stack,"",@ progbits

二、分析


    下面对hello.s进行逐句分析。
    第1行为gcc留下的文件信息;第2行标识下面一段是代码段,第3、4行表示这是add函数的入口,第5行为入口标号;6~12行为add函数体,稍后 分析;13行为add函数的代码段的大小;14行指示下面是数据段;15~18行定义了main中要用到的两个字符串常量;19行同第二行,20、21行 定义了main函数入口,22行为main入口标号。23行开始正式进入main函数,直至49行;50行为main函数代码段体积。51、52行为 gcc留下的信息。
    下面从main函数开始单步分析每一句话,并跟踪堆栈状态。
    初始状态,堆栈状态如 图一
高    +----------------+ <-- esp (栈顶)                          高    +----------------+
 |    |                  |                                                     |    |                  |
 |   +----------------+                                                     |   +    若干      +
 |    |                 |                                                    |    |                 |
 |   +----------------+                                                     |   +----------------+    <-- esp
 |    |                 |                                                    |    |                 |   
 |   +----------------+                                                     |   +----------------+
 V     |                  |                                                      V    |                  |
低    +    ....        +                                                     低   +    ....        +
            图一                                                                               图二

23     leal   4(% esp), % ecx
将esp所指地址加4得到的地址存入ecx。
24     andl   $- 16, % esp  
-16的补码为11...10000,这句话使esp指针下移若干位,新地址末四位是0,
故按16字节对齐,如图二。对齐是为了加速CPU访存。
25     pushl  - 4(% ecx)
将ecx所指地址(也就是程序开始时esp所指位置,如 图一所示)的内容压栈。这个内容是eip。关于这句的用途,后面有详细解释。
26     pushl  % ebp
将ebp压栈,保存ebp的值,以便在退出函数时恢复。
27     movl   % esp, % ebp
将ebp移动到esp的位置。
28     pushl  % ecx
将ecx的值压栈,保存,在退出函数时,通过这个值来恢复esp的初始值。
现在,堆栈状态如图三
高    +----------------+  <-- old esp
 |    |                  |                               
 |   +---- 若干 ----+                               
 |    |                 |    
 |   +----------------+  
 |    |                 |  
 |   +----------------+                                        
 |    |      eip      |       25     pushl  - 4(% ecx)                            
 |   +----------------+ <-- ebp   27     movl   % esp, % ebp    
 |    |   old ebp  |      26     pushl  % ebp (we don't know what old ebp is, but we have to backup it)
 |   +----------------+ <-- esp
 |    | old esp+4 |        28     pushl  % ecx (ecx =old esp + 4)                               
 |   +----------------+  
 |    |                |  
 |   +----------------+  
 V     |                 |                                                 
低    +    ....        +                                          
            图三
29     subl   $ 36, % esp
esp向下移动36字节,留出空间给局部变量使用,每个存储单元4字节,故共9格。这里预留的空间有些多,在后续的分析中会发现,很多空都没用上。在第四部分的优化后的代码中也可以看到,36被优化成了20,预留的空间正好用满。
30     movl   $ 3, - 8(% ebp)
a = 3, 将a的值存入堆栈(加载到内存中)。
31     movl   $ 4, - 12(% ebp)
b = 4, 将b的值存入堆栈(加载到内存中)。
32      movl   - 12(% ebp), % eax
33      movl   % eax, 4(% esp)
将b的值调入寄存器,并且入栈,为调用add函数准备参数。
34      movl   - 8(% ebp), % eax
35      movl   % eax, (% esp)
将a的值调入寄存器,并且入栈,为调用add函数准备参数。
36      call   add
调用add函数。注意,在这里,call指令隐含执行了一条push %eip的指令,记录当前代码段执行的位置。
下面进入add函数代码。
 6      pushl  % ebp
将ebp值压栈保存。
 7      movl   % esp, % ebp
移动ebp至当前esp位置。
 8      movl   12(% ebp), % edx
 9      movl   8(% ebp), % eax
将两个参数加载到寄存器。
10      addl   % edx, % eax
相加,结果存入eax寄存器。
11      popl   % ebp
12      ret
出栈,恢复ebp原来的值,函数返回,结果保存在eax中。注意,在ret指令中隐含执行了pop %eip的指令,从pop出来的eip所指的代码处继续执行。
下面回到main函数中。
37     movl   % eax, - 16(% ebp)
将函数返回值存入堆栈(内存)。
38      movl   - 16(% ebp), % eax
将变量c的值加载到寄存器。(此句冗余,编译时加优化选项可消除)
39      movl   % eax, 4(% esp)
40      movl   $ .LC 0, (% esp)
将变量c的值和.LC0的地址存入堆栈,为调用printf函数准备参数。
41      call   printf
调用printf函数,不跟踪分析。
这个过程中堆栈状态如 图四

高    +----------------+  <-- old esp
 |    |                  |                               
 |   +---- 若干 ----+                               
 |    |                 |                                 
 |   +----------------+                                        
 |    |      eip      |                          
 |   +----------------+     
 |    |   old ebp  |     
 |   +----------------+
 |    | old esp+4 |                                   
 |   +----------------+
 |    |                |                    
 |   +----------------+  
 |    |        3      |       30     movl   $ 3, - 8(% ebp)            a = 3             
 |   +----------------+
 |    |        4      |       31     movl   $ 4, - 12(% ebp)          b = 4
 |   +----------------+  
 |    |        7      |       37      movl   % eax, - 16(% ebp)      eax中为add函数的返回值。             
 |   +----------------+
 |    |                |                                 
 |   +----------------+
 |    |                |                                 
 |   +----------------+
 |    |                |                                 
 |   +----------------+
 |    |                |                                 
 |   +----------------+
 |    |     4 / 7    |         33      movl   % eax, 4(% esp) / 39      movl   % eax, 4(% esp)
 |   +----------------+ <-- esp    ( 29     subl   $ 36, % esp)
 |    |  3 / .LC0 |         35      movl   % eax, (% esp)   / 40      movl   $ .LC 0, (% esp)
 |   +----------------+
 |    |      eip     |                            
 |   +----------------+ <-- ebp    (7      movl   % esp, % ebp)
 |    |     ebp      |            6      pushl  % ebp  
 |   +----------------+  
 |    |                |                                 
 |   +----------------+   
  V    |                |  
低    +    ....        +        
             图四

42      movl   $ .LC 1, (% esp)
将.LC1地址存入堆栈,注意,这里gcc将printf“偷换”成了puts,所以只传一个参数。
43      call   puts
调用puts函数。
44      movl   $ 0, % eax
主函数将要返回0,将0存入eax寄存器。
45      addl   $ 36, % esp
将esp回到函数开始时的位置。
46      popl   % ecx
47      popl   % ebp
48      leal   - 4(% ecx), % esp
这三句与程序开始正好相对,恢复寄存器状态到进入函数前的状态。开始的这句话: 25  pushl  - 4(% ecx),存入了esp初始时刻指向单元的内容(应该是eip),但整个程序中都没用上。
49      ret
从main函数返回,返回值由eax带回。图五是图三的拷贝,可以从此图看清楚备份了哪些东西。

高    +----------------+  <-- old esp
 |    |                  |                               
 |   +---- 若干 ----+                               
 |    |                 |                                 
 |   +----------------+                                        
 |    |      eip      |           
 |   +----------------+     
 |    |   old ebp  |   
 |   +----------------+
 |    | old esp+4 |            
 |   +----------------+
 |    |                 |                                 
 |   +----------------+  
 V     |                  |                                                 
低    +    ....        +                                      
            图五
         

三、总结


    分析完这简单的代码后,我们进行一些小小的总结。
    1、我们体会一些x86是如何使用堆栈的。堆栈是个动态的空间,在运行的过程中,其中保存的内容主要有两种:局部变量和堆栈转移时保存的指针(寄存器的值)。
    2、esp是栈顶指针,pop和push操作将会自动调整esp的值,其他操作,除非esp作为算术运算的结果寄存器外,esp不会改变。个人觉得这里堆 栈称之为堆栈有一点点不合理,因为对堆栈的操作并不是完全的pop/push操作的集合,更多的时候是直接通过地址来取数。发生函数调用 时,4(%esp)是第一个参数,8(%esp)是第二个参数,依此类推,注意,这里加的4,是隐含指令push %eip导致的。push的操作,首先将esp向低地址方向移动4位,然后在这个单元里存入数据;pop的操作,现从esp所指向的单元里取出数据到指定 寄存器,然后将esp向高地址方向移动4位。
    3、一个代码段(这里一个函数就是一个代码段)运行时使用堆栈空间中连续的空间,ebp总是指向当前运行中的函数的堆栈空间的第一个位置,也就是基地址的 意思。一个代码段在存取自己所使用的数据时总是通过ebp来索引,而获取参数总是通过esp索引。所以在进入一个函数时,必须保存ebp的值,然后将 ebp指向自己的数据其实地址,在退出函数时,恢复ebp的值,使调用它的函数在它返回后能继续正常运行。在main函数开始时改变了esp的值,所以改 变之前也需要备份esp的值。
    4、函数返回值默认存放在eax寄存器中。
    5、寻址方法:
  • 立即数寻址:$num,num为数值,也就是字面数值
  • 寄存器寻址:%reg,reg为任一寄存器,取出%reg中保存的值
  • 寄存器间址:disp(base, index, scale),取出 (disp+base+index*scale) 所表示的内存单元中保存的内容。disp, index, scale都可以省略。

    6、main函数中为何要按16字节对齐esp?Linux下面GCC默认的堆栈是16字节对齐的,而这样对齐是为了加快CPU访问效率。这里,不对esp进行16字节对齐并不会影响程序的正确执行。具体的解释参见瀚海xhacker的文章:
http://202.38.64.3/cgi/bbscon?bn=ASM&fn=M47918B7C&num=2388
      7、 25     pushl  - 4(% ecx)的作用。(以下解释摘自瀚海foxman和xhacker的帖子)

*** foxman ***
一般来说这不是必需的,当进入一个函数之后,堆栈是这样的

|返回地址 |
|old_ebp |  <-  ebp
|  var1    |
|  var2    |
|  var3    |

也就是说在一个函数内部,是根据(ebp+4)来找到这个函数返回地址的。

不过对于main函数,进入之后需要堆栈16字节对齐(即andl $-16, %esp),这样就在原
来的main返回地址,与old_ebp之间插入了一些padding字节。为了还能ebp找到main的返
回地址,所以这儿再一次将main的返回地址入栈pushl -4(%ecx),在栈里放置在old_ebp
上方,如下:

| main返回地址 |
| 填充               |
| 填充               |
| main返回地址 |
| old_ebp          |  <- ebp
| ...                   |

一般gcc就是这么做的。 这么做主要是为了gcc扩展__builtin_return_address.

__builtin_return_address(LEVEL) 返回当前函数或其调用者的返回地址,参数LEVEL
 指定在栈上搜索框架的个数,0 表示当前函数的返回地址,1 表示当前函数的调用
者所在函数的返回地址,依此类推。

这就是根据%ebp来找到返回地址的。

为了能使用__builtin_return_address(0),就需要在push %ebp之前将main返回地
址入栈。如果你不用它,那就没什么问题

*** xhacker ***
另外再加上这个gcc的这个参数
-mpreferred-stack-boundary=x
x=2,3,4 etc,表示栈要2^x字节对齐

cc -mpreferred-stack-boundary=2 -S aa.c
可以看出此时没有那句push -4(%ecx)了,说明正是因为main的对齐,而为了仍然支持
__builtin_return_address扩展加上这条push指令了

***************


四、编译器优化后的代码

gcc -O3 -S -ohello_O3.s hello.c输出文件:
/* file: hello_O3.s */
 1      .file  " hello. c"
 2      .text
 3     . p2align  4,, 15
 4 .globl  add
 5      .type   add, @ function
 6 add:
 7      pushl  % ebp
 8      movl   % esp, % ebp
 9      movl   12(% ebp), % eax
10      addl   8(% ebp), % eax
11      popl   % ebp
12      ret
13      .size   add, .- add
14      .section   .rodata.str 1. 1," aMS",@ progbits, 1
15 .LC0:
16      .string    " a+ b=% d/ n"
17 .LC 1:
18      .string    " Hello  World!"
19      .text
20     . p2align  4,, 15
21 .globl  main
22      .type   main, @ function
23 main:
24      leal   4(% esp), % ecx
25      andl   $- 16, % esp
26      pushl  - 4(% ecx)
27      pushl  % ebp
28      movl   % esp, % ebp
29      pushl  % ecx
30      subl   $ 20, % esp
31      movl   $ 7, 8(% esp)
32      movl   $ .LC 0,  4(% esp)
33      movl   $ 1, (% esp)
34      call   __printf_chk
35      movl   $ .LC 1, (% esp)
36      call   puts
37      addl   $ 20, % esp
38      xorl   % eax, % eax
39      popl   % ecx
40      popl   % ebp
41      leal   - 4(% ecx), % esp
42      ret
43      .size   main, .- main
44      .ident " GCC: ( Ubuntu  4. 3. 2- 1 ubuntu12) 4. 3. 2"
45      .section   .note.GNU- stack,"",@ progbits
    从代码中,我们看到add函数虽然得到了相应的代码,但并没有被调用,而c=a+b则直接在编译时计算出了其值:7!其它地方并没有太多的优化。函数调用时相应的保存寄存器状态/返回时恢复等结构化的操作都没有改变。

五、进一步讨论


    main函数的参数argc,argv是如何传递的?看下面的代码:
/* t.c */
 1 #include <stdio.h>
 2
 3 int main( int argc, char **argv){
 4      char *c;
 5      if(argc == 1)
 6          return  1;
 7      else{
 8         c = argv[ 1];
 9         puts(c);
10     }
11      return  0;
12 }
13

gcc -S -ot.s t.c的输出文件:
/* g.s */
 1      .file  " hello. c"
 2      .text
 3 .globl  main
 4      .type   main, @ function
 5 main:
 6      leal   4(% esp), % ecx
 7      andl   $- 16, % esp
 8      pushl  - 4(% ecx)
 9      pushl  % ebp
10      movl   % esp, % ebp
11      pushl  % ecx
12      subl   $ 36, % esp
13      movl   % ecx, - 28(% ebp)
14      movl   - 28(% ebp), % eax
15      cmpl   $ 1, (% eax)
16      jne    . L2
17      movl   $ 1, - 24(% ebp)
18      jmp    . L3
19 . L2:
20      movl   - 28(% ebp), % edx
21      movl   4(% edx), % eax
22      addl   $ 4, % eax
23      movl   (% eax), % eax
24      movl   % eax, - 8(% ebp)
25      movl   - 8(% ebp), % eax
26      movl   % eax, (% esp)
27      call   puts
28      movl   $ 0, - 24(% ebp)
29 . L3:
30      movl   - 24(% ebp), % eax
31      addl   $ 36, % esp
32      popl   % ecx
33      popl   % ebp
34      leal   - 4(% ecx), % esp
35      ret
36      .size   main, .- main
37      .ident " GCC: ( Ubuntu  4. 3. 2- 1 ubuntu12) 4. 3. 2"
38      .section   .note.GNU- stack,"",@ progbits

这里面,第6~12行与之前相同,备份寄存器,移动esp,为代码段预留数据空间。执行完这一段后,这里,%ecx是一个“指针”,指向%esp+4的位置,也就是存放argc的位置。(注意,这里的指针不完全同于C语言中指针的概念,这里的指针是指 某寄存器的值是一个内存单元的地址,C语言中,指针是指 某变量的值是一个内存单元的地址。)
13      movl   % ecx, - 28(% ebp)
将ecx这个“指针”复制到堆栈。
14      movl   - 28(% ebp), % eax
再把这个“指针”加载到寄存器。
15      cmpl   $ 1, (% eax)
注意,因为%eax中存放的是“指针”,所以这里有括号。(%eax)即为初始时刻的4(%esp)。
16      jne    . L2
比较,如果argc!=1,跳转到.L2处。
17      movl   $ 1, - 24(% ebp)
18      jmp    . L3
如果相等,将main函数欲返回的值存到堆栈中,并且跳转到.L3。
下面看.L2的内容:
20      movl   - 28(% ebp), % edx
注意,这里-28(%ebp)是指向存放argc单元的“指针”。
21      movl   4(% edx), % eax
再将这个指针向上移动4字节,取出其中的值,即为argv的地址,更准确的说是argv[0]的地址。argv在C语言中是char**型指针。也就是说,%eax-->argv[0],(%eax)==argv[0]
22      addl   $ 4, % eax
%eax(argv[0]的地址)是一个内存地址,加4后就变成argv[1]的地址。(%eax)==argv[1]
23      movl   (% eax), % eax
再将这个地址的内容加载到%eax,此时%eax=argv[1]。
24      movl   % eax, - 8(% ebp)
注意,这里%eax外面没有括号,所以复制的是argv[1],也就是一个char*型的参数。
25      movl   - 8(% ebp), % eax
将参数加载到寄存器,这句话有些冗余,优化后会被去除。
26      movl   % eax, (% esp)
为puts准备参数。
27      call   puts
28      movl   $ 0, - 24(% ebp)
从puts返回后,准备该分支main函数的返回值,0。可以看到,保存这个返回值的地方同17行。这样,无论从哪个分支出来,都可以直接返回- 24(% ebp)的内容。
L3则是函数的一些扫尾工作,不需要再分析了。


六、实践


    使用工具Insight跟踪运行hello和t两个文件(gcc -g -o hello hello.s,注意加上-g参数,可以保留汇编代码中的符号信息),观察call语句时push入栈的eip,观察数据段、代码段、堆栈段的地址,观察 各寄存器值的变化,体会几种寻址方法。

  • 1
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值