====== 编程规范 ======
为了能够重复利用现有代码,在代码与代码之间调用,所有程序必须遵守本编程规范。
约定是必须遵守的规定,是程序之间沟通的桥梁,不遵守会导致程序不能与现有的程序库交互。
规范是强烈建议遵守的规定,是为了方便开发者理解代码,使代码易于维护,方便不同开发者使用相同的方法理解不同人编写的代码。
===== 用户程序约定 =====
* 用户程序代码必须在所有库函数代码之后。
* 用户程序必须包含一个 ''_start'' 函数,在其中编写用户代码。
===== 汇编语言风格规范 =====
==== 指令 ====
每个指令与其操作数使用一个空格隔开。操作数之间使用一个逗号 + 空格('', '')隔开。
MOV ax, bx
==== 标签 ====
所有标签使用下划线分词。除 [[#Calling convention|Calling convention]] 所规定的特殊前后缀外,一律使用小写字母。
nimshab_miga:
==== 注释 ====
如果注释为自然语言,使用一个分号 + 空格 + 正文,确保首字母大写,且在单词间换行时使用连字符。
如果是汇编对应的 C 代码,使用两个分号 + 正文,确保首字母小写。
; Test running n
; -uclear reactor
; for a while
;;out_h_for_whi
;;le(0, 5);
MOV c, 0
MOV d, 5
CALL out_h_for_while
===== 量、数值约定 =====
* 0 1 2 3 分别代表前、后、左、右四个 I/O 口。
===== 函数规范 =====
==== 结构 ====
若函数所遵循的调用约定规定要在函数中通过压栈的方式暂存寄存器状态,则分三块结构编写:
- 函数标记
- 初始化寄存器
- 主要逻辑
- 复原寄存器
遵照如下所示的结构:
; 【FUNC】<参数及返回值说明>====
<函数名>:
; Init regs
PUSH xx
PUSH xx
; Main logics
............
; Exit
POP xx
POP xx
RET
如果函数内有分支(如 b1 b2 两个分支),那么遵照如下所示的结构:
; 【FUNC】<参数及返回值说明>====
<函数名>:
; Init regs
PUSH xx
PUSH xx
; Main logic
............
<函数名缩写>__b1:
............
JMP <函数名缩写>__exit
<函数名缩写>__b2:
............
JMP <函数名缩写>__exit
<函数名缩写>__exit:
POP xx
POP xx
RET
例如:
**汇编**
; 【FUNC】d,c====
; d: b fast_boot
start_machine:
; Init regs.
PUSH c
; Main logic.
;;if(fast_boot)
CMP d, 0
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:
;;}
; 通知启动成功 逻辑
MOV c, 0x10
MOV d, 0
CALL out_h_for_while
; Exit.
POP c
RET
**C 语言**
#include
void start_machine(bool fast_boot) {
if (fast_boot) {
// 快速启动 逻辑
} else {
// 慢速启动 逻辑
}
// 通知启动成功 逻辑
out_h_for_while(0, 0x10);
}
==== 如何写参数及返回值说明 ====
函数标记中包含「参数及返回值说明」。它的规则如下:
如果函数没有返回用寄存器列表,则使用下面的语法:
[传参用寄存器列表][,内部使用的寄存器列表]
如果函数有返回用寄存器列表,则使用下面的语法:
[传参用寄存器列表],[内部使用的寄存器列表],[返回用寄存器列表]
如果函数既没有传参用寄存器,也没有内部使用的寄存器和返回用寄存器,则使用下面的语法:
==== 文档 ====
每个非内部函数(与其他程序共享的函数而非自己使用的函数)都需要具备文档。文档需要按顺序阐明:
- 调用约定,若为 StdCall 则不写。
- 作用
- 参数(按照传参方式、值类型、作用 / 说明的顺序写)
- 内部使用到的寄存器(按照寄存器名、用途的顺序写)
- 返回(按照返回方式、值类型、作用 / 说明的顺序写)
===== Calling convention =====
调用约定有以下几种:
StdCall, StdCallR, QCall, CDecl
''_start'' 函数是例外的,它不遵守任何调用约定。''_start'' 函数不需要做任何额外的事情(如 PUSH 内部用到的寄存器)。
==== 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 编程,比给现实世界中的计算机编程要困难很多。所以才出现了很多「怪异」的事情。如:
* 调用约定只对传参方式做笼统的约定,而具体事情如函数的传参顺序、用哪几个寄存器则完全由函数开发者规定。
* 还原调用方代码使用的寄存器状态,有好几种方式且随着每个调用约定都不一样(而现实世界的计算机无论何种调用约定,还原调用方代码使用的寄存器状态的方式是固定的:移动栈顶指针)。
等。