c语言尚未实现的虚拟函数,编译原理之学习 lua 1.1 笔记 (二) 函数调用与局部变量...

本文详细解析了Lua 1.1中函数调用的实现机制,包括使用栈来保存调用信息,如返回地址和基址指针,并通过CALLFUNC和RETCODE指令进行调用和返回操作。同时,介绍了如何通过PUSHLOCAL和STORELOCAL指令读写局部变量,以及在函数调用时如何处理参数数量不匹配的情况。此外,还探讨了局部变量声明和全局变量的区别。
摘要由CSDN通过智能技术生成

函数(过程)是程序中重要的抽象, 过程调用一般用栈实现. Lua 1.1 中尚未实现闭包(closure),

对于函数使用栈实现即已满足需求了.在理论上, 在栈中要保存为实现调用以及返回调用处的足够

信息, 这些信息当前是返回地址(return-address,栈基址指针(base-pointer).

在虚拟机指令层次, 指令 CALLFUNC, RETCODE 用于函数调用的核心实现. 另有一些与调用参数, 局部

变量相关的指令, 稍后遇到的时候研究.

对于一个函数, 如例子 function f(a,b) ... end, 对其的调用代码为:

PUSH f  -- 将函数对象 f 压入堆栈.

PUSH MARK  --- 将特殊标记值 MARK 压入堆栈, 作用下面描述.

PUSH a --- 计算得到 a 的代码, 最终为将 a 的值压入堆栈.

PUSH b --- 同 a 的情况, 此时栈中数据为: [f, MARK, a, b  )

CALLFUNC --- 实际产生调用.

指令 CALLFUNC 的执行如下:

case CALLFUNC:  --- 位于 opcode.c 的虚拟机执行函数 lua_execute() 中.

1. 从栈顶向上找 MARK 标记, 这一标记的前面是函数 f, 后面是第一个函数参数 a.

2. 从函数对象 f 中获得该函数的代码地址 new-pc;

把当前 pc (也即返回地址)保存在函数对象 f 中. (注1)

设置 pc = new-pc, 此即实现执行地址的改变.

3. 将栈 base 指针保存在 MARK 的值中, 设置新的 base = MARK+1, 即指向栈中第一个

参数 a 的位置.

此后再执行的下一条指令就是函数的入口指令了.

上述的步骤, 所做工作就是保存 , 设置新的运行地址和基址指针. 与机器码实现

函数调用几乎是一致的. 略有不同的是由于调用者提供的参数数量可能是0个或多个(可变数量的), 所以

通过查找 MARK 方式找到, 同时 MARK 还用作保存 base 指针, 这种方式较为巧妙.

注1: 将返回地址保存到函数对象中, 这是一个"不好的"方式, 这样函数就不能重入,递归了?

指令 RETCODE 的执行与 CALLFUNC 想对应:

case RETCODE:  --- 虚拟机函数 lua_execute() 中.

1. 由于当前 base 指针-1, -2 分别是保存了 base 的MARK, 和保存了返回地址的函数 f 对象,

故此从中恢复 pc, base 的值.

2. 复制/移动函数返回值 (函数的多返回值机制以后分析, 此处暂略)

此后下一条指令即返回到原调用处的下一条指令位置.

对函数产生调用的代码, 在如下产生式中生成(代码生成):

1. stat1 -> functioncall

2. expr -> functioncall

3. functioncall -> functionvalue {代码块1} '(' exprlist ')' {代码块2}

对上面的产生式3略作记录:

1. functionvalue 产生计算函数 f 的值的代码, 例如压入全局变量 f 到堆栈中. PUSHGLOBAL f

2. 代码块1: 产生代码 PUSHMARK, 更新 ntemp 值(其用于跟踪堆栈使用量)

3. exprlist 产生所有参数的计算和压栈代码, 如 PUSH a, PUSH b

4. 代码块2: 产生代码 CALLFUNC, 更新 ntemp 值.

这里生成了调用函数 f 的前述代码序列.

在前面提到过基址指针 base, 相当于 80x86 体系中的寄存器 BP, 用于寻址位于堆栈上的地址.

在 Lua 中, 函数的参数和局部变量存放在栈中, 并使用 base 指针寻址. 下面研究对局部变量访问的

指令, 及其代码生成.

在 Lua 中, 读取局部变量的指令为 PUSHLOCAL(及其同系列的 PUSHLOCAL0~~9),

写入局部变量的指令为 STORELOCAL(及其同系列的 STORELOCAL0~~9). 那些同系列的指令仅仅

是减少(优化)了指令大小, 语义是一致的, 因此只需要研究 PUSHLOCAL, STORELOCAL 即可.

case PUSHLOCAL:  --- 带一个字节的指令立即数 i, 局部变量压入栈中.

*top++ = stack[base + i];

case STORELOCAL: --- 带一个字节的指令立即数 i, 栈顶值弹出存入局部变量.

stack[base + i] = *(--top);

PUSHLOCAL, STORELOCAL 都带有一个字节的指令参数, 表示所访问的局部变量的索引 i, 从 0 开始,

寻址到堆栈 stack[base + i] 位置. 由于只有一个字节, 也即限制最多只能有 256 个局部变量.

由于调用函数时 base 被自动维护, 因此每函数都有自己的局部变量. 又 base 指针从第一个参数开始,

因此参数实现上也是被当做局部变量看待的. 这隐含的几个问题, 第一个是如果函数所需参数数量,

和实际调用者传递的参数数量不一致的问题.

举例说明, 设函数声明为 function f(a,b), 而调用者调用为 f(1,2,3), 或 f(4), 即函数参数多或少的情况.

在函数的入口代码中, 会产生一条 ADJUST n 指令, 其中 n 是函数声明时的参数数量, 该指令执行如下:

case ADJUST:

在语义上:

1. 如果调用者提供的参数不足 n 个, 则不足的部分以 NIL 值填充.

2. 如果调用者提供的参数多于 n 个, 则多出的被裁剪掉.

最终设置栈顶指针 top = base + n (即多出的参数被裁剪, 如果有的话)

这一指令在产生式 function -> FUNCTION NAME '(' parlist ')' block END 中生成.

隐含的第二个问题是, 由于多出的参数被裁剪掉了, 这样表示无法提供 f(args, ...) 后面可变参数语义的实现.

预计 lua 以后的版本会使用某种方法实现.

局部变量的声明, 例如 local x, y, 相关产生式为:

stat1 -> LOCAL localdeclist decinit

localdeclist -> NAME {代码块1} | localdeclist ',' NAME

其中代码块1:

localvar[nlocalvar]=lua_findsymbol($1);   --- 查找 $1(即NAME), 并加入到局部变量表中.

$$ = 1;  --- 已声明的局部变量数.

在函数中声明的局部变量被放置在 localvar[] 表中, 值是 lua_findsymbol() 的返回值, 即到符号表

symtab 的索引.

在产生式 var -> NAME 中, 第一篇研究全局变量的文章中也有遇见, 其中代码块为:

Word s = lua_findsymbol(NAME) --- 查找 NAME 在符号表中的索引.

int local = lua_localname(s)  ---  在 localvar[] 表中查找是否有 s, 如果有则表示这是个局部

--- 变量.

if (是局部变量) $$ = -(local+1) --- 是一个负数, 从而与全局变量的 正的索引 区分开.

在 lua_pushvar() 为访问 var 生成代码时, 前一篇文章研究全局变量时也碰到,

为全局变量生成代码为: PUSHGLOBAL idx-of-symtab

为局部变量生成代码为: PUSHLOCAL idx-of-localvar, 也即表示在 stack[base+idx]

在产生式 varlist1 -> var ... 中, 记录 var.$$ 到 varbuffer[] 表中, 在为其生成写入指令时,

根据是正数生成 全局变量的(STOREGLOBAL), 是负数生成局部变量访问指令 (STORELOCAL).

生成指令的时候, 如果 idx 在 0~9, 则产生较短的指令 PUSHLOCAL0~9, STORELOCAL0~9.

函数的返回值也是放在栈顶的, 是在 RETCODE 指令中设置好返回给调用者的, 由于 lua 支持多赋值,多返回值, 将它们单独放一个地方再研究也许更合适一些.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值