How to develop a custom Linux bootloader

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
slammar
Posts: 24
Joined: Fri Feb 07, 2020 7:35 pm
Libera.chat IRC: slammar

How to develop a custom Linux bootloader

Post by slammar »

Hello. I am trying to understand the Linux Boot Protocol and came across this github repo. I also came across this YouTube video showing how to use the linux kernel to develop your own OS. In minute 21:35 you can see how he basically made an init executable that shows the 'TICK!' message every few seconds. What I am basically trying to do is what the guy form the YouTube video made but using the tiny linux bootloader from the GitHub repository instead of Grub, but I can't because I get a Guru meditation error in VirtualBox and I wanted to see if you can tell me what I am doing wrong. Keep in mind that I successfully made the 'TICK!' thing with Grub as shown in youtube.

In VirtualBox I have 2 machines: The one I use to develop and the one I use to try the OS. In the first one I have attached 2 hard disks: One with Ubuntu installed (/dev/sda) and another where the new OS goes (/dev/sdb). In the second machine there is only one hard disk attached which is the same I use in the main one with Ubuntu to install the new OS. In other words, /dev/sdb in main machine is /dev/sda in machine with custom OS.

These are the files and commands used in the youtube video, summarized for you:
files.zip
(4.97 KiB) Downloaded 33 times
start.S

Code: Select all

.globl _start
.text
_start:
    call main

.globl _syscall
_syscall:
    movq %rdi, %rax
    movq %rsi, %rdi
    movq %rdx, %rsi
    movq %rcx, %rdx
    movq %r8, %r10
    movq %r9, %r8
    movq 8(%rsp), %r9
    syscall
    ret
init.c

Code: Select all

#include <syscall.h>
#include <fcntl.h>

unsigned long _syscall(int num, void *a0, void *a1, void *a2, void *a3, void *a4, void *a5);

unsigned long _strlen(char *sz) {
    int count = 0;

    while(*sz++) {
        count++;
    }

    return count;
}

void delay(int ticks) {
    for (int i=0; i<ticks; i++) {
        //nothing...
    }
}

void print_string(char *str) {
    _syscall(SYS_write, (void *)1 /*stdout*/, str, (void *)_strlen(str), 0, 0, 0);
}

int main() {
    char *msg = "MyOS 0.0.0.1 Initializing...\n";

    print_string(msg);

    while(1) {
        //event loop, for now just tick...
        delay(10000000);
        print_string("TICK!\n");
    }

    return 0;
}
grub.cfg

Code: Select all

set default=0
set timeout=30

menuentry "MyOS 0.0.0.1" {
    linux /boot/vmlinuz root=/dev/sda1 ro
    initrd /boot/initrd.img
}
The commands:

Code: Select all

# Execute all commands as root
sudo su

# Install GCC
apt install -y gcc
# Clean the contents of the drive
dd if=/dev/zero of=/dev/sdb bs=300000000 count=1
# Format the disk with a single partition that takes up the whole disk
sfdisk --label=dos /dev/sdb <<<', , , *'
# Create the filesystem
mkfs.ext4 /dev/sdb1
mkdir /mnt/myos
mount /dev/sdb1 /mnt/myos
rm -rf /mnt/myos/lost+found
mkdir -pv /mnt/myos/{bin,sbin,etc,lib,lib64,var,dev,proc,sys,run,tmp,boot}
mknod -m 600 /mnt/myos/dev/console c 5 1
mknod -m 666 /mnt/myos/dev/null c 1 3
# Copy the linux kernel and initrd image
cp /boot/vmlinuz-$(uname -r) /mnt/myos/boot/vmlinuz
cp /boot/initrd.img-$(uname -r) /mnt/myos/boot/initrd.img
# Install grub in /dev/sdb
grub-install /dev/sdb --skip-fs-probe --boot-directory=/mnt/myos/boot
# Compile and use the init.c file
gcc -nostdlib -ffreestanding -no-pie init.c start.S -o init
cp init /mnt/myos/sbin/
# Put the grub.cfg file in /myos/boot/grub
cp grub.cfg /mnt/myos/boot/grub/































