对gcc编译汇编码解析.docx

上传人:b****6 文档编号:5963893 上传时间:2023-01-02 格式:DOCX 页数:15 大小:20.80KB
下载 相关 举报
对gcc编译汇编码解析.docx_第1页
第1页 / 共15页
对gcc编译汇编码解析.docx_第2页
第2页 / 共15页
对gcc编译汇编码解析.docx_第3页
第3页 / 共15页
对gcc编译汇编码解析.docx_第4页
第4页 / 共15页
对gcc编译汇编码解析.docx_第5页
第5页 / 共15页
点击查看更多>>
下载资源
资源描述

对gcc编译汇编码解析.docx

《对gcc编译汇编码解析.docx》由会员分享,可在线阅读,更多相关《对gcc编译汇编码解析.docx(15页珍藏版)》请在冰豆网上搜索。

对gcc编译汇编码解析.docx

对gcc编译汇编码解析

对gcc编译汇编码解析

本文通过对由gcc对简单C语言代码编译生成的汇编码进行逐句分析解读,来学习x86的汇编结构和堆栈机制。

文章涉及细节较多,难免出错,望读者不吝赐教!

一、代码

C语言代码:

/*file:

hello.c*/

1#include<stdio.h>

2

3intadd(inta,intb){

4return(a+b);

5}

6

