函数调用栈Word格式.docx
- 文档编号:22837722
- 上传时间:2023-02-05
- 格式:DOCX
- 页数:15
- 大小:97.88KB
函数调用栈Word格式.docx
《函数调用栈Word格式.docx》由会员分享,可在线阅读,更多相关《函数调用栈Word格式.docx(15页珍藏版)》请在冰豆网上搜索。
;
如此递归,就形成了函数调用栈;
函数内局部变量布局示例:
#include<
stdio.h>
string.h>
structC
{
inta;
intb;
intc;
};
inttest2(intx,inty,intz)
printf("
hello,test2\n"
);
return0;
}
inttest(intx,inty,intz)
inta=1;
intb=2;
intc=3;
structCst;
addrx=%u\n"
(unsignedint)(&
x));
addry=%u\n"
y));
addrz=%u\n"
z));
addra=%u\n"
a));
addrb=%u\n"
b));
addrc=%u\n"
c));
addrst=%u\n"
st));
addrst.a=%u\n"
st.a));
addrst.b=%u\n"
st.b));
addrst.c=%u\n"
st.c));
intmain(intargc,char**argv)
intx=1;
inty=2;
intz=3;
test(x,y,z);
x=%d;
y=%d;
z=%d;
\n"
x,y,z);
memset(&
y,0,8);
打印输出如下:
addrx=4288282272
addry=4288282276
addrz=4288282280
addra=4288282260
addrb=4288282256
addrc=4288282252
addrst=4288282240
addrst.a=4288282240
addrst.b=4288282244
addrst.c=4288282248
a=1;
b=2;
c=3;
a=0;
b=0;
示例效果图:
该图中的局部变量都是在该示例中定义的;
这个图片中反映的是一个典型的函数调用栈的内存布局;
访问函数的局部变量和访问函数参数的区别:
局部变量总是通过将ebp减去偏移量来访问,函数参数总是通过将ebp加上偏移量来访问。
对于32位变量而言,第一个局部变量位于ebp-4,第二个位于ebp-8,以此类推,32位局部变量在栈中形成一个逆序数组;
第一个函数参数位于ebp+8,第二个位于ebp+12,以此类推,32位函数参数在栈中形成一个正序数组。
汇编代码示例
(1):
////////////////////////////////////////////////////////////////////
比如我们有这样一个C函数
#include<
longtest(inta,intb)
a=a+1;
b=b+100;
returna+b;
voidmain()
{
printf("
%d"
test(1000,2000));
写成32位汇编就是这样
//////////////////////////////////////////////////////////////////////////////////////////////////////
.386
.modelflat,stdcall;
这里我们用stdcall就是函数参数压栈的时候从最后一个开始压,和被调用函数负责清栈
optioncasemap:
none;
区分大小写
includelibmsvcrt.lib;
这里是引入类库相当于#include<
了
printfPROTOC:
DWORD,:
VARARG;
这个就是声明一下我们要用的函数头,到时候汇编程序会自动到msvcrt.lib里面找的了
:
VARARG表后面的参数不确定因为C就是这样的printf(constchar*,...);
这样的函数要注意不是被调用函数负责清栈因为它本身不知道有多少个参数
而是有调用者负责清栈下面会详细说明
.data
szTextFmtBYTE'
%d'
0;
这个是用来类型转换的,跟C的一样,字符用字节类型
adword1000;
假设
bdword2000;
处理数值都用双字没有int跟long的区别
/////////////////////////////////////////////////////////////////////////////////////////
.code
_testproc;
A:
DWORD,B:
DWORD
pushebp
movebp,esp
moveax,dwordptrss:
[ebp+8]
addeax,1
movedx,dwordptrss:
[ebp+0Ch]
addedx,100
addeax,edx
popebp
retn8
_testendp
_mainproc
pushdwordptrds:
b;
反汇编我们看到的b就不是b了而是一个[*****]数字dwordptr就是我们在ds(数据段)把[*****]
开始的一个双字长数值取出来
a;
跟她对应的还有byteptr****就是取一个字节出来比如这样moval,byteptrds:
szTextFmt
就把%取出来而不包括d
call_test
pusheax;
假设pusheax的地址是×
×
pushoffsetszTextFmt
callprintf
addesp,8
ret
_mainendp
end_main
汇编代码示例
(2):
研究函数的调用过程
intbar(intc,intd)
inte=c+d;
returne;
intfoo(inta,intb)
returnbar(a,b);
intmain(void)
foo(2,3);
如果在编译时加上-g选项(在第10章
gdb讲过-g选项),那么用objdump反汇编时可以把C代码和汇编代码穿插起来显示,这样C代码和汇编代码的对应关系看得更清楚。
反汇编的结果很长,以下只列出我们关心的部分。
$gccmain.c-g
$objdump-dSa.out
...
08048394<
bar>
8048394:
55push%ebp
8048395:
89e5mov%esp,%ebp
8048397:
83ec10sub$0x10,%esp
804839a:
8b550cmov0xc(%ebp),%edx
804839d:
8b4508mov0x8(%ebp),%eax
80483a0:
01d0add%edx,%eax
80483a2:
8945fcmov%eax,-0x4(%ebp)
80483a5:
8b45fcmov-0x4(%ebp),%eax
80483a8:
c9leave
80483a9:
c3ret
080483aa<
foo>
80483aa:
80483ab:
80483ad:
83ec08sub$0x8,%esp
80483b0:
8b450cmov0xc(%ebp),%eax
80483b3:
89442404mov%eax,0x4(%esp)
80483b7:
80483ba:
890424mov%eax,(%esp)
80483bd:
e8d2ffffffcall8048394<
80483c2:
80483c3:
080483c4<
main>
80483c4:
8d4c2404lea0x4(%esp),%ecx
80483c8:
83e4f0and$0xfffffff0,%esp
80483cb:
ff71fcpushl-0x4(%ecx)
80483ce:
80483cf:
80483d1:
51push%ecx
80483d2:
80483d5:
c7442404030000movl$0x3,0x4(%esp)
80483dc:
00
80483dd:
c7042402000000movl$0x2,(%esp)
80483e4:
e8c1ffffffcall80483aa<
80483e9:
b800000000mov$0x0,%eax
80483ee:
83c408add$0x8,%esp
80483f1:
59pop%ecx
80483f2:
5dpop%ebp
80483f3:
8d61fclea-0x4(%ecx),%esp
80483f6:
要查看编译后的汇编代码,其实还有一种办法是gcc-Smain.c,这样只生成汇编代码main.s,而不生成二进制的目标文件。
整个程序的执行过程是main调用foo,foo调用bar,我们用gdb跟踪程序的执行,直到bar函数中的inte=c+d;
语句执行完毕准备返回时,这时在gdb中打印函数栈帧。
(gdb)start
main()atmain.c:
14
14foo(2,3);
(gdb)s
foo(a=2,b=3)atmain.c:
9
9returnbar(a,b);
bar(c=2,d=3)atmain.c:
3
3inte=c+d;
(gdb)disassemble
Dumpofassemblercodeforfunctionbar:
0x08048394<
bar+0>
push%ebp
0x08048395<
bar+1>
mov%esp,%ebp
0x08048397<
bar+3>
sub$0x10,%esp
0x0804839a<
bar+6>
mov0xc(%ebp),%edx
0x0804839d<
bar+9>
mov0x8(%ebp),%eax
0x080483a0<
bar+12>
add%edx,%eax
0x080483a2<
bar+14>
mov%eax,-0x4(%ebp)
0x080483a5<
bar+17>
mov-0x4(%ebp),%eax
0x080483a8<
bar+20>
leave
0x080483a9<
bar+21>
ret
Endofassemblerdump.
(gdb)si
0x0804839d3inte=c+d;
0x080483a03inte=c+d;
0x080483a23inte=c+d;
4returne;
5}
(gdb)bt
#0bar(c=2,d=3)atmain.c:
5
#10x080483c2infoo(a=2,b=3)atmain.c:
#20x080483e9inmain()atmain.c:
(gdb)inforegisters
eax0x55
ecx0xbff1c440-1074674624
edx0x33
ebx0xb7fe6ff4-1208061964
esp0xbff1c3f40xbff1c3f4
ebp0xbff1c4040xbff1c404
esi0x8048410134513680
edi0x80482e0134513376
eip0x80483a80x80483a8<
eflags0x200206[PFIFID]
cs0x73115
ss0x7b123
ds0x7b123
es0x7b123
fs0x00
gs0x3351
(gdb)x/20$esp
0xbff1c3f4:
0x000000000xbff1c6f70xb7efbdae0x00000005
0xbff1c404:
0xbff1c4140x080483c20x000000020x00000003
0xbff1c414:
0xbff1c4280x080483e90x000000020x00000003
0xbff1c424:
0xbff1c4400xbff1c4980xb7ea36850x08048410
0xbff1c434:
0x080482e00xbff1c4980xb7ea36850x00000001
(gdb)
这里又用到几个新的gdb命令。
disassemble可以反汇编当前函数或者指定的函数,单独用disassemble命令是反汇编当前函数,如果disassemble命令后面跟函数名或地址则反汇编指定的函数。
以前我们讲过step命令可以一行代码一行代码地单步调试,而这里用到的si命令可以一条指令一条指令地单步调试。
inforegisters可以显示所有寄存器的当前值。
在gdb中表示寄存器名时前面要加个$,例如p$esp可以打印esp寄存器的值,在上例中esp寄存器的值是0xbff1c3f4,所以x/20$esp命令查看内存中从0xbff1c3f4地址开始的20个32位数。
在执行程序时,操作系统为进程分配一块栈空间来保存函数栈帧,esp寄存器总是指向栈顶,在x86平台上这个栈是从高地址向低地址增长的,我们知道每次调用一个函数都要分配一个栈帧来保存参数和局部变量,现在我们详细分析这些数据在栈空间的布局,根据gdb的输出结果图示如下[29]:
图19.1.函数栈帧
图中每个小方格表示4个字节的内存单元,例如b:
3这个小方格占的内存地址是0xbff1c420~0xbff1c423,我把地址写在每个小方格的下边界线上,是为了强调该地址是内存单元的起始地址。
我们从main函数的这里开始看起:
foo(2,3);
要调用函数foo先要把参数准备好,第二个参数保存在esp+4指向的内存位置,第一个参数保存在esp指向的内存位置,可见参数是从右向左依次压栈的。
然后执行call指令,这个指令有两个作用:
1.foo函数调用完之后要返回到call的下一条指令继续执行,所以把call的下一条指令的地址0x80483e9压栈,同时把esp的值减4,esp的值现在是0xbff1c418。
2.修改程序计数器eip,跳转到foo函数的开头执行。
现在看foo函数的汇编代码:
push%ebp指令把ebp寄存器的值压栈,同时把esp的值减4。
esp的值现在是0xbff1c414,下一条指令把这个值传送给ebp寄存器。
这两条指令合起来是把原来ebp的值保存在栈上,然后又给ebp赋了新值。
在每个函数的栈帧中,ebp指向栈底,而esp指向栈顶,在函数执行过程中esp随着压栈和出栈操作随时变化,而ebp是不动的,函数的参数和局部变量都是通过ebp的值加上一个偏移量来访问,例如foo函数的参数a和b分别通过ebp+8和ebp+12来访问。
所以下面的指令把参数a和b再次压栈,为调用bar函数做准备,然后把返回地址压栈,调用bar函数:
returnbar(a,b);
现在看bar函数的指令:
这次又把foo函数的ebp压栈保存,然后给ebp赋了新值,指向bar函数栈帧的栈底,通过ebp+8和ebp+12分别可以访问参数c和d。
bar函数还有一个局部变量e,可以通过ebp-4来访问。
所以后面几条指令的意思是把参数c和d取出来存在寄存器中做加法,计算结果保存在eax寄存器中,再把eax寄存器存回局部变量e的内存单元。
在gdb中可以用bt命令和frame命令查看每层栈帧上的参数和局部变量,现在可以解释它的工作原理了:
如果我当前在bar函数中,我可以通过ebp找到bar函数的参数和局部变量,也可以找到foo函数的ebp保存在栈上的值,有了foo函数的ebp,又可以找到它的参数和局部变量,也可以找到main函数的ebp保存在栈上的值,因此各层函数栈帧通过保存在栈上的ebp的值串起来了。
现在看bar函数的返回指令:
returne;
c3ret
bar函数有一个int型的返回值,这个返回值是通过eax寄存器传递的,所以首先把e的值读到eax寄存器中。
然后执
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 函数 调用