您的位置:首页 > 其它

问题:调用函数时,相关参数在堆栈中是如何存放的?

2008-04-25 09:31 260 查看
举例如下:

void Fun()
{
......
char chData[40]={0};
memset(chData,'a',50);//这句越界了
}

int main()
{
......
Fun();

}

main中调用Fun()时,怎么样将Fun()函数的相关信息入栈的;我试过,如果memset()这句中越界超过10个字节的话,函数返回main时就会出错(如果越界不超过10个,一般就不会出错。改变数组chData的大小也是一样的结果),不知道是不是这个memset把系统保留的一些函数的返回信息给改写了,导致返回出错。


回复内容
【Kenmark】:
有参数和平衡堆栈的东西,还有回调地址,触及到回调地址就会core错误了~
看看“缓存区溢出”相关的文章还有微软的《write secure code》里面都介绍了
这个和编译器也有关系,不同的编译器维护堆栈也是稍微不同~

【fishly_0】:
用vc6调试时,出错了就会跳到nt.dll里,但是有时候地址是0x000000,有时候又不是0x0000,这个又是怎么回事呢?我总觉得,函数调用时,相关的堆栈信息应该不会放在数组chData后面的空间中。

【taodm】:
不要数组越界是正事。

【fishly_0】:
呵呵,我只是觉得奇怪,函数运行的时候到底把那些参数放哪里了,怎么会跟其中定义的一个数组冲突呢。

【taodm】:
不同的操作系统和不同的编译器下,有不同的结果,讨论其实没有可推广性。
在PPC体系的CPU下,参数和返回地址一般不在栈里,都是放在寄存器里的。

【babala512】:
函数的参数放在程序堆栈中,这点是必定无疑了。
比如在main中在调用fun,没有参数入栈,执行到fun体时, 首先在栈上创建一个chData临时对象,调用memset。由于menset是cdecl方式调用,因而,参数从右往左入栈,一般x86机器的的栈低在高地址,倒着长,直到这一点,就可以反汇编以下,到底参数在哪里了

【fishly_0】:
那程序出错的原因是什么呢?是哪部分的数据被擦除或者改写了,导致程序出错呢?BTW:我想学一下怎么看反汇编,大家能不能帮我推荐一些书或者资料?我以前有学过x86的汇编。多谢了:)

【healer_kx】:
void Fun()
{
char chData[40]={0};
memset(chData,'a',50);//这句越界了
}

int main()
{

Fun();

}

确实越界了(是个人估计都看出来了)
这个和编译器有关了,有10个字节覆盖了不该覆盖的区域了。。

这个要说到call a function's details,

首先堆(动词)栈是从高往低的堆的,)(Linux 和Windows, MAC都是这么做的)
那么EIP的下一个指令指针在高地址 被push ed.
然后是EBP在高地址,
然后是你func上面的栈变量。其实前面还会一些其他的寄存器值。

而memset(chData,'a',50)是从低到高写。
所以你的chData被刷的同时, 可能其他变量也被搞了,然后就是栈上的EBP, EIP.

【jxlczjp77】:
假设有这样一个函数
int f(int x,int y,int z)
{
int a;
int b;
int c;

a=x;
b=x+y;
c=x+y+z;
return c;
}

int main()
{
int m;
m=f(1,2,3);

return 0;
}

**********************************************************************************
int m;
int m=f(1,2,3);

004010C6 push 3 ;/Arg3 = 00000003 //从有往左压入参数
004010C8 push 2 ;|Arg2 = 00000002
004010CA push 1 ;|Arg1 = 00000001
004010CC call 00401080 ;------->跳到下面00401080处开始执行函数
;00401080为函数f的地址
;函数返回值保存在eax中

004010D1 add esp, 0x0C ;恢复到函数调用前的堆栈,即push 3,2,1前
004010D4 mov dword ptr [ebp-4], eax ;ebp-4即为m,为什么?
;看了下面的分析就明白了,相当于m=返回值

