Reputation: 4391
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.
Upvotes: 7
Views: 6579
Reputation: 15229
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 aslgdt
)
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 jmp
s 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,÷_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"::)
Upvotes: 10