无法修改数据段寄存器。尝试时抛出一般保护错误

Era*_*ebi 6 x86 assembly gcc osdev isr

我一直在尝试按照James Molloy 的本教程创建一个 ISR 处理程序 ,但我被卡住了。每当我抛出软件中断时,通用寄存器和数据段寄存器都会被推送到堆栈中,而 CPU 会自动推送变量。然后将数据段更改为 0x10(内核数据段描述符)的值,从而更改权限级别。然后在处理程序返回后,这些值被pop编辑。但是,无论何时ds更改中的值,都会抛出错误代码为 0x2544 的 GPE,几秒钟后 VM 会重新启动。(链接器和编译器 i386-elf-gcc ,汇编器 nasm)

我尝试在hlt指令之间放置指令以定位哪个指令正在抛出 GPE。在那之后,我能够找到“mov ds,ax”指令。我尝试了各种方法,例如删除由引导程序代码初始化的堆栈以删除代码的权限更改部分。我可以从公共存根返回的唯一方法是删除更改权限级别的代码部分,但是当我想转向用户模式时,我仍然希望它们保留。

这是我常用的存根:

isr_common_stub:
    pusha                    ; Pushes edi,esi,ebp,esp,ebx,edx,ecx,eax
    xor eax,eax
    mov ax, ds               ; Lower 16-bits of eax = ds.
    push eax                 ; save the data segment descriptor

    mov ax, 0x10  ; load the kernel data segment descriptor
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax

    call isr_handler

    xor eax,eax
    pop eax
    mov ds, ax ; This is the instruction everything fails;
    mov es, ax
    mov fs, ax
    mov gs, ax
    popa
    iret
Run Code Online (Sandbox Code Playgroud)

我的 ISR 处理程序宏:

extern isr_handler

%macro ISR_NOERRCODE 1
  global isr%1        ; %1 accesses the first parameter.
  isr%1:
    cli
    push byte 0
    push %1
    jmp isr_common_stub
%endmacro

%macro ISR_ERRCODE 1
  global isr%1
  isr%1:
    cli
    push byte %1
    jmp isr_common_stub
%endmacro
ISR_NOERRCODE 0
ISR_NOERRCODE 1
ISR_NOERRCODE 2
ISR_NOERRCODE 3
...
Run Code Online (Sandbox Code Playgroud)

我的 C 处理程序导致“收到中断:0xD 错误。代码 0x2544”

#include <stdio.h>
#include <isr.h>
#include <tty.h>

void isr_handler(registers_t regs) {
    printf("ds: %x \n" ,regs.ds);
    printf("Received interrupt: %x with err. code: %x \n", regs.int_no, regs.err_code);
}

Run Code Online (Sandbox Code Playgroud)

而我的主要功能:

void kmain(struct multiboot *mboot_ptr) {
    descinit(); // Sets up IDT and GDT
    ttyinit(TTY0); // Sets up the VGA Framebuffer
    asm volatile ("int $0x1"); // Triggers a software interrupt
    printf("Wow"); // After that its supposed to print this
}
Run Code Online (Sandbox Code Playgroud)

正如你所看到的代码应该输出,

ds: 0x10
Received interrupt: 0x1 with err. code: 0
Run Code Online (Sandbox Code Playgroud)

但结果是,

...
ds: 0x10
Received interrupt: 0xD with err. code: 0x2544

ds: 0x10
Received interrupt: 0xD with err. code: 0x2544
...
Run Code Online (Sandbox Code Playgroud)

这一直持续到 VM 自行重新启动。

我究竟做错了什么?

Mic*_*tch 5

代码并不完整,但我猜你看到的是 James Molloy 的 OSDev 教程中一个众所周知的错误的结果。OSDev 社区在勘误表中编制了一份已知错误列表。我建议查看并修复那里提到的所有错误。特别是在这种情况下,我认为导致问题的错误是这个:

问题:中断处理程序破坏了中断状态

这篇文章之前告诉你要了解ABI。如果你这样做了,你会在教程建议的中断中看到一个巨大的问题:它破坏了结构传递的 ABI!它在堆栈上创建结构寄存器的一个实例,然后按值将其传递给 isr_handler 函数,然后假设结构是完整的。但是,堆栈上的函数参数属于该函数,并且可以按照它认为合适的方式丢弃这些值(如果您需要知道编译器是否真的这样做了,那么您的想法是错误的,但它确实这样做了)。有两种方法可以解决这个问题。最实用的方法是将结构体作为指针传递,这允许您在需要时显式编辑寄存器状态 - 对于系统调用非常有用,无需编译器随机为您执行此操作。当不是特别需要时,编译器仍然可以编辑堆栈上的指针。第二种选择是制作另一个副本结构并传递它

问题是 32 位 System V ABI 不能保证按值传递的数据在堆栈上不会被修改!编译器可以自由地为它选择的任何目的重用该内存。编译器可能生成的代码破坏了堆栈上存储DS的区域。当DS设置为虚假值时,它崩溃了。您应该做的是通过引用而不是值传递。我建议在汇编代码中更改这些代码:

irq_common_stub:
    pusha
    mov ax, ds
    push eax
    mov ax, 0x10 ;0x10
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    push esp                 ; At this point ESP is a pointer to where GS (and the rest
                             ; of the interrupt handler state resides)
                             ; Push ESP as 1st parameter as it's a 
                             ; pointer to a registers_t  
    call irq_handler
    pop ebx                  ; Remove the saved ESP on the stack. Efficient to just pop it 
                             ; into any register. You could have done: add esp, 4 as well
    pop ebx
    mov ds, bx
    mov es, bx
    mov fs, bx
    mov gs, bx
    popa
    add esp, 8
    sti
    iret
Run Code Online (Sandbox Code Playgroud)

然后修改irq_handler为使用registers_t *regs而不是registers_t regs

void irq_handler(registers_t *regs) {
    if (regs->int_no >= 40) port_byte_out(0xA0, 0x20);
    port_byte_out(0x20, 0x20);

    if (interrupt_handlers[regs->int_no] != 0) {
        interrupt_handlers[regs->int_no](*regs);
    }
    else
    {
        klog("ISR: Unhandled IRQ%u!\n", regs->int_no);
    }
}
Run Code Online (Sandbox Code Playgroud)

我实际上建议每个中断处理程序都使用一个指针registers_t来避免不必要的复制。如果您的中断处理程序和interrupt_handlers数组使用了registers_t *作为参数的函数(而不是registers_t),那么您需要修改代码:

interrupt_handlers[r->int_no](*regs); 
Run Code Online (Sandbox Code Playgroud)

成为:

interrupt_handlers[r->int_no](regs);
Run Code Online (Sandbox Code Playgroud)

重要提示:您还必须对ISR 处理程序进行这些相同类型的更改。IRQ 和 ISR 处理程序以及相关代码都存在同样的问题。

  • 我能够测试您的答案,这似乎是它触发 GPE 的原因。我实际上检查了有关本教程的已知错误的链接,但我从未想到这一点。谢谢你的回答 (2认同)