报头中的偏移量作用_C语言中函数的实现

f35a3727698359c3683eccf721b608c0.png

符号表

C编译器使用符号表来记录程序中遇到的变量,类似汇编器中的label表,不过C编译器中的符号表包括更多信息:名字、类型、已分配的内存地址、变量申明域或作用域等

e.g 程序中有6个变量声明,编译器会生成6个表项的符号表,采用偏移量的方式记录变量在内存中的位置

fa02afc05018afa5e74fbd490cdf1380.png
编译器的符号表

变量的空间分配

存放变量内容的内存有两种区段:全局数据段(global data section)和运行时栈(run-time stack)。全局数据段是内存中存放全局变量的区段,即所有静态变量所在的地方;运行时栈则是局部变量所在的地方。

在LC-3体系计算机中,R4是一个专用寄存器,存放的是全局数据段的基地址,所以R4可以被看做成一个全局指针。

e.g 全局变量earth偏移量为4,将earth的内容读入寄存器R3,LC-3指令如下:
LDR R3, R4, #4

局部变量就会存放在一个被称作”活动记录“(activation record)或者”堆栈帧“(stack frame)的内存模板(memory template)中。活动记录就是一段连续的内存,包含了当前函数中所有的局部变量。每个函数都有自己的活动记录,当我们调用一个函数的时候,该函数活动记录的最大地址存放在寄存器R5中,R5被称为帧指针(frame pointer)。注意,在活动记录中,变量的排放顺序与他们在程序中的声明顺序是相反的。

e.g 变量amount在程序中是最先被声明的变量,它也是距离帧指针R5最近的变量,访问变量seconds,LC-3指令如下:
LDR R0, R5, #-5

0a2ba9e2c747fc82b2bc9f3e7895d158.png
LC-3内存中的一个活动记录

当我们调用一个函数的时候,该函数的活动记录被压入当前栈,同时R5内容被调整,指向当前栈顶,当该函数结束的时候,控制权被交还给调用者,活动记录将被弹出当前栈,同时R5的内容也将被修改,指向调用者活动记录所在位置。整个过程中,寄存器R6始终指向运行时栈的顶部,R6为栈指针。

f2e260c24b61fdfaf9191bdd84f2ea8f.png

C语言中,函数调用包括三个步骤:

  1. 调用:调用者将参数传递给被调用者,并交出控制权;
  2. 执行:被调用者执行任务;
  3. 被调用者返回函数结果,并将控制权交还给调用者;

运行时栈

在函数被调用时,我们需要激活被调用函数,也就是说在函数被执行前,必须要在内存中,为该函数的局部变量分配空间。

每个函数的活动记录,在其每次被调用的时候,都会被系统在内存中分配一个活动记录空间。函数返回的时候,则将该活动记录返还,以供其他函数调用使用,这样是允许函数的递归操作的。其中采用数据结构”栈“来跟踪函数调用模式。

e.g

int main()
{
int a;
int b;

:
b = Watt(a);
b = Volta(a,b);
}

int Watt(int a)
{
int w;

:
w = Volta(w, 10);
return w;
}

int Volta(int q; int r)
{
int k;
int m;

:
return k;
}

a246d489897fbd95d7ad7e4c676897fd.png

上图是栈在代码运行的不同时刻的快照,每个阴影区代表一个特定函数的活动记录,将数据项压入栈时,栈顶总是向着低的内存地址方向移动。其中R5总是指向当前活动记录的某个地址(局部变量的起始地址),R6总是指向运行时栈顶。

实现机制

  1. 调用者将参数拷贝到被调用者所能访问的内存区域
  2. 被调用函数的开始代码,将活动记录压入栈,并在栈中保存一些备忘信息(保证控制权返回调用者后,调用者的原局部变量和寄存器内容在调用前后没有变化
  3. 被调用者完成自己的工作
  4. 被调用函数完成之后,将活动记录出栈,并将控制权返回调用者
  5. 调用者重获控制权之后,读取被调用者的返回值

e.g

w = Volta(w, 10);

1、调用

通过将两个参数值压入运行时栈,向Volta传递参数,R6指向运行时栈顶部,每当向栈中压入一个数据项,首先递减R6值,然后将数据存入R6指向的地址。在LC-3结构中,C函数的参数,按照它们在函数调用的顺序,从右到左依次被压入栈。

通过JSR指令,把控制权交给Volta

AND R0, R0, #0
ADD R0, R0, #10
ADD R6, R6, #-1
STR R0, R6, #0
LDR R0, R5, #0
ADD R6, R6, #-1
STR R0, R6, #0
JSR Volta

e6af4b277270db7fe940c71d5d9af6e5.png

2、被调用函数的开始

被调用函数的初始代码,要完成与调用信息相关的备份操作:

为返回值预留一个内存位置,通过栈指针递减,将一个内存空间“压”入栈,在被调用函数返回调用者之前,将返回值填入此内存。

调用者相关的信息的保存,即R7中调用者的返回地址和R5中调用者的帧指针(动态链)。

被调用者调整R6的值,在栈空间中为它的局部变量分配足够的空间,同时设置R5指向这些局部变量的基地址。

Volta:
ADD R6, R6, #-1
ADD R6, R6, #-1
STR R7, R6, #0
ADD R6, R6, #-1
STR R5, R6, #0
ADD R5, R6, #-1
ADD R6, R6, #-2

3、被调用函数的结束

如果存在返回值,则填写活动记录的返回值字段。

将局部变量弹出栈

恢复原动态链的内容

恢复返回地址

通过RET指令,返回调用者

fea23d1f5d3826ee15b61fb19f18441b.png
LDR R0, R5, #0
STR R0, R5, #3
ADD R6, R5, #1
LDR R5, R6, #0
ADD R6, R6, #1
LDR R7, R6, #0
ADD R6, R6, #1
RET

4、返回调用函数

将返回值(如果有)出栈

参数出栈

JSR Volta
LDR R0, R6, #0
STR R0, R5, #0 ; w = Volta(w, 10);
ADD R6, R6, #1
ADD R6, R6, #2
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值