用计算机底层的逻辑看待C语言中的函数调用

上一篇博客对栈没有进行一个详细的介绍,是因为对一个函数的调用与栈有着紧密的联系,加之在学逆向的时候对栈有过详细的接触,所以今天就把栈和函数放在一起做一个详细的总结。

首先说一下数据结构中的栈和内存中的栈这两者之间的区别。

数据结构中的栈

栈作为一种数据结构,它是一种操作受限的线性表,它只允许在表的一端进行插入与删除操作,它按照先入后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。栈具有记忆作用,对栈的插入与删除操作中,不需要改变栈底指针。
以简单的顺序栈为例:

#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
这段代码为函数返回代码

我们注意堆栈窗口:

在这里插入图片描述
执行这行代码后,函数返回母函数。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

walkerrev_ll

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值