This page looks best with JavaScript enabled

MercuryOS Assembly Functions

 ·  🎃 kr0m

From the CPU’s point of view, a function is nothing more than a jump to a memory address where the instructions of a routine are located and a return jump to the instruction immediately following the first jump. In this article, I will explain how functions should be called to make them reusable and safe to use.

Before we begin, it is recommended that you read these previous articles:


In this example, we will use the al register as a parameter for our function my_print_function.

vi boot_sect_function.asm

mov al, 'H'

jmp my_print_function

return_to_here:
    jmp the_end

my_print_function:
    mov ah, 0x0e ; tty mode
    int 0x10 ; print the character in al
    jmp return_to_here ; return from the function call

the_end:

jmp $
times 510-($-$$) db 0
dw 0xaa55

We generate the image:

nasm -f bin boot_sect_function.asm -o boot_sect_function.bin

We load it into qemu:

qemu-system-x86_64 boot_sect_function.bin

SeaBIOS (version rel-1.12.1-0-ga5cab58e9a3f-prebuilt.qemu.org)  
iPXE (http://ipxe.org) 00:03.0 C980 PCI2.10 PnP PMM+07F91410+07EF1410 C980  
  
Booting from Hard Disk...  
H

The problem with this approach is that the function is not reusable since the return address is hardcoded. If we called the function from another point in the code, the return point of the function would be incorrect. But what if we could store the position where the code is being executed just before calling the function, execute it, and return to that saved point?

This is precisely what the call/ret instructions do:

  • Save the ip(Instruction pointer) on the stack -> push ip
  • Jump to the function
  • Execute the function
  • Retrieve the ip from the stack -> pop ip
  • Jump to the ip

In this way, the return address of the function is correct and the function is therefore reusable at any point in the code. Let’s see an example:

vi boot_sect_function2.asm
mov al, 'H'
call my_print_function
call my_print_function

jmp the_end

my_print_function:
    mov ah, 0x0e ; tty mode
    int 0x10 ; print the character in al
    ret

the_end:

jmp $
times 510-($-$$) db 0
dw 0xaa55

We generate the image:

nasm -f bin boot_sect_function2.asm -o boot_sect_function2.bin

We load it in qemu:

qemu-system-x86_64 boot_sect_function2.bin  
SeaBIOS (version rel-1.12.1-0-ga5cab58e9a3f-prebuilt.qemu.org)  
iPXE (http://ipxe.org) 00:03.0 C980 PCI2.10 PnP PMM+07F91410+07EF1410 C980  
  
Booting from Hard Disk...  
HH

We have called the same function twice without any problem.

When a function is called, it can modify the contents of the registers to perform the tasks programmed, so it is not safe to continue the execution of the main thread of the program relying on the registers to have the correct values. To solve this, there are two instructions that “push/pop” all the registers onto the stack. These instructions are pusha/popa.

The previous example adapted would be as follows:

vi boot_sect_function3.asm
mov bp, 0x8000 ; this is an address far away from 0x7c00 so that we don't get overwritten
mov sp, bp ; if the stack is empty then sp points to bp

mov al, 'H'
call my_print_function
call my_print_function

jmp the_end

my_print_function:
    pusha ; push all registers to stack
    mov ah, 0x0e ; tty mode
    int 0x10 ; print the character in al
    popa ; pop all registers from stack
    ret

the_end:

jmp $
times 510-($-$$) db 0
dw 0xaa55

We generate the image:

nasm -f bin boot_sect_function3.asm -o boot_sect_function3.bin

We load it in qemu:

qemu-system-x86_64 boot_sect_function3.bin

SeaBIOS (version rel-1.12.1-0-ga5cab58e9a3f-prebuilt.qemu.org)  
iPXE (http://ipxe.org) 00:03.0 C980 PCI2.10 PnP PMM+07F91410+07EF1410 C980  
  
Booting from Hard Disk...  
HH

Once again, we have used the function twice, but this time safely, since when we returned to the main thread of the program, the registers were exactly in the same state as before calling the function.

Now that we have clear functions, let’s create a program that will print on the screen the string we indicate, but for this we must end the string with the 0x00 (null byte) character.

vi boot_sect_function4.asm
[org 0x7c00]

; ---- main ----
; The main routine makes sure the parameters are ready and then calls the function
mov bp, 0x8000 ; this is an address far away from 0x7c00 so that we don't get overwritten
mov sp, bp ; if the stack is empty then sp points to bp

mov bx, HELLO; set memory address value of HELLO string to bx register
call print

mov bx, GOODBYE; set memory address value of GOODBYE string to bx register
call print

jmp $
; ---- main ----

; ---- print function ----
print:
    pusha

; keep this in mind:
; while (string[i] != 0) { print string[i]; i++ }

; the comparison for string end (null byte)
start:
    mov al, [bx] ; 'bx' is the base address for the string
    cmp al, 0 
    je done

    mov ah, 0x0e; tty mode
    int 0x10 ; print char

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

done:
    popa
    ret
; ---- print function ----

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

GOODBYE:
    db 'Goodbye', 0
; ---- data ----

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

We generate the image:

nasm -f bin boot_sect_function4.asm -o boot_sect_function4.bin

We load it in qemu:

qemu-system-x86_64 boot_sect_function4.bin

SeaBIOS (version rel-1.12.1-0-ga5cab58e9a3f-prebuilt.qemu.org)  
iPXE (http://ipxe.org) 00:03.0 C980 PCI2.10 PnP PMM+07F91410+07EF1410 C980  
  
Booting from Hard Disk...  
Hello, WorldGoodbye

Functions can be programmed in an external file to be later imported. We will extract our print function and add a new one, print_nl.

vi boot_sect_print.asm

; ---- print function ----
print:
    pusha

; keep this in mind:
; while (string[i] != 0) { print string[i]; i++ }

; the comparison for string end (null byte)
start:
    mov al, [bx] ; 'bx' is the base address for the current char
    cmp al, 0 
    je done

    mov ah, 0x0e; tty mode
    int 0x10 ; print char

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

done:
    popa
    ret
; ---- print function ----

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

We repeat the prints, but this time importing the functions from the external library.

vi boot_sect_function5.asm

[org 0x7c00]

mov bp, 0x8000 ; this is an address far away from 0x7c00 so that we don't get overwritten
mov sp, bp ; if the stack is empty then sp points to bp

mov bx, HELLO; set memory address value of HELLO string to bx register
call print

call print_nl

mov bx, GOODBYE; set memory address value of GOODBYE string to bx register
call print

jmp $

%include "./boot_sect_print.asm"

HELLO:
    db 'Hello, World', 0

GOODBYE:
    db 'Goodbye', 0

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

We generate the image:

nasm -f bin boot_sect_function5.asm -o boot_sect_function5.bin

We load it into Qemu:

qemu-system-x86_64 boot_sect_function5.bin
SeaBIOS (version rel-1.12.1-0-ga5cab58e9a3f-prebuilt.qemu.org)  
iPXE (http://ipxe.org) 00:03.0 C980 PCI2.10 PnP PMM+07F91410+07EF1410 C980  
  
Booting from Hard Disk...  
Hello, World  
Goodbye
If you liked the article, you can treat me to a RedBull here