Interrupt handler on C doesn't work after one interrupt

3

I'm trying to implement keyboard interrupt handler using C and QEMU. But when I execute the program my handler print only one character. After that the handler doesn't work at all.

My IDT setup:

struct IDT_entry {
    unsigned short int offset_lowerbits;
    unsigned short int selector;
    unsigned char zero;
    unsigned char type_attr;
    unsigned short int offset_higherbits;
};

void setup_idt() {
    struct IDT_entry IDT[256];
    unsigned long keyboard_address;
    unsigned long idt_address;
    unsigned long idt_ptr[2];

    keyboard_address = (unsigned long) keyboard_handler;
    IDT[0x21].offset_lowerbits = keyboard_address & 0xffff;
    IDT[0x21].selector = 0x8;
    IDT[0x21].zero = 0;
    IDT[0x21].type_attr = 0x8e;
    IDT[0x21].offset_higherbits = (keyboard_address & 0xffff0000) >> 16;

    /*
                PIC1   PIC2
    Commands    0x20   0xA0
    Data        0x21   0xA1

    */

    // ICW1 - init
    outb(0x20, 0x11);
    outb(0xA0, 0x11);

    // ICW2 - reset offset address if IDT
    // first 32 interrpts are reserved
    outb(0x21, 0x20);
    outb(0xA1, 0x28);

    // ICW3 - setup cascading
    outb(0x21, 0b0);
    outb(0xA1, 0b0);

    // ICW4 - env info
    outb(0x21, 0b00000011);
    outb(0xA1, 0b00000011);
    // init finished

    // disable IRQs except IRQ1
    outb(0x21, 0xFD);
    outb(0xA1, 0xff);

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

    __asm__ __volatile__("lidt %0" :: "m" (*idt_ptr));
    __asm__ __volatile__("sti");
}

My keyboard handler:

// Variables for printing ==
unsigned int location = 0;
char* vga = (char*)0xb8000;
char letter;
// =========================

void keyboard_handler() {
    if (inb(0x64) & 0x01 && (letter = inb(0x60)) > 0) {
        vga[location] = keyboard_map[letter];
        vga[location+1] = 0x4;
        location += 2;
    }

    outb(0x20, 0x20);

    // __asm__ __volatile__("iret");
}

Main function (it is executed from my asm bootloader):

void kmain() {
    setup_idt();

    for (;;) {}
}

I think the problem is in "iret" instruction. Without it my kernel prints at least something (only one charachter, like I said before). But when I execute asm volatile("iret"); QEMU prints some garbage and then clear it after every keystroke ("SeaBios ..."). What do I have to do? Thank you!

c
gcc
assembly
interrupt-handling
osdev
asked on Stack Overflow Jun 28, 2019 by Гоша Обыночный • edited Jun 28, 2019 by Peter Cordes

1 Answer

6

If you compile without optimization, asm("iret") will probably run while the stack pointer is still pointing at a saved EBP value, because -fno-omit-frame-pointer is the default and the cleanup epilogue happens after the last C statement of the function.

Or it could be pointing at other saved registers. Anyway, tricking the compiler and jumping out of an inline asm statement is never going to be safe (unless you use asm goto to maybe jump to a C label inside the function, but that doesn't solve your problem).


Also, the C calling convention allows functions to clobber EAX, ECX, EDX, and the FPU state. Even if you did manage to hack an iret into your function, it would corrupt the state of the code that was interrupted. GCC will use SSE/x87 to implement _Atomic int64_t load/store in 32-bit mode, and for copying large objects, unless you compile with -mgeneral-regs-only

Also see @MichaelPetch's answer on the linked duplicate: Creating a C function without compiler generated prologue/epilogue & RET instruction? for more interesting points, and some non-GCC info.


There are 2 solutions here:

  • write a pure-asm wrapper that saves the call-clobbered regs, calls your C function, then returns with iret

  • declare your function with __attribute__((interrupt)) to tell GCC it's an interrupt handler. The gcc manual's x86 function attributes has an example.

    x86 support for that attribute is somewhat recent compared to traditionally-embedded ISAs like ARM, but modern GCC does know how emit functions that preserve all regs and end with iret. But you still need -mgeneral-regs-only.

See also https://wiki.osdev.org/Interrupt_Service_Routines#GCC_.2F_G.2B.2B which tells you the same thing as this answer.

(It also suggests an evil hack with pushad / popad; leave; iret which only works with optimzation disabled. I would not recommend that if you can possibly use a newer GCC that supports the interrupt attribute.)

The earlier parts of the wiki page cover the general problems with trying to use your own iret, so you can see what the total asm (compiler-generated + yours) would look like for your attempt.

answered on Stack Overflow Jun 28, 2019 by Peter Cordes • edited Jun 20, 2020 by Community

User contributions licensed under CC BY-SA 3.0