如何写网站建设策划案,网站建设中请期待,小程序的推广方法,广州现在哪个区不能去在开发软件的过程中我们经常会遇到错误#xff0c;如果你用 Google 搜过出错信息#xff0c;那你多少应该都访问过Stack Overflow这个网站。作为全球最大的程序员问答网站#xff0c;Stack Overflow 的名字来自于一个常见的报错#xff0c;就是栈溢出#xff08;stack ove… 在开发软件的过程中我们经常会遇到错误如果你用 Google 搜过出错信息那你多少应该都访问过Stack Overflow这个网站。作为全球最大的程序员问答网站Stack Overflow 的名字来自于一个常见的报错就是栈溢出stack overflow。
今天我们就从程序的函数调用开始讲讲函数间的相互调用在计算机指令层面是怎么实现的以及什么情况下会发生栈溢出这个错误。
为什么我们需要程序栈
和前面一样我们还是从一个非常简单的 C 程序 function_example.c 看起。
// function_example.c
#include stdio.h
int static add(int a, int b)
{return ab;
}int main()
{int x 5;int y 10;int u add(x, y);
}
这个程序定义了一个简单的函数 add接受两个参数 a 和 b返回值就是 ab。而 main 函数里则定义了两个变量 x 和 y然后通过调用这个 add 函数来计算 uxy最后把 u 的数值打印出来。
$ gcc -g -c function_example.c
$ objdump -d -M intel -S function_example.o
我们把这个程序编译之后objdump 出来。我们来看一看对应的汇编代码。
int static add(int a, int b)
{0: 55 push rbp1: 48 89 e5 mov rbp,rsp4: 89 7d fc mov DWORD PTR [rbp-0x4],edi7: 89 75 f8 mov DWORD PTR [rbp-0x8],esireturn ab;a: 8b 55 fc mov edx,DWORD PTR [rbp-0x4]d: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8]10: 01 d0 add eax,edx
}12: 5d pop rbp13: c3 ret
0000000000000014 main:
int main()
{14: 55 push rbp15: 48 89 e5 mov rbp,rsp18: 48 83 ec 10 sub rsp,0x10int x 5;1c: c7 45 fc 05 00 00 00 mov DWORD PTR [rbp-0x4],0x5int y 10;23: c7 45 f8 0a 00 00 00 mov DWORD PTR [rbp-0x8],0xaint u add(x, y);2a: 8b 55 f8 mov edx,DWORD PTR [rbp-0x8]2d: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]30: 89 d6 mov esi,edx32: 89 c7 mov edi,eax34: e8 c7 ff ff ff call 0 add39: 89 45 f4 mov DWORD PTR [rbp-0xc],eax3c: b8 00 00 00 00 mov eax,0x0
}41: c9 leave 42: c3 ret
可以看出来在这段代码里main 函数和上一节我们讲的的程序执行区别并不大它主要是把 jump 指令换成了函数调用的 call 指令。call 指令后面跟着的仍然是跳转后的程序地址。
这些你理解起来应该不成问题。我们下面来看一个有意思的部分。
我们来看 add 函数。可以看到add 函数编译之后代码先执行了一条 push 指令和一条 mov 指令在函数执行结束的时候又执行了一条 pop 和一条 ret 指令。这四条指令的执行其实就是在进行我们接下来要讲压栈Push和出栈Pop操作。
你有没有发现函数调用和上一节我们讲的 if…else 和 for/while 循环有点像。它们两个都是在原来顺序执行的指令过程里执行了一个内存地址的跳转指令让指令从原来顺序执行的过程里跳开从新的跳转后的位置开始执行。
但是这两个跳转有个区别if…else 和 for/while 的跳转是跳转走了就不再回来了就在跳转后的新地址开始顺序地执行指令就好像徐志摩在《再别康桥》里面写的“我挥一挥衣袖不带走一片云彩”继续进行新的生活了。而函数调用的跳转在对应函数的指令执行完了之后还要再回到函数调用的地方继续执行 call 之后的指令就好像贺知章在《回乡偶书》里面写的那样“少小离家老大回乡音未改鬓毛衰”不管走多远最终还是要回来。
那我们有没有一个可以不跳转回到原来开始的地方来实现函数的调用呢直觉上似乎有这么一个解决办法。你可以把调用的函数指令直接插入在调用函数的地方替换掉对应的 call 指令然后在编译器编译代码的时候直接就把函数调用变成对应的指令替换掉。
不过仔细琢磨一下你会发现这个方法有些问题。如果函数 A 调用了函数 B然后函数 B 再调用函数 A我们就得面临在 A 里面插入 B 的指令然后在 B 里面插入 A 的指令这样就会产生无穷无尽地替换。就好像两面镜子面对面放在一块儿任何一面镜子里面都会看到无穷多面镜子。 Infinite Mirror Effect如果函数 A 调用 BB 再调用 A那么代码会无限展开图片来源
看来把被调用函数的指令直接插入在调用处的方法行不通。那我们就换一个思路能不能把后面要跳回来执行的指令地址给记录下来呢就像前面讲 PC 寄存器一样我们可以专门设立一个“程序调用寄存器”来存储接下来要跳转回来执行的指令地址。等到函数调用结束从这个寄存器里取出地址再跳转到这个记录的地址继续执行就好了。 //未完待续....