c语言push_函数调用堆栈图c语言

文章来源:https://blog.seclibs.com/函数调用堆栈图-c语言/

我们就使用一个简单的c语言程序来对描述一下在函数调用的时候都发生了什么。

02a233dcb48684bff009f81e4026f624.png

中间的一小段没有意义的汇编语言是为了方便设置断点,为后面的调试做好铺垫,因为有时会碰到找不到断点位置的情况,使用这个方法,可以在找不到断点的时候向后执行一次,而不破坏我们想调试的程序当前的堆栈状态,这里对main函数和sum函数的效果是类似的,这里直接跟着断点来执行分析sum函数的堆栈操作。

我们先假设初始状态下的堆栈图如下,esp与ebp的真实距离我们省略。

bbcd1b2d4fee0f75cca369f26acf12ea.png

接下来我们来看一下后面的操作。

2d2d44c0cace198ed3b13db47520bbce.png

在程序的执行当中,我们一般都是按照从右向左的方式去处理的,这里也不例外,我们可以发现当我们调用sum函数对数字1和数字2进行处理的时候,将数字2和1依次压入栈中,这个时候堆栈的情况是这个样子的,esp的值已经减8。

7599a714eddc914069802d1b22de6e9a.png

接下来调用了call,这时进行了两步操作,先将call后面的地址push进堆栈,然后再jmp到call所调用的地址。

723d0e879fc0855a77bb4f234e449d14.png

因为jmp是不会影响堆栈的,所以现在的堆栈情况是这样的

99d2c60cc842e057adec371c8b586b06.png

然后因为编译器的原因在call的时候还会有一个jmp来中转到后面的处理函数,因为jmp不影响堆栈,我们可以忽略掉它,这里是跳转到了sum函数的处理位置。

c8dad8823bb86d6bfb1d37333d1d1ae1.png

此时的堆栈是没有发生变化的,现在开始到了函数调用的关键阶段了。

首先先将ebp的值push到堆栈中,因为用到了ebp寻址的方式,所以这里用这种方式来保存ebp中原本的值,然后将esp的值赋给ebp,用ebp寻址来代替esp寻址,因为esp的值一直在不断的发生变化,使用esp寻址会带来很大的计算负担,此时esp与ebp都指向了同一块地址,其中的内容是原来的ebp的值。

d43abe6d9c2e44709be7b620f9e727e4.png

然后让esp减去了0c0h位,开始提升堆栈了,为程序的运行开辟一个存储空间,这个区域也就是平时所说的缓冲区,因为一个单元是四个字节,c0也就是往上提了48个格,由于位置有限中间依旧省略,此时堆栈就变成了如下的样子。

a775710b8495b62eb559a3ea6cfab427.png

后面又进行了一系列的push操作,也是为了方便在后续使用这些寄存器的时候保证它们初始的值不丢失,与前面保存ebp的值是一样的方式。

2bb5197d24c8617abda8e98f134acc6b.png

然后接下来的四步操作只有一个目的,那就是将中间的48格全部值为CC,CC在调试的时候相当于断点,也就是如果你程序跑过的话,就会触发断点不会再继续执行了。

cc7bff7b0a433d5d60d37f9e32dd7aff.png

lea是交换地址中的值,给eax和ecx赋值是为rep的执行做准备的,stos是将eax中的值赋给edi,rep是执行后面的指令ecx次。

8a2dd4e77d55e6b89c020fdd99bb17ea.png

接下来的两步指令我们忽略,它们是vs编译器添加的调试指令

7f2245c68e4add777bf3f595e3c208fc.png

因为eax一般是来用作返回值的,所以这里的计算都是跟eax进行计算的,因为我们这里直接使用的是return返回,没有涉及到临时变量,所以不会用到缓冲区来存储。

576b89d63b659667de06d5b0c570672d.png

接下来的三步pop,是将之前存储在栈中的元素都恢复到它原来的位置。

6c27c259707918b8e15f1fe920919593.png

此时的堆栈情况就变成了,上面的值还是没有清除的,它们现在已经是垃圾数据的,下一次填充的时候会把它们覆盖掉,这也就造成了可以在其中获取到某些程序不想让人知道的临时变量值。

6ac42001bc4ae4a12b3d488bb76b2fa5.png

接下来让esp增加0c0,也就恢复到了提升堆栈之前的位置,此时esp与ebp到了一个位置。

1ef85a2a210f396fbc1225faac254aca.png

接下来的三步操作依旧可以忽略,它们是vs编译器生成的,用来检测堆栈是否平衡,如果不平衡的话在这里就会产生报错。

23420352fdedbdc7cb52f86155d7549f.png

最后就是使用pop,将ebp恢复到之前的位置。

e8870f8d3224c6267ca729c76fa625e6.png
3e519f033cfe3d58b9fd578fd95ddda1.png

最后使用ret回到堆栈中存储的地址,也就是call调用的下一个地址。

a46e79bcb1176bd025876c62cdef599a.png
cac55e2ea1a3ab35db5f4dcd49cdbccc.png

