Implementing FRED
A while back on January 30th of this year a few people in my online communities were discussing who would be the first hobby operating system to end up implementing FRED which is a new mechanism for handling interrupts and syscalls separate from the legacy IDT.
I ended up having a once over at the specification for it and decided it wouldn't be too bad to implement into my kernel especially since the existing linux patches are relatively simple. Although I decided to not look too closely at the linux kernel due to it being more complicated for my desires and having the skill issue of being unable to read linux kernel code at times.
The IDT
Before we get into the details of FRED I would like to talk about what was used before. The Interrupt Descriptor Table. On x86-64 which is the architecture I am most familiar with it takes the form of an array of 256 structs with a strangeish format due to it originating from the 8086 as the IVT and progressing with the x86 architecture along with the GDT added with the 286 and the 16 bit protected modes causing the structure to look something like this when represented in C
struct [[gnu::packed]] IDTEntry {
uint16_t offset_low;
uint16_t selector;
uint8_t ist;
uint8_t type_attr;
uint16_t offset_mid;
uint32_t offset_high;
uint32_t zero;
};
In this structure the offset/address of the has ended up split across so when filling it in you have to do some shifts like this to properly get it filled in
void set_idt_entry(struct IDTEntry idt[], int index, int ist, int attr, void (*handler)()) {
uint64_t addr = (uint64_t)handler;
idt[index] = (struct IDTEntry){
.offset_low = addr & 0xFFFF,
.selector = 0x08,
.ist = ist,
.type_attr = attr,
.offset_mid = (addr >> 16) & 0xFFFF,
.offset_high = (addr >> 32),
.zero = 0
};
}
Then once you use the lidt instruction with an IDTR to set the address and the size of the structure the IDT is loaded and you can start getting
interrupts to the functions you set. But you shouldn't although you can
do this in C or any high level language for that matter due to how you need to handle an interrupt made partly worse by how the CPU give you information.
When you get an interrupt the CPU pushes the following things onto the stack (which may be changed based on an IST or the RSP0 in the TSS)
- Error Code (!!! May not even get pushed based on the vector !!!)
- The Instruction Pointer
- The Code Segment
- The Flags Register
- The Stack Pointer
- The Stack Segment
You may have noticed two things. The CPU does not push the vector so the code does not know what vector it is assigned to. The CPU also may or may not push some things so you will need special handling for each case.
So you will end up using assembly macros to generate 256 stubs which for many exceptions need a slightly separate macro so you can have a unified frame in C and so you can know what vector was called to have a common entry point. They would end up looking like this using NASM
%macro ISR 1
global isr%1
isr%1:
push 0
push %1
jmp dispatch_interupt_asm
%endmacro
%macro ISR_ERR 1
global isr%1
isr%1:
push %1
jmp dispatch_interupt_asm
%endmacro
THEN finally after all of this you can have a common handler in assembly to push the GPRs you need to save according to your ABI to then call some C code
and give it a pointer to the stack with all the data in needs and then have an exit prologue after you call C to restore the things and run iretq.
SYSCALL/SYSRET
I am not covering these again, go read my fedi post on it. It also happens to mention FRED albeit in less detail.
Flexible Return and Event Delivery (FRED)
Now onto FRED. Since the IDT and syscall/sysret are well not exactly good Intel & AMD have worked on this to solve the problem and make a much better system to work with.
To get interupts you now only use two entry points. One for when it happened in CPL0 and another for CPL3. You simply need the entry point for ring3 to be aligned
to a 4kb page and load the value into the IA32_FRED_CONFIG MSR added with FRED and the CPL0 entry follows 256 bytes after. So with NASM you would use some
code like this to align it right
section .text
align 4096
fred_ring3_entry_asm_stub:
jmp fred_ring3_entry_asm
times 256 - ($ - fred_ring3_entry_asm_stub) db 0
fred_ring0_entry_asm_stub:
jmp fred_ring0_entry_asm
Then whenever an interrupt happens it calls those stubs. They jump to their handler and pass the arguments to run C code. You can see the full code for this on my old repo for my kernel before the rewrite (which will get FRED port soon ofc) on this commit do note that a few of the parts in the old kernel are bad due to me not wanting to change much of the IRQ system so i converted structures though a lot of this can be avoided which I will do in the rewrite properly.
The stuff pushed to the stack is similar to with the IDT but it is fully uniform and always 64 bytes and encodes much more data in it. You get stuff like the vector to properly handle things. The way this is encoded also allows for more than 256 interrupts for one CPU core. You can also check what type it is so if it's a hardware or a software or even a syscall event. There is NMI source reporting so you can get that information easily too.
I am not going to verbatim spit off the entire spec here but you can look at the code from the commit as well of course the spec itself. FRED is an excellent addition to the x86 architecture and implementing it was easy. Well except finding an emulator for it.
Finding an emulator
Now since this is such a new feature when I was writing the code for this i of course needed something to test the code on. Normally I would use QEMU since it is a very good emulator. Sadly this was not an option. Bochs also "supports" FRED now but when making this it did not at the time as far as I am aware and later when i tried to try bochs with it after compiling from source with FRED enabled. It
- Could not properly boot my kernel with FRED
- Could not properly boot my kernel without FRED
- Could not properly boot linux (64bit)
- Could not properly boot the astral kernel
- Could boot 32bit linux atleast
So the only option happend to be Intels own emulator SIMICS. Which does support FRED! But given that this is corpo software it was VERY fucking annoying to figure out. Thankfully they do provide a linux native version which works. But when you want to get into an VM and run an ISO you have to use the GUI to make a project folder and install a bunch of the packages to it AND THEN goto your terminal and run the script and run this series of commands to make it boot properly and this is not very well documented and it took longer to get this to work than it did to even implement FRED.
$cpu_comp_class = "x86-experimental-fred"
run-script ./targets/qsp-x86/qsp-dvd-boot.simics iso_image="/home/evalyn/Documents/Programming/evalynOS/evalynOS/evalynOS.iso"
run
This assuming the ISO file is at the path you set should start up a VM. But of course because this is intels peice o' crap and i am on AMD you get no acceleration and it is much slower than QEMU TCG (possibly because it is more accurate despite its PS/2 emulation being strange when playing doom)
Despite all of that it wasn't too bad to get working and testing and FRED is very nice to have and support in my kernel projects. And as far as i am aware besides Linux and possibly some BSD's along with windows my kernel is one of the first to get support for this and the first out of my hobby operating system groups that do this for fun.

