Chinaunix首页 | 论坛 | 博客
  • 博客访问: 305732
  • 博文数量: 214
  • 博客积分: 4258
  • 博客等级: 上校
  • 技术积分: 2021
  • 用 户 组: 普通用户
  • 注册时间: 2010-12-02 09:16
个人简介

http://blog.csdn.net/ly21st http://ly21st.blog.chinaunix.net

文章分类

全部博文(214)

文章存档

2018年(16)

2015年(1)

2014年(2)

2012年(22)

2011年(173)

分类: C/C++

2012-01-09 15:38:56

基于x86Hello World汇编代码分析 AT&T汇编风格)

    本文通过对由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行标识下面一段是代码段,第34行表示这是add函数的入口,第5行为入口标号;6~12行为add函数体,稍后分析;13行为add函数的代码段的大小;14行指示下面是数据段;15~18行定义了main中要用到的两个字符串常量;19行同第二行,2021行定义了main函数入口,22行为main入口标号。23行开始正式进入main函数,直至49行;50行为main函数代码段体积。5152行为 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地址存入堆栈,注意,这里gccprintf“偷换成了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是栈顶指针,poppush操作将会自动调整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
、寻址方法:

  • 立即数寻址:$numnum为数值,也就是字面数值
  • 寄存器寻址:%regreg为任一寄存器,取出%reg中保存的值
  • 寄存器间址:disp(base, index, scale),取出 (disp+base+index*scale) 所表示的内存单元中保存的内容。disp, index, scale都可以省略。


    6
main函数中为何要按16字节对齐espLinux下面GCC默认的堆栈是16字节对齐的,而这样对齐是为了加快CPU访问效率。这里,不对esp进行16字节对齐并不会影响程序的正确执行。具体的解释参见瀚海xhacker的文章:

      725     pushl  -4(%ecx)的作用。(以下解释摘自瀚海foxmanxhacker的帖子)

*** 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
234 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]的地址。argvC语言中是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跟踪运行hellot两个文件(gcc -g -o hello hello.s,注意加上-g参数,可以保留汇编代码中的符号信息),观察call语句时push入栈的eip,观察数据段、代码段、堆栈段的地址,观察各寄存器值的变化,体会几种寻址方法。

 

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