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
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
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.