7intmain(intargc,char**argv){

8inta,b,c;

9a=3;

10b=4;

11c=add(a,b);

12printf("a+b=%d\n",c);

13printf("HelloWorld!

\n");

14return0;

15}

16gcc-S-ohello.shello.c输出文件:

/*file:

hello.s*/

1.file"hello.c"

2.text

3.globladd

4.typeadd,@function

5add:

6pushl%ebp

7movl%esp,%ebp

8movl12(%ebp),%edx

9movl8(%ebp),%eax

10addl%edx,%eax

11popl%ebp

12ret

13.sizeadd,.-add

14.section.rodata

15.LC0:

16.string"a+b=%d\n"

17.LC1:

18.string"HelloWorld!

"

19.text

20.globlmain

21.typemain,@function

22main:

23leal4(%esp),%ecx

24andl$-16,%esp

25pushl-4(%ecx)

26pushl%ebp

27movl%esp,%ebp

28pushl%ecx

29subl$36,%esp

30movl$3,-8(%ebp)

31movl$4,-12(%ebp)

32movl-12(%ebp),%eax

33movl%eax,4(%esp)

34movl-8(%ebp),%eax

35movl%eax,(%esp)

36calladd

37movl%eax,-16(%ebp)

38movl-16(%ebp),%eax

39movl%eax,4(%esp)

40movl$.LC0,(%esp)

41callprintf

42movl$.LC1,(%esp)

43callputs

44movl$0,%eax

45addl$36,%esp

46popl%ecx

47popl%ebp

48leal-4(%ecx),%esp

49ret

50.sizemain,.-main

51.ident"GCC:

(Ubuntu4.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||低+....+低+....+图一图二23leal4(%esp),%ecx

将esp所指地址加4得到的地址存入ecx。

24andl$-16,%esp

-16的补码为11...10000,这句话使esp指针下移若干位,新地址末四位是0,故按16字节对齐,如图二。

对齐是为了加速CPU访存。

25pushl-4(%ecx)

将ecx所指地址(也就是程序开始时esp所指位置,如图一所示)的内容压栈。

这个内容是eip。

关于这句的用途,后面有详细解释。

26pushl%ebp

将ebp压栈,保存ebp的值,以便在退出函数时恢复。

27movl%esp,%ebp

将ebp移动到esp的位置。

28pushl%ecx

将ecx的值压栈,保存,在退出函数时,通过这个值来恢复esp的初始值。

现在,堆栈状态如图三:

高+----------------+<--oldesp

|||

|+----若干----+

|||

|+----------------+

|||

|+----------------+

||eip|25pushl-4(%ecx)

|+----------------+<--ebp27movl%esp,%ebp

||oldebp|26pushl%ebp(wedon'tknowwhatoldebpis,butwehavetobackupit)

|+----------------+<--esp

||oldesp+4|28pushl%ecx(ecx=oldesp+4)

|+----------------+

|||

|+----------------+

V||

低+....+

图三

29subl$36,%esp

esp向下移动36字节,留出空间给局部变量使用,每个存储单元4字节,故共9格。

这里预留的空间有些多,在后续的分析中会发现,很多空都没用上。

在第四部分的优化后的代码中也可以看到,36被优化成了20,预留的空间正好用满。

30movl$3,-8(%ebp)

a=3,将a的值存入堆栈(加载到内存中)。

31movl$4,-12(%ebp)

b=4,将b的值存入堆栈(加载到内存中)。

32movl-12(%ebp),%eax

33movl%eax,4(%esp)

将b的值调入寄存器,并且入栈,为调用add函数准备参数。

34movl-8(%ebp),%eax

35movl%eax,(%esp)

将a的值调入寄存器,并且入栈,为调用add函数准备参数。

36calladd

调用add函数。

注意,在这里,call指令隐含执行了一条push%eip的指令,记录当前代码段执行的位置。

下面进入add函数代码。

6pushl%ebp

将ebp值压栈保存。

7movl%esp,%ebp

移动ebp至当前esp位置。

8movl12(%ebp),%edx

9movl8(%ebp),%eax

将两个参数加载到寄存器。

10addl%edx,%eax

相加,结果存入eax寄存器。

11popl%ebp

12ret

出栈,恢复ebp原来的值,函数返回,结果保存在eax中。

注意,在ret指令中隐含执行了pop%eip的指令,从pop出来的eip所指的代码处继续执行。

下面回到main函数中。

37movl%eax,-16(%ebp)

将函数返回值存入堆栈(内存)。

38movl-16(%ebp),%eax

将变量c的值加载到寄存器。

(此句冗余,编译时加优化选项可消除)

39movl%eax,4(%esp)

40movl$.LC0,(%esp)

将变量c的值和.LC0的地址存入堆栈,为调用printf函数准备参数。

41callprintf

调用printf函数,不跟踪分析。

这个过程中堆栈状态如图四:

高+----------------+<--oldesp

|||

|+----若干----+

|||

|+----------------+

||eip|

|+----------------+

||oldebp|

|+----------------+

||oldesp+4|

|+----------------+

|||

|+----------------+

||3|30movl$3,-8(%ebp)a=3

|+----------------+

||4|31movl$4,-12(%ebp)b=4

|+----------------+

||7|37movl%eax,-16(%ebp)eax中为add函数的返回值。

|+----------------+

|||

|+----------------+

|||

|+----------------+

|||

|+----------------+

|||

|+----------------+

||4/7|33movl%eax,4(%esp)/39movl%eax,4(%esp)

|+----------------+<--esp(29subl$36,%esp)

||3/.LC0|35movl%eax,(%esp)/40movl$.LC0,(%esp)

|+----------------+

||eip|

|+----------------+<--ebp(7movl%esp,%ebp)

||ebp|6pushl%ebp

|+----------------+

|||

|+----------------+

V||

低+....+

图四42movl$.LC1,(%esp)

将.LC1地址存入堆栈,注意,这里gcc将printf“偷换”成了puts,所以只传一个参数。

43callputs调用puts函数。

44movl$0,%eax

主函数将要返回0,将0存入eax寄存器。

45addl$36,%esp

将esp回到函数开始时的位置。

46popl%ecx

47popl%ebp

48leal-4(%ecx),%esp

这三句与程序开始正好相对,恢复寄存器状态到进入函数前的状态。

开始的这句话:

25pushl-4(%ecx),存入了esp初始时刻指向单元的内容(应该是eip),但整个程序中都没用上。

49ret

从main函数返回,返回值由eax带回。

图五是图三的拷贝,可以从此图看清楚备份了哪些东西。

高+----------------+<--oldesp

|||

|+----若干----+

|||

|+----------------+

||eip|

|+----------------+

||oldebp|

|+----------------+

||oldesp+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、25pushl-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,4etc,表示栈要2^x字节对齐cc-mpreferred-stack-boundary=2-Saa.c

可以看出此时没有那句push-4(%ecx)了,说明正是因为main的对齐,而为了仍然支持

__builtin_return_address扩展加上这条push指令了***************四、编译器优化后的代码

gcc-O3-S-ohello_O3.shello.c输出文件:

/*file:

hello_O3.s*/

1.file"hello.c"

2.text

3.p2align4,,15

4.globladd

5.typeadd,@function

6add:

7pushl%ebp

8movl%esp,%ebp

9movl12(%ebp),%eax

10addl8(%ebp),%eax

11popl%ebp

12ret

13.sizeadd,.-add

14.section.rodata.str1.1,"aMS",@progbits,1

15.LC0:

16.string"a+b=%d\n"

17.LC1:

18.string"HelloWorld!

"

19.text

20.p2align4,,15

21.globlmain

22.typemain,@function

23main:

24leal4(%esp),%ecx

25andl$-16,%esp

26pushl-4(%ecx)

27pushl%ebp

28movl%esp,%ebp

29pushl%ecx

30subl$20,%esp

31movl$7,8(%esp)

32movl$.LC0,4(%esp)

33movl$1,(%esp)

34call__printf_chk

35movl$.LC1,(%esp)

36callputs

37addl$20,%esp

38xorl%eax,%eax

39popl%ecx

40popl%ebp

41leal-4(%ecx),%esp

42ret

43.sizemain,.-main

44.ident"GCC:

(Ubuntu4.3.2-1ubuntu12)4.3.2"

45.section.note.GNU-stack,"",@progbits

从代码中,我们看到add函数虽然得到了相应的代码,但并没有被调用,而c=a+b则直接在编译时计算出了其值:

7!

其它地方并没有太多的优化。

函数调用时相应的保存寄存器状态/返回时恢复等结构化的操作都没有改变。

五、进一步讨论

main函数的参数argc,argv是如何传递的?

看下面的代码:

/*t.c*/

1#include<stdio.h>

2

3intmain(intargc,char**argv){

4char*c;

5if(argc==1)

6return1;

7else{

8c=argv[1];

9puts(c);

10}

11return0;

12}

13gcc-S-ot.st.c的输出文件:

/*g.s*/

1.file"hello.c"

2.text

3.globlmain

4.typemain,@function

5main:

6leal4(%esp),%ecx

7andl$-16,%esp

8pushl-4(%ecx)

9pushl%ebp

10movl%esp,%ebp

11pushl%ecx

12subl$36,%esp

13movl%ecx,-28(%ebp)

14movl-28(%ebp),%eax

15cmpl$1,(%eax)

16jne.L2

17movl$1,-24(%ebp)

18jmp.L3

19.L2:

20movl-28(%ebp),%edx

21movl4(%edx),%eax

22addl$4,%eax

23movl(%eax),%eax

24movl%eax,-8(%ebp)

25movl-8(%ebp),%eax

26movl%eax,(%esp)

27callputs

28movl$0,-24(%ebp)

29.L3:

30movl-24(%ebp),%eax

31addl$36,%esp

32popl%ecx

33popl%ebp

34leal-4(%ecx),%esp

35ret

36.sizemain,.-main

37.ident"GCC:

(Ubuntu4.3.2-1ubuntu12)4.3.2"

38.section.note.GNU-stack,"",@progbits这里面,第6~12行与之前相同,备份寄存器,移动esp,为代码段预留数据空间。

执行完这一段后,这里,%ecx是一个“指针”,指向%esp+4的位置,也就是存放argc的位置。

(注意,这里的指针不完全同于C语言中指针的概念,这里的指针是指某寄存器的值是一个内存单元的地址,C语言中,指针是指某变量的值是一个内存单元的地址。

13movl%ecx,-28(%ebp)

将ecx这个“指针”复制到堆栈。

14movl-28(%ebp),%eax

再把这个“指针”加载到寄存器。

15cmpl$1,(%eax)

注意,因为%eax中存放的是“指针”,所以这里有括号。

(%eax)即为初始时刻的4(%esp)。

16jne.L2

比较,如果argc!

=1,跳转到.L2处。

17movl$1,-24(%ebp)

18jmp.L3

如果相等,将main函数欲返回的值存到堆栈中,并且跳转到.L3。

下面看.L2的内容:

20movl-28(%ebp),%edx

注意,这里-28(%ebp)是指向存放argc单

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 自然科学

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1