在 x86 实模式汇编中编写中断处理程序

Writing interrupt handler in x86 real mode assembly

我正在使用汇编学习 x86 实模式下的中断处理。我正在遵循以下摘自 here 的示例:

.include "common.h"
BEGIN
    CLEAR
    /* Set address of the handler for interrupt 0. */
    movw $handler, 0x00
    /* Set code segment of the handler for interrupt 0. */
    mov %cs, 0x02
    int [=10=]
    PUTC $'b
    hlt
handler:
    PUTC $'a
    iret

但是当我编译运行上面的代码时,

$ as --32 -o main.o main.S -g
$ ld -T linker.ld -o main.img --oformat binary -m elf_i386 -nostdlib main.o
$ qemu-system-i386 -hda main.img

我收到以下错误:

qemu-system-i386: Trying to execute code outside RAM or ROM at 0xf00fff53
This usually means one of the following happened:

(1) You told QEMU to execute a kernel for the wrong machine type, and it crashed on startup (eg trying to run a raspberry pi kernel on a versatilepb QEMU machine)
(2) You didn't give QEMU a kernel or BIOS filename at all, and QEMU executed a ROM full of no-op instructions until it fell off the end
(3) Your guest kernel has a bug and crashed by jumping off into nowhere

This is almost always one of the first two, so check your command line and that you are using the right type of kernel for this machine.
If you think option (3) is likely then you can try debugging your guest with the -d debug options; in particular -d guest_errors will cause the log to include a dump of the guest register state at this point.

Execution cannot continue; stopping here.

我在这里错过了什么?为什么需要 mov %cs, 0x02 或者它的真正作用是什么?

我尝试在 gdb 下调试这个,当我逐行执行时,我在 gdb 下没有遇到这个错误,这很奇怪,我还在检查。

编辑

BEGIN 是这样定义的:

.macro BEGIN
    .local after_locals
    .code16
    cli
    /* Set %cs to 0. */
    ljmp [=13=], f
    1:
    xor %ax, %ax
    /* We must zero %ds for any data access. */
    mov %ax, %ds
    mov %ax, %es
    mov %ax, %fs
    mov %ax, %gs
    mov %ax, %bp
    /* Automatically disables interrupts until the end of the next instruction. */
    mov %ax, %ss
    /* We should set SP because BIOS calls may depend on that. TODO confirm. */
    mov %bp, %sp
    /* Store the initial dl to load stage 2 later on. */
    mov %dl, initial_dl
    jmp after_locals
    initial_dl: .byte 0
after_locals:
.endm

我只能假设您在教程中最初提供的代码中引入了错误。例如你说你 assemble with:

as --32 -o main.o main.S -g

如果您包含本教程中显示的 common.h,此命令应该会失败,并显示如下内容:

common.h: Assembler messages:
common.h:399: Warning: stray `\'
common.h:400: Warning: stray `\'
common.h:401: Warning: stray `\'
common.h:421: Warning: stray `\'
common.h:422: Warning: stray `\'
common.h:423: Warning: stray `\'
common.h:424: Warning: stray `\'
common.h:425: Warning: stray `\'

这些错误的发生是因为教程代码的编写方式要求 C 预处理器在汇编代码中 运行。最简单的方法是使用 GCC assemble 将代码传递给后端 AS assembler:

gcc -c -g -m32 -o main.o main.S

GCC 将在通过 AS assembler 传递文件之前采用 .S 扩展名和 运行 .S 上的 C 预处理器。作为替代方案,您可以 运行 C 预处理器直接使用 cpp 然后 运行 as 分别。

要使用 GCC 构建 main.img,您需要使用如下命令:

gcc -c -g -m32 -o main.o main.S
ld -T linker.ld -o main.img --oformat binary -m elf_i386 -nostdlib main.o

要使用 C 预处理器构建它,您可以这样做:

cpp main.S > main.s
as -g --32 -o main.o main.s
ld -T linker.ld -o main.img --oformat binary -m elf_i386 -nostdlib main.o

当 运行 使用 QEMU 使用时,代码按预期工作:

qemu-system-i386 -hda main.img

输出应类似于:

关于 CS 和实模式 IVT 的问题

您查询的是这个代码:

/* Set address of the handler for interrupt 0. */
movw $handler, 0x00
/* Set code segment of the handler for interrupt 0. */
mov %cs, 0x02
int [=16=]

在实模式下,默认的 IBM-PC 中断向量 Table (IVT) 是内存的前 1024 个字节,从物理地址 0x00000 (0x0000:0x0000) 开始到 0x00400 (0x0000:0x0400) . IVT 中的每个条目都是 4 个字节(每个条目 4 个字节 * 256 个中断 = 1024 个字节)。中断向量所在的指令指针 (IP)(也称为偏移量)的一个字(2 个字节)后跟一个包含该段的字(2 个字节)。

中断 0 从 IVT 的最底部开始,位于内存 0x000000 (0x0000:0x0000)。中断 1 从 0x00004 (0x0000:0x0004) 开始...中断 255 从 0x003FC (0x0000:0x03FC) 开始。

指令:

/* Set address of the handler for interrupt 0. */
movw $handler, 0x00

handler 的 16 位偏移量移动到内存地址 DS:0x0000 。对于 16 位寻址,DS 始终是隐含段,除非寄存器 BP 出现在内存引用中(即 (%bp)),然后段假定为 SS.

DSBEGIN 宏中设置为 0x0000 所以 DS:0x00 是 0x0000:0x0000 这是中断的 IP(偏移量)部分0 的 segment:offset 地址。指令:

/* Set code segment of the handler for interrupt 0. */
mov %cs, 0x02

