How to develop a custom Linux bootloader
Posted: Sat Oct 09, 2021 5:52 pm
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: start.S
init.c
grub.cfg
The commands:
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
bsect.asm
build.sh
The commands:
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: 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
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;
}
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
}
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)
config.inc
Code: Select all
%define cmdLineDef "root=/dev/sda1 ro"
;initRdSizeDef - defined in build script automatically
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
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))"
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