I am building a simple OS in c++ to learn about the topic. I am using the limine bootloader (Limine 6.20231210.0, as stated by the bootloader version request). One of the first things i am doing in the main kernel function, which is the following:
extern "C" void _start(void) {
// Ensure the bootloader actually understands our base revision (see spec) and fetch first fb
if(LIMINE_BASE_REVISION_SUPPORTED == false) {
hcf();
}
struct limine_framebuffer* framebuffer = framebuffer_request.response->framebuffers[0];
if(framebuffer_request.response == NULL || framebuffer_request.response->framebuffer_count < 1) {
hcf();
}
KERNEL::VGA::VGA_Driver vgaDriver(framebuffer);
KERNEL::TERMINAL::kernel_terminal terminal(&terfont7x14, &vgaDriver);
terminal.printf("%s %s", bootloader_info_request.response->name, bootloader_info_request.response->version);
gdt_init();
hcf();
}
Is load the GDT. As you see, the gdt_init() function (code below) is called. The vgaDriver and terminal Objects are simple objects that allow me to write to the screen (they use the limine framebuffer). Also the declaration of gdt_init() inside the .h is marked ar extern "C", so it cant be about function names, the compiler would have given me an error
bits 64
align 0x10
gdt:
null_descriptor:
dw 0x0000
dw 0x0000
db 0x00
db 00000000b
db 00000000b
db 0x00
kernel_code_64:
dw 0x0000
dw 0x0000
db 0x00
db 10011010b
db 00100000b
db 0x00
kernel_data_64:
dw 0x0000
dw 0x0000
db 0x00
db 10010010b
db 00100000b
db 0x00
user_code_64:
dw 0x0000
dw 0x0000
db 0x00
db 11111010b
db 00100000b
db 0x00
user_data_64:
dw 0x0000
dw 0x0000
db 0x00
db 11110010b
db 00100000b
db 0x00
gdt_end:
gdt_ptr:
dw gdt_end - gdt - 1
dq gdt
CODE_SEG equ kernel_code_64 - gdt
DATA_SEG equ kernel_data_64 - gdt
global gdt_init
gdt_init:
lgdt [gdt_ptr]
mov rax, rsp
push DATA_SEG
push rax
pushfq
push CODE_SEG
push flush
iretq
flush:
mov ax, DATA_SEG
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
ret
I even tried different version of the gdt_load(), like the following:
lgdt [rdi] ; i was passing the parameters by function here
push 0x8 ; the code segment offset
lea rax, [test] ; i declare the test label later in the code, after crash point
push rax
iretq
; fails after iretq
But the code always fails at the iretq instruction. I tried debugging with gdb but the registers seem all fine as of my knowledge. The gdt entries are always like the ones shown in the first code example, even in the c++ implementation, so it cant be that (unless the entries themselves are the problem). The platform i am trying to build the OS for is x86_64, and here is my linker.ld file:
/* Tell the linker that we want an x86_64 ELF64 output file */
OUTPUT_FORMAT(elf64-x86-64)
OUTPUT_ARCH(i386:x86-64)
/* We want the symbol _start to be our entry point */
ENTRY(_start)
/* Define the program headers we want so the bootloader gives us the right */
/* MMU permissions */
PHDRS
{
text PT_LOAD FLAGS((1 << 0) | (1 << 2)) ; /* Execute + Read */
rodata PT_LOAD FLAGS((1 << 2)) ; /* Read only */
data PT_LOAD FLAGS((1 << 1) | (1 << 2)) ; /* Write + Read */
dynamic PT_DYNAMIC FLAGS((1 << 1) | (1 << 2)) ; /* Dynamic PHDR for relocations */
}
SECTIONS
{
/* We wanna be placed in the topmost 2GiB of the address space, for optimisations */
/* and because that is what the Limine spec mandates. */
/* Any address in this region will do, but often 0xffffffff80000000 is chosen as */
/* that is the beginning of the region. */
. = 0xffffffff80000000;
.text : {
*(.text .text.*)
} :text
/* Move to the next memory page for .rodata */
. += CONSTANT(MAXPAGESIZE);
.rodata : {
*(.rodata .rodata.*)
} :rodata
/* Move to the next memory page for .data */
. += CONSTANT(MAXPAGESIZE);
.data : {
*(.data .data.*)
} :data
/* Dynamic section for relocations, both in its own PHDR and inside data PHDR */
.dynamic : {
*(.dynamic)
} :data :dynamic
/* NOTE: .bss needs to be the last thing mapped to :data, otherwise lots of */
/* unnecessary zeros will be written to the binary. */
/* If you need, for example, .init_array and .fini_array, those should be placed */
/* above this. */
.bss : {
*(.bss .bss.*)
*(COMMON)
} :data
/* Discard .note.* and .eh_frame since they may cause issues on some hosts. */
/DISCARD/ : {
*(.eh_frame)
*(.note .note.*)
*(.comment)
}
}
As you can see i based my code mostly on the limine bare-bones on osdev
Here are also my build command (replace .c and .o name)
g++ -o build/memory.o -c -std=c++17 -Wall -Wno-missing-field-initializers -Wextra -Wno-switch-bool -fno-rtti -fno-stack-protector -fno-stack-check -fno-lto -m64 -march=x86-64 -g src/memory.cpp
nasm -f elf64 -o build/x86.o src/x86.asm
ld -o build/kernel -m elf_x86_64 -static --no-dynamic-linker -T src/linker.ld build/kernel.o build/memory.o build/ports.o build/terminal.o build/vga.o build/gdt.o build/x86.o
The code seems to be able to load the address of GDTD in the register, because I once tried to remove the segments update and only load the gdt and it worked, and i could get the address by using sgdt. The returned address could be cast to a valid GDT structure (same implementation as in the assembly example but in c++)
This is the disassembly of the gdt_init() function (up until iretq)
│ 0xffffffff80001608 <kernel_code_64> add BYTE PTR [rax],al │
│ 0xffffffff8000160a <kernel_code_64+2> add BYTE PTR [rax],al │
│ 0xffffffff8000160c <kernel_code_64+4> add BYTE PTR [rdx+0x20],bl │
│ 0xffffffff80001612 <kernel_data_64+2> add BYTE PTR [rax],al │
│ 0xffffffff80001614 <kernel_data_64+4> add BYTE PTR [rdx+0x20],dl │
│ 0xffffffff8000161a <user_code_64+2> add BYTE PTR [rax],al │
│ 0xffffffff8000161c <user_code_64+4> add dl,bh │
│ 0xffffffff8000161e <user_code_64+6> and BYTE PTR [rax],al │
│ 0xffffffff80001620 <user_data_64> add BYTE PTR [rax],al │
│ 0xffffffff80001622 <user_data_64+2> add BYTE PTR [rax],al │
│ 0xffffffff80001624 <user_data_64+4> add dl,dh │
│ 0xffffffff80001626 <user_data_64+6> and BYTE PTR [rax],al │
│ 0xffffffff80001628 <gdt_ptr> (bad) │
│ 0xffffffff80001629 <gdt_ptr+1> add BYTE PTR [rax],al │
│ 0xffffffff8000162b <gdt_ptr+3> (bad) │
│ 0xffffffff8000162c <gdt_ptr+4> add BYTE PTR [rax-0x1],al │
│ > 0xffffffff80001632 <gdt_init> lgdt ds:0xffffffff80001628 │
│ 0xffffffff8000163a <gdt_init+8> mov rax,rsp │
│ 0xffffffff8000163d <gdt_init+11> push 0x10 │
│ 0xffffffff8000163f <gdt_init+13> push rax │
│ 0xffffffff80001640 <gdt_init+14> pushf │
│ 0xffffffff80001641 <gdt_init+15> push 0x8 │
│ 0xffffffff80001643 <gdt_init+17> push 0xffffffff8000164a │
│ 0xffffffff80001648 <gdt_init+22> iretq
These are the registers state right before the iretq instruction:
rax 0xffff800007e9ff48 -140737355579576
rbx 0x0 0
rcx 0xffff8000fd000000 -140733243719680
rdx 0x0 0
rsi 0x0 0
rdi 0xffff800007e9ffb0 -140737355579472
rbp 0xffff800007e9fff0 0xffff800007e9fff0
rsp 0xffff800007e9ff28 0xffff800007e9ff28
r8 0x0 0
r9 0x0 0
r10 0x84 132
r11 0xd 13
r12 0x0 0
r13 0x0 0
r15 0x0 0
rip 0xffffffff80001643 0xffffffff80001643 <gdt_init+17>
eflags 0x46 [ IOPL=0 ZF PF ]
cs 0x28 40
ss 0x30 48
ds 0x30 48
es 0x30 48
fs 0x30 48
gs 0x30 48
fs_base 0x0 0
gs_base 0x0 0
k_gs_base 0x0 0
cr0 0x80010011 [ PG WP ET PE ]
cr3 0x7e8f000 [ PDBR=32399 PCID=0 ]
cr4 0x20 [ PAE ]
cr8 0x0 0
efer 0xd00 [ NXE LMA LME ]
mxcsr 0x1f80 [ IM DM ZM OM UM PM ]
Note that as you can see the segment registers are already set to some reasonable value, thats just because Limine loads a default gdt with some entries. I want to load my own though.