如何在 NASM 中对汇编标号进行任意计算?
可以计算,但要先消除表达式中的重定位依赖
在 NASM 中,通常先计算同一 section 内两个标号的差值,得到标量后再进行移位、掩码、除法或取负运算。如果需要计算完整地址,还要加上已知的数值基址。
通用形式是:
scalar_offset equ target - section_start
scalar_address equ KNOWN_BASE + scalar_offset
section_start 必须与 target 位于同一 section。KNOWN_BASE 必须是数值常量,准确表示 section_start 在目标地址体系中的地址或偏移。
如果最终地址只能由链接器或加载器决定,就不能靠这一写法提前得到任意地址运算的结果。
为什么直接对标号移位或取掩码会失败?
标号通常带有所属 section 的信息,不能直接当作完全确定的整数。例如:
; 对需要重定位的 handler,以下表达式可能无法汇编
dw handler & 0xffff
dw handler >> 16
这些表达式需要对最终地址执行运算,但目标文件格式未必能记录对应的重定位操作。
即使用 nasm -f bin 和固定的 ORG,也不能据此认定所有标号都已成为标量。NASM 的 bin 后端承担了内置链接器的作用,标号仍会经过重定位处理。参见 NASM 的 bin 与 ORG 文档。
同一 section 内的标号相减时,共同的 section 基址可以抵消,差值因此能作为普通整数使用。
用已知基址还原地址,再拆分字段
下面只演示 32 位 offset 的拆分。假设使用单一 .text section,代码段基址为 0,镜像按 ORG 指定的位置放置:
bits 32
IMAGE_BASE equ 0x10000
org IMAGE_BASE
section .text
image_start:
handler:
nop
HANDLER_OFFSET equ IMAGE_BASE + (handler - image_start)
offset_fields:
dw HANDLER_OFFSET & 0xffff
dw (HANDLER_OFFSET >> 16) & 0xffff
保存为 example.asm 后,可使用:
nasm -f bin example.asm -o example.bin
handler - image_start 是 section 内的差值。加上数值常量 IMAGE_BASE 后,整个表达式仍是标量,可以执行 & 和 >> 运算。
输出的两个 word 分别表示 offset 的低 16 位和高 16 位,并不是完整的 IDT 条目。实际填写 IDT 时,应使用处理程序相对于目标代码段基址的 offset;代码段基址不为 0 时,不能直接把线性地址当作该 offset。
使用 $$ 时要确认当前 section
$$ 表示当前 section 的起始位置。在对应 section 中,也可以写成:
HANDLER_OFFSET equ IMAGE_BASE + (handler - $$)
handler - $$ 本身只是 section 内的偏移,不包含 ORG 指定的起点。切换 section 后,$$ 的含义也会改变;跨位置引用时,显式的起始标号通常更清楚。参见 NASM 表达式文档。
计算常驻部分占用的 paragraph 数量
计算两个边界之间的数据长度时,不必知道加载地址:
bits 16
org 0x100
section .text
resident_start:
; Resident code and data go here.
nop
resident_end:
RESIDENT_BYTES equ resident_end - resident_start
RESIDENT_PARAGRAPHS equ (RESIDENT_BYTES + 15) >> 4
一个 paragraph 为 16 字节。对于非负长度,(长度 + 15) >> 4 会向上取整,避免少保留最后不足 16 字节的部分。
这段代码计算的是 [resident_start, resident_end) 的大小。如果 DOS 常驻接口要求从 PSP 起算保留范围,还要计入 PSP 到常驻区起点之间的空间。对于以 ORG 0x100 开始、常驻区从镜像开头起算的 .COM 程序,可使用:
KEEP_PARAGRAPHS equ (0x100 + RESIDENT_BYTES + 15) >> 4
实际常驻布局还必须覆盖运行时需要的栈、数据和缓冲区,不能只按代码长度计算。
在寻址表达式中减去标号对应的偏移
如果直接写 -field 会要求目标格式不支持的负重定位,可以在基址已知时先得到标量偏移:
bits 16
IMAGE_BASE equ 0x100
org IMAGE_BASE
section .text
image_start:
field:
db 0
FIELD_OFFSET equ IMAGE_BASE + (field - image_start)
mov al, [bx - FIELD_OFFSET]
这里减去的是数值常量 FIELD_OFFSET,因此不需要对 field 生成负重定位。实际使用时,仍须确保 DS、BX 和该偏移采用一致的地址含义。
哪些情况需要换一种处理方式?
以下情况不能直接套用“标号相减后计算”的方法:
- 标号位于不同 section。两个 section 的相对布局可能到链接时才确定,基址不能直接抵消。
- 标号来自外部目标文件。汇编当前文件时,通常无法确定它的位置。
- 最终地址会在加载时变化。固定数值基址无法代表每次运行时的地址。
可以根据构建流程选择处理方式:使用支持相关表达式的 linker script、在链接后生成或修补数据,或者在运行时计算并填写字段。链接后处理必须基于最终布局;如果处理结果又改变了布局,就需要重新核对地址。
单纯写 value equ handler 也不会消除重定位属性。需要让表达式真正变成标量,并明确它表示的是 section 内偏移、段内偏移还是线性地址。
备注:内容仅供参考。
