La línea de comando más simple en NASM y QEMU

imagen


Entonces, directo al grano. Escribiremos bajo Linux, en NASM y usando QEMU. Esto es fácil de instalar, así que omita este paso.


Se entiende que el lector está familiarizado con la sintaxis de NASM al menos en el nivel básico (sin embargo, no habrá nada particularmente complicado aquí) y entiende qué son los registros.


Teoría básica


Lo primero que inicia el procesador cuando la computadora está encendida es el código del BIOS (o UEFI, pero aquí solo hablaré sobre el BIOS), que está "conectado" en la memoria de la placa base (específicamente, en 0xFFFFFFF0).


Inmediatamente despu√©s de encender el BIOS, se inicia la autoprueba de encendido (POST), autocomprobaci√≥n despu√©s de encender. El BIOS verifica el estado de la memoria, detecta e inicializa los dispositivos conectados, verifica los registros, determina el tama√Īo de la memoria, y as√≠ sucesivamente.


El siguiente paso es identificar el disco de inicio desde el que puede iniciar el sistema operativo. Un disco de arranque es un disco (o cualquier otra unidad) que tiene los √ļltimos 2 bytes del primer sector (el primer sector significa los primeros 512 bytes de la unidad, porque 1 sector = 512 bytes) es 55 y AA (en formato hexadecimal). Tan pronto como se encuentre un disco de arranque, el BIOS cargar√° sus primeros 512 bytes en la RAM en la direcci√≥n 0x7c00 y transferir√° el control al procesador en esta direcci√≥n.


Por supuesto, en estos 512 bytes no funcionará para adaptarse a un sistema operativo completo. Por lo tanto, generalmente en este sector coloque el cargador primario, que carga el código del sistema operativo principal en la RAM y le transfiere el control.


Desde el principio, el procesador ha estado funcionando en modo real (= modo de 16 bits). Esto significa que solo puede funcionar con datos de 16 bits y utiliza direccionamiento de memoria segmentado, y también solo puede direccionar 1 MB de memoria. Pero no usaremos el segundo aquí. La siguiente imagen muestra el estado de la RAM al transferir el control a nuestro código (la imagen se toma desde aquí ).


imagen


Lo √ļltimo que hay que decir antes de la parte pr√°ctica son las interrupciones. Una interrupci√≥n es una se√Īal especial (por ejemplo, desde un dispositivo de entrada, como un teclado o mouse) a un procesador que dice que es necesario interrumpir inmediatamente la ejecuci√≥n del c√≥digo actual y ejecutar el c√≥digo del controlador de interrupci√≥n. Todas las direcciones de los manejadores de interrupciones se encuentran en la Tabla de descriptores de interrupci√≥n (IDT) en la memoria principal. Cada interrupci√≥n tiene su propio manejador de interrupciones. Por ejemplo, cuando se presiona una tecla del teclado, se llama a una interrupci√≥n, el procesador se detiene, recuerda la direcci√≥n de la instrucci√≥n interrumpida, guarda todos los valores de sus registros (en la pila) y procede a ejecutar el controlador de interrupci√≥n. Tan pronto como finaliza su ejecuci√≥n, el procesador restaura los valores de los registros y vuelve a la instrucci√≥n interrumpida y contin√ļa la ejecuci√≥n.


Por ejemplo, para mostrar algo en la pantalla, el BIOS usa la interrupción 0x10 (formato hexadecimal), y la interrupción 0x16 se usa para esperar que se presione una tecla. De hecho, estas son todas las interrupciones que necesitaremos aquí.


Además, cada interrupción tiene su propia subfunción que determina la peculiaridad de su comportamiento. Para mostrar algo en el formato de texto (!), Debe ingresar el valor 0x0e en el registro AH. Además, las interrupciones tienen sus propios parámetros. 0x10 toma valores de ah (define una subfunción específica) y al (el carácter a imprimir). De esta manera


mov ah, 0x0e mov al, 'x' int 0x10 

muestra el caracter 'x'. 0x16 toma el valor de ah (subfunción específica) y carga el valor de la clave ingresada en el registro al. Utilizaremos la función 0x0.


Parte pr√°ctica


Comencemos con el código auxiliar. Necesitaremos la función de comparar dos líneas y la función de mostrar una línea en la pantalla. Traté de describir el funcionamiento de estas funciones en los comentarios lo más claramente posible.


str_compare.asm:


 compare_strs_si_bx: push si ;         push bx push ax comp: mov ah, [bx] ;     , cmp [si], ah ;      ah jne not_equal ;    ,     cmp byte [si], 0 ;    ,    je first_zero ;    inc si ;     bx  si inc bx jmp comp ;   first_zero: cmp byte [bx], 0 ;    bx != 0,  ,   jne not_equal ;  ,    not_equal mov cx, 1 ;     ,  cx = 1 pop si ;     pop bx pop ax ret ;     not_equal: mov cx, 0 ;  ,  cx = 0 pop si ;    pop bx pop ax ret ;     

La función acepta los registros SI y BX como parámetros. Si las líneas son iguales, entonces CX se establece en 1, de lo contrario 0.


