内存管理--栈

 

1.栈的简介

在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。压栈操作使得栈增大,而弹出操作使栈减小。栈在程序运行中具有举足轻重的地位。最重要的,栈保存了一个函数调用所需要的维护信息,这常常被称为堆栈帧(Stack Frame)或活动记录(Activate Record)。堆栈帧一般包括如下几方面内容:

(1)函数的返回地址和参数。

(2)临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量。

(3)保存的上下文:包括在函数调用前后需要保持不变的寄存器。

我们可以看出,函数调用要保存函数调用之前的操作系统的环境和返回地址。还要保存参数和临时变量给被调函数使用。这些信息都要保存在栈中。

2.函数调用过程

void swap(int * a, int *b) 

  int c; 

  c = *a; *a = *b; *b = c; 

int main() 

   int a, b; 

   a = 16; b = 32; 

   swap(&a, &b); 

   return (a - b); 

wpsD21D.tmp

(1)两个指针:ebp和esp

esp:是堆栈指针,也就是始终指向栈顶

ebp:是基址指针,顾名思义就是以它为基址进行寻址

(2)调用过程

当调用一个函数时,把ebp保存起来。这个ebp也是被调用者开始的地址。把这个值赋值给esp。也就是将之前的栈顶作为新的基址(栈底),然后再这个基址上开辟相应的空间用作被调用函数的堆栈。函数返回后,从而是ebp中可取出之前的esp值,使栈顶恢复函数调用前的位置;再从恢复后的栈顶可弹出之前的ebp值,因为这个值在函数调用前一步被压入堆栈。这样,ebp和esp就都恢复了调用前的位置,堆栈恢复函数调用前的状态。

(3)执行的代码

_swap: 

pushl %ebp 

movl %esp,%ebp

这两行代码,保存了调用者的ebp,并且设置了被调用者的帧指针 

subl $4,%esp   

movl 8(%ebp),%eax    

movl (%eax),%ecx  

movl %ecx,-4(%ebp) 

movl 8(%ebp),%eax

movl 12(%ebp),%edx 

movl (%edx),%ecx 

movl %ecx,(%eax) 

movl 12(%ebp),%eax 

movl -4(%ebp),%ecx 

  movl %ecx,(%eax)

#这几行指令,是以ebp作为基址,读取了调用者的参数,传递给被调用者

movl %ebp,%esp;

  popl %ebp;

#这两行是恢复到函数调用前的状态。ebp指向调用者栈帧开始的地方,把ebp赋值给esp,esp就指向了栈帧开始的地方。pop(ebp)不仅是出栈,也就是把存在栈中的值弹出来赋给ebp。也就是说ebp恢复到了原来的状态。同时,ebp出栈了,栈顶也发生了变化,esp指向了函数返回地址。

ret 

3总结:

1.两句的mov ebp,esp实际上是把ebp进栈后的栈顶地址给了ebp。

2.在ebp没有出栈钱,它会一直保存ebp进栈以后的栈顶值,也就是1的值。

3.在ebp出栈前,需要把esp恢复到只有ebp在栈中时的值。

4.出栈后,esp自然恢复到ebp进栈以前的初始值,而pop ebp则恢复了ebp的初始值。

5.pop的语义很重要,pop  ebp的意思是把当前栈顶的元素出栈,送入ebp中,而不是让ebp出栈,这点必须明确!

4函数的返回值的传递

(1)小于四字节返回类型

ax是传递返回值的通道。函数将返回值存储在eax中,返回后函数的调用方再读取eax。但是eax本身只有4个字节.

(2)对于返回5~8字节返回类型

几乎所有的调用惯例都是采用eax和edx联合返回的方式进行的。其中eax存储返回值要低4字节,而edx存储返回值要高1~4字节。

(3)超过8字节的返回类型

1.首先main函数在栈上额外开辟了一片空间,并将这块空间的一部分作为传递返回值的临时对象,这里称为temp。

2.将temp对象的地址作为隐藏参数传递给return_test函数。

3.return_test函数将数据拷贝给temp对象,并将temp对象的地址用eax传出。

4.return_test返回之后,main函数将eax指向的temp对象的内容拷贝给n。

具体的过程:

(1)我们来看看main函数一开始初始化的汇编代码:

int main()

{

00411470  push        ebp 

00411471  mov         ebp, esp

00411473  sub         esp,1D4h

00411479  push        ebx 

0041147A  push        esi 

0041147B  push        edi 

0041147C  lea         edi,[ebp-1D4h]

00411482  mov         ecx,75h

00411487  mov         eax,0CCCCCCCCh

0041148C  rep stos    dword ptr es:[edi]

0041148E  mov         eax,dword ptr [___security_cookie (417000h)]

00411493  xor         eax,ebp

00411495  mov         dword ptr [ebp-4],eax

}

