上一篇博客对栈没有进行一个详细的介绍,是因为对一个函数的调用与栈有着紧密的联系,加之在学逆向的时候对栈有过详细的接触,所以今天就把栈和函数放在一起做一个详细的总结。
栈
首先说一下数据结构中的栈和内存中的栈这两者之间的区别。
数据结构中的栈
栈作为一种数据结构,它是一种操作受限的线性表,它只允许在表的一端进行插入与删除操作,它按照先入后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。栈具有记忆作用,对栈的插入与删除操作中,不需要改变栈底指针。
以简单的顺序栈为例:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define N 100
typedef struct Sqstack
{
int data[N];
int top;
}Sqstack;
Sqstack *Init_sqstack()
{
Sqstack *s;
s = (Sqstack *)malloc(sizeof(Sqstack));
s->top = -1;
return s;
}
int Push_sqstack(Sqstack *s,int x)
{
if (s->top == N)
{
return 0;
}
else
{
s->data[++s->top] = x;
return 1;
}
}
int Top_sqstack(Sqstack *s,int *x)
{
if (s->top == -1)
{
return 0;
}
else
{
*x = s->data[s->top];
return 1;
}
}
int isEmpty(Sqstack *s)
{
if (s->top == -1)
{
return 0;
}
else
{
return 1;
}
}
int Pop_sqstack(Sqstack *s, int *x)
{
if (s->top == -1)
{
return 0;
}
else
{
*x = s->data[s->top--];
}
}
int main()
{
Sqstack *s;
int x;
s = Init_sqstack();
Push_sqstack(s,100);
Top_sqstack(s,&x);
printf("%d\n", x);
x = isEmpty(s);
printf("%d\n", x);
Pop_sqstack(s, &x);
printf("%d\n", x);
system("pause");
return 0;
}
上面是对顺序栈简单的C语言代码实现,链栈的入栈操作和单链表的头插法建表相类似,这里就不再赘述。
内存栈
如果您关注网络安全的话,想必一定听说过缓冲区溢出这个术语吧,简单的说,缓冲区溢出就是大缓冲区中的数据向小缓冲区中的数据复制时,由于没有注意小缓冲区的边界,从而冲掉了和小缓冲区相邻其他内存区域的其它数据而引起的内存问题,缓冲溢出是最常见的内存错误之一。缓冲区溢出的利用方式和缓冲区属于哪个内存区域密不可分,栈溢出就是在内存栈中发生的情形。
内存栈实际上就是系统栈,系统栈由操作系统自行维护,它主要用于实现高级语言中函数的调用。一般来说,只有在使用汇编语言开发程序的时候才需要直接和它打交道。
函数调用
接下来主要总结一下在C语言中,函数是如何调用的。接下来总结的这些东西有点偏底层。
首先需要介绍一下栈帧这个概念,对于每一个函数,它都有一个函数栈帧,需要介绍两个寄存器:
ESP:栈指针寄存器,简单的说,这个寄存器里面存放着一个指针,它永远指向内存栈的顶部,也可以说成它永远指向系统栈最上面一个栈帧的栈顶。
EBP:基址指针寄存器,它里面也存放着一个指针,这个指针永远指向系统栈最上面一个栈帧的栈底。
然后介绍函数栈帧的概念:ESP和EBP之间的内存空间为当前函数的栈帧。
在函数栈帧中,一般包含以下几类重要信息:
1、局部变量:为当前函数的局部变量开辟的内存空间。
2、栈帧状态值:当前栈帧需要保存前一个栈帧的顶部和底部,用于在当前栈帧被弹出后恢复上一个栈帧。
3、函数返回地址:也就是函数调用前的指令位置,以便函数在返回时能够恢复到函数被调用前的代码区继续执行指令。
好了,函数栈帧就介绍完了,接下来主要介绍一下函数是如何调用的。首先需要说明一点,在C语言中,函数参数入栈是从右向左的。
函数调用大概包括以下几个步骤:
1、参数入栈。
2、返回地址入栈:将当前代码区调用指令的下一条指令压栈,供函数返回时继续执行。
3、代码区跳转,处理器从当前代码区跳转到被调用函数的入口处。
4、栈帧调整。
同样的,函数返回的步骤如下:
1、保存返回值:通常把函数返回值保存在EAX寄存器中。
2、弹出当前栈帧,恢复上一个栈帧。如何恢复,就不详细介绍了。
3、跳转:按照函数返回地址跳入母函数中继续执行。
这就是函数调用的步骤。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int f(int *a, int *b)
{
int temp;
temp = *b;
*b = *a;
*a = temp;
return 0;
}
int main(void)
{
printf("hello,world!\n");
int a = 10;
int b = 20;
int c =f(&a, &b);
printf("%d", c);
system("pause");
return 0;
}
这是我写的一个简单程序,通过VS2017编译链接成程序,然后反汇编,接下来详细介绍:
004518BC 83C4 04 add esp, 4
004518BF C745 F4 0A00000>mov dword ptr [ebp-C], 0A int a = 10
004518C6 C745 E8 1400000>mov dword ptr [ebp-18], 14 int b = 20
004518CD 8D45 E8 lea eax, dword ptr [ebp-18] 取b的地址
004518D0 50 push eax 函数参数从右想做入栈,首先是b
004518D1 8D4D F4 lea ecx, dword ptr [ebp-C] 取a的地址
004518D4 51 push ecx 然后是a
004518D5 E8 F2F8FFFF call 004511CC 调用f()函数
004518DA 83C4 08 add esp, 8
004518DD 8945 DC mov dword ptr [ebp-24], eax
004518E0 8B45 DC mov eax, dword ptr [ebp-24]
004518E3 50 push eax
004518E4 68 407B4500 push 00457B40 ; ASCII "%d"
004518E9 E8 5DF7FFFF call 0045104B
这个是汇编代码,接下来我们看下堆栈窗口:
当此函数的两个参数压栈后,注意,此时call并未执行。我们记录下call下一条指令的地址。
004518DA 83C4 08 add esp, 8
f7进入此函数,注意观察堆栈窗口变化:
注意到,这个返回地址被压栈。
然后跳转到f函数
00451810 55 push ebp
00451811 8BEC mov ebp, esp
00451813 81EC CC000000 sub esp, 0CC
00451819 53 push ebx
0045181A 56 push esi
0045181B 57 push edi
0045181C 8DBD 34FFFFFF lea edi, dword ptr [ebp-CC]
00451822 B9 33000000 mov ecx, 33
00451827 B8 CCCCCCCC mov eax, CCCCCCCC
0045182C F3:AB rep stos dword ptr es:[edi]
0045182E B9 06C04500 mov ecx, 0045C006
00451833 E8 DFF9FFFF call 00451217
00451838 8B45 0C mov eax, dword ptr [ebp+C]
0045183B 8B08 mov ecx, dword ptr [eax]
0045183D 894D F8 mov dword ptr [ebp-8], ecx
00451840 8B45 0C mov eax, dword ptr [ebp+C]
00451843 8B4D 08 mov ecx, dword ptr [ebp+8]
00451846 8B11 mov edx, dword ptr [ecx]
00451848 8910 mov dword ptr [eax], edx
0045184A 8B45 08 mov eax, dword ptr [ebp+8]
0045184D 8B4D F8 mov ecx, dword ptr [ebp-8]
00451850 8908 mov dword ptr [eax], ecx
00451852 33C0 xor eax, eax
00451854 5F pop edi
00451855 5E pop esi
00451856 5B pop ebx
00451857 81C4 CC000000 add esp, 0CC
0045185D 3BEC cmp ebp, esp
0045185F E8 BDF9FFFF call 00451221
00451864 8BE5 mov esp, ebp
00451866 5D pop ebp
00451867 C3 retn
核心代码为这几块
00451838 8B45 0C mov eax, dword ptr [ebp+C]
0045183B 8B08 mov ecx, dword ptr [eax]
0045183D 894D F8 mov dword ptr [ebp-8], ecx
00451840 8B45 0C mov eax, dword ptr [ebp+C]
00451843 8B4D 08 mov ecx, dword ptr [ebp+8]
00451846 8B11 mov edx, dword ptr [ecx]
00451848 8910 mov dword ptr [eax], edx
0045184A 8B45 08 mov eax, dword ptr [ebp+8]
0045184D 8B4D F8 mov ecx, dword ptr [ebp-8]
00451850 8908 mov dword ptr [eax], ecx
00451852 33C0 xor eax, eax 把返回值存在eax中
这段代码为交换代码
00451864 8BE5 mov esp, ebp
00451866 5D pop ebp
00451867 C3 retn
这段代码为函数返回代码
我们注意堆栈窗口:
执行这行代码后,函数返回母函数。