Salsa – A Teaching Operating System – Part 3
Last time we discussed the x86 assembly language. We really only scratch the surface of this subject. There is much more to learn. Today we need to address addressing modes (pun intended) and the stack.
First, what is an address mode? When we have an instruction such as “mov” that accesses memory, that address must come from somewhere. The address mode determines how the address for the memory location in the instruction is obtained. For example:
mov ebx, 0x7cff
mov word [ebx], 0x7ce0
In the above example, the first instruction “mov ebx, 0x7cff” load the immediate value “0x7cff” into the EBX register. This is known as immediate mode. In this mode the data to be loaded into the register is part of the instruction and is encoded into the actual machine instruction. You’ll recognize immediate mode by the fact that no memory access (other than fetching the instruction itself) is needed to perform the instruction.
In the second example above, the value 0x7ce0 is moved not into the ebx register but into the memory location pointed to by the value in ebx. So, with the two instructions above 0x7ceo would be placed into memory at address 0x7cff because that is the value in ebx. Anytime you see a register in square braces you can think of this as the register saying “Use the address I contain”. This mode of addressing is called register indirect.The x86 family supports many addressing modes. However, we wont need them all. We’ll introduce others as we move along.
One more thing we should comment on is the use of the “word” value in the second instruction. Many assemblers can infer the proper data size to transfer from memory based on the register used. However, nasm may require the use of a data size to tell it how many bytes to transfer from memory. Here we use the “word” size to transfer 16-bits (two bytes) of data. We can also use byte (8-bits), dword (32-bits), and qword (64-bits). However, for our boot loader we wont be using any 64 bit instructions or registers. This is because our processor will initially be in “real” mode (16-bit mode).
Let’s see another example of storing a byte to memory using an address in the base register bx:
mov byte [ebx], al
As stated, this instruction will move the byte stored in register al (the low order byte of ax) to the memory location pointed to by the value in base register bx.
Now what if we want to transfer the high order byte of ax to memory?
We, we can just use the ah register from the ax pair. Like so:
mov byte [ebx+1], ah
Notice that here we passed a constant offset to the destination by adding 1 to the ebx register. This is very handy when we know ahead of time where the byte should be placed in respect to the base value.
Most x86 assembly instructions (using Intel Syntax) will follow the pattern:
<operation> <destination>, <source>
mov [ebx], eax
This instruction moves the value in eax the location pointed to by ebx.
Some instructions have an implicit source or destination. For example:
add ebx, 4
This instruction uses ebx as both a source and destination as the instruction takes the value in ebx, adds the immediate value 4 and then places the result back into ebx.
Now let’s look at a simple loop example:
xor eax, eax ; zeros out eax mov ecx, 10 ; load counter with 10 start: inc eax ; increment eax, our loop code goes here loop start
The ecx register is usually used to store a count when doing repetitive operations. The line “start:” is a label. Labels are special markers in the code that basically names the current code address and must end with a colon. This allows you to refer to points in your code by a human readable label rather than trying to calculate and use the address actual value.
Here we’ve loaded ecx with the number of times we want to repeat our block of code (in this case the inc eax instruction) and marked the start of the block of code with the label “start”. Finally, we use the “loop” operation followed by the label of the block we want to repeat.
The loop operation decrements the value of ecx. Then if ecx is non-zero, it jumps to the address (label) provided. When ecx is zero, execution will continue to the instruction following the “loop” operation.
We’ve seen enough assembly language to get our feet wet. Now let’s return to our operating system.
One of the first task we want our OS to perform will be to print something to the screen so we know it’s alive. We’ve seen how to use the BIOS’s teletype function to print characters to the screen. But how does the BIOS do this?
When the PC is first powered on the BIOS initializes the video display to a 16 color text mode. It places video memory at location 0xB8000. We can write directly to video memory to display text. However, we need to know a bit more first.
The default video mode stores it’s display data as two byte values. The low order byte determines the character to display and the high order byte determines the foreground and background color of the character.
So how to we know what values correspond to which characters? Well the default is to support the ASCII character set. You can find ASCII character set tables all over the internet. But for completeness check out this one.
Now, what about that color byte? The high order byte of each pair contains two nibbles (4-bit values). The lower order nibble (bits 0-3) contain the four bit code for the foreground color and the high order nibble contains the background color. To see the sixteen possible colors supported check out this link: https://en.wikipedia.org/wiki/BIOS_color_attributes.
Now let’s try displaying some colorful text to the screen in video mode 0. In your favorite text editor, enter the following x86 assembly code:
; ; File: alphabet.asm ; Auth: Randall Morgan <email@example.com> ; Desc: Program to display the English alphabet on the display in ; Mode 0, a 25 (row) x 40 (cols) x 16 color display mode. ; start: mov byte al, 'A' ; Character mov byte ah, 0x0F ; Color: background 0-black, foreground f = white mov ecx, 26 ; 26 letters in the alphabet mov ebx, 0xB8000 ; Set base register to start of video memory loop_start: mov word [ebx], ax ; Move character and color code into video memory inc al ; Increment al for next character inc ah ; Increment next color pair add ebx, 2 ; Move to next character loop loop_start end: jmp end ; infinitly jump here times 510-($-$$) db 0 ; Pad file to 510 bytes dw 0xaa55 ; Add the magic number
Save this file in slasa/src as alphabet.asm. Then cd to the salsa directory and execute the following command at the terminal:
nasm src/alphabet.asm -o /bin/alphabet.bin
This will assemble the program into machine code and the BIOS will happily execute it for us as long as we half the magic number properly placed at the end of a 512 byte code block. We wont even need to place it on a 1.44MB disk image for qemu to run it.
Next we need to run the binary file in qemu. We can do this with the following command:
This command requires that you are still in the salsa directory. If everything works you should see something like this:
Notice the alphabet in the top left of the screen. Notice that we’ve not only printed the alphabet but we have also changed the foreground and background colors of each character. Now we’re getting somewhere…
Ok, next we need to learn a little more about the x86 architecture before we move on to our next step in OS development.
A stack is a simple First-In-Last-Out data structure. The hardware stack used by the x86 is a reserved section of RAM and is used to store temporary data such as local variables and intermediate values, and parameters.
Keeping track of the stack and the next open location in memory that we can push data to is the job of the SP (Stack Pointer) register. On the 8086 this register is 16-bits and is named SP. On 32 bit x86 architectures the register has been extended to 32 bits and has been renamed ESP (Extended Stack Pointer).
To initialize a stack we only need to set the stack pointer to a valid location in memory. It should be noted that the x86 stack grows downward. So each time we push a byte onto the stack the stack pointer (sp) is decremented. If our stack is initialized to 0xFFFF and we push the al register onto the stack it will save the contents of al to 0xFFFF and then decrement the stack pointer to 0xFFFE. If we then push the bl register, the contents of bl will be placed in memory at 0xFFFE and then the stack pointer (sp) will be decremented again, leaving SP with a value of 0xFFFD. When we pop (remove) a value off the stack, it is first incremented and then the value at it’s new location is returned.
In our example above, the stack is pointing to 0xFFFD which is the next available memory location we can use. If we pop bl, SP is incremented to point to 0xFFFE and then the value at 0XFFFE is returned. Now, we don’t have to push and pop values to/from the same registers all the time. Though often this is the case. It should also be mentioned that it is convention to refer to the top of the stack as the high address (the address SP was initialized with) and the bottom of the stack as the address currently in SP. All memory address are unsigned. So they never go negative. If we initialized SP to 0xFFFF in a 64KB memory system ans the popped a value without first pushing a value onto the stack, SP would underflow with out error and wrap it’s value around to 0x0000. However, with x86 we need to initialize the stack pointer to a high value so it has space to ground downward.
We’ve been using the jmp instruction without really touching on it. The x86 architecture has many branch (sometimes called flow-control) instructions. Some of these instructions are:
- jmp – unconditional jump, i.e. jump always
- je – jump when equal
- jne – jump when not equal
- jz – jump when last result was zero
- jg – jump when greater than
- jge – jump when greater than or equal to
- jl – jump when less than
- jle – jump when less than or equal to
These instructions are usually used in combination with a label:
jz start ; jump if the result of the last operation was zero
These instructions are the only instructions that can modify the value of the IP (Instruction Pointer) register. The IP cannot be directly modified.
This brings us to the Flag register. This register contains various bits that are used to indicate various state information. The most common types of flags used in the Flag register are the Error flags which indicate error states such as arithmetic overflow. Status flags such as the interrupt enable flag, and result flags, such as the carry flag which is set if a carry or borrow occurs during an arithmetic operation.
The Flag register cannot be accessed or modified except for the use of “popfd” and “pushfd” instructions.
The most common use of the Flag register is in combination with the “cmp” (Compare) instruction which subtracts one operand from the other, setting the c (carry), z (zero), and sign (negative). Usually a “cmp” instruction is followed by one of the conditional branch operations. For example:
mov eax, [ebx] cmp eax, 0x0a jne some_label
The above code load a value pointed to be ebx into the eax register then compares that value with the decimal value 10 (0x0a). If the value in eax is not equal to 10 then the conditional jump, “jne some_label” (jump not equal) is taken. Otherwise execution continues with the next instruction.
We’re gaining ground in our understanding of the x86 architecture and assembly language. To get comfortable with writing and assembling programs take a little time and try to write the following programs:
- Write an x86 assembly program to fill the BIOS boot screen with ‘Q’ characters.
- Modify the above program to print each row in a different foreground and background color.
- Write a program that loads the last byte of the boot sector (byte 512) and subtracts 0x55 from it. If the result is zero, print “zero” to the screen. If the result is not zero, print the resulting value to the screen in decimal. Hint: Checkout the ascii character codes for decimal digits.
OK, that’s enough for today. Next time we’ll look at a few registers we haven’t mentioned before. The x86 debug registers. We’ll also start on a few routines we’ll need for our boot loader and OS.
Until next time, Happy Coding!