我们可以看到main函数在保存了ebp之后,就直接将栈增大了1D4h个字节,因此ebp-1D0h就正好落在这个扩大区域的末尾,而区间[ebp-1D0h, ebp-1D0h + 128)也正好处于这个扩大区域的内部。至于这块区域剩下的内容,则留作它用。

这个区间就是在main中开辟的空间,地址为[ebp-1D0h] 。

让我们首先来反汇编(MSVC9)一下main函数,结果如下:

big_thing n = return_test();

00411498  lea         eax,[ebp-1D0h]

0041149E  push        eax 

0041149F  call        _return_test

004114A4  add         esp,4

004114A7  mov         ecx,20h

004114AC  mov         esi,eax

004114AE  lea         edi,[ebp-88h]

004114B4  rep movs    dword ptr es:[edi],dword ptr [esi]

将其中第二行:

00411498  lea         eax,[ebp-1D0h]

将栈上的一个地址(ebp-1D0h)存储在eax里,接着下一行:

push        eax

将这个地址压入栈中然后就紧接着调用return_test函数。这从形式上无疑是将数据ebp - 1D0h作为参数传入return_test函数,然而return_test是没有参数的,因此我们可以将这个数据称为是“隐含参数”。换句话说,return_test的原型实际是:

big_thing return_test(void* addr);

这段汇编最后4行(斜体部分)是一个整体,我们可以想象在函数返回之后,函数的调用方需要获取函数的返回对象并对n赋值。rep movs是一个复合指令,它的大致意义是重复movs指令直到ecx寄存器为0。于是“rep movs a, b”的意思就是将b指向位置上的若干个双字(4字节)拷贝到由a指向的位置上,拷贝双字的个数由ecx指定,实际上这句复合指令的含义相当于memcpy(a, b, ecx * 4)。所以说,最后4行的含义相当于:memcpy(ebp-88h, eax, 0x20 * 4)即将eax指向位置上的0x20个双字拷贝到ebp-88h的位置上。毫无疑问,ebp-88h这个地址就是变量n的地址,如果有所怀疑,可以比较一下n的地址和ebp-88h的值即可确信这一点。而0x20个双字就是128个字节,正是big_thing的大小。现在我们可以将这段汇编略微还原了:

return_test(ebp-1D0h)

memcpy(&n, (void*)eax, sizeof(n));

可见,return_test返回的结构体仍然是由eax传出的,只不过这次eax存储的是结构体的指针。那么return_test具体是如何返回一个结构体的呢?让我们来看看return_test的实现:

big_thing return_test()

{

...

    big_thing b;

    b.buf[0] = 0;

004113C8  mov         byte ptr [ebp-88h],0

    return b;

004113CF  mov         ecx,20h

004113D4  lea         esi,[ebp-88h]

004113DA  mov         edi,dword ptr [ebp+8]

004113DD  rep movs    dword ptr es:[edi],dword ptr [esi]

004113DF  mov         eax,dword ptr [ebp+8]

}

在这里,ebp-88h存储的是return_test的局部变量b。根据rep movs的功能,加粗的4条指令可以翻译成如下的代码:

memcpy([ebp+8],&b, 128);

在这里,[ebp+8]指的是*(void**)(ebp+8),即将地址ebp+8上存储的值作为地址,由于ebp实际指向栈上保存的旧的ebp,因此ebp+4指向压入栈中的返回地址,ebp+8则指向函数的参数。而我们知道,return_test是没有真正的参数的,只有一个“伪参数”由函数的调用方悄悄地传入,那就是ebp-1D0h(这里的ebp是return_test调用前的ebp)这个值。

关键性的指令:

(1)big_thing n = return_test();

00411498  lea         eax,[ebp-1D0h]

0041149E  push        eax 

0041149F  call        _return_test

004114A4  add         esp,4

004114A7  mov         ecx,20h

004114AC  mov         esi,eax

004114AE  lea         edi,[ebp-88h]

004114B4  rep movs    dword ptr es:[edi],dword ptr [esi]

相当于:return_test(ebp-1D0h)

memcpy(&n, (void*)eax, sizeof(n));

把地址当做参数传递过去,最后从eax中取得了返回值。

(2)

004113CF  mov         ecx,20h

004113D4  lea         esi,[ebp-88h]

004113DA  mov         edi,dword ptr [ebp+8]

004113DD  rep movs    dword ptr es:[edi],dword ptr [esi]

相当于:memcpy([ebp+8],&b, 128);

我们知道,return_test是没有真正的参数的,只有一个“伪参数”由函数的调用方悄悄地传入,那就是ebp-1D0h(这里的ebp是return_test调用前的ebp)这个值。换句话说,[ebp+8]=old_ebp-1D0h。

(3)004113DF  mov         eax,dword ptr [ebp+8]

把这个值的地址传给eax,再由eax传递给n

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值