对gcc编译汇编码解析Word格式.docx
《对gcc编译汇编码解析Word格式.docx》由会员分享,可在线阅读,更多相关《对gcc编译汇编码解析Word格式.docx(15页珍藏版)》请在冰豆网上搜索。
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"
17.LC1:
18.string"
"
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访存。
将ecx所指地址(也就是程序开始时esp所指位置,如图一所示)的内容压栈。
这个内容是eip。
关于这句的用途,后面有详细解释。
将ebp压栈,保存ebp的值,以便在退出函数时恢复。
将ebp移动到esp的位置。
将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||
低+....+
图三
esp向下移动36字节,留出空间给局部变量使用,每个存储单元4字节,故共9格。
这里预留的空间有些多,在后续的分析中会发现,很多空都没用上。
在第四部分的优化后的代码中也可以看到,36被优化成了20,预留的空间正好用满。
a=3,将a的值存入堆栈(加载到内存中)。
b=4,将b的值存入堆栈(加载到内存中)。
将b的值调入寄存器,并且入栈,为调用add函数准备参数。
将a的值调入寄存器,并且入栈,为调用add函数准备参数。
调用add函数。
注意,在这里,call指令隐含执行了一条push%eip的指令,记录当前代码段执行的位置。
下面进入add函数代码。
将ebp值压栈保存。
移动ebp至当前esp位置。
将两个参数加载到寄存器。
相加,结果存入eax寄存器。
出栈,恢复ebp原来的值,函数返回,结果保存在eax中。
注意,在ret指令中隐含执行了pop%eip的指令,从pop出来的eip所指的代码处继续执行。
下面回到main函数中。
将函数返回值存入堆栈(内存)。
将变量c的值加载到寄存器。
(此句冗余,编译时加优化选项可消除)
将变量c的值和.LC0的地址存入堆栈,为调用printf函数准备参数。
调用printf函数,不跟踪分析。
这个过程中堆栈状态如图四:
||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)
--ebp(7movl%esp,%ebp)
||ebp|6pushl%ebp
V||
图四42movl$.LC1,(%esp)
将.LC1地址存入堆栈,注意,这里gcc将printf&
#8220;
偷换&
#8221;
成了puts,所以只传一个参数。
43callputs调用puts函数。
主函数将要返回0,将0存入eax寄存器。
将esp回到函数开始时的位置。
这三句与程序开始正好相对,恢复寄存器状态到进入函数前的状态。
开始的这句话:
25pushl-4(%ecx),存入了esp初始时刻指向单元的内容(应该是eip),但整个程序中都没用上。
从main函数返回,返回值由eax带回。
图五是图三的拷贝,可以从此图看清楚备份了哪些东西。
图五
三、总结
分析完这简单的代码后,我们进行一些小小的总结。
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&
amp;
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返回地址|
|填充|
|old_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输出文件:
hello_O3.s*/
3.p2align4,,15
4.globladd
5.typeadd,@function
6add:
7pushl%ebp
8movl%esp,%ebp
9movl12(%ebp),%eax
10addl8(%ebp),%eax
14.section.rodata.str1.1,"
aMS"
@progbits,1
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"
45.section.note.GNU-stack,"
@progbits
从代码中,我们看到add函数虽然得到了相应的代码,但并没有被调用,而c=a+b则直接在编译时计算出了其值:
7!
其它地方并没有太多的优化。
函数调用时相应的保存寄存器状态/返回时恢复等结构化的操作都没有改变。
五、进一步讨论
main函数的参数argc,argv是如何传递的?
看下面的代码:
/*t.c*/
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*/
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"
38.section.note.GNU-stack,"
@progbits这里面,第6~12行与之前相同,备份寄存器,移动esp,为代码段预留数据空间。
执行完这一段后,这里,%ecx是一个&
指针&
,指向%esp+4的位置,也就是存放argc的位置。
(注意,这里的指针不完全同于C语言中指针的概念,这里的指针是指某寄存器的值是一个内存单元的地址,C语言中,指针是指某变量的值是一个内存单元的地址。
)
将ecx这个&
复制到堆栈。
再把这个&
加载到寄存器。
注意,因为%eax中存放的是&
,所以这里有括号。
(%eax)即为初始时刻的4(%esp)。
比较,如果argc!
=1,跳转到.L2处。
如果相等,将main函数欲返回的值存到堆栈中,并且跳转到.L3。
下面看.L2的内容:
注意,这里-28(%ebp)是指向存放argc单