5

I'm trying to write an operating system using OSDev and others. Now, I'm stuck making a keyboard interrupt handler. When I compile my OS and run the kernel using qemu-system-i386 -kernel kernel/myos.kernel everything works perfectly. When I put everything into an ISO image and try to run it using qemu-system-i386 -cdrom myos.iso, it restarts when I press a key. I think it's caused by some problems in my interrupt handler or a bad IDT entry.

My keyboard handler (AT&T syntax):

.globl   keyboard_handler
.align   4

keyboard_handler:

    pushal
    cld 
    call keyboard_handler_main
    popal
    iret

My main handler in C:

void keyboard_handler_main(void) {
    unsigned char status;
  char keycode;
    /* write EOI */
    write_port(0x20, 0x20);

    status = read_port(KEYBOARD_STATUS_PORT);
    /* Lowest bit of status will be set if buffer is not empty */
    if (status & 0x01) {
        keycode = read_port(KEYBOARD_DATA_PORT);
        if(keycode < 0)
            return;

        if(keycode == ENTER_KEY_CODE) {
            printf("\n");
            return;
        }
        printf("%c", keyboard_map[(unsigned char) keycode]);
    }
}

C function I use to load the:

void idt_init(void)
{
    //unsigned long keyboard_address;
    unsigned long idt_address;
    unsigned long idt_ptr[2];

    auto keyboard_address = (*keyboard_handler);

    IDT[0x21].offset_lowerbits = keyboard_address & 0xffff;
    IDT[0x21].selector = KERNEL_CODE_SEGMENT_OFFSET;
    IDT[0x21].zero = 0;
    IDT[0x21].type_attr = INTERRUPT_GATE;
    IDT[0x21].offset_higherbits = (keyboard_address & 0xffff0000) >> 16;

    /*     Ports
    *    PIC1   PIC2
    *Command 0x20   0xA0
    *Data    0x21   0xA1
    */

    write_port(0x20 , 0x11);
    write_port(0xA0 , 0x11);

    write_port(0x21 , 0x20);
    write_port(0xA1 , 0x28);

    write_port(0x21 , 0x00);
    write_port(0xA1 , 0x00);

    write_port(0x21 , 0x01);
    write_port(0xA1 , 0x01);

    write_port(0x21 , 0xff);
    write_port(0xA1 , 0xff);

    idt_address = (unsigned long)IDT ;
    idt_ptr[0] = (sizeof (struct IDT_entry) * IDT_SIZE) + ((idt_address & 0xffff) << 16);
    idt_ptr[1] = idt_address >> 16 ;

    load_idt(idt_ptr);

    printf("%s\n", "loadd");
}

Files are organized the same way as OSDev's Meaty Skeleton. I do have a different bootloader.

Michael Petch
  • 46,082
  • 8
  • 107
  • 198
  • 3
    You are using printf() inside an interrupt-handler? – Martin James Jan 21 '18 at 19:11
  • I have my own implementation of printf. Shouldn't I? – Antoni Kiszka Jan 21 '18 at 19:21
  • Should you? Its seems a bit...strange. Can your printf be safely interrupted and reentered from interrupt-state? – Martin James Jan 21 '18 at 19:31
  • I think, yes. It's just checking for '\0' and % and then sending chars one by one to putchar(). – Antoni Kiszka Jan 21 '18 at 19:37
  • You can view it in [http://wiki.osdev.org/Meaty_Skeleton](http://wiki.osdev.org/Meaty_Skeleton), it's the same. – Antoni Kiszka Jan 21 '18 at 19:37
  • 1
    you don't show us all the code but consider this - the Multiboot spec requires you to set up your own GDT. The Multibootloader sets up its own but in the spec it says that the GDTR should assumed to be invalid and you should establish your own before reloading any of the segment registers. Using interrupts requires segment register changes. Just so happens when using QEMU (with the `-kernel` option) the GDTR is often valid, but when done via real GRUB (on an ISO) it is not. – Michael Petch Jan 21 '18 at 23:19
  • 2
    It appears meaty skeleton doesn't set up a GDT. Probably because they don't use interrupts and don't set any of the segment registers. If you added IDT code on top of meaty skeleton without adding code to set up the GDT then there is a good chance that is what is wrong here. But without all your code I'm making the best educate guess I can – Michael Petch Jan 21 '18 at 23:20
  • @MichaelPetch I didn't set up GDT, that was the problem. If you want to post an answer, do it. Thanks! – Antoni Kiszka Jan 22 '18 at 17:59

1 Answers1

3

Based on experience I believed that this issue was related to a GDT not being set up. Often when someone says interrupts work with QEMU's -kernel option but not a real version of GRUB it is often related to the kernel developer not creating and loading their own GDT. The Mulitboot Specification says:

‘GDTR’ Even though the segment registers are set up as described above, the ‘GDTR’ may be invalid, so the OS image must not load any segment registers (even just reloading the same values!) until it sets up its own ‘GDT’.

When using QEMU with the -kernel option the GDTR is usually valid but it isn't guaranteed to be that way. When using a real version of GRUB (installed to a hard drive, virtual image, ISO etc) to boot you may discover that the GDTR isn't in fact valid. The first time you attempt to reload any segment register with a selector (even if it is the same value) it may fault. When using interrupts the code segment (CS) will be modified which may cause a triple fault and reboot.

As well the Multiboot Specification doesn't say which selectors point to code or data descriptors. Since the layout of the GDT entries is not known or guaranteed by the Multiboot specification it poses a problem for filling in the IDT entries. Each IDT entry needs to specify a specific selector that points to a code segment.

The Meaty Skeleton tutorial on OSDev doesn't set up interrupts, nor do they modify any of the segment registers so that code likely works with QEMU's -kernel option and a real version of GRUB. Adding IDT and interrupt code on top of the base tutorial probably lead to the failure you are seeing when booting with GRUB. That tutorial should probably make it clearer that you should set up your own GDT and not rely on the one set up by the Multiboot loader.

Michael Petch
  • 46,082
  • 8
  • 107
  • 198