Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Setting up interrupts in protected mode (x86)

What is the process of setting up interrupts for protected mode?

This link says one should:

  • Make space for the interrupt descriptor table
  • Tell the CPU where that space is (see GDT Tutorial: lidt works the very same way as lgdt)
  • Tell the PIC that you no longer want to use the BIOS defaults (see Programming the PIC chips)
  • Write a couple of ISR handlers (see Interrupt Service Routines) for both IRQs and exceptions
  • Put the addresses of the ISR handlers in the appropriate descriptors
  • Enable all supported interrupts in the IRQ mask (of the PIC)

The third step makes no sense to me (I looked at this link but there wasn't anything about telling the PIC anything) so I ignored it and completed the next two steps, only to be clueless once again when I reached the final step. However, from my understanding of interrupts, both of the steps I didn't understand relate to hardware interrupts from the PIC controller and shouldn't affect the interrupts raised by the PIT on IRQ 0. I therefore ignored this step as well.

When I ran my code it compiled fine and even ran in a virtual machine, but the interrupt seemed to fire only once. I then realised that I wasn't sending EOI to the PIC, preventing it from raising any more interrupts. However, adding mov al, 0x20 and out 0x20, al just before the iret instruction makes the virtual machine crash.

Here's my IDT:

; idt
idt_start :

    dw 0x00         ; The interrupt handler is located at absolute address 0x00
    dw CODE_SEG     ; CODE_SEG points to the GDT entry for code
    db 0x0          ; The unused byte
    db 0b11101001   ; 1110 Defines a 32 bit Interrupt gate, 0 is mandatory, privilege level = 0 (0b00), the last bit is one so that the CPU knows that the interrupt will be used
    dw 0x00         ; The higher part of the offset (0x00) is 0x00

idt_end:

idt_descriptor :
    dw idt_end - idt_start - 1 ; Size of our idt, always one less than the actual size
    dd idt_start ; Start address of our idt

Here's my interrupt handler (located at absolute location 0x00 in memory):

ISR_0:
    push eax
    add [0x300], byte 
    mov al, 0x20
    out 0x20, al
    pop eax
    iret    
    times 512-($-$$) db 0

This is the code I use to enter protected mode and load the GDT and IDT into memory:

[bits 16]

switch_to_pm:

    cli
    lgdt [gdt_descriptor]
    lidt [idt_descriptor]
    mov eax, cr0
    or eax, 1
    mov cr0,eax
    jmp CODE_SEG:init_pm

[bits 32]

init_pm :

    mov ax, DATA_SEG
    mov ds, ax
    mov ss, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ebp, 0x90000
    mov esp, ebp
    sti
    call BEGIN_PM

My main function (that checks the value of 0x300) is as follows:

void main() {
    char iii[15];
    int * aa = (int *)0x300;
    for (;;)
    {
        setCursor(0, 0);
        print(itoab(*aa, iii));
    }
}

By the way, I have verified using a memory dump that everything is loaded at the correct address and everything is exactly where it is expected. For example, 0x300 is a free part of memory used simply to simplify my code.

like image 774
DividedByZero Avatar asked Jan 01 '16 23:01

DividedByZero


People also ask

How are interrupts handled in protected mode?

In this mode, instruction execution is stopped until either a nonmaskable interrupt or a reset signal is received. There are two special instructions to load (lidt) and store (sidt) the contents of the IDTR register. Both instructions take the address of a 6-byte memory as the operand.

How do I enable interrupts in x86?

The x86 has an interrupt flag (IF) in the FLAGS register. When this flag is set to 0, hardware interrupts are disabled, otherwise they are enabled. The command cli sets this flag to 0, and sti sets it to 1. Instructions that load values into the FLAGS register (such as popf and iret) may also modify this flag.

How interrupts are handled in protected mode & Idtr?

Protected and long mode The IDTR register is used to store both the linear base address and the limit (length in bytes minus 1) of the IDT. When an interrupt occurs, the processor multiplies the interrupt vector by the entry size (8 for protected mode, 16 for long mode) and adds the result to the IDT base address.

What is protected mode in x86?

Protected mode is an operational mode of the Intel 80286-compatible CPU. It permits system software to use features such as virtual memory, paging and safe multi-tasking. It is also designed to increase the OS's control over application software. This term is also known as protected virtual address mode.


1 Answers

Let's look at how some comparably small kernel, i.e., Linux 0.01 does it!

  • Make space for the interrupt descriptor table

This is done two times (well, technically only one time): first, the bootloader (the path is /boot/boot.s) initializes the IDTR, so the CPU is happy when jumping into Protected Mode. The IDTR content is as follows:

idt_48:
    .word   0            | idt limit=0
    .word   0,0        | idt base=0L

The IDTR is loaded like this:

lidt     idt_48     | load idt with 0,0

Now, the jump can be performed.
Note that there is no IDT here. It's just a dummy, so no error occurs somewhere in the kernel.

Afterwards, the real IDT is initialized (the path is /boot/head.s). The space is allocated like this:

_idt:   .fill 256,8,0       # idt is uninitialized
  • Tell the CPU where that space is (see GDT Tutorial: lidt works the very same way as lgdt)

lidt expects a linear address containing the content of the IDTR. That content looks like this:

idt_descr:
    .word 256*8-1       # idt contains 256 entries
    .long _idt

The IDTR is initialized as follows:

lidt idt_descr
  • Tell the PIC that you no longer want to use the BIOS defaults (see Programming the PIC chips)

As @RossRidge mentioned in the comments to your question, that means remapping the IRQ interrupt vectors (IVs).
Since the PIC IVs overlap with the Intel x86 exception addresses, we have to remap one of them. The exception addresses are hard-wired, so we need to remap the PIC vectors.
See also this comment right above the corresponding code by Linus:

| well, that went ok, I hope. Now we have to reprogram the interrupts :-(
| we put them right after the intel-reserved hardware interrupts, at
| int 0x20-0x2F. There they won't mess up anything. Sadly IBM really
| messed this up with the original PC, and they haven't been able to
| rectify it afterwards. Thus the bios puts interrupts at 0x08-0x0f,
| which is used for the internal hardware interrupts as well. We just
| have to reprogram the 8259's, and it isn't fun.

Now, here's the real code. The jmps in between are for synchronizing CPU and PIC, so the CPU won't send data the PIC cannot receive yet. This is comparable to wait states when writing to memory: when the CPU is faster than the memory/memory arbiter, it needs to wait some time before accessing memory the next time.

mov al,#0x11        | initialization sequence
out #0x20,al        | send it to 8259A-1
.word   0x00eb,0x00eb       | jmp $+2, jmp $+2
out #0xA0,al        | and to 8259A-2
.word   0x00eb,0x00eb
mov al,#0x20        | start of hardware int's (0x20)
out #0x21,al
.word   0x00eb,0x00eb
mov al,#0x28        | start of hardware int's 2 (0x28)
out #0xA1,al
.word   0x00eb,0x00eb
mov al,#0x04        | 8259-1 is master
out #0x21,al
.word   0x00eb,0x00eb
mov al,#0x02        | 8259-2 is slave
out #0xA1,al
.word   0x00eb,0x00eb
mov al,#0x01        | 8086 mode for both
out #0x21,al
.word   0x00eb,0x00eb
out #0xA1,al
.word   0x00eb,0x00eb
mov al,#0xFF        | mask off all interrupts for now
out #0x21,al
.word   0x00eb,0x00eb
out #0xA1,al
  • Write a couple of ISR handlers (see Interrupt Service Routines) for both IRQs and exceptions

For exceptions, you can find the handler code in /kernel/traps.c and /kernel/asm.s.
Some exceptions push an error code on the stack prior to jumping to the handler, which you have to pop off or the iret instruction will fail. A page fault also writes the corresponding virtual address to cr2 in addition.
The IRQ handlers are spread across the whole system. -.- The timer and disk interrupt handlers are in /kernel/system_call.s, the keyboard interrupt handler is in /kernel/keyboard.s, for example.

  • Put the addresses of the ISR handlers in the appropriate descriptors

The initialization for exceptions is done in /kernel/traps.c in the trap_init function:

void trap_init(void)
{
    int i;

    set_trap_gate(0,&divide_error);
    set_trap_gate(1,&debug);
    set_trap_gate(2,&nmi);
    set_system_gate(3,&int3);   /* int3-5 can be called from all */
    set_system_gate(4,&overflow);
    set_system_gate(5,&bounds);
    set_trap_gate(6,&invalid_op);
    set_trap_gate(7,&device_not_available);
    set_trap_gate(8,&double_fault);
    set_trap_gate(9,&coprocessor_segment_overrun);
    set_trap_gate(10,&invalid_TSS);
    set_trap_gate(11,&segment_not_present);
    set_trap_gate(12,&stack_segment);
    set_trap_gate(13,&general_protection);
    set_trap_gate(14,&page_fault);
    set_trap_gate(15,&reserved);
    set_trap_gate(16,&coprocessor_error);
    for (i=17;i<32;i++)
        set_trap_gate(i,&reserved);
/*  __asm__("movl $0x3ff000,%%eax\n\t"
        "movl %%eax,%%db0\n\t"
        "movl $0x000d0303,%%eax\n\t"
        "movl %%eax,%%db7"
        :::"ax");*/
}

The IRQ handler entry initializations are again spread across several files. sched_init from /kernel/sched.c initializes the timer interrupt handler's address, for instance.

  • Enable all supported interrupts in the IRQ mask (of the PIC)

This is done in /init/main.c in the main function with the macro sti. It is defined in /asm/system.h as follows:

#define sti() __asm__ ("sti"::) 
like image 140
cadaniluk Avatar answered Sep 28 '22 03:09

cadaniluk