如果我使用 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 espop es的内容。我还在 IVT 的更新周围放置了 CLISTI 指令。这是一项安全预防措施,以防在我们完全更新中断向量 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 它从堆栈弹出 IPCS 的旧值,然后弹出旧的 FLAGS 从堆栈中注册内容。未能将 FLAGS 值放入堆栈不会使堆栈在调用后保持与之前相同的状态,因为中断 returns 与 IRET 和不是 RET.