尝试从软盘驱动器读取扇区时 INT 13、2 在 x86 实模式上挂起
INT 13, 2 hanging on x86 real mode when trying to read sectors from floppy drive
我正在为一个学校项目编写 DOS 克隆,我正在尝试使用 BIOS INT 13、2 从软盘驱动器(主要是 FAT12 文件系统的根目录,扇区 19)读取一些扇区。我正确设置了参数——或者至少我认为我这样做了——然后调用 INT 0x13 并设置 AH = 2。然后,系统挂起。我不明白为什么。
我正在使用以下代码调用中断:
mov ah, 0x2 ;Read sectors function
mov al, 1 ;I want to read one sector
mov ch, 0 ;From track 0
mov cl, 2 ;Sector 2
mov dh, 1 ;Head 1
mov dl, [device_number] ;Obtained from BIOS during boot
mov bx, 0x7C0 ;Buffer located at 0x7C00:0x0000*
mov es, bx
mov bx, 0
stc ;Set carry for older BIOSes that just unset it.
int 0x13 ;Call BIOS INT 0x13
jc .error ;If there's an error, jump to the .error subroutine.
上面的磁盘读取代码 运行 在处理击键的键盘中断处理程序中。当中断处理程序找到它识别的命令(即 DIR
)时,它会 运行 调用磁盘读取代码的例程。我的键盘处理程序看起来像:
; Keyboard ISR
; Installed to Real mode IVT @ 0x0000:0x0024 (IRQ1) replacing
; original BIOS interrupt vector
isr_teclado:
pusha
in al, 0x60 ; Get keystroke
call check_pressed_key ; Process Character
call fin_int_pic1 ; Send EOI to Master PIC
popa
iret
; Routine to send EOI to master PIC.
fin_int_pic1:
push ax
mov al, 0x20 ;PIC EOI signal
out 0x20, al ;Send signal to PIC 1
pop ax
ret
我一直在使用 QEMU 和 Virtual Box 进行测试。它们不会跳转到 .error
子程序,也不会在调用中断后继续执行。我也用 Bochs 测试过,但 Bochs 确实提升了进位标志并跳转到 .error
。真的不知道为什么。
请务必注意,我不是在编写引导加载程序。我的系统已经使用实际有效的类似过程加载到内存中(不是硬编码值,这些仅用于测试,但使用从其他 BIOS 函数获得的其他值似乎也不起作用)。同样的程序在这里也行不通。另外,我的系统在 0x0500:0x0000 加载,堆栈段设置为 0x0780:0x0000,堆栈基址和堆栈指针都从 0x0780:0x0400 (1KiB) 开始。
我做错了什么吗?我的参数不正确,我错过了什么吗?有没有我没有在这里发布的有用信息?
您的代码无法正常工作,因为 int 0x13
BIOS 调用返回 0x80 状态代码(超时)。这是因为您正在中断处理程序 (ISR) 中执行磁盘读取 BIOS 调用。
当 CPU 在实模式下将控制权转移到您的 ISR 时,它会清除 IF 标志。这导致 CPU 忽略可屏蔽的外部中断。即使您使用 STI
启用中断,您也不会再收到从 PIC 发送的中断,这些中断的优先级低于或等于当前中断。本质上,IRQ0(比 IRQ1 优先级更高的中断)是您在发送 EOI 之前唯一可以获得的中断。您不会得到 BIOS 调用正确完成请求所需的软盘控制器中断。这可能是超时的原因。
执行 ISR 的最佳想法是将它们限制在最低限度,并在尽可能短的时间内完成。除非您知道自己在做什么,否则您应该避免从您的 ISR 进行其他 BIOS 调用。
在键盘 ISR 中,您可以将击键读入缓冲区并推迟处理它们。 ring buffer 经常被内核用来处理键盘数据。一旦字符被读入缓冲区,您就可以发送 EOI 并退出您的 ISR。将内核的主循环 JMP $
替换为处理键盘 ISR 存储的键的循环。然后您可以采取任何适当的行动。您可以将 JMP $
替换为:
main_loop:
hlt ; Halt processor until next interrupt occurs
[check for characters in the keyboard buffer and process them as needed]
...
jmp main_loop
由于这是在 ISR 之外完成的,因此您不会受到在 ISR 中遇到的问题的限制 运行。
可以与一个消费者和生产者一起工作的中断安全无锁环形缓冲区的示例实现如下所示。该示例有一个键盘 ISR,它获取每个扫描码并将其放入缓冲区(如果缓冲区未满)。主循环检查每次迭代是否有可用的扫描码(缓冲区不为空)。如果可用,则将其转换为 ASCII 并打印到控制台。
KBD_BUFSIZE equ 32 ; Keyboard Buffer length. **Must** be a power of 2
; Maximum buffer size is 2^15 (32768)
KBD_IVT_OFFSET equ 0x0024 ; Base address of keyboard interrupt (IRQ) in IVT
bits 16
org 0x7c00
start:
xor ax, ax
mov ds, ax ; DS=0 since we use an ORG of 0x7c00.
; 0x0000<<4+0x7C00=0x07C00
mov ss, ax
mov sp, 0x7c00 ; SS:SP stack pointer set below bootloader
cli ; Don't want to be interrupted when updating IVT
mov word [KBD_IVT_OFFSET], kbd_isr
; 0x0000:0x0024 = IRQ1 offset in IVT
mov [KBD_IVT_OFFSET+2], ax ; 0x0000:0x0026 = IRQ1 segment in IVT
sti ; Enable interrupts
mov ax, 0xb800
mov es, ax ; Set ES to text mode segment (page 0)
xor di, di ; DI screen offset = 0 (upper left)
mov ah, 0x1F ; AH = White on Blue screen attribute
mov bx, keyboard_map ; BX = address of translate table used by XLAT
cld ; String instructions set to forward direction
.main_loop:
hlt ; Halt processor until next interrupt
mov si, [kbd_read_pos]
cmp si, [kbd_write_pos]
je .main_loop ; If (read_pos == write_pos) then buffer empty and
; we're finished
lea cx, [si+1] ; Index of next read (tmp = read_pos + 1)
and si, KBD_BUFSIZE-1 ; Normalize read_pos to be within 0 to KBD_BUFSIZE
mov al, [kbd_buffer+si] ; Get next scancode
mov [kbd_read_pos], cx ; read_pos++ (read_pos = tmp)
test al, 0x80 ; Is scancode a key up event?
jne .main_loop ; If so we are finished
xlat ; Translate scancode to ASCII character
test al, al
je .main_loop ; If character to print is NUL we are finished
stosw ; Display character on console in white on blue
jmp .main_loop
; Keyboard ISR (IRQ1)
kbd_isr:
push ax ; Save all registers we modify
push si
push cx
in al, 0x60 ; Get keystroke
mov cx, [cs:kbd_write_pos]
mov si, cx
sub cx, [cs:kbd_read_pos]
cmp cx, KBD_BUFSIZE ; If (write_pos-read_pos)==KBD_BUFSIZE then buffer full
je .end ; If buffer full throw char away, we're finished
lea cx, [si+1] ; Index of next write (tmp = write_pos + 1)
and si, KBD_BUFSIZE-1 ; Normalize write_pos to be within 0 to KBD_BUFSIZE
mov [cs:kbd_buffer+si], al ; Save character to buffer
mov [cs:kbd_write_pos], cx ; write_pos++ (write_pos = tmp)
.end:
mov al, 0x20
out 0x20, al ; Send EOI to Master PIC
pop cx ; Restore all registers modified
pop si
pop ax
iret
align 2
kbd_read_pos: dw 0
kbd_write_pos: dw 0
kbd_buffer: times KBD_BUFSIZE db 0
; Scancode to ASCII character translation table
keyboard_map:
db 0, 27, '1', '2', '3', '4', '5', '6', '7', '8' ; 9
db '9', '0', '-', '=', 0x08 ; Backspace
db 0x09 ; Tab
db 'q', 'w', 'e', 'r' ; 19
db 't', 'y', 'u', 'i', 'o', 'p', '[', ']', 0x0a ; Enter key
db 0 ; 29 - Control
db 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';' ; 39
db "'", '`', 0 ; Left shift
db "\", 'z', 'x', 'c', 'v', 'b', 'n' ; 49
db 'm', ',', '.', '/', 0 ; Right shift
db '*'
db 0 ; Alt
db ' ' ; Space bar
db 0 ; Caps lock
db 0 ; 59 - F1 key ... >
db 0, 0, 0, 0, 0, 0, 0, 0
db 0 ; < ... F10
db 0 ; 69 - Num lock
db 0 ; Scroll Lock
db 0 ; Home key
db 0 ; Up Arrow
db 0 ; Page Up
db '-'
db 0 ; Left Arrow
db 0
db 0 ; Right Arrow
db '+'
db 0 ; 79 - End key
db 0 ; Down Arrow
db 0 ; Page Down
db 0 ; Insert Key
db 0 ; Delete Key
db 0, 0, 0
db 0 ; F11 Key
db 0 ; F12 Key
times 128 - ($-keyboard_map) db 0 ; All other keys are undefined
times 510 - ($-$$) db 0 ; Boot signature
dw 0xAA55
注意:此实现是一个演示。一个真正的 OS 可能会有一个主循环将检查的事件队列。 ISR 会将事件推入队列,主循环会将每个事件弹出并处理它们。该演示效率低下,因为无论是否发生键盘事件,它总是检查缓冲区中的扫描码。
该代码基于环形缓冲区实现,伪代码如下所示:
buffer[BUFSIZE]; /* BUFSIZE has to be power of 2 and be <= 32768 */
uint16_t read_pos = 0;
uint16_t write_pos = 0;
normalize(val) { return val & (BUFSIZE - 1); }
saveelement(val) { buffer[normalize(write_pos++)] = val; }
getelement() { return buffer[normalize(read_pos++)]; }
numelements() { return write_pos - read_pos; }
isfull() { return numelements() == BUFSIZE; }
isempty() { return write_pos == read_pos; }
在使用 saveelement
之前,您必须调用 isfull
以确保缓冲区未满。在使用 getelement
之前,您必须调用 isempty
以确保有值可读。
我正在为一个学校项目编写 DOS 克隆,我正在尝试使用 BIOS INT 13、2 从软盘驱动器(主要是 FAT12 文件系统的根目录,扇区 19)读取一些扇区。我正确设置了参数——或者至少我认为我这样做了——然后调用 INT 0x13 并设置 AH = 2。然后,系统挂起。我不明白为什么。
我正在使用以下代码调用中断:
mov ah, 0x2 ;Read sectors function
mov al, 1 ;I want to read one sector
mov ch, 0 ;From track 0
mov cl, 2 ;Sector 2
mov dh, 1 ;Head 1
mov dl, [device_number] ;Obtained from BIOS during boot
mov bx, 0x7C0 ;Buffer located at 0x7C00:0x0000*
mov es, bx
mov bx, 0
stc ;Set carry for older BIOSes that just unset it.
int 0x13 ;Call BIOS INT 0x13
jc .error ;If there's an error, jump to the .error subroutine.
上面的磁盘读取代码 运行 在处理击键的键盘中断处理程序中。当中断处理程序找到它识别的命令(即 DIR
)时,它会 运行 调用磁盘读取代码的例程。我的键盘处理程序看起来像:
; Keyboard ISR
; Installed to Real mode IVT @ 0x0000:0x0024 (IRQ1) replacing
; original BIOS interrupt vector
isr_teclado:
pusha
in al, 0x60 ; Get keystroke
call check_pressed_key ; Process Character
call fin_int_pic1 ; Send EOI to Master PIC
popa
iret
; Routine to send EOI to master PIC.
fin_int_pic1:
push ax
mov al, 0x20 ;PIC EOI signal
out 0x20, al ;Send signal to PIC 1
pop ax
ret
我一直在使用 QEMU 和 Virtual Box 进行测试。它们不会跳转到 .error
子程序,也不会在调用中断后继续执行。我也用 Bochs 测试过,但 Bochs 确实提升了进位标志并跳转到 .error
。真的不知道为什么。
请务必注意,我不是在编写引导加载程序。我的系统已经使用实际有效的类似过程加载到内存中(不是硬编码值,这些仅用于测试,但使用从其他 BIOS 函数获得的其他值似乎也不起作用)。同样的程序在这里也行不通。另外,我的系统在 0x0500:0x0000 加载,堆栈段设置为 0x0780:0x0000,堆栈基址和堆栈指针都从 0x0780:0x0400 (1KiB) 开始。
我做错了什么吗?我的参数不正确,我错过了什么吗?有没有我没有在这里发布的有用信息?
您的代码无法正常工作,因为 int 0x13
BIOS 调用返回 0x80 状态代码(超时)。这是因为您正在中断处理程序 (ISR) 中执行磁盘读取 BIOS 调用。
当 CPU 在实模式下将控制权转移到您的 ISR 时,它会清除 IF 标志。这导致 CPU 忽略可屏蔽的外部中断。即使您使用 STI
启用中断,您也不会再收到从 PIC 发送的中断,这些中断的优先级低于或等于当前中断。本质上,IRQ0(比 IRQ1 优先级更高的中断)是您在发送 EOI 之前唯一可以获得的中断。您不会得到 BIOS 调用正确完成请求所需的软盘控制器中断。这可能是超时的原因。
执行 ISR 的最佳想法是将它们限制在最低限度,并在尽可能短的时间内完成。除非您知道自己在做什么,否则您应该避免从您的 ISR 进行其他 BIOS 调用。
在键盘 ISR 中,您可以将击键读入缓冲区并推迟处理它们。 ring buffer 经常被内核用来处理键盘数据。一旦字符被读入缓冲区,您就可以发送 EOI 并退出您的 ISR。将内核的主循环 JMP $
替换为处理键盘 ISR 存储的键的循环。然后您可以采取任何适当的行动。您可以将 JMP $
替换为:
main_loop:
hlt ; Halt processor until next interrupt occurs
[check for characters in the keyboard buffer and process them as needed]
...
jmp main_loop
由于这是在 ISR 之外完成的,因此您不会受到在 ISR 中遇到的问题的限制 运行。
可以与一个消费者和生产者一起工作的中断安全无锁环形缓冲区的示例实现如下所示。该示例有一个键盘 ISR,它获取每个扫描码并将其放入缓冲区(如果缓冲区未满)。主循环检查每次迭代是否有可用的扫描码(缓冲区不为空)。如果可用,则将其转换为 ASCII 并打印到控制台。
KBD_BUFSIZE equ 32 ; Keyboard Buffer length. **Must** be a power of 2
; Maximum buffer size is 2^15 (32768)
KBD_IVT_OFFSET equ 0x0024 ; Base address of keyboard interrupt (IRQ) in IVT
bits 16
org 0x7c00
start:
xor ax, ax
mov ds, ax ; DS=0 since we use an ORG of 0x7c00.
; 0x0000<<4+0x7C00=0x07C00
mov ss, ax
mov sp, 0x7c00 ; SS:SP stack pointer set below bootloader
cli ; Don't want to be interrupted when updating IVT
mov word [KBD_IVT_OFFSET], kbd_isr
; 0x0000:0x0024 = IRQ1 offset in IVT
mov [KBD_IVT_OFFSET+2], ax ; 0x0000:0x0026 = IRQ1 segment in IVT
sti ; Enable interrupts
mov ax, 0xb800
mov es, ax ; Set ES to text mode segment (page 0)
xor di, di ; DI screen offset = 0 (upper left)
mov ah, 0x1F ; AH = White on Blue screen attribute
mov bx, keyboard_map ; BX = address of translate table used by XLAT
cld ; String instructions set to forward direction
.main_loop:
hlt ; Halt processor until next interrupt
mov si, [kbd_read_pos]
cmp si, [kbd_write_pos]
je .main_loop ; If (read_pos == write_pos) then buffer empty and
; we're finished
lea cx, [si+1] ; Index of next read (tmp = read_pos + 1)
and si, KBD_BUFSIZE-1 ; Normalize read_pos to be within 0 to KBD_BUFSIZE
mov al, [kbd_buffer+si] ; Get next scancode
mov [kbd_read_pos], cx ; read_pos++ (read_pos = tmp)
test al, 0x80 ; Is scancode a key up event?
jne .main_loop ; If so we are finished
xlat ; Translate scancode to ASCII character
test al, al
je .main_loop ; If character to print is NUL we are finished
stosw ; Display character on console in white on blue
jmp .main_loop
; Keyboard ISR (IRQ1)
kbd_isr:
push ax ; Save all registers we modify
push si
push cx
in al, 0x60 ; Get keystroke
mov cx, [cs:kbd_write_pos]
mov si, cx
sub cx, [cs:kbd_read_pos]
cmp cx, KBD_BUFSIZE ; If (write_pos-read_pos)==KBD_BUFSIZE then buffer full
je .end ; If buffer full throw char away, we're finished
lea cx, [si+1] ; Index of next write (tmp = write_pos + 1)
and si, KBD_BUFSIZE-1 ; Normalize write_pos to be within 0 to KBD_BUFSIZE
mov [cs:kbd_buffer+si], al ; Save character to buffer
mov [cs:kbd_write_pos], cx ; write_pos++ (write_pos = tmp)
.end:
mov al, 0x20
out 0x20, al ; Send EOI to Master PIC
pop cx ; Restore all registers modified
pop si
pop ax
iret
align 2
kbd_read_pos: dw 0
kbd_write_pos: dw 0
kbd_buffer: times KBD_BUFSIZE db 0
; Scancode to ASCII character translation table
keyboard_map:
db 0, 27, '1', '2', '3', '4', '5', '6', '7', '8' ; 9
db '9', '0', '-', '=', 0x08 ; Backspace
db 0x09 ; Tab
db 'q', 'w', 'e', 'r' ; 19
db 't', 'y', 'u', 'i', 'o', 'p', '[', ']', 0x0a ; Enter key
db 0 ; 29 - Control
db 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';' ; 39
db "'", '`', 0 ; Left shift
db "\", 'z', 'x', 'c', 'v', 'b', 'n' ; 49
db 'm', ',', '.', '/', 0 ; Right shift
db '*'
db 0 ; Alt
db ' ' ; Space bar
db 0 ; Caps lock
db 0 ; 59 - F1 key ... >
db 0, 0, 0, 0, 0, 0, 0, 0
db 0 ; < ... F10
db 0 ; 69 - Num lock
db 0 ; Scroll Lock
db 0 ; Home key
db 0 ; Up Arrow
db 0 ; Page Up
db '-'
db 0 ; Left Arrow
db 0
db 0 ; Right Arrow
db '+'
db 0 ; 79 - End key
db 0 ; Down Arrow
db 0 ; Page Down
db 0 ; Insert Key
db 0 ; Delete Key
db 0, 0, 0
db 0 ; F11 Key
db 0 ; F12 Key
times 128 - ($-keyboard_map) db 0 ; All other keys are undefined
times 510 - ($-$$) db 0 ; Boot signature
dw 0xAA55
注意:此实现是一个演示。一个真正的 OS 可能会有一个主循环将检查的事件队列。 ISR 会将事件推入队列,主循环会将每个事件弹出并处理它们。该演示效率低下,因为无论是否发生键盘事件,它总是检查缓冲区中的扫描码。
该代码基于环形缓冲区实现,伪代码如下所示:
buffer[BUFSIZE]; /* BUFSIZE has to be power of 2 and be <= 32768 */
uint16_t read_pos = 0;
uint16_t write_pos = 0;
normalize(val) { return val & (BUFSIZE - 1); }
saveelement(val) { buffer[normalize(write_pos++)] = val; }
getelement() { return buffer[normalize(read_pos++)]; }
numelements() { return write_pos - read_pos; }
isfull() { return numelements() == BUFSIZE; }
isempty() { return write_pos == read_pos; }
在使用 saveelement
之前,您必须调用 isfull
以确保缓冲区未满。在使用 getelement
之前,您必须调用 isempty
以确保有值可读。