**********************************************************************************

::::::::::::::::::::::::调用开始::::::::::::::::::::::::::::::::::::::::::::::::::::

//call 00401080调用后到了这里,烦人的编译器优化,害我写了N久

00401080 push ebp
00401081 mov ebp, esp //保存esp的值到ebp,这句执行后,堆栈情况如下
------------------------------------------------------------------------------------
------------------------------------------------------------------------------------
地址 值
0013FF60 /0013FF80 ;<-----------------esp=ebp=0013FF60,都指向栈顶 <-----栈顶
;对局部变量和参数的操纵都通过ebp来完成
0013FF64 |004010D1 ;004010D1为函数的返回地址

0013FF68 |00000001 ;三个参数,ebp-0x8为第一个参数 <------ebp+0x8
0013FF6C |00000002 ;ebp-0xc为第二个参数 <------ebp+0xc
0013FF70 |00000003 ;ebp-0x10为第三个参数 <------ebp+0x10
------------------------------------------------------------------------------------
------------------------------------------------------------------------------------

00401083 sub esp, 0xC ;给局部变量在堆栈分配空间 int a,b,c;需要12个字节
00401086 push ebx ;下面用到了ebx,edi两个寄存器,先在堆栈上保存起来
00401087 push edi ;堆栈情况如下
-----------------------------------------------------------------------------------
-----------------------------------------------------------------------------------
地址 值
0013FF4C 7C910738 ;保存ecx(7C910738)的值 <------esp现在指向这里了 <-----栈顶
0013FF50 7FFD9000 ;保存ebx(7FFD9000)的值
0013FF54 00405D83 ;变量c <------ebp-0xc
0013FF58 00420790 ;变量b <------ebp-0x8
0013FF5C 00000098 ;变量a <------ebp-0x4
0013FF60 /0013FF80 <------ebp指向这里
0013FF64 |004010D1 ;004010D1为返回地址,即函数调用call的下一行的地址
0013FF68 |00000001 <------ebp+0x8
0013FF6C |00000002 <------ebp+0xc
0013FF70 |00000003 <------ebp+0x10
-----------------------------------------------------------------------------------
-----------------------------------------------------------------------------------

00401088 lea edi, dword ptr [ebp-0x4] ;ebp-4为变量a的地址
0040108E mov ebx, dword ptr [ebp+0x8] ;ebp+8为参数x,ebx = x
00401091 mov dword ptr [edi], ebx ;相当于a=x

00401093 mov ebx, dword ptr [ebp+0xC] ;ebp+c为参数y的地址
00401096 add ebx, dword ptr [edi] ;ebx=ebx+y 即ebx=x+y

00401098 lea edi, dword ptr [ebp-0x8] ;edi指向变量b
0040109E mov dword ptr [edi], ebx ;b=x+y

004010A0 mov ebx, dword ptr [ebp+0x10] ;ebp+0x10为参数z,即ebx = z
004010A3 add ebx, dword ptr [edi] ;ebx加上b的值(此时edi仍然指向b)
;即ebx=x+b=x+y+z
004010A5 lea edi, dword ptr [ebp-0xC] ;edi指向变量c
004010AB mov dword ptr [edi], ebx ;c=ebx,即c=x+y+z

004010AD mov eax, ebx ;对于Win32程序来说,很多使用eax保存返回值

004010AF pop edi
004010B0 pop ebx ;恢复ebx,edi的值
004010B1 mov esp, ebp ;恢复堆栈
004010B3 pop ebp ;堆栈情况如下
-----------------------------------------------------------------------------------
-----------------------------------------------------------------------------------
地址 值
0013FF54 00000006 ;局部变量c
0013FF58 00000003 ;局部变量b
0013FF5C 00000001 ;局部变量在esp上面,不受到保护,几个push,pop操作就可能将这个
;改变,所以返回局部变量的地址是危险的
0013FF60 /0013FF80 ;利用这个值语句 pop ebp 将ebp寄存器恢复

