GRUB Higher half kernel

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
protegee6155
Posts: 24
Joined: Tue Dec 16, 2025 4:17 am

GRUB Higher half kernel

Post by protegee6155 »

Hi! It’s been some time since I started working on my OS project. I’ve implemented many of the basics, including paging, interrupts, and a few basic drivers, all that while using a "lower half" kernel. In the passing two weeks i have desided that i want to change my kernel to be a higher half one, however, I’m completely stuck on implementing it.

I tried following the Higher Half x86 Bare Bones guide on the OSDev Wiki, using the provided linker script with a few small modifications, but without success. Whenever I use my current linker script (attached below, along with the Makefile and boot.asm), I end up seeing garbage instructions at the memory location where the _start label is supposed to be.

What i see in gdb:

Code: Select all

B+ 0x10000c <_start>        mov    %eax, 0x164(%esi)
>  0x100012 <_start+6>     mov    $0x7f98, %ecx
   0x100017 <_start+11>    mov    $0x9000, %edi
   0x10001c <loop_start+1> rep movsb %ds:(%esi), %es:(%edi)
   0x10001e <loop_start+3> mov    $0x9025, %esi
   0x100023 <loop_start+8> jmp    *%esi
   0x100025 <loop_start+10> mov   $0x10fa0, %edi
   0x10002a <loop_start+15> mov   $0x18740, %ecx
   0x10002f <loop_start+20> sub   %edi, %ecx
   0x100031 <loop_start+22> xor   %eax, %eax
my linker script:

Code: Select all

ENTRY (_start)
OUTPUT_FORMAT(elf32-i386)
OUTPUT_ARCH(i386:i386)

STACK_SIZE = 0x200000;      /* 2 MiB  */
HEAP_SIZE  = 0x10000000;    /* 200 MiB  */

SECTIONS
{
    /*
     * Start physical address (typical for bootloaders like GRUB).
     * This is where everything before 0xC0000000 virtual is loaded.
     */
    . = 0x00100000;

    /*
     * The multiboot header and initial entry point must be in the identity-mapped
     * physical address space so the bootloader can find them and execute them
     * before paging is enabled.
     */
    __kernel_start = .;

    .multiboot.data : AT(0x00100000)
    {
        KEEP(*(.multiboot.data))
    }

    /* Ensure the startup code is placed right after the header, physically. */
    .multiboot.text : AT(ADDR(.multiboot.text))
    {
        KEEP(*(.multiboot.text))
    }
    
    /*
     * Now we switch to the higher-half virtual address space for the rest
     * of the kernel sections. We define the virtual address relative to 
     * the physical load address using the 0xC0000000 offset.
     */
    . += 0xC0000000;

    /* Add a symbol that indicates the start address of the kernel in virtual memory. */

    .text ALIGN (4K) : AT (ADDR (.text) - 0xC0000000)
    {
        *(.text)
    }
    .rodata ALIGN (4K) : AT (ADDR (.rodata) - 0xC0000000)
    {
        *(.rodata)
    }
    .data ALIGN (4K) : AT (ADDR (.data) - 0xC0000000)
    {
        *(.data)
    }
    .bss ALIGN (4K) : AT (ADDR (.bss) - 0xC0000000)
    {
        *(COMMON)
        *(.bss)
        *(.bootstrap_stack)
    }
    
    /* -------- Heap Section (1GiB) at end -------- */
    .heap ALIGN(4096) (NOLOAD) : 
    {
        heap_start = .;
        . = . + HEAP_SIZE;
        heap_end = .;
    }

    /* Add a symbol that indicates the end address of the kernel. */
    __kernel_end = .;

    /DISCARD/ : {
        *(.eh_frame)
    }
}
my boot.asm script:

Code: Select all

; Declare constants for the multiboot header.
%define ALIGN    1<<0             ; align loaded modules on page boundaries
%define MEMINFO  1<<1             ; provide memory map
%define FLAGS    ALIGN | MEMINFO  ; this is the Multiboot 'flag' field
%define MAGIC    0x1BADB002       ; 'magic number' lets bootloader find the header
%define CHECKSUM -(MAGIC + FLAGS) ; checksum of above, to prove we are multiboot

