Experiments with IRQs and the iret instruction

Question about which tools to use, bugs, the best way to implement a function, etc should go here. Don't forget to see if your question is answered in the wiki first! When in doubt post here.
Post Reply
sunnysideup
Member
Member
Posts: 106
Joined: Sat Feb 08, 2020 11:11 am
Libera.chat IRC: sunnysideup

Experiments with IRQs and the iret instruction

Post by sunnysideup »

I've been writing a hobby OS, and I'm trying to do interrupt/exception handling in the kernel. I'm in ring 0, so there's no inter privilege stack switch,etc. These are my routines:

Code: Select all

#include <stdint.h>
#include "dadio.h"

#define MAX_INTERRUPTS 256
#define IDT_DESC_BIT16 0x06	//00000110
#define IDT_DESC_BIT32 0x0E	//00001110
#define IDT_DESC_RING1 0x40	//01000000
#define IDT_DESC_RING2 0x20	//00100000
#define IDT_DESC_RING3 0x60	//01100000
#define IDT_DESC_PRESENT 0x80//10000000

//Structs used in this routine

typedef struct __attribute__ ((__packed__)) idtr {
	uint16_t		limit;
	uint32_t		base;
}idtr_t;

typedef struct __attribute__ ((__packed__)) gdtr {
	uint16_t		limit;
	uint32_t		base;
}gdtr_t;

typedef struct __attribute__ ((__packed__)) idt_descriptor {
uint16_t		baseLo;
uint16_t		sel;
uint8_t			reserved;
uint8_t			flags;
uint16_t		baseHi;
}idt_descriptor_t;

typedef struct __attribute__((__packed__)) gdt_descriptor {
uint16_t		limit;
uint16_t		baseLo;
uint8_t			baseMid;
uint16_t		flags;
uint8_t			baseHi;
} gdt_descriptor_t;

//External assembly functions
void init_pic();
void install_idt(idtr_t* address);
void enable_interrupts();

//Global variables in this routine
static idt_descriptor_t _idt[MAX_INTERRUPTS];
static idtr_t _idtr; //This will be the 6 byte base + limit

//Helper functions
static void install_ir(uint32_t index,uint16_t flags, uint16_t sel, uint32_t* handler_address);
static void default_handler();



void idt_init()
{
	_idtr.base = (uint32_t)_idt;
	_idtr.limit = (sizeof (idt_descriptor_t) * MAX_INTERRUPTS) -1 ;
	
	for (int i=0;i<MAX_INTERRUPTS;i++)
	{
		_idt[i].baseLo = 0;
		_idt[i].sel = 0;
		_idt[i].reserved = 0;
		_idt[i].flags = 0;
		_idt[i].baseHi = 0;
	}

	for (int i=0;i<MAX_INTERRUPTS;i++)
		install_ir(i,IDT_DESC_BIT32 | IDT_DESC_PRESENT, 0x08, (uint32_t*) default_handler);

	init_pic();
	install_idt(& _idtr);
	enable_interrupts();
}
static void install_ir(uint32_t index,uint16_t flags, uint16_t sel, uint32_t* handler_address)
{
	if (index >=MAX_INTERRUPTS) return;

	_idt[index].baseLo = (uint32_t)handler_address & 0xffff;
	_idt[index].baseHi = ((uint32_t)handler_address >> 16) & 0xffff;
	_idt[index].reserved = 0;
	_idt[index].flags = flags;
	_idt[index].sel = sel;
}

static void default_handler()
{
	monitor-puts("This is the default exception handler"); //This is a routine that prints messages on the screen... The gist is that it writes to 0xb8000 and so on...
	for (;;);
}
Assembly routines

Code: Select all

init_pic:
	mov al, 0x11  ;ICW 1  ;Expect IC4|single?|0|level?|init?|000
	out 0x20,al
	out 0xA0,al
	
	mov al,0x20  ;Remapping the IRQs
	out 0x21,al
	mov al,0x28
	out 0xA1,al

	; Send ICW 3 to primary PIC
	mov al, 0x4	 ; 0x4 = 0100 Second bit (IR Line 2)
	out 0x21, al ; write to data register of primary PIC

	; Send ICW 3 to secondary PIC
	mov al, 0x2	; 010=> IR line 2
	out 0xA1, al ; write to data register of secondary PIC

	; Send ICW 4 - Set x86 mode --------------------------------
 
	mov al, 1	; bit 0 enables 80x86 mode
 
	out 0x21, al
	out 0xA1, al
	
	; Zeroing out the data registers
 
	mov al, 0
	out 0x21, al
	out 0xA1, al
	
	ret

enable_interrupts:
	sti
	ret
A minimal kernel would be:

Code: Select all

