La ligne de commande la plus simple sur NASM et QEMU

image


Donc, droit au but. Nous écrirons sous Linux, sur NASM et en utilisant QEMU. C'est facile à installer, alors sautez cette étape.


Il est entendu que le lecteur connaît la syntaxe de NASM au moins au niveau de base (cependant, il n'y aura rien de particulièrement compliqué ici) et comprend ce que sont les registres.


Théorie de base


La première chose qui démarre le processeur lorsque l'ordinateur est allumé est le code BIOS (ou UEFI, mais ici je ne parlerai que du BIOS), qui est "câblé" dans la mémoire de la carte mère (en particulier - à 0xFFFFFFF0).


Immédiatement après avoir allumé le BIOS, l'autotest à la mise sous tension (POST) démarre - l'autotest après la mise sous tension. Le BIOS vérifie l'intégrité de la mémoire, détecte et initialise les périphériques connectés, vérifie les registres, détermine la taille de la mémoire, etc.


L'étape suivante consiste à identifier le disque de démarrage à partir duquel vous pouvez démarrer le système d'exploitation. Un disque de démarrage est un disque (ou tout autre lecteur) qui a les 2 derniers octets du premier secteur (le premier secteur signifie les 512 premiers octets du lecteur, car 1 secteur = 512 octets) est 55 et AA (au format hexadécimal). Dès qu'un disque de démarrage est trouvé, le BIOS charge ses 512 premiers octets dans la RAM à l'adresse 0x7c00 et transfère le contrôle au processeur à cette adresse.


Bien sûr, dans ces 512 octets, cela ne fonctionnera pas pour s'adapter à un système d'exploitation à part entière. Par conséquent, généralement dans ce secteur, placez le chargeur principal, qui charge le code principal du système d'exploitation dans la RAM et lui transfère le contrôle.


Depuis le tout début, le processeur fonctionne en mode réel (= mode 16 bits). Cela signifie qu'il ne peut fonctionner qu'avec des données 16 bits et utilise un adressage de mémoire segmenté, et ne peut également adresser que 1 Mo de mémoire. Mais nous n'utiliserons pas le second ici. L'image ci-dessous montre l'état de la RAM lors du transfert du contrôle vers notre code (l'image est prise à partir d'ici ).


image


La dernière chose à dire avant la partie pratique, ce sont les interruptions. Une interruption est un signal spécial (par exemple, provenant d'un périphérique d'entrée, tel qu'un clavier ou une souris) à un processeur qui dit qu'il est nécessaire d'interrompre immédiatement l'exécution du code actuel et d'exécuter le code du gestionnaire d'interruption. Toutes les adresses des gestionnaires d'interruptions se trouvent dans la table des descripteurs d'interruption (IDT) dans la mémoire principale. Chaque interruption a son propre gestionnaire d'interruption. Par exemple, lorsqu'une touche du clavier est enfoncée, une interruption est appelée, le processeur s'arrête, se souvient de l'adresse de l'instruction interrompue, enregistre toutes les valeurs de ses registres (sur la pile) et procède à l'exécution du gestionnaire d'interruption. Dès la fin de son exécution, le processeur restaure les valeurs des registres et revient à l'instruction interrompue et poursuit l'exécution.


Par exemple, pour afficher quelque chose à l'écran, le BIOS utilise l'interruption 0x10 (format hexadécimal) et l'interruption 0x16 est utilisée pour attendre qu'une touche soit pressée. En fait, ce sont toutes des interruptions dont nous aurons besoin ici.


De plus, chaque interruption a sa propre sous-fonction qui détermine la particularité de son comportement. Pour afficher quelque chose au format texte (!), Vous devez entrer la valeur 0x0e dans le registre AH. De plus, les interruptions ont leurs propres paramètres. 0x10 prend des valeurs de ah (définit une sous-fonction spécifique) et al (le caractère à imprimer). De cette façon


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

affiche le caractère «x». 0x16 prend la valeur de ah (sous-fonction spécifique) et charge la valeur de la clé entrée dans le registre al. Nous utiliserons la fonction 0x0.


Partie pratique


Commençons par le code d'assistance. Nous avons besoin des fonctions de comparaison de deux lignes et de la fonction d'affichage d'une ligne à l'écran. J'ai essayé de décrire le fonctionnement de ces fonctions dans les commentaires aussi clairement que possible.


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 fonction accepte les registres SI et BX comme paramètres. Si les lignes sont égales, CX est mis à 1, sinon 0.


Il convient également de noter que les registres AX, BX, CX et DX sont divisés en deux parties à un octet: AH, BH, CH et DH pour l'octet haut et AL, BL, CL et DL pour l'octet bas.


Initialement, il est entendu que dans bx et si il y a des pointeurs (!) (C'est-à-dire stocke l'adresse en mémoire) vers une adresse en mémoire dans laquelle se trouve le début de la ligne. L'opération [bx] prendra un pointeur de bx, il ira à cette adresse et en prendra une valeur. inc bx signifie que maintenant le pointeur fera référence à l'adresse immédiatement après l'adresse d'origine.


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 

En paramètre, la fonction prend le registre SI et octet par octet imprime une chaîne.


Passons maintenant au code principal. Définissons d'abord toutes les variables (ce code sera à la toute fin du fichier):


 ; 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 

Le caractère de retour chariot déplace le chariot vers le bord gauche de l'écran, c'est-à-dire jusqu'au début de la ligne.


 input: times 64 db 0 

signifie que nous allouons 64 octets sous le tampon pour l'entrée et les remplissons de zéros.


Les autres variables sont nécessaires pour afficher certaines informations, plus bas dans le code, vous comprendrez pourquoi elles sont toutes nécessaires.


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

signifie que nous définissons explicitement la taille du fichier de sortie (avec l'extension .bin) à 512 octets, remplissons les 510 premiers octets avec des zéros (bien sûr, ils sont remplis avant l'exécution de tout le code), et les deux derniers octets avec les mêmes octets "magiques" 55 et AA . $ signifie l'adresse de l'instruction en cours, et $$ est l'adresse de la toute première instruction de notre code.


Passons au code réel:


 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). Cette commande indique clairement à NASM que nous exécutons du code à partir de 0x7c00. Cela lui permet de biaiser automatiquement toutes les adresses par rapport à cette adresse afin que nous ne le fassions pas explicitement.
(2). Cette commande indique à NASM que nous fonctionnons en mode 16 bits.
(3). Une fois lancé, QEMU imprime beaucoup d'informations dont nous n'avons pas besoin. Pour ce faire, réglez sur ah 0x00, sur al 0x03 et appelez 0x10 pour effacer l'écran de tout.
(4). Pour enregistrer les registres sur la pile, vous devez spécifier à quelle adresse son sommet sera situé à l'aide du pointeur de pile SP. SP indiquera la zone en mémoire dans laquelle la prochaine valeur sera écrite. Ajoutez la valeur à la pile - SP descend la mémoire de 2 octets (puisque nous sommes en mode réel, où tous les opérandes de registre sont des valeurs de 16 bits, c'est-à-dire à deux octets). Nous avons spécifié 0x7c00, donc les valeurs sur la pile seront stockées juste à côté de notre code en mémoire. Encore une fois - la pile se développe (!). Cela signifie que plus il y a de valeurs sur la pile, moins le pointeur de la pile SP indiquera de mémoire.


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