但是此时还有个问题,esp并没有回到调用前的位置,所以堆栈还是没有平衡的,如果堆栈不平衡,那在不断的执行的过程中,就会发生堆栈溢出,这里编译器是使用外平栈的方式来使堆栈恢复平衡的,它在esp的基础上增加了8。

530a64cc99d760311c2692825b9d3bba.png

此时堆栈也就恢复到了平衡状态

81aa2f8cd34f0c126213fff79215a805.png

还有另一种方式是使用内平栈的方式,即在函数内部就将堆栈恢复平衡,使用ret 8的方式。

再往后面的操作就是main函数的堆栈平衡的处理了,与上面的函数调用类似,就不提了。

文章首发公众号和个人博客

公众号:无心的梦呓(wuxinmengyi)

a897084cab998c45b2422ca8f3a055da.png

博客:https://blog.seclibs.com/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在 C 语言中,可以使用堆栈(也称为栈)来实现函数的嵌套调用和递归调用堆栈是一种数据结构,具有先进后出的特性,类似于一叠盘子,后放入的盘子必须先取出来。 在函数调用时,每次调用都会将当前函数的返回地址、参数和局部变量等信息压入堆栈中,然后跳转到被调用函数的入口地址执行。当被调用函数执行完毕后,会从堆栈中弹出之前保存的信息,恢复到调用函数之前的执行状态,然后继续执行原来的函数。 下面是一个使用堆栈实现函数嵌套调用的示例代码: ```c #include <stdio.h> #include <stdlib.h> #define STACK_SIZE 100 // 定义堆栈结构体 typedef struct { int top; int data[STACK_SIZE]; } Stack; // 初始化堆栈 void initStack(Stack *stack) { stack->top = -1; } // 压入元素 void push(Stack *stack, int value) { if (stack->top >= STACK_SIZE - 1) { printf("Stack overflow!\n"); exit(EXIT_FAILURE); } stack->data[++stack->top] = value; } // 弹出元素 int pop(Stack *stack) { if (stack->top < 0) { printf("Stack underflow!\n"); exit(EXIT_FAILURE); } return stack->data[stack->top--]; } // 判断堆栈是否为空 int isEmpty(Stack *stack) { return stack->top < 0; } // 函数嵌套调用 int foo(int x) { printf("enter foo(%d)\n", x); int y = x + 1; int z = bar(y); printf("exit foo(%d)\n", x); return z; } int bar(int x) { printf("enter bar(%d)\n", x); int y = x * 2; printf("exit bar(%d)\n", x); return y; } int main() { Stack stack; initStack(&stack); push(&stack, 1); push(&stack, 2); push(&stack, 3); while (!isEmpty(&stack)) { int value = pop(&stack); printf("%d\n", value); } int result = foo(10); printf("result=%d\n", result); return 0; } ``` 在上面的示例代码中,我们定义了一个堆栈结构体,包含一个整型数组和一个指向栈顶的指针。然后实现了初始化堆栈、压入元素、弹出元素和判断堆栈是否为空等基本操作。 在 `main` 函数中,我们首先演示了如何使用堆栈来存储一些数据,并依次弹出输出。然后演示了函数嵌套调用的过程,其中函数 `foo` 调用函数 `bar`。每次调用函数时,我们将当前函数的返回地址和参数压入堆栈中,然后跳转到被调用函数的入口地址执行。当被调用函数执行完毕后,我们从堆栈中弹出之前保存的信息,恢复到调用函数之前的执行状态,然后继续执行原来的函数。 类似地,我们可以使用堆栈实现递归调用。下面是一个使用堆栈实现阶乘函数的示例代码: ```c #include <stdio.h> #include <stdlib.h> #define STACK_SIZE 100 // 定义堆栈结构体 typedef struct { int top; int data[STACK_SIZE]; } Stack; // 初始化堆栈 void initStack(Stack *stack) { stack->top = -1; } // 压入元素 void push(Stack *stack, int value) { if (stack->top >= STACK_SIZE - 1) { printf("Stack overflow!\n"); exit(EXIT_FAILURE); } stack->data[++stack->top] = value; } // 弹出元素 int pop(Stack *stack) { if (stack->top < 0) { printf("Stack underflow!\n"); exit(EXIT_FAILURE); } return stack->data[stack->top--]; } // 判断堆栈是否为空 int isEmpty(Stack *stack) { return stack->top < 0; } // 阶乘函数 int factorial(int n) { Stack stack; initStack(&stack); push(&stack, n); int result = 1; while (!isEmpty(&stack)) { int value = pop(&stack); if (value <= 1) { break; } result *= value; push(&stack, value - 1); } return result; } int main() { int result = factorial(5); printf("result=%d\n", result); return 0; } ``` 在上面的示例代码中,我们定义了一个阶乘函数 `factorial`,使用堆栈实现了递归调用。在递归调用过程中,每次调用函数时,我们将当前函数的返回地址和参数压入堆栈中,然后跳转到被调用函数的入口地址执行。当被调用函数执行完毕后,我们从堆栈中弹出之前保存的信息,恢复到调用函数之前的执行状态,然后继续执行原来的函数

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值