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);
}
(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