Single stage bootloader failing to load the linux 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
rhelin
Posts: 4
Joined: Tue Aug 26, 2025 2:25 pm
Libera.chat IRC: rhelin

Single stage bootloader failing to load the linux kernel

Post by rhelin »

Hello everyone,

As a hobby, I'm trying to build a single stage bootloader of a recent version of a linux kernel. My final goal is to have a signle binary containing my bootloader and the bzImage of linux, then use qemu to test it, so I know in advance the place of bzImage inside my final binary that I can use later for loading sectors using LBA. An example of my Makefile

Code: Select all

	as -g boot.s -o boot.o
	ld -g -Ttext 0x7C00 ./boot.o -o boot.elf
	objcopy -O binary ./boot.elf  boot.bin
	dd if=./linux-6.16/arch/x86_64/boot/bzImage >> boot.bin
	qemu-system-x86_64 -hda ./boot.bin

I followed what was documented on the official linux boot protocol, I started by loading the linux boot sector at the address 0x10000 to facilitate the calculation of the linear address, then reading the size of the setup sectors then loading them. I used the bios interrupt 0x13 for this matter.
Then I have populated the header with the required values ( the heap end pointer, the address of cmd_line).
When it is time to load the rest of the kernel ( the size is found in the syssize entry in the header ) at the address 0x100000. Since I'm using the real mode, I wanted to jump temporarily to the protected mode, load the rest of the linux kernel and switch back to the real mode. I have used 16 bit of code/data segments for this purpose to reload the segment selectors properly and avoid having any 32 bit related attributes. When switching back to real mode, I have tested that the bios interrupts are working as expected to make sure that everything was reloaded correctly.
https://wiki.osdev.org/Real_Mode#Switch ... _Real_Mode

However, when it is time to make the jump to the linux kernel, it was not working, when debugging and setting up a breakpoint just before the jump, the breakpoint was hit like it is in an infinite loop, the display one the qemu kept blinking. I tried to debug and verifying the code and the values injected into the headers, everything seems to be fine.

I was expecting to have a kernel panic or having some error messages, but not a qemu screen blinking and reloading. This is the first time I'm doing a switch from protected mode to real mode, I believe this part is very tricky, I followed what was published on the osdev wiki, and the execution of a bios interrupt was confirming that we are in the real mode again.

I'm joining my code here if someone can help me out, I will be grateful, it has been a while I'm stuck in this problem.

Code: Select all

.code16
.global _start

.equ BASE_SEG, 0x1000

_start:
  cli
  xor %ax, %ax
  mov %cs, %ax
  mov %ax, %ds
  mov $BASE_SEG, %ax
  mov %ax, %es
  xor %bx, %bx
  call _load_boot_sector
  jmp _switch_pm 

_load_boot_sector:
  mov $0x02, %ah
  mov $1, %al
  mov $0, %ch
  mov $2, %cl 
  mov $0, %dh
  mov $0x80, %dl
  int $0x13
  jc _disk_error
  jmp _load_setup_sectors

_load_setup_sectors:
   mov $0x200, %bx
   mov $0x02, %ah
   movb %es:0x1F1, %al
   mov $0x3, %cl
   int $0x13
   jc _disk_error

_init_kernel_header:
  mov $BASE_SEG, %ax
  mov %ax, %es
  movb $0xFF, %es:0x210
  // set heap in use bit 
  orb $0x80, %es:0x211
  // heap end_ptr
  movw $0xDE00, %es:0x224
  // initramfs: not initramfs for now
  movw $0x0, %es:0x218
  mov $0x0, %es:0x21c
  // cmd_line_ptr
  movl $0x1E000, %es:0x228

_copy_cmd_line:
  // copy the cmd_line to its location: 0x1E000 (%es:%di)
  mov $cmd_line_size, %cx
  mov $cmd_line, %si 
  mov $0xE000, %di 
  rep movsb
  ret

_disk_error:
  call _print_error
  jmp .

_print_error:
  mov $0x0E, %ah
  mov $'E', %al
  mov $0, %bh
  mov $0, %bl
  int $0x10 
  ret

_switch_pm:
  lgdt gdt_desc
  mov %cr0, %eax
  or $0x1, %eax
  mov %eax, %cr0
  mov $2, %al
  out %al, $0x92 
  ljmp $0x8, $_start_32

.code32
_start_32:
  mov $0x10, %ax
  mov %ax, %ds
  mov %ax, %ss
  mov %ax, %es 
  mov %ax, %fs
  mov %ax, %gs
  mov $0x90000, %esp

