StellatorOS: Funciones en ensamblador


Desde el punto de vista de la CPU una función no es mas que un salto a una dirección de memoria donde se encuentran las intrucciones de una rutina y un salto de retorno a la instrucción inmediatamente posterior al primer salto. En este artículo explicaré como se deben de llamar a las funciones para que sean reutilizables y que sea seguro su uso.

 

Antes de comenzar es recomendable que leas estos artículos anteriores:

En este ejemplo utilizaremos el registro al a modo de parámetro de nuestras función 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

Generamos la imagen:

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

La cargamos en 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

El problema de esta aproximación es que la función no es reutilizable ya que la dirección de retorno está hardcodeada, si llamásemos a la función desde otro punto del código el punto de retorno de la función sería incorrecto. Pero y si pudiésemos almacenar la posición donde se está ejecutando el código justo antes de llamar a la función, ejecutarla y volver a esa punto guardado?

Esto es justo lo que hacen las instrucciones call/ret:

  • Guarda el ip(Instruction pointer) en la pila -> push ip
  • Salta a la función
  • Ejecuta la función
  • Recupera el ip de la pila -> pop ip
  • Salta al ip

De este modo la dirección de retorno de la función es correcta y la función por lo tanto reutilizable en cualquier punto del código, veamos un ejemplo:

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

Generamos la imagen:

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

La cargamos en 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

Hemos llamado dos veces a la misma función sin problema.

Cuando se llama a una función esta puede modificar el contenido de los registros para realizar las tareas programadas por lo tanto no es seguro seguir la ejecución del hilo principal del programa confiando en que los registros sigan con los valores correctos, para solventar esto existen dos intrucciones que lo que hacen es "pushear/popear" todos los registros en la pila, estas instrucciones son pusha/popa.

El ejemplo anterior adaptado quedaría del siguiente modo:

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

Generamos la imagen:

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

La cargamos en 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

Una vez mas hemos utilizado la función dos veces pero esta vez de forma segura ya que cuando se ha retornado al hilo principal del programa los registros estaban exactamente en el mismo estado que antes de llamar a la función.

Ahora que ya tenemos claras las funciones vamos a crear un programa que imprimirá por pantalla la cadena que le indiquemos, pero para ello debemos terminar la cadena con el carácter 0x00 (null byte).

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

Generamos la imagen:

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

La cargamos en 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

Las funciones se pueden programar en un fichero externo para luego ser importado, nosotros vamos a sacar nuestra función print y además añadimos una nueva, 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 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 ----

; ---- 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 ----

Repetimos los prints pero esta vez importando las funciones de la librería externa.

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

Generamos la imagen:

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

La cargamos en 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
Si te ha gustado el artículo puedes invitarme a un redbull aquí.
Autor: kr0m -- 10/06/2020 01:04:03