這是本文件的舊版!
编程规范
为了能够重复利用现有代码,在代码与代码之间调用,所有程序必须遵守本编程规范。
函数
结构
若函数所遵循的调用约定规定要在函数中通过压栈的方式暂存寄存器状态,则分三块结构编写:
- 初始化寄存器
- 主要逻辑
- 复原寄存器
遵照如下所示的结构:
<函数名>: ; Init regs PUSH xx PUSH xx ; Main logics ............ ; Exit POP xx POP xx RET
如果函数内有分支(如 b1 b2 两个分支),那么遵照如下所示的结构:
<函数名>: ; Init regs PUSH xx PUSH xx ; Main logic ............ <函数名缩写>__b1: ............ JMP <函数名缩写>__exit <函数名缩写>__b2: ............ JMP <函数名缩写>__exit <函数名缩写>__exit: POP xx POP xx RET
例如:
汇编
start_machine: ; if(fast_boot) CMP pf, 0xff JZ sm__if_fbot JMP sm__if_fbot_el ; { sm__if_fbot: ; 快速启动 逻辑 JMP sm__ps_if_fbot ; } else { sm__if_fbot_el: ; 慢速启动 逻辑 sm__ps_if_fbot: ; } ; 检查启动是否成功 逻辑
C 语言
#include <stdbool.h> void start_machine() { bool fast_boot = pf; if (fast_boot) { // 快速启动 逻辑 } else { // 慢速启动 逻辑 } // 检查启动是否成功 逻辑 }
注释
每个非内部函数(与其他程序共享的函数而非自己使用的函数)都需要具备注释。注释需要按顺序阐明:
- 调用约定,若为 StdCall 则不写。
- 作用
- 参数(按照传参方式、值类型、作用 / 说明的顺序写)
- 内部使用到的寄存器(按照寄存器名、用途的顺序写)
- 返回(按照返回方式、值类型、作用 / 说明的顺序写)
Calling convention
调用约定有以下几种:
StdCall, StdCallR, QCall, CDecl
StdCall
“Standard call”.
使用寄存器传参,没有返回值。
采用 StdCall 的函数必须使用 d c b a 的顺序用寄存器保存参数。
采用 StdCall 的函数不加任何后缀。
被调用的函数在所有逻辑开始前必须将自身使用的寄存器通过压栈的方式暂存状态,退出前必须复原自身使用的寄存器的状态。
StdCallR
“Standard call with return(s)”.
使用寄存器传参,使用寄存器返回值。
采用 StdCallR 的函数最好使用 d c b a 的顺序用寄存器先后保存返回值与参数。
采用 StdCallR 的函数必须加后缀 _Rx
。其中 x 为返回值使用的寄存器的列表。如欲编写函数 “get_train_speed” 使用寄存器 d 返回列车的速度,则应当命名为 get_train_speed_Rd
。
被调用的函数在所有逻辑开始前必须将自身使用的寄存器中,除了返回值占用的寄存器通过压栈的方式暂存状态;退出前必须复原自身使用的寄存器,除了返回值占用的寄存器的状态。
若调用方使用了被调用函数返回值占用的寄存器,则在 CALL 函数前必须将函数返回值占用的寄存器通过压栈的方式暂存状态,CALL 后择机复原寄存器状态。
QCall
“Quirks call”,「怪诞调用」。本调用约定是为了应对数量较多的返回值。
使用寄存器传参,使用栈返回值。
采用 QCall 的函数最好使用 a b c d 的顺序用寄存器保存自身状态,使用 d c b a 的顺序用寄存器保存参数。
采用 StdCallR 的函数必须加后缀 _Q
。
无论调用方使用是否使用了被调用函数内部使用的寄存器,调用方在 CALL 函数前都必须将被调用函数内部使用的寄存器通过压栈的方式暂存状态,CALL 结束后自行择机复原寄存器状态。
CDecl
“C Declaration”,C 语言的调用约定。本调用约定是为了应对数量较多的参数。
使用栈传参,使用寄存器返回值。
采用 CDecl 的函数必须加前缀 _
。
调用方调用一个 CDecl 函数的流程是:
- 将被调用函数内部使用的寄存器通过压栈的方式暂存状态。
- 将参数压入栈。
- CALL 函数。
- 将所有参数出栈。
- 自行选择时机复原之前暂存的寄存器状态。
被调用的函数在退出前不复原自身使用的寄存器的状态,不将参数出栈。
为啥这么奇怪
首先要说明的是,微处理器 mod 的硬件极其有限,且与现实世界中的计算机非常不同。这些不同体现在:
- 栈顶指针不可改变。对栈的操作只有 PUSH 和 POP。
- 不能对指针解引用。如 「将『栈顶 + 4』位置的值移动到某寄存器」,在微处理器 mod 中是不可能的。
是故,给微处理器 mod 编程,比给现实世界中的计算机编程要困难很多。所以才出现了很多「怪异」的事情。如:
- 调用约定只对传参方式做笼统的约定,而具体事情如函数的传参顺序、用哪几个寄存器则完全由函数开发者规定。
- 还原调用方代码使用的寄存器状态,有好几种方式且随着每个调用约定都不一样(而现实世界的计算机无论何种调用约定,还原调用方代码使用的寄存器状态的方式是固定的:移动栈顶指针)。
等。