; Extern symbols
extern kernel_main
extern __kernel_start
extern __kernel_end

; Declare a multiboot header that marks the program as a kernel.
section .multiboot.data
multiboot_header:
    align 4
    dd MAGIC
    dd FLAGS
    dd CHECKSUM

; Allocate the initial stack.
section .bootstrap_stack
stack_bottom:
    resb 16384 ; 16 KiB
stack_top:

; Preallocate pages used for paging. Don't hard-code addresses and assume they
; are available, as the bootloader might have loaded its multiboot structures or
; modules there. This lets the bootloader know it must avoid the addresses.
section .bss
    align 4096
boot_page_directory:
    resb 4096
boot_page_table1:
    resb 4096
; Further page tables may be required if the kernel grows beyond 3 MiB.

; The kernel entry point.
section .multiboot.text exec
global _start
_start:
	; Physical address of boot_page_table1.
	; TODO: I recall seeing some assembly that used a macro to do the
	;       conversions to and from physical. Maybe this should be done in this
	;       code as well?
	mov edi, (boot_page_table1 - 0xC0000000)
	; First address to map is address 0.
	; TODO: Start at the first kernel page instead. Alternatively map the first
	;       1 MiB as it can be generally useful, and there's no need to
	;       specially map the VGA buffer.
	mov esi, 0
	; Map 1023 pages. The 1024th will be the VGA text buffer.
	mov ecx, 1023

loop_start:
	; Only map the kernel.
	cmp esi, __kernel_start
	jl skip_map
	cmp esi, (__kernel_end - 0xC0000000)
	jge loop_end

	; Map physical address as "present, writable". Note that this maps
	; .text and .rodata as writable. Mind security and map them as non-writable.
	mov edx, esi
	or edx, 0x003
	mov [edi], edx

skip_map:
	; Size of page is 4096 bytes.
	add esi, 4096
	; Size of entries in boot_page_table1 is 4 bytes.
	add edi, 4
	; Loop to the next entry if we haven't finished.
	loop loop_start

loop_end:
	; Map VGA video memory to 0xC03FF000 as "present, writable".
	mov dword [(boot_page_table1 - 0xC0000000) + 1023 * 4], (0x000B8000 | 0x003)

	; The page table is used at both page directory entry 0 (virtually from 0x0
	; to 0x3FFFFF) (thus identity mapping the kernel) and page directory entry
	; 768 (virtually from 0xC0000000 to 0xC03FFFFF) (thus mapping it in the
	; higher half). The kernel is identity mapped because enabling paging does
	; not change the next instruction, which continues to be physical. The CPU
	; would instead page fault if there was no identity mapping.

	; Map the page table to both virtual addresses 0x00000000 and 0xC0000000.
	mov dword [(boot_page_directory - 0xC0000000) + 0], (boot_page_table1 - 0xC0000000 + 0x003)
	mov dword [(boot_page_directory - 0xC0000000) + 768 * 4], (boot_page_table1 - 0xC0000000 + 0x003)

	; Set cr3 to the address of the boot_page_directory.
	mov ecx, (boot_page_directory - 0xC0000000)
	mov cr3, ecx

	; Enable paging and the write-protect bit.
	mov ecx, cr0
	or ecx, 0x80010000
	mov cr0, ecx

	; Jump to higher half with an absolute jump. 
	lea ecx, [higher_half_start]
	jmp ecx

section .text

higher_half_start:
	; At this point, paging is fully set up and enabled.

	; Unmap the identity mapping as it is now unnecessary. 
	mov dword [boot_page_directory + 0], 0

	; Reload crc3 to force a TLB flush so the changes to take effect.
	mov ecx, cr3
	mov cr3, ecx

	; Set up the stack.
	mov esp, stack_top

	; Enter the high-level kernel.
	call kernel_main

	; Infinite loop if the system has nothing more to do.
	cli