_load_lba:
  // load the syssize from the header
  mov %es:0x101F4, %edx
  // the syssize is the number of 16 bytes => the number of sectors is (syssize * 16) /512 
  shr $5, %edx
  mov %dx, %bx
  // address of the dest buffer in memory
  mov $0x100000, %edi
  // init 
  mov $0x1F6, %dx
  mov $0x40, %al
  out %al, %dx
  // send sector count high byte
  mov $0x1F2, %dx 
  mov %bh, %al
  out %al, %dx
  // send lba high byte
  mov $0x1F3, %dx 
  mov $0, %al
  out %al, %dx
  mov $0x1F4, %dx 
  mov $0, %al
  out %al, %dx
  mov $0x1F5, %dx 
  mov $0, %al
  out %al, %dx
  // send sector count low byte
  mov $0x1F2, %dx
  mov %bl, %al
  out %al, %dx
  // send lba low byte
  mov $0x1F3, %dx 
  movb %es:0x101F1, %al
   // add 3 sectors: sector of my bootloader, linux boot sector and one more sector according to the documentation
  add $3, %al
  out %al, %dx
  mov $0x1F4, %dx 
  mov $0, %al
  out %al, %dx
  mov $0x1F5, %dx 
  mov $0, %al
  out %al, %dx
  // send read command
  mov $0x1F7, %dx
  mov $0x24, %al
  out %al, %dx
  jmp _wait_loop

_wait_loop:
  mov $0x1F7, %dx
  in %dx, %al
  testb $0x08, %al
  jz _wait_loop
  jmp _read

_read:
  mov $256, %cx
  mov $0x1F0, %dx
  rep insw

_sleep:
  mov $0x3F6, %dx 
  in %dx, %al
  in %dx, %al
  in %dx, %al
  in %dx, %al
  dec %bl
  cmp $0, %bl
  je _reset_16
  jmp _wait_loop

// use a 16 bit data/code segment to go back to the real mode
_reset_16:
  cli
  mov $0x20, %eax
  mov %eax, %ds
  mov %eax, %ss
  mov %eax, %es 
  mov %eax, %fs
  mov %eax, %gs
  ljmp $0x18, $_switch_rm

.code16
_switch_rm:
  mov %cr0, %eax
  and $~1, %eax
  mov %eax, %cr0
  ljmp $0x0000, $_start_16

_start_16:
  // reload the segments with the base seg value: 0x1000
  mov $BASE_SEG, %ax
  mov %ax, %ds 
  mov %ax, %es 
  mov %ax, %ss
  mov %ax, %gs
  mov %ax, %fs
  mov $0xE000, %esp
  // Print on the screen 'A' using bios interrupts to make sure that the real mode is working
  mov $0x0E, %ah
  mov $'A', %al
  mov $0, %bh
  mov $0, %bl
  int $0x10 
  ljmp $0x1020, $0x0

gdt:
gdt_null:
  .long 0x0
  .long 0x0
gdt_code:
  .word 0xFFFF
  .word 0x0 
  .byte 0x0
  .byte 0x9A 
  .byte 0xCF
  .byte 0x0 
gdt_data:
  .word 0xFFFF
  .word 0x0 
  .byte 0x0
  .byte 0x92 
  .byte 0xCF
  .byte 0x0 
gdt_16_code:
  .word 0xFFFF
  .word 0x0 
  .byte 0x0
  .byte 0x9A 
  .byte 0x8F
  .byte 0x0 
gdt_16_data:
 .word 0xFFFF
  .word 0x0 
  .byte 0x0
  .byte 0x92 
  .byte 0x8F
  .byte 0x0 

gdt_end:
gdt_desc:
  .word gdt_end - gdt - 1 
  .long gdt

cmd_line:
  .asciz "root=/dev/zero console=ttyS0"
  .byte 0
  
cmd_line_size = . - cmd_line

.org 510
.word 0xAA55
I have commented the code for the parts that are not obvious. Some instructions regarding the sectors to load are not that clear but because I have run the code and checked the values, For example:

number of setup sectors is: 39
I have used the linux kernel version: 6.16

NOTE: I know that I can use the bios interrupt 0x15 when being in the real mode, I have found this idea while doing my research, but I think understanding and fixing the problem that I have is a great way to strenghen my knowledge.

Many thanks.
Octocontrabass
Member
Member
Posts: 6249
Joined: Mon Mar 25, 2013 7:01 pm

Re: Single stage bootloader failing to load the linux kernel

Post by Octocontrabass »

rhelin wrote: Tue Aug 26, 2025 2:55 pmHowever, when it is time to make the jump to the linux kernel, it was not working, when debugging and setting up a breakpoint just before the jump, the breakpoint was hit like it is in an infinite loop, the display one the qemu kept blinking.
That sounds like a triple fault. Use "-no-reboot" to stop QEMU from rebooting while you're debugging it. Use "-d int" to see a log of the exceptions leading up to the triple fault.
MichaelPetch
Member
Member
Posts: 857
Joined: Fri Aug 26, 2016 1:41 pm
Libera.chat IRC: mpetch

Re: Single stage bootloader failing to load the linux kernel

Post by MichaelPetch »

I decided to use your code and a bzImage built with make defconfig on a 6.16 kernel. I built boot.bin with your commands and then ran it in QEMU with

Code: Select all

qemu-system-x86_64 -hda boot.bin -d int -M smm=off -no-shutdown -no-reboot
The result I got was a GPF (v=0d with error code e=0000) while executing address 0x100008. The full trace output I got from QEMU was:

Code: Select all

check_exception old: 0xffffffff new 0xd
     0: v=0d e=0000 i=0 cpl=0 IP=0010:0000000000100008 pc=0000000000100008 SP=0018:000000000001df90 env->regs[R_EAX]=0000000000000000
EAX=00000000 EBX=00000000 ECX=00000000 EDX=00000000
ESI=00014770 EDI=00000000 EBP=00000000 ESP=0001df90
EIP=00100008 EFL=00000046 [---Z-P-] CPL=0 II=0 A20=1 SMM=0 HLT=0
ES =0000 00000000 00000000 00000000
CS =0010 00000000 ffffffff 00cf9b00 DPL=0 CS32 [-RA]
SS =0018 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
DS =0000 00000000 00000000 00000000
FS =0018 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
GS =0018 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
LDT=0000 00000000 00000000 00008200 DPL=0 LDT
TR =0020 00001000 00000067 00008900 DPL=0 TSS32-avl
GDT=     00014310 00000027
IDT=     00000000 00000000
CR0=00000011 CR2=00000000 CR3=00000000 CR4=00000000
DR0=0000000000000000 DR1=0000000000000000 DR2=0000000000000000 DR3=0000000000000000
DR6=00000000ffff0ff0 DR7=0000000000000400
CCS=00010000 CCD=0001df90 CCO=CLR
EFER=0000000000000000
check_exception old: 0xd new 0xd
     1: v=08 e=0000 i=0 cpl=0 IP=0010:0000000000100008 pc=0000000000100008 SP=0018:000000000001df90 env->regs[R_EAX]=0000000000000000
EAX=00000000 EBX=00000000 ECX=00000000 EDX=00000000
ESI=00014770 EDI=00000000 EBP=00000000 ESP=0001df90
EIP=00100008 EFL=00000046 [---Z-P-] CPL=0 II=0 A20=1 SMM=0 HLT=0
ES =0000 00000000 00000000 00000000
CS =0010 00000000 ffffffff 00cf9b00 DPL=0 CS32 [-RA]
SS =0018 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
DS =0000 00000000 00000000 00000000
FS =0018 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
GS =0018 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
LDT=0000 00000000 00000000 00008200 DPL=0 LDT
TR =0020 00001000 00000067 00008900 DPL=0 TSS32-avl
GDT=     00014310 00000027
IDT=     00000000 00000000
CR0=00000011 CR2=00000000 CR3=00000000 CR4=00000000
DR0=0000000000000000 DR1=0000000000000000 DR2=0000000000000000 DR3=0000000000000000
DR6=00000000ffff0ff0 DR7=0000000000000400
CCS=00010000 CCD=0001df90 CCO=CLR
EFER=0000000000000000
check_exception old: 0x8 new 0xd
You can ignore the double fault v=08 output. I then loaded up QEMU and set a breakpoint at 0x100000 to see what code was executing. This is what I see when I did a `disas 0x100000,0x100100 /r`:

Code: Select all

Dump of assembler code from 0x100000 to 0x100100:
=> 0x0000000000100000:  fc      cld
   0x0000000000100001:  fa      cli
   0x0000000000100002:  31 c0   xor    eax,eax
   0x0000000000100004:  8e d8   mov    ds,eax
   0x0000000000100006:  8e c0   mov    es,eax
   0x0000000000100008:  8e d0   mov    ss,eax
   0x000000000010000a:  8e e0   mov    fs,eax
   0x000000000010000c:  8e e8   mov    gs,eax
   0x000000000010000e:  48 8d 2d eb fd ff ff    lea    rbp,[rip+0xfffffffffffffdeb]        # 0xffe00
   0x0000000000100015:  8b 86 30 02 00 00       mov    eax,DWORD PTR [rsi+0x230]
   0x000000000010001b:  ff c8   dec    eax
   0x000000000010001d:  48 01 c5        add    rbp,rax
   0x0000000000100020:  48 f7 d0        not    rax
   0x0000000000100023:  48 21 c5        and    rbp,rax
   0x0000000000100026:  48 81 fd 00 00 00 01    cmp    rbp,0x1000000
   0x000000000010002d:  73 07   jae    0x100036
   0x000000000010002f:  48 c7 c5 00 00 00 01    mov    rbp,0x1000000
   0x0000000000100036:  8b 9e 60 02 00 00       mov    ebx,DWORD PTR [rsi+0x260]
   0x000000000010003c:  81 eb 00 a0 d6 00       sub    ebx,0xd6a000
   0x0000000000100042:  48 01 eb        add    rbx,rbp
   0x0000000000100045:  48 8d a3 00 d0 d2 00    lea    rsp,[rbx+0xd2d000]
   0x000000000010004c:  48 8d 05 ad 7d d2 00    lea    rax,[rip+0xd27dad]        # 0xe27e00
   0x0000000000100053:  48 01 40 02     add    QWORD PTR [rax+0x2],rax
   0x0000000000100057:  0f 01 10        lgdt   [rax]
   0x000000000010005a:  6a 10   push   0x10
   0x000000000010005c:  48 8d 05 03 00 00 00    lea    rax,[rip+0x3]        # 0x100066
   0x0000000000100063:  50      push   rax
   0x0000000000100064:  48 cb   retfq
When I step to the instruction at 0x100008 I get the GPF on `mov ss,eax` where EAX was set to 0. Setting ss to 0 should be allowed in 64-bit mode. I then did:

Code: Select all

(gdb) info registers cr0 efer
cr0            0x11                [ ET PE ]
efer           0x0                 [ ]
This suggests you aren't in 64-bit mode at all and that you are in protected mode still thus the reason why `mov ss, eax` failed by loading a NULL selector into the stack segment. I had a similar experience running your code on BOCHS as well. The code in question is in fact that which is found in arch/x86/boot/compressed/head_64.S startup_64 code

Now I don't know what error you got and where. Getting your trace output from QEMU that shows the state at each exception/interrupt would be helpful. I just wanted to let you know what I saw.
rhelin
Posts: 4
Joined: Tue Aug 26, 2025 2:25 pm
Libera.chat IRC: rhelin

Re: Single stage bootloader failing to load the linux kernel

Post by rhelin »

Hello,

Thank you for your reply, I do appreciate the time and effort you have put to run my code.
When using qemu with logs, I have come to the same conclusion that a triple fault is happening.
The code I wrote is failing due to the following reasons apparently, correct me if I'm wrong:
- When jumping to the kernel the cpu was not in long mode, in my code, I have set the cpu even to real mode ( if you see the logs in qemu just before the first exceptio occured you will find the cr0 is set to 0x10 ) and I'm not sure how the cpu switched to protected mode.
- The boot params were not loaded, ESI is pointing to some memory but I don't think there is something there, maybe it was the last address used to do some memory copying before jumping to the kernel.

I'm still a beginner, trying to learn and understand how the bootloader internals work. I guess I will start by fixing the issues of my bootlaoder in order to make it functionnal for 32 bit kernel instead of 64 bit kernel, I think it will be relatively easier.

Thank you for your help.
rhelin
Posts: 4
Joined: Tue Aug 26, 2025 2:25 pm
Libera.chat IRC: rhelin

Re: Single stage bootloader failing to load the linux kernel

Post by rhelin »

I have solved the errors previously mentioned, the changes aims at loading a 32 bit linux kernel, running the final file did not change except from using qemu-system-i386 instead of x86_64.
Howver I'm still facing the triple fault error, the logs from qemu are showing that the instruction causing the issue is localted at 0x102a4.

Code: Select all

DR6=ffff0ff0 DR7=00000400
CCS=00000004 CCD=00000001 CCO=EFLAGS
EFER=0000000000000000
Servicing hardware INT=0x76
     0: v=76 e=0000 i=0 cpl=0 IP=0010:0001029f pc=0001029f SP=0018:0001dffc env->regs[R_EAX]=00000018
EAX=00000018 EBX=00000000 ECX=00000000 EDX=0001e000
ESI=00090000 EDI=00000000 EBP=00000000 ESP=0001dffc
EIP=0001029f EFL=00000206 [-----P-] CPL=0 II=0 A20=1 SMM=0 HLT=0
ES =0018 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
CS =0010 00000000 ffffffff 00cf9a00 DPL=0 CS32 [-R-]
SS =0018 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
DS =0018 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
FS =0018 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
GS =0018 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
LDT=0000 00000000 0000ffff 00008200 DPL=0 LDT
TR =0000 00000000 0000ffff 00008b00 DPL=0 TSS32-busy
GDT=     00007d73 0000001f
IDT=     00000000 000003ff
CR0=00000011 CR2=00000000 CR3=00000000 CR4=00000000
DR0=00000000 DR1=00000000 DR2=00000000 DR3=00000000 
DR6=ffff0ff0 DR7=00000400
CCS=00000018 CCD=0001e000 CCO=LOGICL
EFER=0000000000000000
check_exception old: 0xffffffff new 0xd
     1: v=0d e=03b2 i=0 cpl=0 IP=0010:0001029f pc=0001029f SP=0018:0001dffc env->regs[R_EAX]=00000018
EAX=00000018 EBX=00000000 ECX=00000000 EDX=0001e000
ESI=00090000 EDI=00000000 EBP=00000000 ESP=0001dffc
EIP=0001029f EFL=00000206 [-----P-] CPL=0 II=0 A20=1 SMM=0 HLT=0
ES =0018 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
CS =0010 00000000 ffffffff 00cf9a00 DPL=0 CS32 [-R-]
SS =0018 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
DS =0018 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
FS =0018 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
GS =0018 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
LDT=0000 00000000 0000ffff 00008200 DPL=0 LDT
TR =0000 00000000 0000ffff 00008b00 DPL=0 TSS32-busy
GDT=     00007d73 0000001f
IDT=     00000000 000003ff
CR0=00000011 CR2=00000000 CR3=00000000 CR4=00000000
DR0=00000000 DR1=00000000 DR2=00000000 DR3=00000000 
DR6=ffff0ff0 DR7=00000400
CCS=00000018 CCD=0001e000 CCO=LOGICL
EFER=0000000000000000
check_exception old: 0xd new 0xd
     2: v=08 e=0000 i=0 cpl=0 IP=0010:0001029f pc=0001029f SP=0018:0001dffc env->regs[R_EAX]=00000018
EAX=00000018 EBX=00000000 ECX=00000000 EDX=0001e000
ESI=00090000 EDI=00000000 EBP=00000000 ESP=0001dffc
EIP=0001029f EFL=00000206 [-----P-] CPL=0 II=0 A20=1 SMM=0 HLT=0
ES =0018 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
CS =0010 00000000 ffffffff 00cf9a00 DPL=0 CS32 [-R-]
SS =0018 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
DS =0018 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
FS =0018 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
GS =0018 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
LDT=0000 00000000 0000ffff 00008200 DPL=0 LDT
TR =0000 00000000 0000ffff 00008b00 DPL=0 TSS32-busy
GDT=     00007d73 0000001f
IDT=     00000000 000003ff
CR0=00000011 CR2=00000000 CR3=00000000 CR4=00000000
DR0=00000000 DR1=00000000 DR2=00000000 DR3=00000000 
DR6=ffff0ff0 DR7=00000400
CCS=00000018 CCD=0001e000 CCO=LOGICL
EFER=0000000000000000
check_exception old: 0x8 new 0xd
Triple fault
When using breakpoints and use disas with gdb, I found that this is the instruction that is failing

Code: Select all

  0x0001029d:	sti
   0x0001029e:	push   %ds
   0x0001029f:	push   $0x66cb02a3
   0x000102a4:	cmpl   $0xaa5539d8,(%esi)
   0x000102aa:	pop    %edx
   0x000102ab:	pop    %edx
   0x000102ac:	jne    0x102c5
   0x000102ae:	mov    $0x63b939e0,%edi
   0x000102b3:	dec    %ebp
   0x000102b4:	xor    %ax,%ax
   0x000102b7:	sub    %edi,%ecx
   0x000102b9:	shr    $0x2,%ecx
   0x000102bc:	rep stos %ax,%es:(%edi)
   0x000102bf:	callw  0x11c6
   0x000102c3:	add    %al,(%eax)
   0x000102c5:	mov    $0x3eb,%ax
I loaded the bootparam at the address 0x90000 and copied the header at the address 0x90000 + 0x1F1, the first bytes are zeroed since the kernel will handle the loading of addional information regarding the screen info/acpi and other stuff.
But the instruction there is expecting to have a value of 0xaa5539d8 at 0x90000.
I recall that 0xaa55 is the magic number for the bootloader, however the rest of 2 bytes are unknown to me.
I searched the boot protocol over and over and searched through the linux kernel to find any mention to this but found none. I don't know from where this is coming out.
As a reminder the boot protocol states that before jumping to 0x10200 ( I loaded the kernel real mode at 0x10000 ) the esi register must point to the boot parameter which is located at 0x90000 for my case.

This is my code

Code: Select all

.code16
.global _start

.equ BASE_SEG, 0x1000

_start:
  cli
  xor %ax, %ax
  mov %cs, %ax
  mov %ax, %ds         # DS = CS for literals
  mov $BASE_SEG, %ax
  mov %ax, %es
  xor %bx, %bx

_load_boot_sector:
  mov $0x02, %ah
  mov $1, %al
  mov $0, %ch
  mov $2, %cl 
  mov $0, %dh
  mov $0x80, %dl
  int $0x13
  jc _disk_error
  jmp _load_setup_sectors

_load_setup_sectors:
   mov $0x200, %bx
   mov $0x02, %ah
   movb %es:0x1F1, %al
   mov $0x3, %cl
   int $0x13
   jc _disk_error

_init_kernel_header:
  mov $BASE_SEG, %ax
  mov %ax, %es
  movb $0xFF, %es:0x210
  // heap 
  orb $0x80, %es:0x211
  // heap end_ptr
  movw $0xDE00, %es:0x224
  // initramfs
  movw $0x0, %es:0x218
  mov $0x0, %es:0x21c
  // cmd_line_ptr
  movl $0x1E000, %es:0x228

_copy_cmd_line:
  mov $cmd_line_size, %cx
  mov $cmd_line, %si 
  mov $0xE000, %di 
  rep movsb

_init_zero_page:
  mov $0x9000, %ax
  mov %ax, %es
  mov $0x1F1, %di
  xor %ax, %ax
  mov $0x1000, %cx
  rep stosw

_copy_header:
  // copy the header from 0x101F1 to 0x901F1
  // destination
  mov $0x1000, %ax
  mov %ax, %ds 
  mov $0x1F1, %si
  // count
  xor %ax, %ax
  movb %ds:0x201, %al
  add $0x0202, %ax
  sub $0x1F1, %ax
  mov %ax, %cx
  // source
  // ES is already set up to 0x9000
  mov $0x1F1, %di

  rep movsb
  jmp _switch_pm

_disk_error:
  call _print_error
  jmp .

_print_error:
  mov $0x0E, %ah
  mov $'E', %al
  mov $0, %bh
  mov $0, %bl
  int $0x10 
  ret

_switch_pm:
  xor %ax, %ax
  mov %ax, %ds
  lgdt gdt_desc
  mov %cr0, %eax
  or $0x1, %eax
  mov %eax, %cr0
  mov $2, %al
  out %al, $0x92 
  ljmp $0x10, $_start_32

.code32
_start_32:
  mov $0x18, %ax
  mov %ax, %ds
  mov %ax, %ss
  mov %ax, %es 
  mov %ax, %fs
  mov %ax, %gs
  mov $0x1E000, %esp

_load_lba:
  // loop control var
  mov %es:0x101F4, %edx
  shr $5, %edx
  mov %dx, %bx
  // address of the dest buffer in memory
  mov $0x100000, %edi
  // init 
  mov $0x1F6, %dx
  mov $0x40, %al
  out %al, %dx
  // send sector count high byte
  mov $0x1F2, %dx 
  mov %bh, %al
  out %al, %dx
  // send lba high byte
  mov $0x1F3, %dx 
  mov $0, %al
  out %al, %dx
  mov $0x1F4, %dx 
  mov $0, %al
  out %al, %dx
  mov $0x1F5, %dx 
  mov $0, %al
  out %al, %dx
  // send sector count low byte
  mov $0x1F2, %dx
  mov %bl, %al
  out %al, %dx
  // send lba low byte
  mov $0x1F3, %dx 
  movb %es:0x101F1, %al
  add $2, %al
  out %al, %dx
  mov $0x1F4, %dx 
  mov $0, %al
  out %al, %dx
  mov $0x1F5, %dx 
  mov $0, %al
  out %al, %dx
  // send read command
  mov $0x1F7, %dx
  mov $0x24, %al
  out %al, %dx
  jmp _wait_loop

_wait_loop:
  mov $0x1F7, %dx
  in %dx, %al
  testb $0x08, %al
  jz _wait_loop
  jmp _read

_read:
  mov $256, %cx
  mov $0x1F0, %dx
  rep insw

_sleep:
  mov $0x3F6, %dx 
  in %dx, %al
  in %dx, %al
  in %dx, %al
  in %dx, %al
  dec %bl
  cmp $0, %bl
  je _run_kernel
  jmp _wait_loop

_run_kernel:
  mov $0x90000, %esi 
  xor %eax, %eax
  mov %eax, %edi
  mov %eax, %ebx
  mov %eax, %ebp
  jmp 0x10200

gdt:
gdt_null:
  .long 0x0
  .long 0x0
gdt_null_2:
  .long 0x0
  .long 0x0
gdt_code:
  .word 0xFFFF
  .word 0x0 
  .byte 0x0
  .byte 0x9A 
  .byte 0xCF
  .byte 0x0 
gdt_data:
  .word 0xFFFF
  .word 0x0 
  .byte 0x0
  .byte 0x92 
  .byte 0xCF
  .byte 0x0 
gdt_end:
gdt_desc:
  .word gdt_end - gdt - 1 
  .long gdt

cmd_line:
  .asciz "root=/dev/zero console=ttyS0"
  .byte 0
  
cmd_line_size = . - cmd_line

.org 510
.word 0xAA55
Even when I force it in my code to put that "magic number" , the following instruction is failing inside the linux kernel, also in the above disassembled code, I feel it is werid the value placed in %edi at the instruction 0x102ae.

