深入了解函数调用与栈

 分类: 致知

有人把程序员比作魔术师,几行简单的代码就能指示电脑作出各种各样不同的操作,作为一个合格的魔术师,只有一步一步掌握魔法的底层运行原理,才能不断创新,创造出更新,效率更高的魔法。在计算机科学中,函数是一个非常重要概念,与此相关的栈,栈如此重要,以至于CPU从底层硬件上实现了它,下面我试着分析一下函数的调用和数据栈,水平有限,如有疏漏和错误欢迎指出。

系统的硬件组成

现代计算机遵守冯·诺依曼体系结构,一般来说由CPU(中央处理器),主存储器(内存),I/O总线和各种外设组成,如下:
IA32体系结构

一个程序能运行,涉及到CPU和内存两个主要设备,而CPU通过寄存器(Register)对内存进行操作(读写),Intel IA32架构下,主要寄存器如下:
IA32 主要寄存器
寄存器不仅可以存储数据,还可以通过它进行内存的操作,如:
movl $0x7c00,%eax ;在寄存器中存储16进制的7c00
movl $45,(%eax) ;把45存放到%eax存储中数据所指向的内存中去

内存中的栈

对于现代分时操作系统来说,程序运行时“独占内存”,对于x86架构来说,进程可访问的内存空间如下:
Linux进程的地址空间
上图中从上往下第四项为进程栈空间,栈以栈帧为单位,为每个进程创建独立的栈帧,寄存器ebp和esp存的值分别指向当前栈帧的栈底和栈顶。
栈帧从高地址到低地址存放:局部变量,调用的其它函数的参数,返回地址,下一个栈帧的栈底。

用汇编语言编写一个add函数:

.global add
    .type add,@function
add:
    push %ebp
    movl %esp,%ebp 
    movl $0,%eax
    addl 8(%ebp),%eax
    addl 12(%ebp),%eax
    pop %ebp
    ret

编译成libadd.so文件:
gcc add.s -fPIC -shared -o libadd.so

用C语言编写文件:

#include <stdio.h>
int main(){
    int a,b;
    printf("Please Input to numbers:");
    scanf("%d %d",&a,&b);
    int c = add(a,b);
    printf("a+b=%d\n",c);
    return 0;
}

编译并连接文件:
gcc main.c -o main -ladd -L.
运行main文件:
[root@vultr ~]# ./main
Please Input to numbers:25 100
a+b=125

add函数运行前后,数据栈的变化如下(大端):
栈的变化动态图

参考资料

  1. Randal E. Bryant 深入理解计算机系统
  2. 深入理解C语言的函数调用过程

发表评论