******上面的值都不在有效的堆栈范围内,不受保护了************************************
0013FF64 |004010D1 ;<------esp现在指向这里了,和调用前的堆栈完全相同 <----栈顶
0013FF68 |00000001 ;<------ebp+0x8
0013FF6C |00000002 ;<------ebp+0xc
0013FF70 |00000003 ;<------ebp+0x10 三个参数是在函数返回后被释放
;call后面的这条语句 004010D1 add esp, 0x0C
-----------------------------------------------------------------------------------
-----------------------------------------------------------------------------------

004010B4 retn ;返回地址004010D1继续执行程序
::::::::::::::::::::::::调用结束::::::::::::::::::::::::::::::::::::::::::::::::::::

通过上面的分析,我们也就明白了为什么函数不能返回局部变量的地址了,因为局部变量的分配,
只不过是通过(esp-**)来完成。调用结束后,通过mov esp,ebp来销毁局部变量,那个地址在esp上面
也就不受到保护,可能随时会被修改掉

总结:
局部变量和参数都是通过堆栈来传递的,在函数中通过ebp寄存器来对他们进行操作,
其中(ebp-**)为局部变量,而(ebp+**)为函数参数,但是有一点要注意,ebp+4保存的是函数的返回
地址,从ebp+8开始才是第一个参数的地址。

对于C语言的函数调用方式,局部变量分配的空间,由函数内部维持堆栈平衡,而参数则在调用
处维持平衡,如
004010C6 push 3 ;/Arg3 = 00000003 //从右往左压入参数
004010C8 push 2 ;|Arg2 = 00000002
004010CA push 1 ;|Arg1 = 00000001
004010CC call 00401080 ;00401080为函数f的地址
004010D1 add esp, 0x0C ;三个参数是在call调用返回后销毁维持堆栈平衡

【Nowish】:
楼上说的貌似正确,那怎么解决呢?

【jxlczjp77】:
对于你这个问题,越界过几个字节并没有出现异常,很明显数组越界后,将堆栈中保存的ebp的值冲掉了,但你以后的程序可能没有用到ebp,所以没有出现异常。

但是,如果你越界超过的位置大于4个字节的话,不但将ebp全部重写,而且还会将函数的返回地址重写,这时函数不能正确返回,所以出现异常。

至于你说的越界超过10个字节才出错,那是由于编译器在分配40个字节的
char chData[40数组时,可能为了内存对齐,实际多分配了一点,所以写到10个字节才会出错,但理论上来说应该是4个字节后就到了返回地址的位置了

举个例子:
push ebp
mov ebp,esp
sub esp,40
. . ;<----esp指向栈顶,从这里到ebp都为局部变量char a[40]的空间
. .
. . ;可能由于对齐,这上面的局部变量其实不只40个字节

0013FF60 /0013FF80 ;<---ebp(0013FF60)指向这里,而里面的值0013FF80为保存的原ebp值
0013FF64 |004010D1 ;<------函数的返回地址
0013FF68 |00000001 ;<------ebp+0x8
0013FF6C |00000002 ;<------ebp+0xc
0013FF70 |00000003 ;<------ebp+0x10 三个参数是在函数返回后被释放

如果没有其他局部变量的话,数组的首地址即为esp所指向的栈顶位置,向下走40个字节都是数组的空间,如果越界的话,就到了保存的ebp,再往下就是函数的返回地址

【ChrisK】:
好像cdecl当中参数是右向左压入栈的

【healer_kx】:
只有delphi的默认传参是左到右的。
而且不是我们C++程序员看到的 PASCAL

反正我所知道的,除此之外都是right->left的。

【healer_kx】:
即便是fastcall,除了两个借助寄存器的其他参数也是RL顺序的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: