====== Lab Work 3 - Bootstrap Loader ======
In this module, we will explore the world of bootstrapping - the process of starting up a system. Since this topic is mostly platform dependent, we will focus on x86 systems.
You will need to go through x86 assembly programming (using [[http://www.nasm.us/|NASM assembler]] - mind your syntax!), which is very similar to 8085 assembly (but with a lot more instructions to learn => more features!). You can get a text file on x86 instruction set [[https://github.com/azman/my1load86/blob/master/doc/intel_x86.txt|here]]. We will also be using [[http://www.qemu.org|QEMU]] to emulate our hardware (so that you do not destroy everything you have on your PC). Both are already available on the provided live system. If you are using your own Linux installation, please make sure you have them installed for this lab.
====== System Startup (x86) ======
Booting a system (a more technical term would be bootstrapping):
* usually BIOS on non-volatile memory (ROM or NVRAM)
* Memory location 0xFFFFFFF0 on x86 systems? (0xFFFF0 on 8086/8088)
* on some modern systems, power-related stuffs may run before handing over to BIOS?
* handles/prepares low-level hardware/software interface
* bios interrupt service (interrupt vector table): 10h (display), 13h (disks), 16h (keyboard)
* stack pointer is 512 bytes after bootsector (stack size!)?
* 512-bytes sector was standard disk sector size
* mostly bypassed by modern 'plug-n-play' operating systems
* PnP OS writes their own service routine (overwrites IVT?)
* loads sector 0, cylinder 0 of selected boot drive
* first sector (512 bytes) will be loaded to 0x7C00 (segment 0)
* some bios used 0x07C0:0x0000 (same physical, different segment!)
* execution transferred to that location
* DL register holds boot drive number (e.g. 0x80 for first hard disk)
* some BIOS will NOT transfer if INVALID MBR (e.g. no boot signature 0xAA55)
* the code in sector 0 (first sector) is usually the bootloader
* for a partitioned disk, there should be an MBR (classic MBR? 'modern' MBR?)
* for non-partitioned disk (e.g. floppy) simply the bootloader?
* enables multiple kernel to be selected at run-time
* can be skipped for static single kernel usage? coreboot?
* loads the kernel specified in its configuration
* BIOS is now being replaced by (U)EFI - [[wp>UEFI|Read more @ wikipedia]]
* but legacy BIOS startup is still supported
[[wp>BIOS|Read more @ wikipedia]]
{{page>notes:x86pc_mbr&noheader}}
===== Disk Layout =====
* partitioned storage usually starts after a pre-defined offset
* this info can be found in MBR partition table or GPT
* e.g. with MBR, first partition usually starts at 1MB offset (2048 sectors)
* some OS use this storage space for various information
* e.g. FreeBSD disklabel can be found within the first 64 sector
* @offset 0x8000 : sequence id => 0x57 0x45 0x56 0x82
====== Bootloader Code Basics ======
This is a simple tutorial on how to develop a simple bootloader for x86 compatible machines, meant to be used with (more specifically, loaded from) a USB drive.
===== Using QEMU for Testing =====
It would be a nightmare to test a bootloader code using an actual USB drive booting an actual machine unless you have other machine than the one you are using for writing/building the code. The best way to do this is using a virtual machine and I recommend QEMU for this.
First we create a 512MB((This is big enough to create FAT32 partition, but still not TOO big to be annoying :-P)) USB disk image using ''qemu-img''qemu-img create -f raw disk.img 512M
Although not necessary, most USB drives nowadays are detected HDD and thus BIOS expects an MBR (I'm not going into GPT for now). To create proper partition table in MBR with a single bootable W95 FAT32 (LBA) partition I use ''fdisk''fdisk disk.img
A 'faster' way would be to simply type
fdisk disk.img <
Notice there are two newline characters (hit ENTER twice) between '1' and 't'. The ''fdisk'' version I'm using writes a random (at least I think it's random) 32-bit disk signature at location 0x1b8 that is optional and can be overwritten. Use hexdump -C disk.img -n 512
to verify 0x55, 0xAA sequence at location 0x1fe and 0x1ff respectively.
This is the part which is previously NOT needed due to the fact that floppy disks do not need partition tables (non-partitioned storage). Since the bootloader code we developed is exactly 512 bytes, it is obvious that the code will overwrite the partition information in the MBR. To avoid this we only write 446 bytes instead on the full 512 bytes, which is fine since the 0x55 0xAA sequence is already there. Due to the fact that we are writing to a disk image, we need to add ''conv=notrunc'' option so that the image file will NOT be truncated. (This is not a problem when the target is a device file!) dd if=boot.bin of=disk.img bs=1 count=446 conv=notrunc
Finally... to test the USB disk image qemu-system-i386 usb_drive.img
===== Helper Scripts and Makefile =====
To make things easier, I have written some shell scripts to help with creation of disk image, copying boot code into the image and testing the boot code on the disk image (running qemu).
Project makefile (using ''nasm'' assembler)
# make for simple bootloader
MY1BOOTLD = boot.bin
AS = nasm
ASFLAGS =
.PHONY: boot
all: boot
boot: $(MY1BOOTLD)
new: clean all
clean:
rm -rf *.bin *.lst
%.bin: %.asm
$(AS) $(ASFLAGS) -f bin $< -o $@
%.lst: %.asm
$(AS) $(ASFLAGS) -f bin $< -l $@
Disk image creator (using qemu-img)
#!/bin/bash
MY1USBDRV=""
MY1IMGLEN=""
PARTLABEL=""
DO_FAT_32=""
QEMU_BIN="qemu-img"
QEMU_IMG=$(which "$QEMU_BIN" 2>/dev/null)
[ ! -x "$QEMU_IMG" ] &&
echo "Cannot find QEMU binary '$QEMU_BIN'! Aborting!" && exit 1
# parse command parameters
while [ "$1" != "" ]; do
case $1 in
--image|-i)
shift
MY1USBDRV=$1
;;
--size|-s)
shift
MY1IMGLEN=$1
;;
--fat32)
DO_FAT_32="YES"
;;
--label)
shift
PARTLABEL=$1
;;
-*)
echo "Unknown option '$1'"
exit 1
;;
*)
echo "Unknown parameter '$1'!"
exit 1
;;
esac
shift
done
# use default settings if necessary
[ "$MY1USBDRV" == "" ] && MY1USBDRV="disk.img"
[ "$MY1IMGLEN" == "" ] && MY1IMGLEN="512M"
[ "$PARTLABEL" == "" ] && PARTLABEL="MY1TESTPART"
# create image file
echo -n "Creating disk image '${MY1USBDRV}' at size '${MY1IMGLEN}'... "
${QEMU_IMG} create -f raw ${MY1USBDRV} ${MY1IMGLEN} >/dev/null
[ $? -ne 0 ] && echo "Failed! Aborting!" && exit 1
echo "done!"
# CHS params provided are bogus values... for now?
echo -n "Creating single partition disk and flag as bootable... "
fdisk -C32 -S63 -H255 ${MY1USBDRV} >/dev/null 2>&1 </dev/null
mkdosfs -F 32 -n "$PARTLABEL" ${MY1TEMP} >/dev/null
dd if=${MY1TEMP} of=${MY1USBDRV} bs=1 count=${PART_SIZE} ${OPTS} 2>/dev/null
rm ${MY1TEMP}
[ $? -ne 0 ] && echo "Failed! Aborting!" && exit 1
echo "done!"
fi
Boot image installer (using dd)
#!/bin/bash
MY1USBDEF="disk.img"
MY1USBDRV=""
MY1BOOTLD=""
# parse command parameters
while [ "$1" != "" ]; do
case $1 in
--image|-i)
shift
MY1USBDRV=$1
;;
-*)
echo "Unknown option '$1'"
exit 1
;;
*)
[ "$MY1BOOTLD" != "" ] &&
echo "Unknown parameter ($1)!" && exit 1
MY1BOOTLD=$1
;;
esac
shift
done
[ "$MY1USBDRV" = "" ] && MY1USBDRV="$MY1USBDEF"
[ ! -f "$MY1USBDRV" ] &&
echo "Cannot find disk image '$MY1USBDRV'! Aborting!" && exit 1
[ "$MY1BOOTLD" = "" ] && MY1BOOTLD="boot.bin"
[ ! -f "$MY1BOOTLD" ] &&
echo "Cannot find boot image '$MY1BOOTLD'! Aborting!" && exit 1
DD_OPT_MBR1="bs=1 count=446 conv=notrunc"
DD_OPT_MBR2="bs=1 count=2 conv=notrunc skip=510 seek=510"
echo -n "Copying boot code '$MY1BOOTLD' to disk image '$MY1USBDRV'... "
dd if=$MY1BOOTLD of=$MY1USBDRV $DD_OPT_MBR1 2>/dev/null
[ $? -ne 0 ] && echo "Failed! Aborting!" && exit 1
dd if=$MY1BOOTLD of=$MY1USBDRV $DD_OPT_MBR2 2>/dev/null
[ $? -ne 0 ] && echo "Failed! Aborting!" && exit 1
echo "done!"
Help script to run qemu
#!/bin/bash
MY1USBDRV="$1"
[ "$MY1USBDRV" == "" ] && MY1USBDRV="disk.img"
QEMU_BIN="qemu-system-i386"
QEMU_SYS=$(which "$QEMU_BIN" 2>/dev/null)
[ ! -x "$QEMU_SYS" ] &&
echo "Cannot find QEMU binary '$QEMU_BIN'! Aborting!" && exit 1
[ ! -f "$MY1USBDRV" ] &&
echo "Cannot find disk image '$MY1USBDRV'! Aborting!" && exit 1
${QEMU_SYS} ${MY1USBDRV}
The makefile is for you to assemble (using NASM) your bootloader code. If your code is ''thing.asm'', run make thing.bin
To create a disk image (instead of running the commands above), you can simply run sh disk_create.sh
to create a 512MB image file named ''disk.img''.
To insert your bootloader code ''thing.bin'' into the disk image, you may run sh disk_init.sh thing.bin
Finally, to test the image, run (duh!)sh disk_test.sh
===== Simple Bootloader Code =====
A simple code that just hangs around...
bits 16
org 0x7c00
; hang around
hang:
jmp hang
; filler
times 510-($-$$) db 0
; the boot signature
db 0x55,0xAA
===== BIOS Interrupts =====
The BIOS (now called legacy BIOS in [U]EFI systems) system is actually very handy to have early on the system. It allows bootloader code to access all available hardware on the system with ease. This is done via the BIOS interrupt. Listed below are the commonly used interrupt routines:
* Video:
* INT 0x10, AH = 1 -- set up the cursor
* INT 0x10, AH = 3 -- cursor position
* INT 0x10, AH = 0xE -- display char
* INT 0x10, AH = 0xF -- get video page and mode
* INT 0x10, AH = 0x11 -- set 8x8 font
* INT 0x10, AH = 0x12 -- detect EGA/VGA
* INT 0x10, AH = 0x13 -- display string
* INT 0x10, AH = 0x1200 -- Alternate print screen
* INT 0x10, AH = 0x1201 -- turn off cursor emulation
* INT 0x10, AX = 0x4F00 -- video memory size
* INT 0x10, AX = 0x4F01 -- VESA get mode information call
* INT 0x10, AX = 0x4F02 -- select VESA video modes
* INT 0x10, AX = 0x4F0A -- VESA 2.0 protected mode interface
* Disk:
* INT 0x13, AH = 0 -- reset floppy/hard disk
* INT 0x13, AH = 2 -- read floppy/hard disk in CHS mode
* INT 0x13, AH = 3 -- write floppy/hard disk in CHS mode
* INT 0x13, AH = 0x41 -- test existence of INT 13 extensions
* INT 0x13, AH = 0x42 -- read hard disk in LBA mode
* INT 0x13, AH = 0x43 -- write hard disk in LBA mode
* Memory:
* INT 0x12 -- get low memory size
* INT 0x15, EAX = 0xE820 -- get complete memory map
* INT 0x15, AX = 0xE801 -- get contiguous memory size
* INT 0x15, AX = 0xE881 -- get contiguous memory size
* INT 0x15, AH = 0x88 -- get contiguous memory size
* INT 0x15, AH = 0xC0 -- Detect MCA bus
* INT 0x15, AX = 0x0530 -- Detect APM BIOS
* INT 0x15, AH = 0x5300 -- APM detect
* INT 0x15, AX = 0x5303 -- APM connect using 32 bit
* INT 0x15, AX = 0x5304 -- APM disconnect
* Keyboard:
* INT 0x16, AH = 0 -- read keyboard scancode (blocking)
* INT 0x16, AH = 1 -- read keyboard scancode (non-blocking)
* INT 0x16, AH = 3 -- keyboard repeat rate
* Other:
* INT 0x11 -- Hardware detection
===== More Bootloader Codes =====
==== Print Single Char ====
This code prints a single letter on the screen...
bits 16
org 0x7c00
; main code
init:
mov al, 0x41 ; 'A'?
mov ah, 0x0e ; function: display char (tty?)
int 0x10 ; video display interrupt
; hang around
hang:
jmp hang
; filler
times 510-($-$$) db 0
; the boot signature
db 0x55,0xAA
==== Print String ====
The code above uses BIOS interrupt to send a character to display. A more useful code would be to display a string instead of a single character.
bits 16
org 0x7c00
; main code
init:
mov bp,mesg
loop:
mov al,[bp]
or al,al
jz hang
mov ah,0x0e ; function: display char (tty?)
int 0x10 ; video display interrupt
inc bp
jmp loop
; hang around
hang:
jmp hang
; message
mesg:
db 13,10,"Hello UniMAP!",13,10,0
; filler
times 510-($-$$) db 0
; the boot signature
db 0x55,0xAA
An even more efficient way to move an array of bytes is to use the ''lodsb'' instruction, which in turn requires the use of ''si'' register instead of ''bp''.
bits 16
org 0x7c00
; main code
init:
mov si,mesg
loop:
lodsb
or al,al
jz hang
mov ah,0x0e ; function: display char (tty?)
int 0x10 ; video display interrupt
jmp loop
; hang around
hang:
jmp hang
; message
mesg:
db 13,10,"Hello UniMAP! ... again!",13,10,0
; filler
times 510-($-$$) db 0
; the boot signature
db 0x55,0xAA
==== Get Char ====
What about getting an input? A keystroke from the keyboard...
bits 16
org 0x7c00
; main code
init:
mov si,mesg
call puts
; read keyboard, once we get one print something else
mov ah,0 ; function: read scancode (blocking)
int 0x16 ; keyboard interrupt
; AH = BIOS scan code ; AL = ASCII character
; for non-blocking version (ah=1)
; ZF set if no keystroke available ; ZF clear if keystroke available
mov si,more
call puts
; hang around
hang:
jmp hang
; message
mesg:
db 13,10,"Hello UniMAP! ... again!",13,10,0
more:
db 13,10,"Got you!",13,10,0
; sub-routine to print string pointed by si
puts:
lodsb
or al,al
jz .done
mov ah,0x0e ; function: display char (tty?)
int 0x10 ; video display interrupt
jmp puts
.done:
ret
; filler
times 510-($-$$) db 0
; the boot signature
db 0x55,0xAA
Notice that the code to print a string has been re-organized as a sub-routine (portion of code that can be reuse, called from other parts of the program).
==== Get String ====
What if we need to read a string? (**Hint**: need to detect a space or the key)
bits 16
org 0x7c00
; main code
init:
mov si,mesg
call puts
mov di,save
call gets
mov si,more
call puts
mov si,save
call puts
mov si,next
call puts
; hang around
hang:
jmp hang
; message
mesg:
db 13,10,"Enter your name: ",0
more:
db 13,10,"Hello, ",0
next:
db "!",13,10,0
save:
times 20 db 0
; sub-routine to print string pointed by si
puts:
lodsb
or al,al
jz .done
mov ah,0x0e ; function: display char (tty?)
int 0x10 ; video display interrupt
jmp puts
.done:
ret
; sub-routine to get a string and save to mem pointer by di
gets:
mov ah,0 ; function: read scancode (blocking)
int 0x16 ; keyboard interrupt
cmp al,13
jz .done
stosb
mov ah,0x0e ; function: display char (tty?)
int 0x10 ; video display interrupt
jmp gets
.done:
xor al,al
stosb
ret
; filler
times 510-($-$$) db 0
; the boot signature
db 0x55,0xAA
Note that both gets and puts subroutines utilizes ''di'' and ''si'' registers (and therefore, need proper assignments befire using them) respectively.
==== Some useful subroutines ====
Figure out what these does and see if you need them =)
str2ui:
xor dx,dx
mov bx,10
.more:
lodsb
or al,al
jz .done
sub al,0x30
xor cx,cx
mov cl,al
mov ax,dx
mul bx ; if bl, only al is multiplied
or dx,dx
jnz .done ; abort if 16-bit value overflows
mov dx,ax
add dx,cx
jmp .more
.done:
mov ax,dx
ret
ui2str:
mov bp,di
mov si,di
mov cx,ax
mov bl,10
.more:
xor ax,ax
mov al,ch
div bl
mov dh,al ; save quotient
mov ch,ah ; save remainder
mov ax,cx
div bl
mov dl,al ; save quotient
mov al,ah ; send out remainder
add al,0x30
mov [bp],al
inc bp
mov cx,dx
or cx,cx
jnz .more
mov [bp],cl ; null
.revs:
dec bp
cmp bp,si
jc .done
jz .done
mov ah,[bp]
lodsb
mov [bp],al
mov al,ah
stosb
jmp .revs
.done:
ret
====== Things to Tinker ======
**Thing1** Write a boot code that display the number in ax (16-bits).
**Thing2** Write a boot code that gets 2 single digit number, adds them up and display the result back on screen.
**Thing3** (CHALLENGE!) Write a boot code for a simple calculator - can you fit them into 510 byte frame?
**Thing4** (CHALLENGE!) Write a boot code that lists the files in the root path of a disk partition.