x86 无法在一条指令中从立即常量远指针加载。有一个远 jmp ptr16:16 可以将 cs:ip 设置为 32 位立即数,但对于加载,我看不到任何不使用段寄存器的选项。
NASM docs suggest 在 16 位调用约定中 DS 被保留调用约定是/是典型的,但 ES 可以被认为是调用破坏的原因正是您想要的(在内存模型中不要将所有内容都放在一个片段中)。
所以你可以考虑为这个函数使用一个调用约定,允许它破坏ES。 (或者 FS 或 GS 如果您的代码只需要在 386 或更高版本上运行。)
您还可以通过对段重复使用相同的寄存器来保存指令。您可以两次都使用si 而不是bx,因为您在设置es 后就完成了bx。
getndrives:
;; return in AL (zero-extended to AX; the upper byte of 0040h happens to be 0)
;; clobbers: AH, ES
mov ax, 0x0040
mov es, ax
mov al, [es:0x0475]
ret
作为奖励,您正在加载到 AL,因此它可以使用特殊的 moffs encoding of mov 跳过 ModR/M 字节,因此它只是(ES 段覆盖前缀)+ A0 75 04。
当然,如果您愿意,您仍然可以在此周围推送/弹出 es 和/或 ax,但这显然更笨重。如果您要保存/恢复段寄存器,@Fifoernik 指出,在mov 上保存前缀也可能是 DS(除非您有任何假定 DS 保持不变的中断处理程序)。
getndrives:
;; return in AL (zero-extended to AX; the upper byte of 0040h happens to be 0)
;; clobbers: AH, or nothing if you uncomment the push/pop of AX
push es
;push ax ; you might as well use a reg other than AX to simplify this, if you do want to preserve AH, too. And also for performance on CPUs that don't rename low8 partial regs separately, so mov al,[mem] has a dependency on the pop.
mov ax, 0x0040
mov es, ax ; or use DS if you don't need it in any interrupt handler
;pop ax
mov al, [es:0x0475]
pop es
ret
如果您只关心 186 及更新版本,push 0040h / pop es 将避免破坏 AH。 (8086 没有push imm8/imm16)。
在您的情况下,您可能首先可以避免修改段寄存器。请注意,0040h:0075h 是0475h 的线性地址,您可以使用从0 到47h 的任何DS 值的偏移量来访问它。 (在实模式下,linear = (segment << 4) + offset,即左移一位十六进制数字)。
使用 DS=0,您仍然可以访问您的引导扇区代码/数据(according to this PC memory map,加载到 07C0:0,又名0:7C00),以及低 64kiB 内存中的任何其他位置。
我实际上并没有编写 16 位代码,所以我不确定你会如何告诉 NASM 你将设置DS=0,然后让它相应地生成偏移量。但希望这是可能的。
作为奖励,它节省了代码大小:xor ax,ax 小于 mov ax, imm16。但我想如果你正在优化代码大小,你会push 0/pop ds。 (但 8086 没有 push imm,后来才出现)。而且我想如果您希望堆栈指针与其他指针兼容,您需要mov ss, ax / mov sp, whatever 这样代码更多。
但无论如何,你的函数应该是这样的。
getndrives:
;; return in AL
;; requires/assumes: DS=0
mov al, [0x0475]
ret
在这一点上,把它变成一个函数是很愚蠢的。让它成为一个宏,或者更好,或者%define BIOS_BDA_ndrives 0x0475,这样你就可以做add dl, [BIOS_BDA_ndrives]之类的东西。或者可能%define BIOS_BDA_ndrives byte [0x0475] 进行构建时类型检查,例如然后mov ax, BIOS_BDA_ndrives 将因操作数大小不匹配而无法组装。