PAYATHON 2026

如何在 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 内偏移、段内偏移还是线性地址。

备注:内容仅供参考。