Невозможно изменить регистр сегмента данных. При попытке общей защиты выдается ошибка

Я пытался создать обработчик ISR, следуя этому учебник Джеймса Моллоя, но я застрял. Всякий раз, когда я запускаю программное прерывание, регистры общего назначения и регистр сегмента данных помещаются в стек, а переменные автоматически помещаются ЦП. Затем сегмент данных изменяется на значение 0x10 (дескриптор сегмента данных ядра), поэтому уровни привилегий меняются. Затем, после того как обработчик вернет эти значения, они будут poped. Но всякий раз, когда значение в ds изменяется, выдается GPE с кодом ошибки 0x2544, и через несколько секунд виртуальная машина перезапускается. (линкер и компилятор 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

Мои макросы обработчика 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, который приводит к «Получено прерывание: 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);
}

И моя основная функция:

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
}

Как видите, код должен был вывести,

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

Это продолжается до тех пор, пока виртуальная машина не перезапустится.

Что я делаю неправильно?


person Eralp Çelebi    schedule 06.06.2019    source источник
comment
Проголосуйте за хорошо заданный вопрос.   -  person fuz    schedule 06.06.2019
comment
Эммм.....вы вызываете printf() в обработчике прерываний?   -  person Martin James    schedule 06.06.2019
comment
Что такое registers_t? Где это определено?   -  person 1201ProgramAlarm    schedule 06.06.2019


Ответы (1)


Код неполный, но я предполагаю, что то, что вы видите, является результатом хорошо известной ошибки в учебнике 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

А затем измените 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); 

to be:

interrupt_handlers[r->int_no](regs);

Важно: такие же изменения необходимо внести и в обработчики ISR. И обработчики IRQ, и ISR, и связанный с ними код имеют одну и ту же проблему.

person Michael Petch    schedule 06.06.2019
comment
Я смог проверить ваш ответ, и, похоже, это причина, по которой он вызвал GPE. На самом деле я проверил ссылку об известных ошибках в учебнике, но это никогда не приходило мне в голову. Спасибо за ответ - person Eralp Çelebi; 07.06.2019