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

Cannot modify data segment register. When tried General Protection Error is thrown

我一直在尝试创建一个 ISR 处理程序 tutorial 作者 James Molloy,但我卡住了。每当我抛出一个软件中断时,通用寄存器和数据段寄存器都会被压入堆栈,变量由 CPU 自动压入。然后将数据段更改为 0x10(内核数据段描述符)的值,从而更改特权级别。然后在处理程序 returns 之后,这些值被 poped。但是,每当 ds 中的值发生更改时,都会抛出错误代码为 0x2544 的 GPE,几秒钟后 VM 将重新启动。 (链接器和编译器 i386-elf-gcc,汇编器 nasm)

我尝试在指令之间放置 hlt 条指令,以找出引发 GPE 的指令。在那之后,我能够发现“mov ds,ax”指令。我尝试了各种方法,比如删除由 bootstrap 代码初始化的堆栈,删除代码的特权更改部分。我可以从公共存根中 return 的唯一方法是删除更改特权级别的代码部分,但由于我想转向用户模式,我仍然希望它们保留。

这是我的常用存根:

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

我的 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
...

我的 C 处理程序导致 "Received interrupt: 0xD err. code 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);
}

我的主要功能:

void kmain(struct multiboot *mboot_ptr) {
    descinit(); // Sets up IDT and GDT
    ttyinit(TTY0); // Sets up the VGA Framebuffer
    asm volatile ("int [=13=]x1"); // Triggers a software interrupt
    printf("Wow"); // After that its supposed to print this
}

如您所见,代码应该输出,

ds: 0x10
Received interrupt: 0x1 with err. code: 0

但结果是,

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

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

这种情况一直持续到 VM 自行重启。

我做错了什么?

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

Problem: Interrupt handlers corrupt interrupted state

This article previously told you to know the ABI. If you do you will see a huge problem in the interrupt.s suggested by the tutorial: It breaks the ABI for structure passing! It creates an instance of the struct registers on the stack and then passes it by value to the isr_handler function and then assumes the structure is intact afterwards. However, the function parameters on the stack belongs to the function and it is allowed to trash these values as it sees fit (if you need to know whether the compiler actually does this, you are thinking the wrong way, but it actually does). There are two ways around this. The most practical method is to pass the structure as a pointer instead, which allows you to explicitly edit the register state when needed - very useful for system calls, without having the compiler randomly doing it for you. The compiler can still edit the pointer on the stack when it's not specifically needed. The second option is to make another copy the structure and pass that

问题在于 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

然后修改 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);
    }
}

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

interrupt_handlers[r->int_no](*regs); 

成为:

interrupt_handlers[r->int_no](regs);

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