7.5 宏、模块与程序组织
第 7.4 节把过程作为运行时复用机制:多个 CALL 位置共享一份指令正文,并通过调用约定合作。汇编工具还提供源码级复用与分别编译。这些机制解决不同问题,程序应当有意识地选择。
宏在指令编码前展开
宏(macro)是命名的源码模板。汇编器遇到宏调用时,把模板正文和实参替换到源码流中,再对展开结果汇编。
CLEAR_WORD MACRO destination
MOV destination,0
ENDM
CLEAR_WORD AX
CLEAR_WORD counter在概念上,汇编器看到的是:
MOV AX,0
MOV counter,0宏实参通常是源码记号,不是运行时栈值。若展开结果形成非法指令,汇编器可以拒绝它;但宏本身不会产生 CALL、返回地址或栈帧。
这形成一组权衡:
- 宏避免 CALL/RET 的运行时工作,还可以针对每组实参特化正文,但每次展开都会增加代码字节。
- 过程只保存一份正文,多次复用时通常更省代码;但每次调用都要执行 CALL,访问返回地址栈,并遵守寄存器与参数契约。
含内部标号的宏需要让每次展开获得唯一名称。在 MASM 风格语法中,LOCAL again 要求汇编器每次都用不同的生成符号替换 again:
REPEAT_STEP MACRO count
LOCAL again
MOV CX,count
again:
; 操作体
LOOP again
ENDM如果没有 LOCAL,多次展开会在同一份展开后源码中重复定义 again:。这是汇编期名称冲突,不是运行时控制流故障。
模块分离所有权并支持分别汇编
模块(module)是可以单独汇编成对象文件的源码单元。模块内部标号保持私有,除非明确导出;外部依赖通过声明表达,而不是预先猜测地址。
在 MASM 风格语法中,PUBLIC 向链接器公开一个定义,EXTRN 声明另一个模块会提供定义:
; main.asm
EXTRN sum_words:NEAR
EXTRN print_hex:NEAR
CALL sum_words
CALL print_hex; math.asm
PUBLIC sum_words
sum_words PROC NEAR
; 实现
RET
sum_words ENDPmain.asm 的汇编器记录未解析引用,而不会编造最终地址。链接器随后建立全局符号表(global symbol table),分配模块区域,并用绑定地址修补每个引用。
有效外部引用需要恰好一个匹配定义:
- 如果 main 调用
print_he,而 io 导出print_hex,两个名称不同,print_he无法解析。
- 如果 math 与 util 都导出名为
sum_words的强定义,链接器无法选择唯一地址,会报告重复符号。
- 多个模块可以把同一名称声明为 EXTRN,因为声明表示使用,不是相互竞争的定义。
按接口与所有权组织程序
可维护的汇编程序可以分成:
- 启动模块:初始化段寄存器,调用高层工作,并负责程序终止。
- 算法模块:公开较小的过程接口。
- 设备或运行环境模块:隔离输入/输出细节。
- 共享包含文件:保存常量、结构偏移和宏定义,但不能在多个位置产生相互冲突的存储定义。
每个过程接口都应记录参数位置、返回值、改变的标志、可能覆盖的寄存器、必须保持的寄存器与清理责任。每个公开数据对象和过程都应只有一个明确所有者。
分别编译会让这些契约更重要,而不是更不重要。链接器可以证明某个名称有唯一地址,却不能证明调用者按被调用者期望的格式传入参数。这种语义一致性仍然要由程序员与测试保证。
第 7 章已经从单条指令推进到完整程序构造:源程序与工具、可观察的控制流、有边界的数据遍历、安全的过程契约,以及分别汇编的模块。第 8 章将深入这些程序之下,追踪处理器的数据通路与控制单元怎样完成每一次取指、译码与执行。