运行时支持
1. 基本的数据类型在运行时是如何表示的
编程语言有多个类型,这些类型在运行时表示和运算,由具体的机器指令来实现。
int:signed 32-bit。load/store
char:一个字节、两个字节。
浮点:单精度、双精度、扩展精度。
枚举值:
数组:
记录:unpacked(各个field对齐)、packed(field连续存放,未对齐)。
指针:
string:x86提供string操作指令。
集合:
2. 寄存器的使用
对于CISC的x86 CPU,RISC风格的指令(operand和result都在reg中),速度比CISC风格的指令更快。
RISC的CPU,所有的计算都必须在寄存器上进行。
3. Local Stack Frame
函数需要有一个内存,用作某些变量的home。这些变量要么被取地址,要么需要index索引。
函数的实参,如果在reg中放不下,也就放在这块内存中。
sp总是有的。是否使用fp,原因仍然不理解(据说为了支持alloca(),要使用fp)。
dynamic link:如果有fp,则dynamic link指向上一个fp;否则,指向上一个sp。
4. Runtime Stack
运行时栈,有dynamic link(指向上一个活动栈),还"可能"有static link(指向词法上的外围,pascal这样的语言需要static link,而C语言不需要)。
5. 参数传递规则
call by value:将实参的值传给形参。所谓的in参数。
call by result:将形参的值传给实参。所谓的out参数。
call by value-result:所谓的in-out参数。
call by reference:实参与形参别名。Fortran提供这种传参方式。如果参数值可以放入reg中,就call by value-result,如果不能(例如数组),就call by reference。
call by name:仅具历史意义。
6. 函数的prologue、epilogue、call、和return
函数调用的步骤:
1. 传参和控制转移:
将参数放入寄存器、或者stack mem。
caller-saved register如果已经在使用,则写入mem。
(如果需要,计算static link)。
保存返回地址到reg,PC跳转到callee代码。一般使用call指令完成该动作。
2. prologue:进入callee之后首先执行的一段代码。
将old fp写入当前stack mem。fp = old sp。sp = old sp - stacksize。
callee-saved regiser如果要使用,则写入mem。
(如果需要,计算display,即static link的映射。)
3. 函数执行body。
4. epilogue:
从mem中恢复callee-saved register。
返回值放入合适位置(reg或mem)。
恢复old fp和old sp。
将PC改为返回地址。此动作由ret指令完成。
5. caller将caller-saved register从mem中恢复。使用返回值。
6.1 用寄存器传参
6.2 用stack来传参(x86)
代码共享和PIC
动态链接需要确定:一个external symbol,是否有定义,是否存在多个定义。
每个共享库有一个table:
1. entry point+external symbol。本库定义的符号和入口。
2. 本库使用的符号和入口。
3. 本库使用的符号所在的其他共享库。
在链接一个共享库的时候,就可以检查table,从而报告那些符号找不到。
对单个程序而言,动态链接对性能的影响:
1. 运行时进行动态链接有一定的开销。
2. 共享库的代码是PIC的。
共享库访问自己定义的符号时,采取PC-relative的方式。
1. global offset table,GOT。保存所有符号相对于本共享库的偏移地址。
2. 动态链接时,GOT中每个符号的地址被修改为绝对地址。同时获得GOT_off。
3. 通过下列指令,将GOT的绝对地址放入gp寄存器。
gp <- GOT_off - 4 ; GOT_off是GOT相对于当前指令的offset ——动态链接时获得
call next, r31 ; 跳转到next执行,r31保存了返回地址(就是next)
next: gp <- gp + r31 ; gp保存了GOT的绝对地址
4. 符号表保存了符号和符号在GOT中的偏移量a_off
r2 <- [gp + a_off] ; 获得a的绝对地址
r3 <- [r2] ; 获得a的值
问题:a_off如果超出了load [reg + offset]指令中offset的上界,则需要如下指令取得a的值。
r3 <- high_part(a_off)
r3 <- gp + r3
r2 <- [r2 + low_part(a_offt)]
r3 <- [r2]
每个共享库都有自己的GOT。
共享库1访问共享库2中的函数func
被共享库1调用的函数func,有一个stub,它是一段代码,但保存在共享库1的数据段中(而不是只读的代码段),因此是可以被修改的。在func首次被调用时,它被加载并链接;以后就可以直接调用了。
stub的工作方式有多种实现方式。例如,stub含有:函数名,对动态链接器的调用指令,当连接器被调用时,将stub替换为调用func的实际代码。下面介绍一种procedure linkage table, PLT的结构。
.PLT0 保存dynamic linker的调用代码
.PLT1 保存共享库2的id
.PLT2 分别保存共享库2中各个函数的stub
.PLT3
...
符号和多态语言支持
论文【Lee91】有详细论述。