Code: Select all

0x102bc:	rep stos %ax,%es:(%edi)

For now, I'm not using any initramfs for simplicity, so my expectation is to have a kernel panic at the end.


Any help regarding this. Many thanks in advance
MichaelPetch
Member
Member
Posts: 857
Joined: Fri Aug 26, 2016 1:41 pm
Libera.chat IRC: mpetch

Re: Single stage bootloader failing to load the linux kernel

Post by MichaelPetch »

From the QEMU log it looks like you got IRQ14 (hard disk disk interrupt). v=76 is the interrupt number it came in on. BIOSes generally default to mapping the Slave PIC to 0x70 [IRQ 8] through 0x77 [IRQ 15]. It is clear from the IDT at that point is likely a real mode IVT as seen from: IDT= 00000000 000003ff. It appears while in protected mode a hard disk interrupt occurred when there was no protected mode IDT set up. The end result was the GPF.
Octocontrabass
Member
Member
Posts: 6249
Joined: Mon Mar 25, 2013 7:01 pm

Re: Single stage bootloader failing to load the linux kernel

Post by Octocontrabass »

rhelin wrote: Sat Aug 30, 2025 1:49 pmWhen using breakpoints and use disas with gdb, I found that this is the instruction that is failing
This looks like it's supposed to be 16-bit code, but the CPU is in 32-bit mode. Are you sure you've jumped to the correct entry point address? Are you sure Linux expects the CPU to be in 32-bit mode when you jump to that address?
rhelin
Posts: 4
Joined: Tue Aug 26, 2025 2:25 pm
Libera.chat IRC: rhelin

Re: Single stage bootloader failing to load the linux kernel

Post by rhelin »

Hello all,

I have finally resolved the issues that I faced for building a single stage bootloader to load a recent linux kernel. But first I want to highlight that I have mixed some concepts in my earlier answers. I'm using qemu for testing and qemu is emulating a legacy bios, for this hardware configuration we don't need to load any boot param struct, we have only to load the boot sector, the setup sectors, fill in the kernel header with the appropriate values and load the protected kernel then jump to the offset 0x200 of the setup sectors, the CPU must remain in the real mode. While for new bioses, it is required to have boot param struct and prepare the CPU to be in a protected mode or long mode depending on the kernel ( whether it is 32 bit kernel or 64 bit one ).

So, the first approach I proposed which had the triple fault error was indeed the correct one, however the code itself had several issues that was introduced due to mainly debugging, then it became tricky to spot these errors:

