如果我使用 call,实模式中断有效,如果我使用 INT,则无效(不会执行)
Real mode interrupt works if I use call, doesn't work(won't execute) if I use INT
我正在尝试向我的 REAL MODE 操作系统添加一个系统调用,如果我这样写就可以了:
call [21h*4]
但是,如果我尝试用
调用它,它就不起作用
int 0x21
这是我用来设置系统调用的代码:
mov word [21h*4],inthandler
mov word [21h*4+2],CODE_SEG ;which is 0(incorrect)
我的中断处理程序定义为:
inthandler:
mov ax,0e64h
int 0x10
iret
中断工作时应该在显示器上打印字母d
。当它失败时它不打印任何东西。
显然我在代码中犯了一些错误,系统调用设置代码应该是:
;es=0
mov word [es:21h*4],inthandler
mov word [es:21h*4+2],CODE_SEG ;which is NOT 0, should be 50h
您的原始问题、评论和您的回答会提示您问题的可能原因。您应该养成生成最小的完整可验证示例的习惯。没有更多上下文的代码片段通常很难诊断,并且通常依赖于您没有告诉我们的详细信息。
在你的回答中你提到了这个
mov word [es:21h*4+2],CODE_SEG ;which is NOT 0, should be 50h
我可以推断出 50h 意味着您在 BIOS Data Area (BDA) 之上的内存中从 0x0050:0x0000 开始加载内核。根据您的回答,我还可以推断 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
所以我知道内核是 运行ning。我的中断处理程序打印 d
。如果我尝试将我的中断处理程序(系统调用)与 int 21h
一起使用,我会得到:
根据现有信息,我相信这与您看到的体验相似。问题是为什么会这样?
问题的解决方案
有几个问题,但真正涉及如何将中断处理程序写入实模式中断向量 Table (IVT),它开始于 0x0000:0x0000 并结束于 0x0000:0x400.你有这个代码:
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) 写入中断 21h 的 IVT。中断 21h 的偏移量位于 0x0000:(0x0021 * 4),段位于 0x0000:(0x0021 * 4+2).
由于 DS 是 0x0050,您的代码实际上将中断向量地址写入 0x0050:(0x0021 * 4) 和 0x0050:(0x0021 * 4+2)。这实际上在您的内核或内核数据的中间某处!因此,当您执行 int 21h
时,您调用了默认的 int 21h
例程,它可能只是一个什么都不做的 IRET
和 returns.
您需要将中断向量写入段 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 之前发生某些中断恰好使用中断向量 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
returns 它从堆栈弹出 IP 和 CS 的旧值,然后弹出旧的 FLAGS 从堆栈中注册内容。未能将 FLAGS 值放入堆栈不会使堆栈在调用后保持与之前相同的状态,因为中断 returns 与 IRET
和不是 RET
.
我正在尝试向我的 REAL MODE 操作系统添加一个系统调用,如果我这样写就可以了:
call [21h*4]
但是,如果我尝试用
调用它,它就不起作用int 0x21
这是我用来设置系统调用的代码:
mov word [21h*4],inthandler
mov word [21h*4+2],CODE_SEG ;which is 0(incorrect)
我的中断处理程序定义为:
inthandler:
mov ax,0e64h
int 0x10
iret
中断工作时应该在显示器上打印字母d
。当它失败时它不打印任何东西。
显然我在代码中犯了一些错误,系统调用设置代码应该是:
;es=0
mov word [es:21h*4],inthandler
mov word [es:21h*4+2],CODE_SEG ;which is NOT 0, should be 50h
您的原始问题、评论和您的回答会提示您问题的可能原因。您应该养成生成最小的完整可验证示例的习惯。没有更多上下文的代码片段通常很难诊断,并且通常依赖于您没有告诉我们的详细信息。
在你的回答中你提到了这个
mov word [es:21h*4+2],CODE_SEG ;which is NOT 0, should be 50h
我可以推断出 50h 意味着您在 BIOS Data Area (BDA) 之上的内存中从 0x0050:0x0000 开始加载内核。根据您的回答,我还可以推断 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
所以我知道内核是 运行ning。我的中断处理程序打印 d
。如果我尝试将我的中断处理程序(系统调用)与 int 21h
一起使用,我会得到:
根据现有信息,我相信这与您看到的体验相似。问题是为什么会这样?
问题的解决方案
有几个问题,但真正涉及如何将中断处理程序写入实模式中断向量 Table (IVT),它开始于 0x0000:0x0000 并结束于 0x0000:0x400.你有这个代码:
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) 写入中断 21h 的 IVT。中断 21h 的偏移量位于 0x0000:(0x0021 * 4),段位于 0x0000:(0x0021 * 4+2).
由于 DS 是 0x0050,您的代码实际上将中断向量地址写入 0x0050:(0x0021 * 4) 和 0x0050:(0x0021 * 4+2)。这实际上在您的内核或内核数据的中间某处!因此,当您执行 int 21h
时,您调用了默认的 int 21h
例程,它可能只是一个什么都不做的 IRET
和 returns.
您需要将中断向量写入段 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 之前发生某些中断恰好使用中断向量 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
returns 它从堆栈弹出 IP 和 CS 的旧值,然后弹出旧的 FLAGS 从堆栈中注册内容。未能将 FLAGS 值放入堆栈不会使堆栈在调用后保持与之前相同的状态,因为中断 returns 与 IRET
和不是 RET
.