Benningtons.net

Stuff what I did

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


Comments are closed.