Tambi√©n vale la pena se√Īalar que los registros AX, BX, CX y DX se dividen en dos partes de un solo byte: AH, BH, CH y DH para el byte alto, y AL, BL, CL y DL para el byte bajo.


Inicialmente, se supone que en bx y si hay punteros (!) (Es decir, almacena la direcci√≥n en la memoria) a alguna direcci√≥n en la memoria en la que se encuentra el comienzo de la l√≠nea. La operaci√≥n [bx] tomar√° un puntero de bx, ir√° a esta direcci√≥n y tomar√° alg√ļn valor desde all√≠. inc bx significa que ahora el puntero se referir√° a la direcci√≥n inmediatamente despu√©s de la direcci√≥n original.


print_string.asm:


 print_string_si: push ax ;  ax   mov ah, 0x0e ;  ah  0x0e,    call print_next_char ;  pop ax ;  ax ret ;   print_next_char: mov al, [si] ;    cmp al, 0 ;  si  jz if_zero ;     int 0x10 ;     al inc si ;    jmp print_next_char ;   ... if_zero: ret 

Como parámetro, la función toma el registro SI y byte a byte imprime una cadena.


Ahora pasemos al código principal. Primero, definamos todas las variables (este código estará al final del archivo):


 ; 0x0d -   , 0xa -    wrong_command: db "Wrong command!", 0x0d, 0xa, 0 greetings: db "The OS is on. Type 'help' for commands", 0x0d, 0xa, 0xa, 0 help_desc: db "Here's nothing to show yet. But soon...", 0x0d, 0xa, 0 goodbye: db 0x0d, 0xa, "Goodbye!", 0x0d, 0xa, 0 prompt: db ">", 0 new_line: db 0x0d, 0xa, 0 help_command: db "help", 0 input: times 64 db 0 ;   - 64  times 510 - ($-$$) db 0 dw 0xaa55 

El carácter de retorno de carro mueve el carro al borde izquierdo de la pantalla, es decir, al comienzo de la línea.


 input: times 64 db 0 

significa que asignamos 64 bytes bajo el b√ļfer para entrada y los llenamos con ceros.


El resto de las variables son necesarias para mostrar cierta información, más adelante en el código comprenderá por qué son necesarias.


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

significa que establecemos expl√≠citamente el tama√Īo del archivo de salida (con la extensi√≥n .bin) en 512 bytes, llenamos los primeros 510 bytes con ceros (por supuesto, se llenan antes de que se ejecute el c√≥digo completo) y los dos √ļltimos bytes con los mismos bytes 55 y AA "m√°gicos" . $ significa la direcci√≥n de la instrucci√≥n actual, y $$ es la direcci√≥n de la primera instrucci√≥n de nuestro c√≥digo.


Pasemos al código real:


 org 0x7c00 ; (1) bits 16 ; (2) jmp start ;    start %include "print_string.asm" ;     %include "str_compare.asm" ; ==================================================== start: mov ah, 0x00 ;   (3) mov al, 0x03 int 0x10 mov sp, 0x7c00 ;   (4) mov si, greetings ;    call print_string_si ;      mainloop 

(1) Este comando deja en claro a NASM que estamos ejecutando código a partir de 0x7c00. Esto le permite sesgar automáticamente todas las direcciones relativas a esa dirección para que no lo hagamos explícitamente.
(2) Este comando le indica a NASM que estamos operando en modo de 16 bits.
(3) Cuando se inicia, QEMU imprime mucha información que no necesitamos. Para hacer esto, configure ah 0x00, al 0x03 y llame a 0x10 para borrar la pantalla de todo.
(4) Para guardar registros en la pila, debe especificar en qué dirección se ubicará su vértice utilizando el puntero de la pila SP. SP indicará el área en memoria en la que se escribirá el siguiente valor. Agregue el valor a la pila: SP baja la memoria en 2 bytes (ya que estamos en modo real, donde todos los operandos de registro son valores de 16 bits, es decir, de doble byte). Especificamos 0x7c00, por lo que los valores en la pila se almacenarán justo al lado de nuestro código en la memoria. Una vez más, la pila crece (!). Esto significa que cuantos más valores haya en la pila, menos memoria indicará el puntero de la pila SP.


 mainloop: mov si, prompt ;   call print_string_si call get_input ;     jmp mainloop ;  mainloop... 

Bucle principal Aquí, con cada iteración, imprimimos el símbolo ">", después de lo cual llamamos a la función get_input, que implementa el trabajo con interrupción del teclado.


 get_input: mov bx, 0 ;  bx      input_processing: mov ah, 0x0 ;    0x16 int 0x16 ;  ASCII  cmp al, 0x0d ;   enter je check_the_input ;   ,   ,  ;    cmp al, 0x8 ;   backspace je backspace_pressed cmp al, 0x3 ;   ctrl+c je stop_cpu mov ah, 0x0e ;     -   ;     int 0x10 mov [input+bx], al ;       inc bx ;   cmp bx, 64 ;  input  je check_the_input ;    ,    enter jmp input_processing ;    

(1) [input + bx] significa que tomamos la dirección del comienzo de la entrada del buffer de entrada y le agregamos bx, es decir, llegamos a bx + el primer elemento del buffer.


 stop_cpu: mov si, goodbye ;   call print_string_si jmp $ ;    ; $     

Aquí todo es simple: si presionó Ctrl + C, la computadora realiza sin cesar la función jmp $.


 backspace_pressed: cmp bx, 0 ;  backspace ,  input ,  je input_processing ;    mov ah, 0x0e ;  backspace.  ,   int 0x10 ;   ,      mov al, ' ' ;      ,  int 0x10 ;   mov al, 0x8 ;       int 0x10 ;     backspace dec bx mov byte [input+bx], 0 ;    input   jmp input_processing ;    

Para no borrar el carácter '>' al presionar la tecla de retroceso, verificamos si la entrada está vacía. Si no, entonces no hagas nada.


 check_the_input: inc bx mov byte [input+bx], 0 ;     ,   ;  (  '\0'  ) mov si, new_line ;     call print_string_si mov si, help_command ;  si     help mov bx, input ;   bx -   call compare_strs_si_bx ;  si  bx (  help) cmp cx, 1 ; compare_strs_si_bx   cx 1,  ;     je equal_help ;  =>    ;  help jmp equal_to_nothing ;   ,   "Wrong command!" 

Aquí, creo que todo está claro por los comentarios.


 equal_help: mov si, help_desc call print_string_si jmp done equal_to_nothing: mov si, wrong_command call print_string_si jmp done 

Dependiendo de lo que se ingresó, mostramos el texto de la variable help_desc o el texto de la variable wrong_command.


 ; done    input done: cmp bx, 0 ;     input   je exit ;   ,    mainloop dec bx ;  ,      mov byte [input+bx], 0 jmp done ;       exit: ret 

En realidad, todo el código es:


prompt.asm:


 org 0x7c00 bits 16 jmp start ;    start %include "print_string.asm" %include "str_compare.asm" ; ==================================================== start: cli ;  ,    ;     mov ah, 0x00 ;   mov al, 0x03 int 0x10 mov sp, 0x7c00 ;   mov si, greetings ;    call print_string_si ;      mainloop mainloop: mov si, prompt ;   call print_string_si call get_input ;     jmp mainloop ;  mainloop... get_input: mov bx, 0 ;  bx      input_processing: mov ah, 0x0 ;    0x16 int 0x16 ;  ASCII  cmp al, 0x0d ;   enter je check_the_input ;   ,   ,  ;    cmp al, 0x8 ;   backspace je backspace_pressed cmp al, 0x3 ;   ctrl+c je stop_cpu mov ah, 0x0e ;     -   ;     int 0x10 mov [input+bx], al ;       inc bx ;   cmp bx, 64 ;  input  je check_the_input ;    ,    enter jmp input_processing ;    stop_cpu: mov si, goodbye ;   call print_string_si jmp $ ;    ; $     backspace_pressed: cmp bx, 0 ;  backspace ,  input ,  je input_processing ;    mov ah, 0x0e ;  backspace.  ,   int 0x10 ;   ,      mov al, ' ' ;      ,  int 0x10 ;   mov al, 0x8 ;       int 0x10 ;     backspace dec bx mov byte [input+bx], 0 ;    input   jmp input_processing ;    check_the_input: inc bx mov byte [input+bx], 0 ;     ,   ;  (  '\0'  ) mov si, new_line ;     call print_string_si mov si, help_command ;  si     help mov bx, input ;   bx -   call compare_strs_si_bx ;  si  bx (  help) cmp cx, 1 ; compare_strs_si_bx   cx 1,  ;     je equal_help ;  =>    ;  help jmp equal_to_nothing ;   ,   "Wrong command!" equal_help: mov si, help_desc call print_string_si jmp done equal_to_nothing: mov si, wrong_command call print_string_si jmp done ; done    input done: cmp bx, 0 ;     input   je exit ;   ,    mainloop dec bx ;  ,      mov byte [input+bx], 0 jmp done ;       exit: ret ; 0x0d -   , 0xa -    wrong_command: db "Wrong command!", 0x0d, 0xa, 0 greetings: db "The OS is on. Type 'help' for commands", 0x0d, 0xa, 0xa, 0 help_desc: db "Here's nothing to show yet. But soon...", 0x0d, 0xa, 0 goodbye: db 0x0d, 0xa, "Goodbye!", 0x0d, 0xa, 0 prompt: db ">", 0 new_line: db 0x0d, 0xa, 0 help_command: db "help", 0 input: times 64 db 0 ;   - 64  times 510 - ($-$$) db 0 dw 0xaa55 

Para compilar todo esto, ingrese el comando:


 nasm -f bin prompt.asm -o bootloader.bin 

Y obtenemos el binario con nuestro código en la salida. Ahora ejecute el emulador QEMU con este archivo (-monitor stdio le permite mostrar el valor de registro en cualquier momento usando el comando print $ reg):


 qemu-system-i386 bootloader.bin -monitor stdio 

Y obtenemos la salida:


imagen

Source: https://habr.com/ru/post/442428/


All Articles