How computers start up! Part 2: Real Mode(16-bit) in X-86 assembly

Deep dive into the process of bringing up the system through various CPU modes of execution.

One can be lucky if BIOS is kind enough to put the CPU in Protected Mode(32-bit) before executing the boot-loader, however cannot be sure. We are going to assume that the CPU is in Real Mode(16-bit) and boot-loader is about to get executed by the BIOS after confirming the presence of the Magic Number(0xAA55) at the end of the boot sector(First 512 Bytes of the drive).

Memory Operations in Real Mode

By now, system have been surviving interrupts using the Real Mode Interrupt Vector Table(IVT) placed at the start of the memory(RAM) by BIOS for us to use. At a certain point, when we will leave Real Mode for Protected Mode, we will have to replace it with our own interrupt vector table. It will be good to have a look at the memory(RAM) map used while working in Real Mode X-86.

Boot-loader address starts @ 0x7C00

Boot-Sector starts @ 0x7C00 in the memory, so we need to place an offset inside our assembly code to address locations appropriately.

;placing this at the top of our assembly code sets a global offset!
[org 0x7c00]

To create a variable inside the boot-loader.

[org 0x7c00] ; global offset of BootSector
private_data:
    db "A"

Now, to access it

[org 0x7c00] ; global offset of BootSector
mov ah, 0x0e; tty mode
mov al, [private_data]
int 0x10 ; this is the interrup for printing system output 

Setting up Stack

Remember that the bp register stores the base address (i.e. bottom) of the stack,
and sp stores the top, and that the stack grows downwards from bp (i.e. sp gets
decremented)

So in order for our stack to work all we have to do is initialize these registers with values far away from where boot sector is loaded(0x7C00), lets say 0x8000,

mov bp, 0x8000 ; this is an address far away from 0x7c00
mov sp, bp ; if the stack is empty then sp points to bp

After this we can Push and Pop from the stack

push 'A'
push 'B'
push 'C'

pop bx
mov al, bl
int 0x10 ; prints C

pop bx
mov al, bl
int 0x10 ; prints B

pop bx
mov al, bl
int 0x10 ; prints A

Functions and File Includes in assembly

We are going to write PRINT function in a new file print_assembly.asm. It will take one parameter, a String. Essentially we loop until we find \0 at the end of the string.

Overall Algorithm:
while (string[i] != 0) { print string[i]; i++ }

; Print function in assembly
print:
    pusha

start:
    mov al, [bx] ; 'bx' is the base address for the string
    cmp al, 0 
    je done

    ; the part where we print with the BIOS help
    mov ah, 0x0e
    int 0x10 ; 'al' already contains the char

    ; increment pointer and do next loop
    add bx, 1
    jmp start

done:
    popa
    ret

Let's also create function for printing new line, \n as 0x0a and carriage return, \r as 0x0d

; Print new Line
print_nl:
    pusha
    
    mov ah, 0x0e
    mov al, 0x0a ; newline char
    int 0x10
    mov al, 0x0d ; carriage return
    int 0x10
    
    popa
    ret

Including and calling Print function from Main.asm code

[org 0x7c00] ; tell the assembler that our offset is bootsector code

; Prepare parameters in the bx register
mov bx, HELLO
call print

call print_nl

mov bx, GOODBYE
call print

call print_nl

; now loop forever
jmp $ ; $: represents current address 

; This is where the code from print_assembly.asm file will be copied
%include "print_assembly.asm"


; data
HELLO:
    db 'Hello, World', 0

GOODBYE:
    db 'Goodbye', 0

; padding and magic number
times 510-($-$$) db 0
dw 0xaa55

Segmentation

Segmentation is a way to manage memory. In other words it is the layout in which programs and data are placed inside the RAM. This feature is in-built in 32-bit x-86 processors. For this to work we need to setup global descriptor table

Initialization

We set up stack by initializing registers sp and bp. Similarly, in order to start segmenting memory we need to initialize segment registers cs, ds, ss and es.

Note: Once you set some value for, say, ds, then all your memory access will be offset by ds

Access

To compute the real address we don't just join the two
addresses, but we overlap them: (segment << 4) + address. For example,
if ds is 0x4d, then [0x20] actually refers to 0x4d0 + 0x20 = 0x4f0

mov bx, 0x7c0 ; segment is automatically shifted left 4 bits for you
mov ds, bx ; now cpu uses ds as all data operations

This article is to be continued in the next issue where we prepare and move the CPU to 32-bit Protected mode after setting up segmentation at a global level using GDT(Global Descriptor Table ), Part 3: Protected Mode(32-bit) & Global Descriptor Table

Visit OS Dev for detailed information about the topic discussed above.

Sign up for the blog-newsletter! I promise that we do not spam...