当前版本: 1.0
完成日期: 2009-04-15
作者: Jack Tan
Email: jiankemeng@gmail.com
版权声明: 转载请注明出处,商业用途请联系作者
先看一个简单的 C 函数调用,在 ISA 层面上怎么被支持:
int test_call(int a, int b, int c) { a = b + c; return a; } int main() { int d; d = test_call(1, 2, 3); return d; }
对应的汇编码为:
注:SPARC 汇编的操作习惯是从左到右,即:左边是源寄存器,右边是目标寄存器,这个和 MIPS 相反,与 x86 AT&T 语法相似。
0000000000000000 <test_call>: 0: 9d e3 bf 40 save %sp, -192, %sp ---> 将父窗口的 sp 减 192 后,置入当前窗口的 sp 4: 82 10 00 18 mov %i0, %g1 ---> 保存 a (param1) 入 g1 8: 84 10 00 19 mov %i1, %g2 ---> b (param2) 入 g2 c: 86 10 00 1a mov %i2, %g3 ---> c (param3) 入 g3 10: c2 27 a8 7f st %g1, [ %fp + 0x87f ] ---> 将 g1, g2, g3 保存于栈后又将其读出 14: c4 27 a8 87 st %g2, [ %fp + 0x887 ] 此代码生成时没有加 -O 优化 18: c6 27 a8 8f st %g3, [ %fp + 0x88f ] 1c: c4 07 a8 87 ld [ %fp + 0x887 ], %g2 20: c2 07 a8 8f ld [ %fp + 0x88f ], %g1 24: 82 00 80 01 add %g2, %g1, %g1 ---> g1 = g2 + g1 28: c2 27 a8 7f st %g1, [ %fp + 0x87f ] ---> stw %g1, [ %fp + 0x87f ],g1 保存于栈 2c: c2 07 a8 7f ld [ %fp + 0x87f ], %g1 ---> lduw [ %fp + 0x87f ], %g1,又将刚保存的读入 g1,很傻吧,实际这是没有优化的代码,gcc 固定生成,逻辑清晰 30: 83 38 60 00 sra %g1, 0, %g1 ---> 算术右移 0 位 34: b0 10 00 01 mov %g1, %i0 ---> 将返回值入 i0,return 后(CWP - 1) 就是前一个窗口的 o0 38: 81 cf e0 08 rett %i7 + 8 <==> return %i7 + 8 3c: 01 00 00 00 nop
save —> 从前一个窗口取值,CWP += 1 后,向当前窗口写值,其是承担转动窗口的大任的
return %i7 + 8 —> 将 CWP -= 1 后跳转到 %i7 + 8 处,其是承担恢复窗口的大任的
stw —> 向内存写入一个 word (32-bit)
lduw —> 从内存加载一个无符号 word
以上 st, ld, rett 皆为汇编助记符号,在上面的环境下分别对应于 stw, lduw, return
0000000000000040 <main>: 40: 9d e3 bf 30 save %sp, -208, %sp 44: 90 10 20 01 mov 1, %o0 ---> 依次置 3 个参数 a, b, c 入 o0, o1, o2 48: 92 10 20 02 mov 2, %o1 在 save 后 (CWP + 1) 其就变为 i0, i1, i2 (名字变了,但依然是同样的物理寄存器) 4c: 94 10 20 03 mov 3, %o2 50: 40 00 00 00 call 0 <test_call> ---> 调用 test_call() 54: 01 00 00 00 nop 58: 82 10 00 08 mov %o0, %g1 ---> 取返回值入 g1 5c: c2 27 a7 eb st %g1, [ %fp + 0x7eb ] 60: c2 07 a7 eb ld [ %fp + 0x7eb ], %g1 64: 83 38 60 00 sra %g1, 0, %g1 68: b0 10 00 01 mov %g1, %i0 ---> 将返回值入 i0,对应于调用者的 o0 6c: 81 cf e0 08 rett %i7 + 8 70: 01 00 00 00 nop
指令 call 在将其自身所在之地址写入 o7 后,就直接跳转目标函数地址处,要留意的是 call 并不会改变 CWP 的值,即不会转动窗口,似乎和 return 不对称
1. 参数传递
父函数写入 o0 ~ o5,依次对应前 5 个参数
子函数使用 i0 ~ i5 接受之
2. 返回值
子函数写入 i0
父函数用 o0 接受之
3. 指令支持
call
call label
两个操作:自身所在之地址写入 o7;跳转到 label 处
return
return %i7 + 8
return %i7 + %g1
两个操作:CWP -= 1;跳转
call 与 return 后皆有一个延迟槽 (delay slot)
save
save r1, r2, rd
save r1, simm13, rd
三个操作:从前一个窗口 sp (o6) 取值;CWP += 1;向当前窗口 sp (o6) 写值
以上指令已足以支持过程调用,为有更大的灵活性,SPARC 又引入了 Jump and Link (jmpl) 指令:
jmpl
jmpl r1+r2, rd
两个操作:将自身所在之地址写入 rd;跳转到 address 处。如:
jmpl %l2+%l3, %o7
jmpl %i7+8, %g0 <=> 汇编助记符为 ret —-> 用于非叶函数的返回
jmpl %o7+8, %g0 <=> 汇编助记符为 retl —-> 用于叶函数的返回
4. 宏观上看返回地址的精巧安排
call 将自身所在之地址写入 o7,则其延迟槽后的指令为子函数返回后的第一条指令,则该指令所在地址为 o7 + 8(o7 + 4 为延迟槽)
子函数来看,i7 (对应于父函数 o7)加 8 即为返回地址
此即为 ABI 规定 i7 为 (return address – 的原因
5. 宏观上来看 sp, fp 的转变
fp (i6), sp (o6)
父函数之 sp (o6) 在进入子函数 save 后,父函数之 sp 值不变,但却变成子函数之 fp,只要子函数不改变 fp 的值,则返回时 sp 的恢复都省了。
子函数之 sp 则直接来之父函数之 sp 减去一个常数(分配局部变量)
实在佩服 SPARC 的这个设计,太 [文明用语] COOL 了