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 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 here. We will also be using 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.
Booting a system (a more technical term would be bootstrapping):
Some notes on MBR used in classic/legacy BIOS-based system.
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.
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 512MB1) 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 <<EOF n p 1 t c a 1 w EOF
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
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 <<EOF n p 1 t c a 1 w EOF [ $? -ne 0 ] && echo "Failed! Aborting!" && exit 1 echo "done!" # create fat32 filesystem... if requested if [ "$DO_FAT_32" == "YES" ] ; then echo -n "Formatting FAT32 partition on '${MY1USBDRV}'... " OPTS="conv=notrunc seek=1048576" MY1TEMP="disk_temp.img" PART_SIZE=$(($(fdisk -s ${MY1USBDRV})-1024)) dd if=/dev/zero of=${MY1TEMP} bs=1024 count=${PART_SIZE} 2>/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
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
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:
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
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
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).
What if we need to read a string? (Hint: need to detect a space or the <ENTER> 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.
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
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.