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

Ins*_*der 3 x86 assembly interrupt gnu-assembler x86-16

我正在使用汇编学习 x86 实模式下的中断处理。我正在遵循从这里获取的以下示例:

.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 $0
    PUTC $'b
    hlt
handler:
    PUTC $'a
    iret
Run Code Online (Sandbox Code Playgroud)

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

$ 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
Run Code Online (Sandbox Code Playgroud)

我收到以下错误:

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.
Run Code Online (Sandbox Code Playgroud)

我在这里缺少什么?为什么mov %cs, 0x02需要或它真正在做什么?

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

编辑

这是如何BEGIN定义的:

.macro BEGIN
    .local after_locals
    .code16
    cli
    /* Set %cs to 0. */
    ljmp $0, $1f
    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
Run Code Online (Sandbox Code Playgroud)

Mic*_*tch 5

我只能假设您在本教程中最初介绍的代码中引入了错误。例如,你说你组装:

as --32 -o main.o main.S -g
Run Code Online (Sandbox Code Playgroud)

如果您包含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 `\'
Run Code Online (Sandbox Code Playgroud)

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

gcc -c -g -m32 -o main.o main.S
Run Code Online (Sandbox Code Playgroud)

GCC 将采用任何带有.S扩展名的文件扩展名,并.S在通过 AS 汇编程序之前运行 C 预处理器。作为替代方案,您可以直接运行C预处理器cpp,然后as单独运行。

main.img使用 GCC构建,您可以使用以下命令:

gcc -c -g -m32 -o main.o main.S
ld -T linker.ld -o main.img --oformat binary -m elf_i386 -nostdlib main.o
Run Code Online (Sandbox Code Playgroud)

要使用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
Run Code Online (Sandbox Code Playgroud)

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

qemu-system-i386 -hda main.img
Run Code Online (Sandbox Code Playgroud)

输出应类似于:

在此处输入图片说明

关于 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 $0
Run Code Online (Sandbox Code Playgroud)

在实模式下,默认的 IBM-PC 中断向量表 (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
Run Code Online (Sandbox Code Playgroud)

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

DSBEGIN宏中设置为 0x0000,因此 DS:0x00 是 0x0000:0x0000,它是中断 0 的段:偏移地址的 IP(偏移)部分。指令:

/* Set code segment of the handler for interrupt 0. */
mov %cs, 0x02
Run Code Online (Sandbox Code Playgroud)

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

int $0
Run Code Online (Sandbox Code Playgroud)

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


最小完整可验证示例的代码

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

链接器.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);
    }
}
Run Code Online (Sandbox Code Playgroud)

常见的.h

/* I really want this for the local labels.
 *
 * The major downside is that every register passed as argument requires `<>`:
 * http://stackoverflow.com/questions/19776992/gas-altmacro-macro-with-a-percent-sign-in-a-default-parameter-fails-with-oper/
 */
.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: http://stackoverflow.com/a/32509555/895245
 */
.macro BEGIN
    LOCAL after_locals
    .code16
    cli
    /* Set %cs to 0. TODO Is that really needed? */
    ljmp $0, $1f
    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?
     * http://stackoverflow.com/questions/10598802/which-value-should-be-used-for-sp-for-booting-process
     */
    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=$0, y=$0
    PUSH_ADX
    mov $0x02, %ah
    mov $0x00, %bh
    mov \x, %dh
    mov \y, %dl
    int $0x10
    POP_DAX
.endm

/* Clear the screen, move to position 0, 0. */
.macro CLEAR
    PUSH_ADX
    mov $0x0600, %ax
    mov $0x7, %bh
    mov $0x0, %cx
    mov $0x184f, %dx
    int $0x10
    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=$0x20
    push %ax
    mov \c, %al
    mov $0x0E, %ah
    int $0x10
    pop %ax
.endm
Run Code Online (Sandbox Code Playgroud)

主.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 $0
    PUTC $'b
    hlt
handler:
    PUTC $'a
    iret
Run Code Online (Sandbox Code Playgroud)

建议

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