07 | 函数调用:为什么会发生stack overflow?

栈溢出(stack overflow)。

 

// function_example.c
#include <stdio.h>
int static add(int a, int b)
{
    return a+b;
}


int main()
{
    int x = 5;
    int y = 10;
    int u = add(x, y);
}
int static add(int a, int b)
{
   0:   55                      push   rbp     //压栈
   1:   48 89 e5                mov    rbp,rsp
   4:   89 7d fc                mov    DWORD PTR [rbp-0x4],edi
   7:   89 75 f8                mov    DWORD PTR [rbp-0x8],esi
    return a+b;
   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    rbp   //出栈
  13:   c3                      ret    
0000000000000014 <main>:
int main()
{
  14:   55                      push   rbp //通常以 rbp 来作为参数和局部变量的基址;
  15:   48 89 e5                mov    rbp,rsp
  18:   48 83 ec 10             sub    rsp,0x10
    int x = 5;
  1c:   c7 45 fc 05 00 00 00    mov    DWORD PTR [rbp-0x4],0x5
    int y = 10;
  23:   c7 45 f8 0a 00 00 00    mov    DWORD PTR [rbp-0x8],0xa
    int 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,edx
  32:   89 c7                   mov    edi,eax
  34:   e8 c7 ff ff ff          call   0 <add>
  39:   89 45 f4                mov    DWORD PTR [rbp-0xc],eax
  3c:   b8 00 00 00 00          mov    eax,0x0
}
  41:   c9                      leave  
  42:   c3                      ret    

if…else 和 for/while 的跳转,是跳转走了就不再回来了,就在跳转后的新地址开始顺序地执行指令,而函数调用的跳转,在对应函数的指令执行完了之后,还要再回到函数调用的地方,继续执行 call 之后的指令。

 

 

那我们有没有一个可以不跳转回到原来开始的地方,来实现函数的调用呢?

可以把调用的函数指令,直接插入在调用函数的地方,替换掉对应的call 指令,然后在编译器编译代码的时候,直接就把函数调用变成对应的指令替换掉。

 

栈的大小也是有限的。如果函数调用层数太多,我们往栈里压入它存不下的内容,程序在执行的过程中就会遇到栈溢出的错误,这就是“stack overflow”。

 

除了无限递归,递归层数过深,在栈空间里面创建非常占内存的变量(比如一个巨大的数组),这些情况都很可能给你带来 stack overflow。

 

 

 

add 函数的第 0 行,push rbp 这个指令,就是在进行压栈。这里的 rbp 又叫栈帧指针,是一个存放了当前栈帧位置的寄存器。push rbp就把之前调用函数的返回地址,压到栈顶。接着,第 1 行的一条命令 mov rbp, rsp 里,则是把 rsp 这个栈指针的值复制到 rbp 里,而rsp 始终会指向栈顶。这个命令意味着,rbp 这个栈帧指针指向的返回地址,变成当前最新的栈顶,也就是 add 函数的返回地址了。而在函数 add 执行完成之后,又会分别调用第 12 行的 pop rbp 来将当前的栈顶出栈,然后调用第 13 行的 ret 指令,将程序的控制权返回到出栈后的栈顶,也就是 main 函数的返回地址。

 

 

 

如何利用函数内联进行性能优化?

函数内联(Inline),一种编译器自动优化的场景 -- 把一个实际调用的函数产生的指令,直接插入到的位置,来替换对应的函数调用指令(被调函数没有调用其他函数)。

内联带来的优化是,CPU 需要执行的指令数变少了,根据地址跳转的过程不需要了,压栈和出栈的过程也不用了。

不过内联并不是没有代价,内联意味着,我们把可以复用的程序指令在调用它的地方完全展开了。如果一个函数在很多地方都被调用了,那么就会展开很多次,整个程序占用的空间就会变大了。

这样没有调用其他函数,只会被调用的函数,我们一般称之为叶子函数(或叶子过程)。

 

通过加入了程序栈,我们相当于在指令跳转的过程种,加入了一个“记忆”的功能,能在跳转去运行新的指令之后,再回到跳出去的位置,能够实现更加丰富和灵活的指令执行流程。这个也为我们在程序开发的过程中,提供了“函数”这样一个抽象,使得我们在软件开发的过程中,可以复用代码和指令,而不是只能简单粗暴地复制、粘贴代码和指令。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  

posted on 2019-05-13 21:15  wzc521  阅读(506)  评论(0)    收藏  举报

导航