halt_loop:
	hlt
	jmp halt_loop

; -----------------------------------------------------------------------------
; SECTION (note) - Inform the linker that the stack does not need to be executable
; -----------------------------------------------------------------------------
section .note.GNU-stack
the makefile:

Code: Select all

# =========================
# Tools
# =========================
CC    = gcc
AS    = nasm
LD    = ld
QEMU  = qemu-system-i386

# =========================
# Directories
# =========================
SOURCE_DIR  = src
INCLUDE_DIR = include
BUILD_DIR   = build
ISO_DIR     = iso

# =========================
# Files
# =========================
KERNEL_ELF   = $(BUILD_DIR)/mykernel.elf
KERNEL_BIN   = $(BUILD_DIR)/mykernel.bin
ISO_IMAGE    = $(BUILD_DIR)/mykernel.iso
VIRTUAL_DISK = $(BUILD_DIR)/vrdisk.img

# =========================
# Flags
# =========================
CFLAGS  = -m32 -nostdlib -fno-builtin -fno-exceptions -fno-leading-underscore -I$(INCLUDE_DIR)
ASFLAGS = -f elf32
LDFLAGS = -m elf_i386

# =========================
# Sources (1-level deep)
# =========================
SOURCES_C := $(wildcard $(SOURCE_DIR)/*.c $(SOURCE_DIR)/*/*.c)
SOURCES_S := $(wildcard $(SOURCE_DIR)/*.asm $(SOURCE_DIR)/*/*.asm)

OBJECTS_C := $(patsubst $(SOURCE_DIR)/%.c,  $(BUILD_DIR)/%.o, $(SOURCES_C))
OBJECTS_S := $(patsubst $(SOURCE_DIR)/%.asm,$(BUILD_DIR)/%.o, $(SOURCES_S))
OBJECTS   := $(OBJECTS_S) $(OBJECTS_C)

# =========================
# Default target
# =========================
all: $(KERNEL_ELF) $(KERNEL_BIN)

# =========================
# Compile C files
# =========================
$(BUILD_DIR)/%.o: $(SOURCE_DIR)/%.c
	@mkdir -p $(dir $@)
	$(CC) $(CFLAGS) -c $< -o $@

# =========================
# Assemble ASM files
# =========================
$(BUILD_DIR)/%.o: $(SOURCE_DIR)/%.asm
	@mkdir -p $(dir $@)
	$(AS) $(ASFLAGS) $< -o $@

# =========================
# Link kernel ELF
# =========================
$(KERNEL_ELF): linker.ld $(OBJECTS)
	@mkdir -p $(BUILD_DIR)
	$(LD) $(LDFLAGS) -T $< -o $@ $(OBJECTS)

# =========================
# Raw binary
# =========================
$(KERNEL_BIN): $(KERNEL_ELF)
	objcopy -O binary $< $@

# =========================
# ISO
# =========================
iso: $(KERNEL_ELF)
	@mkdir -p $(ISO_DIR)/boot
	cp $(KERNEL_ELF) $(ISO_DIR)/boot/mykernel.elf
	grub-mkrescue --output=$(ISO_IMAGE) $(ISO_DIR)

# =========================
# Run
# =========================
run: iso $(VIRTUAL_DISK)
	$(QEMU) -m 4G -cdrom $(ISO_IMAGE) -hda $(VIRTUAL_DISK)

# =========================
# Debug
# enter gdb in wsl
# gdb /mnt/c/Users/DorSh/Projects/MyOSv3/build/mykernel.elf
# target remote :1234
# =========================
debug: CFLAGS += -g
debug: iso $(VIRTUAL_DISK)
	$(QEMU) -m 4G -cdrom $(ISO_IMAGE) -s -S -hda $(VIRTUAL_DISK)

# =========================
# Virtual disk
# =========================
$(VIRTUAL_DISK):
	@mkdir -p $(BUILD_DIR)
	qemu-img create $@ 1G

