Chinaunix首页 | 论坛 | 博客
  • 博客访问: 370718
  • 博文数量: 66
  • 博客积分: 3201
  • 博客等级: 中校
  • 技术积分: 695
  • 用 户 组: 普通用户
  • 注册时间: 2007-07-04 11:17
文章分类

全部博文(66)

文章存档

2016年(1)

2014年(1)

2012年(1)

2011年(2)

2010年(18)

2009年(42)

2008年(1)

分类:

2009-05-27 23:30:30

转自http://blog.csdn.net/stephenjy/archive/2009/03/10/3978161.aspx
 本文通过对由gcc对简单C语言代码编译生成的汇编码进行逐句分析解读,来学习x86的汇编结构和堆栈机制。文章涉及细节较多,难免出错,望读者不吝赐教!

一、代码

C语言代码:
/* file: hello.c */
 1 #include
 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 .LC1:
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   $.LC0, (%esp)
41     call   printf
42     movl   $.LC1, (%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-1ubuntu12) 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   $.LC0, (%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   $.LC0, (%esp)
 |   +----------------+
 |    |      eip     |                            
 |   +----------------+ <-- ebp    (7     movl   %esp, %ebp)
 |    |     ebp      |            6     pushl  %ebp  
 |   +----------------+  
 |    |                |                                 
 |   +----------------+   
  V    |                |  
低    +    ....        +        
             图四

42     movl   $.LC1, (%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的文章:

      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.str1.1,"aMS",@progbits,1
15 .LC0:
16     .string    "a+b=%d\n"
17 .LC1:
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   $.LC0, 4(%esp)
33     movl   $1, (%esp)
34     call   __printf_chk
35     movl   $.LC1, (%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-1ubuntu12) 4.3.2"
45     .section   .note.GNU-stack,"",@progbits
    从代码中,我们看到add函数虽然得到了相应的代码,但并没有被调用,而c=a+b则直接在编译时计算出了其值:7!其它地方并没有太多的优化。函数调用时相应的保存寄存器状态/返回时恢复等结构化的操作都没有改变。

五、进一步讨论


    main函数的参数argc,argv是如何传递的?看下面的代码:
/* t.c */
 1 #include
 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-1ubuntu12) 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,观察数据段、代码段、堆栈段的地址,观察 各寄存器值的变化,体会几种寻址方法。


阅读(946) | 评论(2) | 转发(0) |
给主人留下些什么吧!~~

chinaunix网友2009-07-06 14:27:56

http://www.ibm.com/developerworks/cn/linux/l-cn-gccstack/index.html

chinaunix网友2009-05-27 23:31:00

This extra alignment does consume extra stack space, and generally increases code size. Code that is sensitive to stack space usage, such as embedded systems and operating system kernels, may want to reduce the preferred alignment to `-mpreferred-stack-boundary=2'.