Below are the files and commands I execute to use the tiny linux bootloader with the TICK! init executable. Apart from removing the GNU GPL License from the files for simplicity, these are the modifications that I did to them:
  • config.inc: Changed the cmdLineDef to "root=/dev/sda1 ro" because that's the one used in the grub.cfg file of the youtube video.
  • bsect.asm: Didn't change anything
  • build.sh: Changed the KERN and RD variables to use my kernel and initrd image with $(uname -r)
Also note that the build.sh script prints in the last line the sector where the first partition should start. You will have to replace 187814 with this value in the sfdisk command.

config.inc

Code: Select all

%define cmdLineDef "root=/dev/sda1 ro"

;initRdSizeDef - defined in build script automatically
bsect.asm

Code: Select all

%define DEBUG
%include "config.inc"

[BITS 16]
org	0x7c00

	cli
	xor	ax, ax
	mov	ds, ax
	mov	ss, ax
	mov	sp, 0x7c00			; setup stack 

    ; now get into protected move (32bit) as kernel is large and has to be loaded high
    mov ax, 0x2401 ; A20 line enable via BIOS
    int 0x15
    jc err


    lgdt [gdt_desc]
    mov eax, cr0
    or eax, 1
    mov cr0, eax

    jmp $+2

    mov bx, 0x8 ; first descriptor in GDT
    mov ds, bx
    mov es, bx
    mov gs, bx

    and al, 0xFE ; back to real mode
    mov cr0, eax
    
    xor ax,ax ; restore segment values - now limits are removed but seg regs still work as normal
	mov	ds, ax
	mov	gs, ax
    mov ax, 0x1000 ; segment for kernel load (mem off 0x10000)
	mov	es, ax
    sti

    ; now in UNREAL mode

    mov ax, 1 ; one sector
    xor bx,bx ; offset
    mov cx, 0x1000 ; seg
    call hddread

read_kernel_setup:
    mov al, [es:0x1f1] ; no of sectors
    cmp ax, 0
    jne read_kernel_setup.next
    mov ax, 4 ; default is 4 