# =========================
# Cleanup
# =========================
clean:
	rm -rf $(BUILD_DIR) $(ISO_DIR)/boot/mykernel.elf
Any help is welcomed O:)! and sorry if that isn't the right place to post this kind of things (i am new to the forum).
Octocontrabass
Member
Member
Posts: 6247
Joined: Mon Mar 25, 2013 7:01 pm

Re: GRUB Higher half kernel

Post by Octocontrabass »

protegee6155 wrote: Tue Dec 16, 2025 4:32 amWhat i see in gdb:
Are you sure this is your code and not GRUB's code? The breakpoint can't tell the difference.
protegee6155 wrote: Tue Dec 16, 2025 4:32 am

Code: Select all

CC    = gcc
Why aren't you using a cross-compiler?
protegee6155
Posts: 24
Joined: Tue Dec 16, 2025 4:17 am

Re: GRUB Higher half kernel

Post by protegee6155 »

first of all, thanks for the respone.
Are you sure this is your code and not GRUB's code? The breakpoint can't tell the difference.
to get this information i breakpointed on _start and did layout asm:

Code: Select all

b _start
si
layout asm
i guess this is grub code (since those are garbage instructions), the problem is that i don't know why i get those instructions.
Why aren't you using a cross-compiler?
I do all the compilation in wsl, so i don't need one, or at least gcc works good enough for me (before i tried implementing the higher half kernel everything worked fine).
Octocontrabass
Member
Member
Posts: 6247
Joined: Mon Mar 25, 2013 7:01 pm

Re: GRUB Higher half kernel

Post by Octocontrabass »

protegee6155 wrote: Tue Dec 16, 2025 12:34 pmi guess this is grub code (since those are garbage instructions), the problem is that i don't know why i get those instructions.
Presumably GRUB loads something else at that address before it loads your kernel.
protegee6155 wrote: Tue Dec 16, 2025 12:34 pmI do all the compilation in wsl, so i don't need one, or at least gcc works good enough for me (before i tried implementing the higher half kernel everything worked fine).
WSL is just Linux. You still need a cross-compiler even if you're using Linux.
protegee6155
Posts: 24
Joined: Tue Dec 16, 2025 4:17 am

Re: GRUB Higher half kernel

Post by protegee6155 »

Presumably GRUB loads something else at that address before it loads your kernel.
I know, that is why i am asking for help :?

Code: Select all

WSL is just Linux. You still need a cross-compiler even if you're using Linux.
why? like really, why?
removing (not linking to) libc, and creating yourown linker should be enoughf, no?
Octocontrabass
Member
Member
Posts: 6247
Joined: Mon Mar 25, 2013 7:01 pm

Re: GRUB Higher half kernel

Post by Octocontrabass »

protegee6155 wrote: Tue Dec 16, 2025 1:12 pmI know, that is why i am asking for help :?
...Have you tried letting GRUB run until it loads your kernel?
protegee6155 wrote: Tue Dec 16, 2025 1:12 pmwhy? like really, why?
removing (not linking to) libc, and creating yourown linker should be enoughf, no?
There are several reasons why.
protegee6155
Posts: 24
Joined: Tue Dec 16, 2025 4:17 am

Re: GRUB Higher half kernel

Post by protegee6155 »

I will take a look on reasons for cross compilers! thanks!

when i try to run the kernel, i get an error: "no multiboot header was found". that said, i still "jump" to _start address.
protegee6155
Posts: 24
Joined: Tue Dec 16, 2025 4:17 am

Re: GRUB Higher half kernel

Post by protegee6155 »

here is my full kernel code https://github.com/sDos280/MyOS.
note, that i know my current boot.asm script wouldn't map the whole kernel (since it is too large). i just want to make the higher half trampoline start working and continue to twick it from there.
Octocontrabass
Member
Member
Posts: 6247
Joined: Mon Mar 25, 2013 7:01 pm

Re: GRUB Higher half kernel

Post by Octocontrabass »

