您的原始问题、cmets 和您的答案提示您问题的可能原因。您应该养成制作一个最小的完整可验证示例的习惯。没有更多上下文的代码 sn-ps 通常很难诊断,并且通常依赖于您没有告诉我们的细节。
在你的回答中你提到了这一点
mov word [es:21h*4+2],CODE_SEG ;which is NOT 0, should be 50h
我可以推断 50h 意味着您从内存中的 0x0050:0x0000 开始加载内核,就在 BIOS Data Area (BDA) 上方。从您的回答中,我还可以推断出 DS 不为零,因为您必须用 ES 覆盖,您在代码注释中说它等于 0。您的 DS 寄存器可能设置为 0x0050(以及 CS)。
一个最小的完整示例如下所示:
boot.asm:
org 0x7c00
xor ax, ax
mov ds, ax ; DS=ES=0
mov es, ax
mov ss, ax ; SS:SP starts from top of first 64KiB in memory
mov sp, ax ; and grows down
mov ax, 0x0201 ; AH=2 BIOS disk read, AL=# sectors to read
mov cx, 0x0002 ; CH=cylinder 0, CL=sector number 2
mov dh, 0 ; DH=head 0
mov bx, 0x500 ; ES:BX(0x0000:0x0500) = memory to read to
int 0x13 ; Read 1 sector after bootloader to 0x0000:0x0500
; Insert error checking code here. Left out retries etc for brevity
jmp 0x0050:0x0000 ; Start executing kernel at 0x0050:0x0000
; Sets CS=0x0050, IP=0x0000
; Disk signature
TIMES 510-($-$$) db 0x00
dw 0xaa55
kernel.asm:
CODE_SEG EQU 0x0050
org 0x0000 ; Kernel will be run from 0x0050:0x0000
kernel:
; CS=0x0050 at this point because of FAR JMP that got us here
mov ax, CODE_SEG
mov ds, ax ; DS=ES=0x0050
mov es, ax
mov ss, ax ; SS:SP=0x0050:0x0000 wraps to top of 64KiB on 1st push
xor sp, sp ; and grows down
mov ax, 0x0e << 8 | 'K' ; AH=0x0e BIOS TTY print char service,
; AL=char to print `K`
mov bh, 0 ; Ensure we are using text page 0
int 0x10 ; Print 'K' on the display
mov word [21h*4], inthandler ; Set CS:IP of int 21 handler to CODE_SEG:inthandler
mov word [21h*4+2],CODE_SEG
; call [21h*4] ; This works by printing 'd' to the display
int 21h ; This fails. Doesn't print anything to display
.hltloop ; Infinite loop to stop kernel
hlt
jmp .hltloop
; Int 21h interrupt handler
inthandler:
mov ax, 0x0e << 8 | 'd' ; AH=0x0e BIOS TTY print char service, AL=char to print `K`
int 0x10 ; Print 'K' to display
iret ; Return from interrupt
使用引导加载程序和内核构建磁盘映像:
#!/bin/sh
nasm -f bin boot.asm -o boot.bin
nasm -f bin kernel.asm -o kernel.bin
# Make 1.44MiB floppy disk image with bootloader followed by kernel
dd if=/dev/zero of=floppy.img bs=1024 count=1440
dd if=boot.bin of=floppy.img conv=notrunc
dd if=kernel.bin of=floppy.img conv=notrunc seek=1
这可以通过 QEMU 使用以下命令进行测试:
qemu-system-i386 -fda floppy.img
如果您使用call [21h*4] 运行该版本,它将显示如下内容:
内核打印K,所以我知道内核正在运行。我的中断处理程序打印d。如果我尝试将我的中断处理程序(系统调用)与int 21h 一起使用,我会得到:
我相信这与您根据可用信息看到的体验相似。问题是为什么会这样?
问题的解决方案
有几个问题,但真正涉及如何将中断处理程序写入从 0x0000:0x0000 开始到 0x0000:0x400 结束的实模式中断向量表 (IVT)。你有这个代码:
mov word [21h*4], inthandler ; Set CS:IP of int 21 handler to CODE_SEG:inthandler
mov word [21h*4+2],CODE_SEG
代码相当于:
mov word [ds:21h*4], inthandler ; Set CS:IP of int 21 handler to CODE_SEG:inthandler
mov word [ds:21h*4+2],CODE_SEG
实模式下的每个内存访问都有一个与之关联的默认段寄存器。如果内存地址包含对寄存器BP 的引用,则假定该段为SS(堆栈段),否则为DS(数据段)。在这段代码中CODE_SEG 是 0x0050。
这个想法是将中断处理程序的 CS:IP (CODE_SEG:inthandler) 写入 IVT 以用于中断 21h。中断 21h 的偏移量在 0x0000:(0x0021 * 4),段在 0x0000:(0x0021 * 4+2)。
由于 DS 为 0x0050,您的代码实际上将您的中断向量地址写入 0x0050:(0x0021 * 4) 和 0x0050:(0x0021 * 4+2)。这实际上是在您的内核或内核数据中间的某个地方!因此,当您执行 int 21h 时,您调用了默认的 int 21h 例程,这可能只是一个不执行任何操作并返回的 IRET。
您需要将中断向量写入段 0x0000.. 这可以通过多种方式完成。一种方法是将 ES(额外段)设置为 0x0000 并覆盖内存操作数以使用 ES 而不是默认的 DS。修改后的代码如下:
; push es ; Save previous value of ES
xor ax, ax
mov es, ax ; ES=0
cli ; Make sure no interrupt occurs while we update IVT
mov word [es:21h*4], inthandler; Set CS:IP of int 21 handler to CODE_SEG:inthandler
mov word [es:21h*4+2],CODE_SEG
sti ; Re-enable interrupts
; pop es ; Restore original value of ES
如果您使用 ES 作为暂存段寄存器并且不关心内容,则可以删除 push es 和 pop es。我还针对 IVT 的更新添加了 CLI 和 STI 指令。这是一种安全预防措施,以防在我们完全更新之前发生某些使用中断向量 21h 的中断。这种情况在引导加载程序中几乎不存在,但如果您为 DOS 编写代码,则可能会出现问题。
或者,您可以通过将 DS 更改为 0x0000 并避免段覆盖来解决问题:
push ds ; Save previous value of DS
xor ax, ax
mov ds, ax ; DS=0
cli ; Make sure no interrupt occurs while we update IVT
mov word [21h*4], inthandler ; Set CS:IP of int 21 handler to CODE_SEG:inthandler
mov word [21h*4+2],CODE_SEG
sti ; Re-enable interrupts
pop ds ; Restore original value of DS
由于您可能希望将 DS 设置为其原始值 (0x0050),因此需要保存和恢复其值。
特别说明
您不能可靠地执行此操作来调用中断 21h:
call [21h*4]
在您的代码中,通过获取要从内存偏移量[ds:21h*4] 跳转到的偏移量,在当前段 (CS=0x0050) 中执行 NEAR 调用。它调用您的中断处理程序的事实是一个幸运的偶然事件。尽管它确实将d 打印到显示器上,但您的中断处理程序可能永远不会返回。如果您在int 21h 之后打印了其他内容,它可能永远不会出现,因为IRET 回到了内存中的错误位置。
为了使用CALL 正确模拟中断调用,您必须执行以下操作:
xor ax, ax
mov es, ax ; ES=0
pushf ; An interrupt pushes current FLAGS on the stack so we need
; to do something similar
call far [es:21h*4] ; We need to do a FAR CALL (not a NEAR call)
我们需要执行 FAR CALL 而不是默认的 NEAR CALL,因此我们需要在内存操作数上使用 FAR 属性。当IRET 返回时,它会将旧的 IP 和 CS 值从堆栈中弹出,然后将旧的 FLAGS 寄存器内容从堆栈中弹出.未能在堆栈上放置 FLAGS 值不会使堆栈在调用后保持与之前相同的状态,因为中断返回时返回 IRET 而不是 RET。