函数的帧栈调用过程 | 从零实现一门语言

    本节梳理计算机系统函数调用过程和汇编的一些基本内容,在实现自定义语言的中间代码后,就需要将其翻译成计算机底层能识别的汇编代码了。所以中间代码设计的前提是,了解汇编的相关指令和调用过程。不同cpu架构实现不同,汇编指令也有差别,但基本类似,本节讨论x86-32的实现。

一  汇编指令

    汇编中需要了解存储器和汇编指令的概念,这部分较为基础,理解就行,本节采用intel汇编格式。

    eax:通用寄存器,保留临时数据,常用于返回值

    ebx:通用寄存器,保留临时数据

    ebp:栈底寄存器

    esp:栈顶寄存器

    eip:指令寄存器,保存当前指令的下一条指令的地

    mov:数据转移指令

    push:数据入栈,同时esp栈顶寄存器也要发生改变

    pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变

    sub:减法命令

    add:加法命令

    call:函数调用,1. 压入返回地址 2. 转入目标函数

    jump:通过修改eip,转入目标函数,进行调用

    ret:恢复返回地址,压入eip,类似pop eip命

二  linux的内存布局

    代码在编译后加载到内存,以进程的方式运行在操作系统之中,那操作系统进程的内存布局时什么样的呢?如下图,为32位寻址,空间占4G内存,其中操作系统内核占1G,称为内核空间,剩余分配给应用程序,称为用户空间。

    注意:

    1,内存空间高地址的1G分配给内核空间;

    2,栈空间由高地址走向低地址,栈是一种特殊的容器,通过push放入数据,pop弹出数据;

    3,堆由低地址走向高地址;

    4,bss放未初始化全局变量和静态变量,data放已初始化的全局变量和静态变量,text放程序代码;

三  帧栈调用过程

1  自定义的函数

    main为调用函数,add为被调用函数。

int add(int a, int b) {    int c = a + b;    return c;}int main() {    int x = 10, y = 20;    int z = add(x, y);        return 0;}

2  调用过程

2.1  函数的栈帧

    函数的调用过程是用栈实现的,每一个函数都对应着一个栈帧。栈帧是指完成一个函数调用过程所使用的一段内存区间,具体可定义寄存器ebp到esp的这段地址区间。

    ebp指向栈底,也就是高地址,esp指向栈顶,也就是低地址,如下图所示:

    函数调用时先确定栈顶地址ebp,然后确定所需要的地址空间,不断地向低位申请,也就是esp不断指向更低位的地址,以存放函数的局部变量。根据当前函数所有局部变量的大小确定具体申请多少的内存,即需要移动esp的大小。

    函数调用完毕后需要返回到原来调用的地址,并释放掉当前函数存放局部变量的内存空间。此时ebp应该指向调用函数的栈顶,esp应该指向调用函数的栈底。

    其实,函数的调用过程就是ebp、esp两个指针的移动过程。以下模拟main函数调用add函数的过程。

2.1 确认main函数的栈顶地址

    main函数并不是当前栈的唯一函数,它也是被其他函数调用的,这个调用main的函数我们暂称为invoke_main。

    所以,main函数与其他所有函数一样,用这几汇编指令开辟栈帧。

push ebp       mov  ebp,esp

    第1行,将此时ebp寄存器的值压入栈保存,ebp中的值为invoke_main函数的栈顶地址。

    第2行,将当前esp(栈顶)中的地址指向ebp,即设置当前函数的ebp地址。

2.2  main函数的局部变量的内存空间申请

    确定main函数的栈顶地址后,接下来需要为main函数内的局部变量申请内存。

sub esp,24mov  [ebp-4],10mov  [ebp-8],20push [ebp-8]    push [ebp-4]

    第1行,为main函数的局部变量x,y申请内存,int 占4字节,2个占8字节,一般以16字节对齐;

    第2行,将10保存至[ebp-4]字节的地址处;

    第3行,同理保存20;

    第4行,将20入栈;

    第5行,将10入栈;

2.3  调用ad函数

    main函数的局部变量执行完毕,就到调用add函数了,使用call指令。

使用call指令跳转到add函数的地址,同时call指令会将main函数的下一条指令保存到eip,并将eip的值压入栈中保存。

call <add>

    进入add函数同样需要开辟新的栈帧,同main函数

push ebpmov ebp,espsub esp,16mov edx,[ebp+8]mov eax,[ebp+12]add eax,edxmov [ebp-4],eaxmov eax,[ebp-4]leaveret

    第1行,压入main函数的ebp;

    第2行,将esp的地址指向新的ebp;

    第3行,为局部变量c分配内存空间;

    第4行,将ebp的实参保存到edx中,我们在main函数中将实参“10”入栈,此时实际参数相对于新的ebp相差8字节;

    第5行,同理,将实参”20“保存到eax中;

    第6行,将eax和edx的数值相加,并保存在eax中;

    第7行,将eax的值保存至局部变量c;

    第8行,将局部变量赋值到eax;

    第9行,leave返回过程,包含两条指令,mov esp,ebp和pop ebp;

第一条指令恢复main函数的esp值,第二条弹出ebp的值保存至ebp并使esp+4.

    第10行,弹出栈顶原来保存的eip地址,即到main函数call add的下一条指令。

    执行完add函数我们知道返回到main函数调用的下一行

mov [ebp-12],eaxmov eax, [ebp-12]leaveret

    第1行,将保存在eax里的add函数的返回值保存至[ebp-12]的地址处;

    第2行,将[ebp-12]处的值保存至eax;

   第3,4行,同add函数,恢复invoke_main的ebp和esp,并返回至invoke_main的下一条指令。

完整的汇编代码如下:

.add:        push ebp        mov ebp,esp        sub esp,16        mov edx,[ebp+8]        mov eax,[ebp+12]        add eax,edx        mov [ebp-4],eax        mov eax,[ebp-4]        leave        ret .main:        push ebp               mov  ebp,esp        sub  esp,24        mov  [ebp-4],10        mov  [ebp-8],20        push [ebp-8]            push [ebp-4]         call  <add>        mov  [ebp-12],eax        mov  eax, [ebp-12]        leave        ret

    本节分析了进程的内存基本布局,以及计算机系统进利用栈帧实现函数调用的过程,同时将自定义的语言生成了相应的汇编代码。参考汇编代码的格式和调用过程,下一节我们将着手生成中间代码,并完成语义分析的其他部分,比如函数作用域,esp指针的地址等。

欢迎关注公众号:零点码起。


    目录

     1.一个hello world的诞生

     2.词法解析器

     3.从自然语言认识文法

     4.构造文法

     5.文法及语义代码实现

     6.代码函数的帧栈调用过程

     7.生成中间代码

     8.汇编

     9.编译和链接

     10.终于跑起来了

     11.多文件编译

     12.丰富数据类型

     13.流程控制语句

     14.编译优化算法

     15.文件读取

     16.一个线程的实现

     17.什么是锁

     18.网络编程

     19.面向对象

     20.其他规划

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值