void kmain()
{
idt_init();
return;
}
If I comment out the init_pic() in the idt_init function, I get the message: This is the default exception handler, followed by the for(;;).
I think that this is expected because as soon as I enable interrupts, something like the timer will send an IRQ, and since it's mapped to the (divide by zero?) exception by default, I get the handler message that I've defined.

But if I uncomment the init_pic(), I don't get the message. I understand that the IRQs (0-15) have been remapped to interrupt vectors (32 - 47). But a timer interrupt would still fire, and I should get the message. (All of the 256 possible interrupts/exceptions are mapped to the same routine in my case, I think).
Where have I gone wrong?

Also, a small followup question. I know that some exceptions will push an error code while some do not. But the iret instruction cannot know that right? So is it the programmers responsibility to manually add to the esp (popping of the error code if the exception does push an error) and then doing an iret?

I've read the 80386 developers manual, from where I understand this. Am I wrong somewhere?

PS: I've tried to provide the bare minimum of code, although my project has a lot more code.
Last edited by sunnysideup on Tue Mar 10, 2020 10:21 am, edited 1 time in total.
Octocontrabass
Member
Member
Posts: 5575
Joined: Mon Mar 25, 2013 7:01 pm

Re: Experiments with IRQs and the iret instruction

Post by Octocontrabass »

sunnysideup wrote:

Code: Select all

void kmain()
{
idt_init();
return;
}
What does this kernel return to?
sunnysideup wrote:So is it the programmers responsibility to manually add to the esp (popping of the error code of the exception does push an error) and then doing an iret?
Correct.
sunnysideup
Member
Member
Posts: 106
Joined: Sat Feb 08, 2020 11:11 am
Libera.chat IRC: sunnysideup

Re: Experiments with IRQs and the iret instruction

Post by sunnysideup »

My setup is this... I've written a bootloader that loads the kernel at 1M.
The kernel consists of an entry stub (asm) that calls kmain. So kmain returns to the asm stub. It's something like:

Code: Select all

extern kmain

call kmain
cli
hlt
Octocontrabass
Member
Member
Posts: 5575
Joined: Mon Mar 25, 2013 7:01 pm

Re: Experiments with IRQs and the iret instruction

Post by Octocontrabass »

When you return from kmain, you disable interrupts.

Adjust either kmain or your assembly stub to run an infinite loop with a HLT instruction, without disabling interrupts.
sunnysideup
Member
Member
Posts: 106
Joined: Sat Feb 08, 2020 11:11 am
Libera.chat IRC: sunnysideup

Re: Experiments with IRQs and the iret instruction

Post by sunnysideup »

I will try that :D

But why does commenting out the "init_pic" routine work (get into the handler) in my case? Any idea why that would happen?
Octocontrabass
Member
Member
Posts: 5575
Joined: Mon Mar 25, 2013 7:01 pm

Re: Experiments with IRQs and the iret instruction

Post by Octocontrabass »

The PICs keep track of IRQs they receive, and allow you to delay IRQ delivery depending on your needs. For example, if an IRQ occurs while you have interrupts disabled, you'll receive that IRQ once you enable interrupts.

Initializing the PICs has the side effect of losing any pending IRQs. Your bootloader probably spends a lot of time with interrupts disabled before it jumps to your kernel, long enough for a timer IRQ to occur and be buffered by the PIC. When you don't initialize the PIC, you receive the buffered IRQ as soon as you enable interrupts. When you do initialize the PIC, you lose the buffered IRQ, and there isn't enough time to receive another IRQ before you disable interrupts forever.
nullplan
Member
Member
Posts: 1792
Joined: Wed Aug 30, 2017 8:24 am

Re: Experiments with IRQs and the iret instruction

Post by nullplan »

sunnysideup wrote:Also, a small followup question. I know that some exceptions will push an error code while some do not. But the iret instruction cannot know that right? So is it the programmers responsibility to manually add to the esp (popping of the error code if the exception does push an error) and then doing an iret?
Completely correct. Most OSes will even push an error code in all the exceptions that don't have one on their own, just so the handling on exit is the same. But yes, the error code has to be removed from stack before you can iret.
Carpe diem!
sunnysideup
Member
Member
Posts: 106
Joined: Sat Feb 08, 2020 11:11 am
Libera.chat IRC: sunnysideup

Re: Experiments with IRQs and the iret instruction

Post by sunnysideup »

Alright, I've been doing a bit of further experimenting...
Here's what I've done:

* Comment out the for loop in the default exception handler, and add an enable interrupt routine. It looks as following:

Code: Select all

static void default_handler()
{
        printf("This is the default exception handler");
        enable_interrupts();
//      for (;;);
}
* Change cli ; hlt to :

Code: Select all

intloop:
cli
jmp intloop
In this case, as expected (and predicted by octocontrabass), I get the message. However, I get it only once. Here's my reasoning why it was only once... I didn't send an EOI command to the PIC controller and all the rest of the interrupts have been masked... Am I right??