CSBEGIN 宏中设置为 0x0000。该指令将 0x0000 移动到内存地址 DS:0x02 (0x0000:0x0002)。 0x0000:0x0002 是中断 0 地址的段部分。在这条指令之后,中断 0 的 IVT 条目现在指向我们引导扇区中的 handler 代码。指令:

int [=19=]

调用现在指向 handler 的中断 0。它应该在屏幕上显示 a,然后继续 int [=36=] 之后的代码,打印 b 然后停止。


最小完整可验证示例代码

您的问题缺少最小的完整可验证示例。我修改了 common.h 以仅包含您编写的代码所需的宏,并保持其他所有内容相同:

linker.ld:

SECTIONS
{
    /* We could also pass the -Ttext 0x7C00 to as instead of doing this.
     * If your program does not have any memory accesses, you can omit this.
     */
    . = 0x7c00;
    .text :
    {
        __start = .;

        /* We are going to stuff everything
         * into a text segment for now, including data.
         * Who cares? Other segments only exist to appease C compilers.
         */
        *(.text)

        /* Magic bytes. 0x1FE == 510.
         *
         * We could add this on each Gas file separately with `.word`,
         * but this is the perfect place to DRY that out.
         */
        . = 0x1FE;
        SHORT(0xAA55)

        /* This is only needed if we are going to use a 2 stage boot process,
         * e.g. by reading more disk than the default 512 bytes with BIOS `int 0x13`.
         */
        *(.stage2)

        /* Number of sectors in stage 2. Used by the `int 13` to load it from disk.
         *
         * The value gets put into memory as the very last thing
         * in the `.stage` section if it exists.
         *
         * We must put it *before* the final `. = ALIGN(512)`,
         * or else it would fall out of the loaded memory.
         *
         * This must be absolute, or else it would get converted
         * to the actual address relative to this section (7c00 + ...)
         * and linking would fail with "Relocation truncated to fit"
         * because we are trying to put that into al for the int 13.
         */
        __stage2_nsectors = ABSOLUTE((. - __start) / 512);

        /* Ensure that the generated image is a multiple of 512 bytes long. */
        . = ALIGN(512);
        __end = .;
        __end_align_4k = ALIGN(4k);
    }
}

common.h:

/* I really want this for the local labels.
 *
 * The major downside is that every register passed as argument requires `<>`:
 * 
 */
.altmacro

/* Helpers */

/* Push registers ax, bx, cx and dx. Lightweight `pusha`. */
.macro PUSH_ADX
    push %ax
    push %bx
    push %cx
    push %dx
.endm

/* Pop registers dx, cx, bx, ax. Inverse order from PUSH_ADX,
 * so this cancels that one.
 */
.macro POP_DAX
    pop %dx
    pop %cx
    pop %bx
    pop %ax
.endm


/* Structural. */

/* Setup a sane initial state.
 *
 * Should be the first thing in every file.
 *
 * Discussion of what is needed exactly: 
 */
.macro BEGIN
    LOCAL after_locals
    .code16
    cli
    /* Set %cs to 0. TODO Is that really needed? */
    ljmp [=21=], f
    1:
    xor %ax, %ax
    /* We must zero %ds for any data access. */
    mov %ax, %ds
    /* TODO is it really need to clear all those segment registers, e.g. for BIOS calls? */
    mov %ax, %es
    mov %ax, %fs
    mov %ax, %gs
    /* TODO What to move into BP and SP?
     * 
     */
    mov %ax, %bp
    /* Automatically disables interrupts until the end of the next instruction. */
    mov %ax, %ss
    /* We should set SP because BIOS calls may depend on that. TODO confirm. */
    mov %bp, %sp
    /* Store the initial dl to load stage 2 later on. */
    mov %dl, initial_dl
    jmp after_locals
    initial_dl: .byte 0
after_locals:
.endm

/* BIOS */

.macro CURSOR_POSITION x=[=21=], y=[=21=]
    PUSH_ADX
    mov [=21=]x02, %ah
    mov [=21=]x00, %bh
    mov \x, %dh
    mov \y, %dl
    int [=21=]x10
    POP_DAX
.endm

/* Clear the screen, move to position 0, 0. */
.macro CLEAR
    PUSH_ADX
    mov [=21=]x0600, %ax
    mov [=21=]x7, %bh
    mov [=21=]x0, %cx
    mov [=21=]x184f, %dx
    int [=21=]x10
    CURSOR_POSITION
    POP_DAX
.endm

/* Print a 8 bit ASCII value at current cursor position.
 *
 * * `c`: r/m/imm8 ASCII value to be printed.
 *
 * Usage:
 *
 * ....
 * PUTC $'a
 * ....
 *
 * prints `a` to the screen.
 */
.macro PUTC c=[=21=]x20
    push %ax
    mov \c, %al
    mov [=21=]x0E, %ah
    int [=21=]x10
    pop %ax
.endm

main.S:

.include "common.h"
BEGIN
    CLEAR
    /* Set address of the handler for interrupt 0. */
    movw $handler, 0x00
    /* Set code segment of the handler for interrupt 0. */
    mov %cs, 0x02
    int [=22=]
    PUTC $'b
    hlt
handler:
    PUTC $'a
    iret

建议

GDB(GNU 调试器)不理解实模式 segment:offset 寻址。使用 GDB 调试实模式代码是非常有问题的,我不推荐这样做。您应该考虑使用 BOCHS 来调试实模式代码,因为它了解实模式、segment:offset 寻址,并且更适合调试引导加载程序或进入 32 位保护之前 运行 的任何代码模式或长模式。