Bare Metal OS —
OK this may not count as bare metal programming if I’m relying on the BIOS firmware for keyboard input and video output but it’s still pretty low level stuff. This is an operating system (fernOS) written in assembly language for Intel x86 based computers.
I may also be pushing the definition of ‘operating system’ but fernOS will boot from power-on and it does present the user with some control of the computer hardware.
Before I go any further though I must say thanks to Mike Saunders for his machine code tutorials in LXF (in 2012) and more recently in the brilliant Linux Voice magazine.
Back in my youth I dabbled with some 8-bit Z80 machine code but as CPUs became more complex I gave up on such low level programming because it felt like it was becoming inaccessible to the general hobbyist. So thanks to Mike for introducing me to nasm and re-introducing me to opcodes and operands.
Thinking about it, I also gave-up on electronics around the same time. Thinking surface mount technology would make the subject too complex for the common man. How wrong I was.
Bootloader:
When a PC is powered-up it loads a 512 byte bootloader block from the boot device (that’s the device as selected in the BIOS boot sequence). That small bootloader then loads the operating system from the boot device into memory before passing control on to it.
i.e. The operating system pulls itself up by its bootstraps!
I’m over-simplifying but that is basically what happens. fernOS however is less than 512 bytes (509 actually) so, for now, I have it entirely residing within the bootloader block. Here is the main code.
;******************************************************************** ;* fernOS * ;* An assembly language based operating system that will be * ;* written, and no doubt re-written, as I learn. * ;* www.benningtons.net * ;* with grateful thanks to the tutorials by Mike Saunders in LXF * ;* and the far better Linux Voice magazines. * ;******************************************************************** ; Compiler settings and parameters BITS 16 ;Set nasm to 16-bit mode - required for bootloaders ASCII_NL equ 0 ;null ASCII_BS equ 8 ;back space ASCII_LF equ 10 ;line feed ASCII_CR equ 13 ;carriage return ASCII_SP equ 32 ;space ASCII_DQ equ 34 ;double quotes Section .text ;protected code area fernOS: ;main console service ;fernOS memory map: ;TODO: need to better understand these memory settings ; start end size ; 0 07BF 31k reserved ; 07C0 08DF 4k + 512b ds:si, es:di code + data space ; 08E0 09FF 4k + 512b ss:sp stack space ; mov ax, 07C0h ;x86 memory segmentation: data space mov ds, ax ;can't load directly into ds mov ax, 08E0h ;x86 memory segmentation: stack space mov ss, ax mov sp, 1000h ;stack works backwards from 4k limit into stack space mov ax, ds ;x86 memory segmentation: extra space mov es, ax .fernOS_help: ;HELP command jumps to here to redisplay welcome message mov si, bootmsg ;output boot message to screen call BIOS_put_string .next_prompt: mov si, prompt call BIOS_put_string ;output next prompt mov di, inputbuf ;reset di ready for next command mov word [inpoint], inputbuf ;store pointer to start of input buffer .console: ;inner loop to allow keyboard command line input call BIOS_get_keystroke ;check keyboard for input. Out=al cmp al, ASCII_SP jge .char_input ;printable character input cmp al, ASCII_CR je .carriage_ret ;return key so goto next line and redisplay prompt cmp al, ASCII_BS je .back_space ;back space key so delete previous char jmp .console ;no valid input so try again .char_input: call BIOS_put_char ;output character (al) to screen call UTIL_upper ;convert any lowercase letters to uppercase (fernOS is case insensitive) stosb ;store ax into [di] and di++ cmp di, inpoint ;input buffer overflow? jl .console ;look for next input dec di mov al, ASCII_NL stosb ;reverse one and mark end of input with a null jmp .syntax_error .back_space: cmp di, inputbuf je .console ;no text input so ignore mov si, bs_str call BIOS_put_string ;backspace and erase previous char dec di ;clear last char input from buffer jmp .console .carriage_ret: cmp di, inputbuf je .next_prompt ;no text input so ignore and display next prompt mov al, ASCII_NL stosb ;mark end of input string with a null .any_more: mov si, cmdlist ;start at beginning of the list of commands. mov bx, 0 ;count commands checked .parse_cmds: ;check each command against our input string mov di, [inpoint] ;set pointer to the start of our entered command .pc1: cmp byte [di], ASCII_NL je .next_prompt ;no more so display next prompt cmp byte [di], ASCII_SP ;skip any spaces between commands jne .pc2 inc di jmp .pc1 .pc2: call UTIL_str_cmp cmp ax, 0 je .cmd_match ;matched a valid command .next_cmd: ;not a match so move si to start of the next command cmp byte [si], ASCII_NL je .find_next_cmd inc si jmp .next_cmd .find_next_cmd: inc bx inc si cmp si, synterr ;check location of the string after the command list jge .syntax_error ;gone beyond list of commands so invalid command entered jmp .parse_cmds ;else try again, si will be pointing to the start of next command .cmd_match: ;rough output of selected command to check if in right place mov word [inpoint], di ;store pointer to where we are in input buffer cmp bx,1 je .fernOS_help cmp bx,2 je .fernOS_say cmp bx,3 je .fernOS_wait cmp bx,0 jne .syntax_error call BIOS_boot .syntax_error: mov si, synterr ;display a syntax error message call BIOS_put_string mov si, [inpoint] call BIOS_put_string ;followed by a repeat of the command that was typed jmp .next_prompt .fernOS_say: ;SAY"HELLO" = output HELLO to the screen mov si, nl_str call BIOS_put_string cmp byte [di], ASCII_DQ ;error if not using quotes jne .syntax_error .fs1: inc di ;skip 1st set of quotes cmp byte [di], ASCII_DQ je .fs2 ;look through until reaching 2nd set of quotes cmp byte [di], ASCII_NL je .syntax_error ;reached the end rather than quotes so error mov ax, [di] call BIOS_put_char ;output each character between the quotes jmp .fs1 .fs2: inc di ;skip 2nd set of quotes mov word [inpoint], di ;store pointer to where we are in input buffer jmp .any_more .fernOS_wait: ;WAITn = pause n seconds (roughly) mov bl, [di] ;check how long to wait sub bl, '0' ;convert char input to int cmp bl, 0 ;if no valid wait time then skip to next command jle .any_more cmp bl, 9 ;if no valid wait time then skip to next command jg .any_more inc di ;move past wait time mov word [inpoint], di ;store pointer to where we are in input buffer mov bh, 0 .fw1: call BIOS_get_time mov ax, dx ;check changes to lower tics mov dx, 0 ;ignore upper set of tics mov cx, 10 div cx ;remainder goes to dx (the lowest digit of system time) cmp dx, bx ;so count every 10 tics (roughly) jne .fw1 dec bx cmp bx, 0 jg .fw1 jmp .any_more ;Welcome splash and prompt bootmsg db ASCII_LF, ASCII_CR, "fernOS v0.0.3 x86 version", ASCII_LF, ASCII_CR db "======", ASCII_LF, ASCII_CR db "boot", ASCII_LF, ASCII_CR db "help", ASCII_LF, ASCII_CR db "say", ASCII_DQ, "hi", ASCII_DQ, " = display hi to screen", ASCII_LF, ASCII_CR db "waitn = pause n seconds", ASCII_LF, ASCII_CR, ASCII_NL prompt db ASCII_LF, ASCII_CR, "fernOS>", ASCII_NL ;Commands: [relies on previous null and being followed by synterr] cmdlist db "BOOT", ASCII_NL, "HELP", ASCII_NL, "SAY", ASCII_NL, "WAIT", ASCII_NL ;Error messages: synterr db ASCII_LF, ASCII_CR, "You wot? ", ASCII_NL ;Backspace delete sequence: bs_str db ASCII_BS, ASCII_SP, ASCII_BS, ASCII_NL ;new line sequence: nl_str db ASCII_LF, ASCII_CR, ASCII_NL %include "fernOS_BIOS_library.asm" %include "fernOS_UTIL_library.asm" section .bss ;changeable data section stringbuf resb 4 ;general purpose inputbuf resb 80 ;command line input inpoint resb 2 ;pointer to input buffer section .text ;protected code section times 510-($-$$) db 0 ;pad remainder of 512 bootloader with nulls dw 0AA55h ;valid boot block signature
Unfortunately my WordPress syntax highlighter doesn’t include machine code in its repertoire so this will look quite plain. I’ve also avoided looking at online code examples, thus far, to encourage myself to learn rather than copy. So I’m probably not laying out my code correctly, my inefficient use of registries is probably shocking and I’ll definitely be using sub-optimal methods. But hey, it works and I’m sure it will improve!
Hopefully the in code comments give a clue to what the code does but the main ‘sections’ are:
.console Where keyboard input is echoed to the screen until the return/enter key is pressed.
.parse_cmds Looks through the input commands and calls the requested functions.
Those commands being: BOOT, HELP, SAY and WAIT (well there is only 512 bytes to play with!)
BIOS utilities:
I’ve kept all the BIOS interrupts in a separate file fernOS_BIOS_library.asm, which you’ll see included towards the end of the main fernOS code above.
;******************************************************************** ;* fernOS_BIOS_library * ;* x86 assembly code BIOS calling utilities for fernOS * ;* www.benningtons.net * ;******************************************************************** ; ;INTERRUPTS USED: ; Int AH AL Description ; 10 0E ASCII Video Teletype Output ; 16 00 ASCII Keyboard Get Keystroke ; 1A 00 flag Get system time ; 19 System bootstrap loader ; ;LIST OF FUNCTIONS: ; BIOS_boot Boot the PC ; BIOS_get_keystroke Get keystroke from keyboard ; BIOS_get_time Get system time ; BIOS_put_char Output character to screen ; BIOS_put_string Output string to screen ;***************************************Boot the PC ;In: none ;Out: none ;Destroyed: none BIOS_boot: int 19h ;call bios bootstrap loader ret ;***************************************Get keystroke from keyboard ;In: none ;Out: al=ascii character (default null) ;Destroyed: ah BIOS_get_keystroke: mov ah, 00h ;set upper byte of ax ready for get_keystoke bios call mov al, 00h ;set lower byte to null default #TODO do I need to initialise? int 16h ;call bios to get keystroke ret ;***************************************Get system time ;In: none ;Out: cx:dx = number of clock ticks since midnight, al=midnight flag ;Destroyed: ah BIOS_get_time: mov ah, 00h ;set upper byte of ax ready for bios call int 1Ah ;call bios to get system time ret ;***************************************Output character to screen ;In: al=ascii character to output ;Out: none ;Destroyed: ah BIOS_put_char: mov ah, 0Eh ;set upper byte of ax ready for teletype_output bios call int 10h ;call bios to output ascii char in al to screen ret ;***************************************Output string to screen ;In: si pointer to a null terminated string ;Out: none ;Destroyed: ax BIOS_put_string: mov ah, 0Eh ;set upper byte of ax ready for bios call .loop: lodsb ;step through string, incrementing si and loading char into al cmp al, 0 ;test lower byte of ax je .end ;reached null yet? int 10h ;call bios to output char jmp .loop ;next char .end: ret
The BIOS interrupt codes are a bit of a black art. I picked up the output_char (10h) from Mike’s tutorial and the others were a case of trial and error based on the list in this BIOS Interrupt Jump Table
Other utilities:
One of the joys (some would say pains) of machine code is that if you want to do something then you have to write it yourself. Higher level languages like C or Python have vast pre-written libraries available to do complex tasks for you. They help make the coding quick and the results impressive but at what cost? Are those library routines as efficient as they could be and aren’t they keeping you from learning about what’s really happening?
On the other-hand many will consider programming at such a low level and having to write every minor function a bit too masochistic?
Anyhow, here are the other utilities that I’ve used in fernOS.
;******************************************************************** ;* fernOS_UTIL_library * ;* x86 assembly code general utilities for fernOS * ;* www.benningtons.net * ;******************************************************************** ; ;LIST OF FUNCTIONS: ; UTIL_num_to_str Convert a number into an ASCII string ; UTIL_str_cmp Compare two strings ; UTIL_upper Convert a char to uppercase ;***************************************Convert a number into an ASCII string ;In: ax=number to convert, di=pointer to output string ;Out: [di] memory ;Destroyed: none ;UTIL_num_to_str: ; pusha ;preserve registers ; mov bx, 10 ;Base of the decimal system ; mov cx, 0 ;Number of digits generated ;.divloop: ; mov dx, 0 ;will divide dx:ax so initialise and the remainder will be stored in dx ; div bx ;Divide ax by the number-base ; push dx ;Save remainder on the stack ; inc cx ;And count this remainder ; cmp ax, 0 ;Was the quotient zero? ; jne .divloop ;No, do another division ;.outloop: ; pop ax ;Else pop recent remainder ; add al, '0' ;And convert to a numeral ; stosb ;Store to memory-buffer ; loop .outloop ;Again for other remainders [if(--cx.ne.0) goto .outloop] ; mov al, ASCII_NL ;null terminate string ; stosb ; popa ;recover registers ; ret ;***************************************Compare two strings ;In: si=pointer to string1, di=pointer to string2 ;Out: ax=-1 (s1<s2), ax=0 (match), ax=1 (s1>s2) ;Destroyed: (si-1) and di left pointing to 1st mismatch or end of strings UTIL_str_cmp: .loop: lodsb ;move [si] into ax and inc si cmp al, ASCII_NL ;reached end of s1 and still matching so a match (even though there may be more to s2 je .a_match cmp al, [di] ;else compare char from each string jl .less_than jg .greater_than inc di ;test next char jmp .loop .less_than: mov ax, -1 ret .greater_than: mov ax, 1 ret .a_match: mov ax, 0 ;actually ax is already ascii null ret ;***************************************Convert a char to uppercase ;In: al=ASCII char to convert ;Out: al=uppercase ASCII char ;Destroyed: none UTIL_upper: cmp al, 'a' jl .end ;below ASCII 'a' so no conversion applied cmp al, 'z' jg .end ;above ASCII 'z' so no conversion applied sub al, 32 ;convert a lowercase letter to uppercase .end: ret
You’ll notice the first function UTIL_num_to_str is commented out. It’s no longer used by fernOS and there’s no room left for it in this 512 byte OS. But it was useful as a debugging tool during development so I’ve kept the code available.
Prepare your boot media:
To compile fernOS the file containing the main program (fernOS.asm) must be in the same folder as the utilities fernOS_BIOS_library.asm and fernOS_UTIL_library.asm. From within that folder you can compile fernOS and generate a binary executable image with:
nasm -f bin -w+all -o fernOS.bin fernOS.asm
Assuming there were no error messages, you then need to create a blank floppy disk image (1440k in size) and write fernOS.bin to the start of it. First making sure to remove any existing image files.
rm fernOS.img #remove previous image mkdosfs -C fernOS.img 1440 #Create a new disk image dd if=fernOS.bin of=fernOS.img conv=notrunc #Transfer fernOS to disk image
You then need to put that image onto your chosen boot media. Mike’s tutorial used the QEMU emulator directed to boot from the image file. I started using floppy disks but was surprised at how many of my old stock were now generating errors so I then switched to using a memory stick (please note that the whole memory stick will be erased, even though the boot block is only 512 bytes!).
Here are the dd commands for writing to floppy (commented out) and to a memory stick on device sdb (make doubly doubly sure you use the right device name for your setup!). Plug in the memory stick and then type dmesg. At the end of the display you’ll see something like:
[31383.723577] sd 4:0:0:0: [sdb] Attached SCSI removable disk
Which confirms mine is on sdb, but your’s may be on sdc or another device, so carefully check OR RISK WIPING THE WRONG DEVICE!
#dd if=fernOS.img of=/dev/fd0 #Format floppy disk with new image dd if=fernOS.img of=/dev/sdb #Format USB memory stick with new image
Using fernOS:
Make sure that your BIOS boot sequence will try your fernOS boot media before your hard disk, have your boot media plugged in and power-up.
In the race to reach a command prompt we’re already better than Windows and even Linux! OK, they’ll beat fernOS on everything else (except size) but the time to boot-up is impressive and shows what can be achieved with assembly language.
The welcome display lists the available commands, they can be repeated by the HELP command. The data needed to do that is a waste of precious code space but it will suffice until I learn how to load such data from a separate file outside of the 512 byte boot block.
Now for our first command in, er…, fernScript:
fernOS>say"hello world" HELLO WORLD
And our first sequence of commands:
fernOS>say"this system will reboot in 9 seconds" wait4 say"goodbye" wait5 boot THIS SYSTEM WILL REBOOT IN 9 SECONDS <pause...> GOODBYE <pause...> <fernOS will then pass control to the bootloader on your hard disk>
Well it doesn’t do much but it quickly boots to a console prompt and allows you to interact with the computer via a set of commands. Some could say its a bare metal, single threaded, monolithic kernel operating system. I’d say its not bad for 509 bytes and as I learn it will hopefully improve.
Categorised as: Computer Stuff
Comments are disabled on this post