PS: Also, I should expect some kind of stack smashing because I've not used an iret instruction anywhere, so EFLAGS just gets "accumulated" on the stack... Right?
sunnysideup
Member
Member
Posts: 106
Joined: Sat Feb 08, 2020 11:11 am
Libera.chat IRC: sunnysideup

Re: Experiments with IRQs and the iret instruction

Post by sunnysideup »

I'm guessing I need assembly stubs that would provide the iret instructions... However, it looks like I need to hardcode these assembly stubs as follows (a basic guess):

Code: Select all

vect_0:
   call c_handler_0
   iret
vect_1:
   call c_handler_1
   iret
;And so on....
This seems like really bad practice ... You're never supposed to copy code =P~ . How can I reduce the number of these assembly stubs? Can I have a single assembly stub? If I do have this, how will I know the interrupt vector number?
sunnysideup
Member
Member
Posts: 106
Joined: Sat Feb 08, 2020 11:11 am
Libera.chat IRC: sunnysideup

Re: Experiments with IRQs and the iret instruction

Post by sunnysideup »

Okay, I've done a bit more experimenting and this is my modified IDT_enable() function:

Code: Select all

void isr32();
void isr33();
void isr34();
void isr35();
void isr36();
void isr37();
void isr38();
void isr39();

void idt_init()
{
	_idtr.base = (uint32_t)_idt;
	_idtr.limit = (sizeof (idt_descriptor_t) * MAX_INTERRUPTS) -1 ;
	
	for (int i=0;i<MAX_INTERRUPTS;i++)
	{
		_idt[i].baseLo = 0;
		_idt[i].sel = 0;
		_idt[i].reserved = 0;
		_idt[i].flags = 0;
		_idt[i].baseHi = 0;
	}

	for (int i=0;i<MAX_INTERRUPTS;i++)
		install_ir(i,IDT_DESC_BIT32 | IDT_DESC_PRESENT, 0x08, (uint32_t*) default_handler);

	install_ir(32,IDT_DESC_BIT32|IDT_DESC_PRESENT,0x08,(uint32_t*)isr32);
	install_ir(33,IDT_DESC_BIT32|IDT_DESC_PRESENT,0x08,(uint32_t*)isr33);
	install_ir(34,IDT_DESC_BIT32|IDT_DESC_PRESENT,0x08,(uint32_t*)isr34);
	install_ir(35,IDT_DESC_BIT32|IDT_DESC_PRESENT,0x08,(uint32_t*)isr35);
	install_ir(36,IDT_DESC_BIT32|IDT_DESC_PRESENT,0x08,(uint32_t*)isr36);
	install_ir(37,IDT_DESC_BIT32|IDT_DESC_PRESENT,0x08,(uint32_t*)isr37);
	install_ir(38,IDT_DESC_BIT32|IDT_DESC_PRESENT,0x08,(uint32_t*)isr38);
	install_ir(39,IDT_DESC_BIT32|IDT_DESC_PRESENT,0x08,(uint32_t*)isr39);

	init_pic();
	install_idt(& _idtr);
	enable_interrupts();
}
These interrupt stubs (isr32,isr33,etc. are assembly defined stubs as follows:

Code: Select all

global isr32
global isr33
global isr34
global isr35
global isr36
global isr37
global isr38
global isr39

extern timer_handler
extern keyboard_handler
extern isr34handler
extern isr35handler
extern isr36handler
extern isr37handler
extern isr38handler
extern isr39handler

extern send_EOI_master


isr32:
	pusha
	call timer_handler
	call send_EOI_master
	popa
	iret
isr33:
	pusha
	call keyboard_handler
	call send_EOI_master
	popa
	iret
isr34:
isr35:
isr36:
isr37:
isr38:
isr39:
	pusha
	call send_EOI_master
	popa
	iret

This is supplemented by the following c code:

Code: Select all

void timer_handler(){
	monitor_print("This is handler for interrupt number 32\n");
}
void keyboard_handler(){
	monitor_print("This is handler for interrupt number 33\n");
}
This works as expected... I get "This is handler for interrupt number 32" everytime there is a timer tick. So all is fine here
Suppose I comment out the code inside the timer_handler() such that it's a dummy routine. So I expect that everytime I press a key on my keyboard, I should get "This is handler for interrupt number 33". The first time I press any key, it works as expected, and after that it doesn't respond to any of my key presses... Where could I be wrong here?
Octocontrabass
Member
Member
Posts: 5575
Joined: Mon Mar 25, 2013 7:01 pm

Re: Experiments with IRQs and the iret instruction

Post by Octocontrabass »

sunnysideup wrote:This seems like really bad practice ... You're never supposed to copy code =P~ . How can I reduce the number of these assembly stubs? Can I have a single assembly stub? If I do have this, how will I know the interrupt vector number?
You can reduce the amount of copy/paste by using macros. Some popular (but buggy) tutorials use this method.
sunnysideup wrote:The first time I press any key, it works as expected, and after that it doesn't respond to any of my key presses... Where could I be wrong here?
Most hardware - including the keyboard controller - requires you to acknowledge the IRQ before it will generate another. The timer is unusual for not requiring this.
sunnysideup
Member
Member
Posts: 106
Joined: Sat Feb 08, 2020 11:11 am
Libera.chat IRC: sunnysideup

Re: Experiments with IRQs and the iret instruction

Post by sunnysideup »

Ah cool that makes sense...
What would you advise that I change in my current code?
Does it feel solid? Any suggestions/ ways to improve?
sunnysideup
Member
Member
Posts: 106
Joined: Sat Feb 08, 2020 11:11 am
Libera.chat IRC: sunnysideup

Re: Experiments with IRQs and the iret instruction

Post by sunnysideup »

Here's a hypothetical scenario that should hopefully help me understand the PICs better....

I'm doing a bit of OSdev, and I'm doing interrupt handling now.

I have a few questions regarding the working of the PIC.

Let's assume that we have one PIC. This would have 8 input lines (IRQs /IRs - which is the correct terminology??). It should also have an output to the processor (INTR along with an INTA for request and acknowledgement). Moreover it must also have data lines for communication with the processor and that seems to be it for this scenario. Inside, it must have the In-service-register, Interrupt-request-register and the interrupt-mask-register.

I'm guessing that all of these are 8 bits in size.

Suppose we start off with the PIC in edge triggered operation mode. I'm guessing all the registers will have a value 0 (all bits 0). Is this right?

Now an external device sends a pulse (a short trigger) along IRQ0. I'm guessing that the IRR becomes 0b00000001 now?. What does the PIC do now?

Here's my guess - It checks the IMR, which is 0b00000000. It realizes that the interrupt isn't masked and sets the ISR to 0x00000001 and interrupts the processor (intr line). What happens to the IMR here? What's its state? I know that other interrupts are masked or something... So is IMR = 0b11111111??

So continuing with the scenario, the processor sends inta, to which the PIC sends the vector number of the interrupt to the processor,(which is nothing but the IRQ + the vector offset that was programmed into the PIC initially) and the processor does it's thing, i.e, push the EFLAGS,CS,EIP registers on the stack and jump to the interrupt routine, and so on.

Now the processor is working on the interrupt and hasn't sent the EOI command to the PIC yet.

What happens when some other interrupt (eg. IRQ1) fires now? What will be the state of the PIC registers? I'm guessing that since the IMR is all 1s, the IRR would be 0b00000011 and the ISR would be 0b00000001. I'm pretty sure I'm wrong here somewhere =P~

What would happen after the processor issues the EOI command to the PIC??
Octocontrabass
Member
Member
Posts: 5575
Joined: Mon Mar 25, 2013 7:01 pm

Re: Experiments with IRQs and the iret instruction

Post by Octocontrabass »

sunnysideup wrote:Now an external device sends a pulse (a short trigger) along IRQ0. I'm guessing that the IRR becomes 0b00000001 now?. What does the PIC do now?
The PIC compares the IRR against the IMR. Since the interrupt is unmasked, it then checks the ISR to see if a higher-priority interrupt is currently in service. Since there are no interrupts in service, the PIC attempts to interrupt the CPU. The IMR doesn't change. The ISR doesn't change until after the CPU acknowledges the interrupt (INTA).

After the CPU acknowledges the interrupt, the PIC sends the interrupt vector, sets the appropriate bit in the ISR, and clears the appropriate bit in the IRR. The CPU does the usual interrupt handling stuff, whatever that may be (it varies depending on the CPU mode).
sunnysideup wrote:What happens when some other interrupt (eg. IRQ1) fires now?
The PIC sets the corresponding bit in the IRR, then compares the IRR with the IMR. Since this interrupt is not masked, it then checks the ISR to see if a higher-priority interrupt is currently in service. Since IRQ0 is already in service, the PIC does not attempt to interrupt the CPU.
sunnysideup wrote:What would happen after the processor issues the EOI command to the PIC??
The PIC clears the appropriate bit in the ISR, then compares the IRR with the IMR. Since IRQ1 is not masked, it then checks the ISR to see if a higher-priority interrupt is currently in service. Since there are no longer any IRQs in service, the PIC attempts to interrupt the CPU.

You might also want to read the Intel 8259A datasheet. (PDF)
sunnysideup
Member
Member
Posts: 106
Joined: Sat Feb 08, 2020 11:11 am
Libera.chat IRC: sunnysideup

Re: Experiments with IRQs and the iret instruction

Post by sunnysideup »

Cool thanks :D
Post Reply