Boucle principale. Ici, à chaque itération, nous imprimons le caractère ">", après quoi nous appelons la fonction get_input, qui implémente le travail avec interruption du clavier.


 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) [entrée + bx] signifie que nous prenons l'adresse du début de l'entrée du tampon d'entrée et y ajoutons bx, c'est-à-dire que nous arrivons à bx + le 1er élément du tampon.


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

Tout est simple ici - si vous appuyez sur Ctrl + C, l'ordinateur exécute la fonction jmp $ à l'infini.


 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 ;    

Afin de ne pas effacer le caractère '>' lorsque vous appuyez sur la touche de retour arrière, nous vérifions si l'entrée est vide. Sinon, ne faites rien.


 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!" 

Ici, je pense que tout ressort clairement des commentaires.


 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 

Selon ce qui a été entré, nous affichons soit le texte de la variable help_desc, soit le texte de la variable false_command.


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

En fait, tout le code est:


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 

Pour compiler tout cela, entrez la commande:


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

Et nous obtenons le binaire avec notre code à la sortie. Exécutez maintenant l'émulateur QEMU avec ce fichier (-monitor stdio vous permet d'afficher la valeur du registre à tout moment en utilisant la commande print $ reg):


 qemu-system-i386 bootloader.bin -monitor stdio 

Et nous obtenons la sortie:


image

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


All Articles