- a wrong line at start of _start label: mov %cx,%ax ( I don't know from where it came from )
- the 16 bit segments were not totally correct, they allowed to jump back to real mode, but the limit should be 0xFFFF instead of 0xFFFFFFFF and the granularity bit should be set to 0. ( this was not a stopper in itself )
- in this approach the instrcution that was failing was mov %eax, %ds, in the first lines of the protected kernel, it will load a new gdt, for my case the gdt loaded had a 0x0 as a size, therefore it was failing. When debugging I found that the size of gdt should be loaded as part of the protected kernel (the address is 0xe2d000 when loading the protected kernel at 0x100000). Well the issue was that when loading the protected mode kernel I used %bl instead of %bx to verify the number of sectors loaded ( bx contain the exact number of sys sectors ) so in my case I was just loading the number of %bl ( low byte of sector count ), the rest of the kernel were simply zeros.

It was a quite silly error especially that I know what I'm doing when I was loading the kernel and the choise to use ATA PIO with 48bit LBA was behind the reason to jump temporarily to protected mode. However it was a good apportunity to learn more and debug better using gdb and qemu.

I will paste here the correct cpde in case someone wants to try it. For now you will get a kernel panic but it is expected since no ramfs is used, maybe this will be my next step to improve this bootloader.

Code: Select all

.code16
.global _start

.equ BASE_SEG, 0x1000

_start:
  cli
  xor %ax, %ax
  mov %ax, %ds
  mov %ax, %ss 
  mov $BASE_SEG, %ax
  mov %ax, %es
  xor %bx, %bx
  call _load_boot_sector
  jmp _switch_pm 

_load_boot_sector:
  mov $0x02, %ah
  mov $1, %al
  mov $0, %ch
  mov $2, %cl 
  mov $0, %dh
  mov $0x80, %dl
  int $0x13
  jc _disk_error
  jmp _load_setup_sectors

_load_setup_sectors:
   mov $0x200, %bx
   mov $0x02, %ah
   movb %es:0x1F1, %al
   mov $0x0, %ch
   mov $0x3, %cl
   mov $0, %dh
   mov $0x80, %dl

   int $0x13
   jc _disk_error

_init_kernel_header:
  mov $BASE_SEG, %ax
  mov %ax, %es
  movb $0xFF, %es:0x210
  // heap 
  orb $0x80, %es:0x211
  // heap end_ptr
  movw $0xDE00, %es:0x224
  // initramfs
  movw $0x0, %es:0x218
  mov $0x0, %es:0x21c
  // cmd_line_ptr
  movl $0x1E000, %es:0x228

_copy_cmd_line:
  cld
  mov $cmd_line_size, %cx
  mov $cmd_line, %si 
  mov $0xE000, %di 
  rep movsb
  ret

_disk_error:
  call _print_error
  jmp .

_print_error:
  mov $0x0E, %ah
  mov $'E', %al
  mov $0, %bh
  mov $0, %bl
  int $0x10 
  ret

_switch_pm:
  lgdt gdt_desc
  mov %cr0, %eax
  or $0x1, %eax
  mov %eax, %cr0
  mov $2, %al
  out %al, $0x92 
  ljmp $0x8, $_start_32

.code32
_start_32:
  mov $0x10, %ax
  mov %ax, %ds
  mov %ax, %ss
  mov %ax, %es 
  mov %ax, %fs
  mov %ax, %gs
  mov $0x90000, %esp

_load_lba:
  // loop control var
  mov %es:0x101F4, %edx
  shr $5, %edx
  mov %dx, %bx
  // address of the dest buffer in memory
  mov $0x100000, %edi
  // init 
 // send sector count high byte
  mov $0x1F2, %dx 
  mov %bh, %al
  out %al, %dx
  // send lba high byte
  mov $0x1F3, %dx 
  mov $0, %al
  out %al, %dx
  mov $0x1F4, %dx 
  mov $0, %al
  out %al, %dx
  mov $0x1F5, %dx 
  mov $0, %al
  out %al, %dx
  // send sector count low byte
  mov $0x1F2, %dx
  mov %bl, %al
  out %al, %dx
  // send lba low byte
  mov $0x1F3, %dx 
  movb %es:0x101F1, %al
  add $2, %al
  out %al, %dx
  mov $0x1F4, %dx 
  mov $0, %al
  out %al, %dx
  mov $0x1F5, %dx 
  mov $0, %al
  out %al, %dx
  //
  mov $0x1F6, %dx
  mov $0x40, %al
  out %al, %dx
  // send read command
  mov $0x1F7, %dx
  mov $0x24, %al
  out %al, %dx
  jmp _wait_loop

_wait_loop:
  mov $0x1F7, %dx
  in %dx, %al
  testb $0x08, %al
  jz _wait_loop
  jmp _read

_read:
  mov $256, %cx
  mov $0x1F0, %dx
  rep insw

_sleep:
  mov $0x3F6, %dx 
  in %dx, %al
  in %dx, %al
  in %dx, %al
  in %dx, %al
  dec %bx
  cmp $0, %bx
  je _reset_16
  jmp _wait_loop

// use a 16 bit data/code segment to go back to the real mode
_reset_16:
  cli
  mov $0x20, %eax
  mov %eax, %ds
  mov %eax, %ss
  mov %eax, %es 
  mov %eax, %fs
  mov %eax, %gs
  ljmp $0x18, $_switch_rm

.code16
_switch_rm:
  mov %cr0, %eax
  and $~1, %eax
  mov %eax, %cr0
  ljmp $0x0000, $_start_16

_start_16:
  // reload the segments with the base seg value: 0x1000
  cli
  mov $BASE_SEG, %ax
  mov %ax, %ds 
  mov %ax, %es 
  mov %ax, %ss
  mov %ax, %gs
  mov %ax, %fs
  mov $0xE000, %esp
  ljmp $0x1020, $0x0

gdt:
gdt_null:
  .long 0x0
  .long 0x0
gdt_code:
  .word 0xFFFF
  .word 0x0 
  .byte 0x0
  .byte 0x9A 
  .byte 0xCF
  .byte 0x0 
gdt_data:
  .word 0xFFFF
  .word 0x0 
  .byte 0x0
  .byte 0x92 
  .byte 0xCF
  .byte 0x0 
gdt_16_code:
  .word 0xFFFF
  .word 0x0 
  .byte 0x0
  .byte 0x9b 
  .byte 0x0
  .byte 0x0 
gdt_16_data:
  .word 0xFFFF
  .word 0x0 
  .byte 0x0
  .byte 0x93 
  .byte 0x0
  .byte 0x0 

gdt_end:
gdt_desc:
  .word gdt_end - gdt - 1 
  .long gdt

cmd_line:
  .asciz "root=/dev/zero console=tty0 nokaslr"
  .byte 0
  
cmd_line_size = . - cmd_line

.org 510
.word 0xAA55


Thank you all for your help and responsivness, if you have any advice for a beginner like me who is passionate about low level and os, do not hesitate to share your thoughts.
Post Reply