.next:
    ; ax = count
    mov bx, 512 ; next offset
    mov cx, 0x1000 ; segment
    call hddread

    cmp word [es:0x206], 0x204
    jb err
    test byte [es:0x211], 1
    jz err

    mov byte [es:0x210], 0xe1 ;loader type
    mov byte [es:0x211], 0x81 ;heap use? !! SET Bit5 to Make Kern Quiet
    mov word [es:0x224], 0xde00 ;head_end_ptr
    mov byte [es:0x227], 0x01 ;ext_loader_type / bootloader id
    mov dword [es:0x228], 0x1e000 ;cmd line ptr

    ; copy cmd line 
    mov si, cmdLine
    mov di, 0xe000 
    mov cx, cmdLineLen
    rep movsb ; copies from DS:si to ES:di (0x1e000)

    ; modern kernels are bzImage ones (despite name on disk and so
    ; the protected mode part must be loaded at 0x100000
    ; load 127 sectors at a time to 0x2000, then copy to 0x100000

;load_kernel
    mov edx, [es:0x1f4] ; bytes to load
    shl edx, 4
    call loader

;load initrd
    mov eax, 0x7fab000; this is the address qemu loads it at
    mov [highmove_addr],eax ; end of kernel and initrd load address
    ;mov eax, [highmove_addr] ; end of kernel and initrd load address
    ;add eax, 4096
    ;and eax, 0xfffff000
    ;mov [highmove_addr],eax ; end of kernel and initrd load address

    mov [es:0x218], eax
    mov edx, [initRdSize] ; ramdisk size in bytes
    mov [es:0x21c], edx ; ramdisk size into kernel header
    call loader



kernel_start:
    cli
    mov ax, 0x1000
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    mov sp, 0xe000
    jmp 0x1020:0

    jmp $

; ================= functions ====================
;length in bytes into edx
; uses hddread [hddLBA] and highmove [highmove_addr] vars
;clobbers 0x2000 segment
loader:
.loop:
    cmp edx, 127*512
    jl loader.part_2
    jz loader.finish

    mov ax, 127 ;count
    xor bx, bx ; offset
    mov cx, 0x2000 ; seg
    push edx
    call hddread
    call highmove
    pop edx
    sub edx, 127*512

    jmp loader.loop

.part_2:   ; load less than 127*512 sectors
    shr edx, 9  ; divide by 512
    inc edx     ; increase by one to get final sector if not multiple - otherwise just load junk - doesn't matter
    mov ax, dx
    xor bx,bx
    mov cx, 0x2000
    call hddread
    call highmove

.finish:
    ret

highmove_addr dd 0x100000
; source = 0x2000
; count = 127*512  fixed, doesn't if matter we copy junk at end
; don't think we can use rep movsb here as it wont use EDI/ESI in unreal mode
highmove:
    mov esi, 0x20000
    mov edi, [highmove_addr]
    mov edx, 512*127
    mov ecx, 0 ; pointer
.loop:
    mov eax, [ds:esi]
    mov [ds:edi], eax
    add esi, 4
    add edi, 4
    sub edx, 4
    jnz highmove.loop
    mov [highmove_addr], edi
    ret

err:
%ifdef DEBUG
    mov si, errStr
    call print
%endif
    jmp $

%ifdef DEBUG
; si = source str
print:
    lodsb
    and al, al
    jz print.end
    mov ah, 0xe
    mov bx, 7
    int 0x10
    jmp print
print.end:
    ret
%endif

hddread:
    push eax
    mov [dap.count], ax
    mov [dap.offset], bx
    mov [dap.segment], cx
    mov edx, dword [hddLBA]
    mov dword [dap.lba], edx
    and eax, 0xffff
    add edx, eax       ; advance lba pointer
    mov [hddLBA], edx
    mov ah, 0x42
    mov si, dap
    mov dl, 0x80 ; first hdd
    int 0x13
    jc err
    pop eax
    ret

dap:
    db 0x10 ; size
    db 0 ; unused
.count:
    dw 0 ; num sectors
.offset:
    dw 0 ;dest offset
.segment:
    dw 0 ;dest segment
.lba:
    dd 0 ; lba low bits
    dd 0 ; lba high bits

;descriptor
gdt_desc:
    dw gdt_end - gdt - 1
    dd gdt

; access byte: [present, priv[2] (0=highest), 1, Execbit, Direction=0, rw=1, accessed=0] 
; flags: Granuality (0=limitinbytes, 1=limitin4kbs), Sz= [0=16bit, 1=32bit], 0, 0

gdt:
    dq 0 ; first entry 0
;flat data segment
    dw 0FFFFh ; limit[0:15] (aka 4gb)
    dw 0      ; base[0:15]
    db 0      ; base[16:23]
    db 10010010b  ; access byte 
    db 11001111b    ; [7..4]= flags [3..0] = limit[16:19]
    db 0 ; base[24:31]
gdt_end:

%ifdef DEBUG
    errStr db 'err!!',0
%endif

; config options
    cmdLine db cmdLineDef,0
    cmdLineLen equ $-cmdLine
    initRdSize dd initRdSizeDef ; from config.inc
    hddLBA dd 1   ; start address for kernel - subsequent calls are sequential

;boot sector magic
	times	510-($-$$)	db	0
	dw	0xaa55


; real mode print code
;    mov si, strhw
;    mov eax, 0xb8000
;    mov ch, 0x1F ; white on blue
;loop:
;    mov cl, [si]
;    mov word [ds:eax], cx
;    inc si
;    add eax, 2
;    cmp [si], byte 0
;    jnz loop
build.sh

Code: Select all

INPUT="bsect.asm"
OUTPUT="disk"
KERN="/boot/vmlinuz-$(uname -r)"
RD="/boot/initrd.img-$(uname -r)"

#size of kern + ramdisk
K_SZ=`stat -c %s $KERN`
R_SZ=`stat -c %s $RD`

#padding to make it up to a sector
K_PAD=$((512 - $K_SZ % 512))
R_PAD=$((512 - $R_SZ % 512))

nasm -o $OUTPUT -D initRdSizeDef=$R_SZ $INPUT
cat $KERN >> $OUTPUT
if [[ $K_PAD -lt 512 ]]; then
    dd if=/dev/zero bs=1 count=$K_PAD >> $OUTPUT
fi

cat $RD >> $OUTPUT
if [[ $R_PAD -lt 512 ]]; then
    dd if=/dev/zero bs=1 count=$R_PAD >> $OUTPUT
fi

TOTAL=`stat -c %s $OUTPUT`
echo "concatenated bootloader, kernel and initrd into ::> $OUTPUT"
echo "Note, your first partition must start after sector $(($TOTAL / 512))"
The commands:

Code: Select all

# Execute all commands as root
sudo su

# Install GCC & NASM
apt install -y gcc nasm
# Build the bootloader
./build.sh
# Clean the contents of the drive
dd if=/dev/zero of=/dev/sdb bs=300000000 count=1
# Format the disk with a single partition that takes up the whole disk starting as where the build.sh script said
sfdisk --label=dos /dev/sdb <<<'187814, , , *'
# Create the filesystem
mkfs.ext4 /dev/sdb1
mkdir /mnt/myos
mount /dev/sdb1 /mnt/myos
rm -rf /mnt/myos/lost+found
mkdir -pv /mnt/myos/{bin,sbin,etc,lib,lib64,var,dev,proc,sys,run,tmp,boot}
mknod -m 600 /mnt/myos/dev/console c 5 1
mknod -m 666 /mnt/myos/dev/null c 1 3
# Compile and use the init.c file
gcc -nostdlib -ffreestanding -no-pie init.c start.S -o init
cp init /mnt/myos/sbin/
# Replace the MBR in the generated disk file
dd if=/dev/sdb of=disk bs=1 count=$((512-446)) skip=446 seek=446 conv=notrunc
# Install the bootloader in /dev/sdb
dd if=disk of=/dev/sdb conv=notrunc
Last edited by slammar on Tue Oct 12, 2021 9:22 am, edited 4 times in total.
Octocontrabass
Member
Member
Posts: 5563
Joined: Mon Mar 25, 2013 7:01 pm

Re: How to develop a custom Linux bootloader

Post by Octocontrabass »

Well, that's an awful lot of code to look through.
slammar wrote:I get a Guru meditation error in VirtualBox
Where's the log? More details about the error will help you figure out where to look.
nullplan
Member
Member
Posts: 1790
Joined: Wed Aug 30, 2017 8:24 am

Re: How to develop a custom Linux bootloader

Post by nullplan »

I already see a structural problem in the bootloader: It is using unreal mode to copy the kernel to high memory, but is calling BIOS functions in the meantime. BIOS might need to do some things in 32-bit mode, and when it restores 16-bit mode, it might not restore unreal mode. I would suggest copying memory with function 87h from interrupt 15h.

Another change I would make (because I am a pedant) is that I would test for the A20 gate being enabled before calling the BIOS function to enable it. Maybe the BIOS function is not needed. And maybe the BIOS function worked but returned carry anyway, so I would also test it afterwards, with a longer test, in case system hardware needs a bit of time to catch up. I suggest using something like this:

Code: Select all

a20_test:
; Assume DS = 0, ES = FFFF, CX = loop count, bootsig = symbol at 0x7dfe
; Clobbers AX, DX, returns carry iff a20 gate is closed
; Code idea stolen from Linux kernel (a20_test())
  mov ax, [bootsig]
.loop:
  inc ax
  mov [bootsig], ax
  out 80h, al ; io_delay()
  mov dx, ax
  xor dx, [es:7e0eh]
  jnz .success
  loop .loop
  stc
  ret
.success:
  clc
  ret
Carpe diem!
Post Reply