protegee6155 wrote: Tue Dec 16, 2025 1:47 pmwhen i try to run the kernel, i get an error: "no multiboot header was found".
That points to a problem with your linker script. Try using readelf or objdump to see where the linker decided to place the various sections in your kernel binary. That's usually enough information to figure out how to fix it.
protegee6155 wrote: Tue Dec 16, 2025 1:47 pmthat said, i still "jump" to _start address.
It's the same address, but it has nothing to do with your _start symbol. GRUB didn't load your kernel (because it couldn't find the Multiboot header), so whatever is hitting the breakpoint must be some other code that happened to be loaded at the same address.
protegee6155
Posts: 24
Joined: Tue Dec 16, 2025 4:17 am

Re: GRUB Higher half kernel

Post by protegee6155 »

Code: Select all

> readelf -l build/mykernel.elf

Code: Select all

  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x001000 0x00000000 0x00000000 0x0000c 0x0000c RW  0x1000
  LOAD           0x002000 0x00100000 0x00100000 0x0006b 0x0006b R E 0x1000
  LOAD           0x003000 0xc0101000 0x00101000 0x0381d 0x0381d R E 0x1000
  LOAD           0x007000 0xc0105000 0x00105000 0x00757 0x00757 R   0x1000
  LOAD           0x008000 0xc0106000 0x00106000 0x00194 0x1042c000 RW  0x1000
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x10

 Section to Segment mapping:
  Segment Sections...
   00     .multiboot.data
   01     .multiboot.text
   02     .text .text.__x86.get_pc_thunk.ax .text.__x86.get_pc_thunk.bx .text.__x86.get_pc_thunk.dx .text.__x86.get_pc_thunk.cx
   03     .rodata
   04     .data .got.plt .bss .heap
   05
It's the same address, but it has nothing to do with your _start symbol. GRUB didn't load your kernel (because it couldn't find the Multiboot header), so whatever is hitting the breakpoint must be some other code that happened to be loaded at the same address.
aaaah, ok that makes sence.
MichaelPetch
Member
Member
Posts: 857
Joined: Fri Aug 26, 2016 1:41 pm
Libera.chat IRC: mpetch

Re: GRUB Higher half kernel

Post by MichaelPetch »

Your multiboot header isn't correct because of the way `%define` works. `%define` is text substitution so CHECKSUM ends up being defined as -(MAGIC + ALIGN | MEMINFO) where the `+` (addition) takes precedence over `|` (bitwise OR)` and gets evaluated like this `-((MAGIC + ALIGN) | MEMINFO)`. That is incorrect because you want it to be evaluated as `-(MAGIC + (ALIGN | MEMINFO))`. A simple fix is to change `%define FLAGS ALIGN | MEMINFO` to `%define FLAGS (ALIGN | MEMINFO)` (note the addition of parentheses). You could also use `equ` to define FLAGS rather than `%define`.
nullplan
Member
Member
Posts: 2034
Joined: Wed Aug 30, 2017 8:24 am

Re: GRUB Higher half kernel

Post by nullplan »

In addition, the output of readelf -l you posted tells me that the multiboot header is at offset 0x1000 in the kernel file, which is probably not what you wanted. My guess is that your linker script specifies an alignment of 0x1000 for everything, which is of course wrong. Just get rid of it for all except the .data section, and set the start address to VMA + SIZEOF_HEADERS (where VMA is the address you expect the kernel to be loaded or mapped to). That should get the offset of the multiboot header down massively.
Carpe diem!
protegee6155
Posts: 24
Joined: Tue Dec 16, 2025 4:17 am

Re: GRUB Higher half kernel

Post by protegee6155 »

A simple fix is to change `%define FLAGS ALIGN | MEMINFO` to `%define FLAGS (ALIGN | MEMINFO)` (note the addition of parentheses)
Thanks!!! this fixed my "not multiboot header problem".
My guess is that your linker script specifies an alignment of 0x1000 for everything
I am not sure how to fix that, nor if it is the linker script who does that (since i can't point to any code in my script that does that. i have an align(4) on the multiboot section, but i guess that isn't the thing we are looking for).

Unfurtenetly i have returned to square one, i still have the problem of garbage instructions in the should be _start position.

Code: Select all

remote Thread 1.1 (asm) In: _start
Program received signal SIGTRAP, Trace/breakpoint trap.
0x0000fff0 in ?? ()
(gdb) b _start
Note: breakpoint 1 also set at pc 0x10000c.
Breakpoint 2 at 0x10000c
(gdb) c
Continuing.

Breakpoint 1, 0x0010000c in _start ()
(gdb)

Code: Select all

B+> 0x10000c <_start>        mov    %eax,0x164(%esi)
0x100012 <_start+6>         mov    $0x7f98,%ecx
0x100017 <_start+11>        mov    $0x9000,%edi
0x10001c <loop_start+1>     rep movsb %ds:(%esi),%es:(%edi)
0x10001e <loop_start+3>     mov    $0x9025,%esi
0x100023 <loop_start+8>     jmp    *%esi
0x100025 <loop_start+10>    mov    $0x10fa0,%edi
0x10002a <loop_start+15>    mov    $0x18740,%ecx
0x10002f <loop_start+20>    sub    %edi,%ecx
0x100031 <loop_start+22>    xor    %eax,%eax
0x100033 <skip_map+1>       cld
0x100034 <skip_map+2>       rep stos %al,%es:(%edi)
0x100036 <skip_map+4>       mov    %edx,0x10fa0
0x10003c <skip_map+10>      call   0x10320f
0x100041 <loop_end+4>       rolb   %cl,-0x7c9d0000(%edx)
0x100047 <loop_end+10>      add    %al,(%eax)
0x100049 <loop_end+12>      mov    0x9045,%eax
i have tried to run the code and to not breakpoint in _start, to see that instructs run right as kernel loads, but it seems that i also get garbage instructions there:

Code: Select all

(gdb) p $eip
$1 = (void (*)()) 0xfff0
(gdb) x/32i 0xfff0
=> 0xfff0: add    %al,(%eax)
   0xfff2: add    %al,(%eax)
   0xfff4: add    %al,(%eax)
   0xfff6: add    %al,(%eax)
   0xfff8: add    %al,(%eax)
   0xfffa: add    %al,(%eax)
   0xfffc: add    %al,(%eax)
   0xfffe: add    %al,(%eax)
MichaelPetch
Member
Member
Posts: 857
Joined: Fri Aug 26, 2016 1:41 pm
Libera.chat IRC: mpetch

Re: GRUB Higher half kernel

Post by MichaelPetch »

At bootup while in real mode 0xfff0 is not where the current instruction pointer points to. This is an anomaly in GDB since GDB doesn't understand segment:offset addressing in real mode. The actual address is CS:IP. I bet if you do `p/x $cs` you'll see the CS segment is 0xf000. If it is then the actual instruction being executed is at 0xf000:0xfff0 which is physical address 0xf000<<4+0xfff0=0xffff0 which happens to be the memory location that the CPU starts executing at boot up. This is a bit of a simplification on *most* systems with 32 bit/64-bit Intel processors that use legacy boot. The starting location is actually 0xfffffff0 with a CS of 0xf000 and EIP of 0xfff0 where the hidden descriptor for CS has a base of 0xffff0000. Because the last 64KiB of ROM in system just below he 4GiB mark is aliased in memory to 0xf0000 - to many it appears the first instruction being executed is at 0xffff0 when in fact it is actually 0xfffffff0.

As for why you are not seeing your kernel startup code - I can only guess that GRUB (?) or some other bootup process was executing code above 0x100000 which also happens to be where your kernel is loaded later and executed. You may have to do two `continue` commands in GDB to get to your kernel startup entry point of `_start`.
protegee6155
Posts: 24
Joined: Tue Dec 16, 2025 4:17 am

Re: GRUB Higher half kernel

Post by protegee6155 »

Ok, I have found the problem. I haven’t compiled and linked my boot script and didn’t debug the right elf file 🥲.

Thanks for all the help 🙃.
Post Reply