如何对汇编语言中的标签进行任意运算?
有几个例子是我们想基于汇编语言标签来计算某些东西,但目标格式和链接器并没有提供进行这类特定计算所需的操作。
例如:
- 为386编写中断描述表(IDT),其中偏移量被拆分成若干字段,需要进行移位和掩码处理
- 计算应用程序驻留部分占用的段数,需要移位或除法
- 在寻址模式中减去标签的偏移量,需要负重定位
正如标签所述,我对使用Netwide Assembler (NASM) 来实现此类操作很感兴趣。我们如何对标签进行任意计算?
Earlier Q&As on this topic:
- Solution needed for building a static IDT and GDT at assemble/compile/link time (GNU
ldlinker-script self-answered by @Michael Petch). - How to do computations with addresses at compile/linking time? (IDT/GDT from C, explaining why it doesn't work, and a hacky workaround with a hardcoded fixed address.)
- Invalid operands for binary AND (&) (GNU assembler / ELF object files, explaining why it doesn't work.)
解决方案
有四种解决方法:
- 使用涉及标签差值的标签算术
- 在构建时对目标文件或二进制文件进行后处理
- 在运行时对可执行文件进行后处理
- 在运行时进行计算
标签差值的标签算术
背景
NASM在非绝对节中的标签是可重定位的,这意味着汇编器为每个标签存储一个名称、一个偏移量和一个所属的节。链接器使用该节来计算标签在使用处的最终重定位偏移量。通常在目标格式中只能表达加法,因此不允许进行任意计算。一个可重定位标签与标量相对立,标量是一个不受重定位影响的简单数字。
(默认的NASM输出格式 -f bin 似乎不涉及链接器,因此它是否支持对可重定位标签进行多于加法的运算?答案是否定的。这是因为 -f bin 格式实际上是作为汇编器内部的链接器实现的,其输入以内部对象格式提供。这也是为什么使用 -f bin 格式的列表文件并不总是列出所有指令的最终二进制内容,而是对重定位用方括号 [...] 或圆括号 (...) 标注,与其他输出格式相同。)
解决办法
NASM允许计算同一节中两个标签之间的差值,亦即delta。这对于汇编器来说本质上是一个标量值,在汇编时就会被求值为一个简单数字。这意味着汇编器可以对它进行任意算术运算,包括移位、除法和减去delta。
这本身并不能解决我们所有的问题,但涉及若干delta的标签算术会让这个工具更加强大。举例来说,考虑下面这段代码,可以用来构建一个86-DOS平面格式的.COM可执行程序:
cpu 8086
org 256
section code
code_start:
jmp init
...
align 16
code_end:
section data follows=code
data_start:
...
data_end:
我们如何计算同时容纳代码段和数据段所需的程序大小,单位为段(16字节块)?天真做法会是 mov bx, (data_end + 15) / 16,但由于缺乏除法重定位,这个方法不可行。
delta方案是 mov bx, (256 + code_end - code_start + data_end - data_start + 15) / 16,其中256用来补偿 org,另外两部分相加的结果是用于得到代码段和数据段大小的delta。请注意,为了进行此计算,我们需要知道链接器在 code_start 和 data_start 处将找到的偏移量,这些偏移量取决于用 org 指定的起点以及所有前面区段的大小(如有)。
构建时的后处理
此方案可以通过扩展工具链来实现,可以通过修改汇编器、修改链接器,或添加额外的工具来进行后处理。它需要在源代码中指明在何处应用后处理,以及要使用什么计算。
我修改了WarpLink OMF链接器以支持一个扩展,名为 wlcalc,允许在对最终可执行文件进行“后链接”遍历时,在链接器程序中进行某些类型的计算。该特性通过传入 /XC 开关启用,使链接器能够检测以短语 wlcalc 开头的全局符号。
下面给出一个示例,用于创建一个简单的扁平格式.COM风格的DOS程序:
cpu 8086
group programgroup code data
section code
times 256 db 0
..start:
jmp init
db "Test",13,10,26
init:
mov sp, stack.top
mov ah, 4Ah
mov bx, data_end + 15
global wlcalc_word_shr_4
wlcalc_word_shr_4: equ $ - 2
int 21h
mov ah, 09h
mov dx, msg
int 21h
mov ax, 4C00h
int 21h
section data
align 16
msg: db "Test Message",13,10,36
align 2
stack:
times 512 db '^'
align 2
.top:
data_end:
构建步骤如下(使用NASM、WarpLink,以及 x2b2):
nasm -f obj test.asm
warplink /xc /mx test.obj,test.exe,test.map;
x2b2 test.exe test.com
运行时的后处理
要在运行时进行后处理,可以在程序加载时让某种“链接器”运行。它可以使用链接表来找出需要修补程序的位置。这些表必须在构建时设置好。
例如,我们调试器的lDebug(ELDs)的扩展在加载ELD时会运行一个运行时链接器,用于解析调试器应用程序的内部代码和数据重定位以及代码和数据链接的外部链接。这种情况与对标签进行任意计算无关,但可用相同的总体设计来实现。该链接器在手册中描述,地址为 https://pushbx.org/ecm/doc/ldebug.htm#eldlinker
运行时进行计算
这种解决方案在86-DOS应用程序开发的鼎盛时期非常常见。取前面的同样示例,它看起来会是:
mov bx, data_end + 15
mov cl, 4
shr bx, cl
将加法运算也可以在运行时完成,而非在构建时完成。相对于使用delta运算或链接器扩展,这种方法的明显缺点是执行此计算的代码会增加应用程序的文件大小和内存占用,且运行时间更长。此外,可能需要比其他方案更多的寄存器。