Guide de l'assembleur X86 pour les débutants

De nos jours, il est rarement nécessaire d'écrire en pur assembleur, mais je le recommande définitivement à toute personne intéressée par la programmation. Vous verrez les choses sous un angle différent et les compétences vous seront utiles lors du débogage de code dans d'autres langues.

Dans cet article, nous allons écrire à partir de zéro une calculatrice RPN (Reverse Polish Notation) en assembleur x86 pur. Lorsque nous avons terminé, nous pouvons l'utiliser comme ceci:

$ ./calc "32+6*" # "(3+2)*6"    30 

Tout le code de l'article est ici . Il est abondamment commenté et peut servir de matériel éducatif à ceux qui connaissent déjà l'assembleur.

Commençons par écrire le programme de base Hello world! pour vérifier les paramètres d'environnement. Passons ensuite aux appels système, à la pile d'appels, aux cadres de pile et à la convention d'appel x86. Ensuite, pour la pratique, nous allons écrire quelques fonctions de base dans l'assembleur x86 - et commencer à écrire une calculatrice RPN.

On suppose que le lecteur a une certaine expérience de programmation en C et une connaissance de base de l'architecture informatique (par exemple, qu'est-ce qu'un registre de processeur). Puisque nous utiliserons Linux, vous devriez également pouvoir utiliser la ligne de commande Linux.

Configuration de l'environnement


Comme déjà mentionné, nous utilisons Linux (64 bits ou 32 bits). Le code ci-dessus ne fonctionne pas sur Windows ou Mac OS X.

Pour l'installation, vous n'avez besoin que de l'éditeur de liens GNU ld de binutils , qui est préinstallé sur la plupart des distributions, et de l'assembleur NASM. Sur Ubuntu et Debian, vous pouvez installer les deux avec une seule commande:

 $ sudo apt-get install binutils nasm 

Je recommanderais également de garder une table ASCII à portée de main.

Bonjour tout le monde!


Pour vérifier l'environnement, enregistrez le code suivant dans le fichier calc.asm :

 ;    _start     ; . global _start ;   .rodata   (  ) ;     ,       section .rodata ;     hello_world.   NASM ;   ,     , ;  . 0xA =  , 0x0 =    hello_world: db "Hello world!", 0xA, 0x0 ;   .text,     section .text _start: mov eax, 0x04 ;   4   eax (0x04 = write()) mov ebx, 0x1 ;   (1 =  , 2 =  ) mov ecx, hello_world ;     mov edx, 14 ;   int 0x80 ;    0x80,   ;     mov eax, 0x01 ; 0x01 = exit() mov ebx, 0 ; 0 =   int 0x80 

Les commentaires expliquent la structure générale. Pour une liste des registres et des instructions générales, consultez le Guide de l'assembleur x86 de l' Université de Virginie . Avec une discussion plus approfondie des appels système, cela sera d'autant plus nécessaire.

Les commandes suivantes collectent le fichier assembleur dans un fichier objet, puis compilent le fichier exécutable:

 $ nasm -f elf_i386 calc.asm -o calc $ ld -m elf_i386 calc.o -o calc 

Après avoir commencé, vous devriez voir:

 $ ./calc Hello world! 

Makefile


C'est une partie facultative, mais vous pouvez créer un Makefile pour simplifier la construction et la mise en page à l'avenir. Enregistrez-le dans le même répertoire que calc.asm :

 CFLAGS= -f elf32 LFLAGS= -m elf_i386 all: calc calc: calc.o ld $(LFLAGS) calc.o -o calc calc.o: calc.asm nasm $(CFLAGS) calc.asm -o calc.o clean: rm -f calc.o calc .INTERMEDIATE: calc.o 

Ensuite, au lieu des instructions ci-dessus, lancez simplement make.

Appels système


Les appels système Linux indiquent au système d'exploitation de faire quelque chose pour nous. Dans cet article, nous utilisons uniquement deux appels système: write() pour écrire une ligne dans un fichier ou un flux (dans notre cas, il s'agit d'un périphérique de sortie standard et d'une erreur standard) et exit() pour quitter le programme:

 syscall 0x01: exit(int error_code) error_code -  0         (  1)   syscall 0x04: write(int fd, char *string, int length) fd —  1   , 2      string —      length —     

Les appels système sont configurés en stockant le numéro d'appel système dans le registre eax , puis ses arguments dans ebx , ecx , edx dans cet ordre. Vous pouvez remarquer que exit() qu'un seul argument - dans ce cas, ecx et edx n'ont pas d'importance.

eaxebxecxedx
Numéro d'appel systèmearg1arg2arg3


Pile d'appels




Une pile d'appels est une structure de données qui stocke des informations sur chaque appel à une fonction. Chaque appel a sa propre section dans la pile - le "cadre". Il stocke quelques informations sur l'appel en cours: les variables locales de cette fonction et l'adresse de retour (où le programme doit aller après l'exécution de la fonction).

Immédiatement, je note une chose non évidente: la pile augmente la mémoire. Lorsque vous ajoutez quelque chose en haut de la pile, il est inséré à une adresse mémoire inférieure à l'élément précédent. En d'autres termes, à mesure que la pile grandit, l'adresse mémoire en haut de la pile diminue. Pour éviter toute confusion, je vous rappellerai toujours ce fait.

L'instruction push quelque chose au-dessus de la pile, et pop sortir les données de là. Par exemple, push alloue une place en haut de la pile et y place la valeur du registre eax , et pop transfère toutes les données du haut de la pile vers eax et libère cette zone de mémoire.

Le but du registre esp est de pointer vers le haut de la pile. Toute donnée au-dessus de esp considérée comme n'atteignant pas la pile, il s'agit de données d'ordures. L'exécution d'une instruction push (ou pop ) déplace esp . Vous pouvez manipuler esp directement, si vous donnez un rapport sur vos actions.

Le registre ebp est similaire à esp , mais il pointe toujours approximativement vers le milieu de la trame de pile actuelle, juste avant les variables locales de la fonction courante (nous en parlerons plus tard). Cependant, appeler une autre fonction ne déplace pas automatiquement ebp , cela doit être fait manuellement à chaque fois.

Convention d'appel d'architecture X86


Dans x86, il n'y a pas de concept de fonction intégré comme dans les langages de haut niveau. L' goto call goto simplement jmp ( goto ) vers une autre adresse mémoire. Pour utiliser des routines comme fonctions dans d'autres langages (qui peuvent prendre des arguments et renvoyer des données), vous devez suivre la convention d'appel (il existe de nombreuses conventions, mais nous utilisons CDECL, la convention la plus populaire pour x86 parmi les compilateurs C et les programmeurs assembleurs). Il garantit également que les registres de routine ne sont pas confondus lors de l'appel d'une autre fonction.

Règles d'appelant


Avant d'appeler la fonction, l'appelant doit:

  1. Enregistrez les registres que l'appelant doit enregistrer sur la pile. La fonction appelée peut changer certains registres: pour ne pas perdre de données, l'appelant doit les sauvegarder en mémoire jusqu'à ce qu'elles soient poussées sur la pile. Ce sont les edx eax , ecx et edx . Si vous n'en utilisez aucun, vous ne pouvez pas les enregistrer.
  2. Écrivez les arguments de la fonction dans la pile dans l'ordre inverse (premier dernier argument, premier premier argument à la fin). Cet ordre garantit que la fonction appelée reçoit ses arguments de la pile dans le bon ordre.
  3. Appelez le sous-programme.

Si possible, la fonction enregistrera le résultat dans eax . Immédiatement après l' call appelant doit:

  1. Supprimez les arguments de fonction de la pile. Cela se fait généralement en ajoutant simplement le nombre d'octets à esp . N'oubliez pas que la pile grandit, donc pour la retirer de la pile, vous devez ajouter des octets.
  2. Restaurez les registres enregistrés en les sortant de la pile dans l'ordre inverse. La fonction appelée ne changera aucun autre registre.

L'exemple suivant montre comment ces règles s'appliquent. Supposons que la fonction _subtract deux arguments entiers (4 octets) et renvoie le premier argument moins le second. Dans la sous _mysubroutine routine _mysubroutine appelez _subtract avec les arguments 10 et 2 :

 _mysubroutine: ; ... ;  -  ; ... push ecx ;   (    eax) push edx push 2 ;  ,      push 10 call _subtract ; eax   10-2=8 add esp, 8 ;  8    (   4 ) pop edx ;    pop ecx ; ... ;  - ,        eax ; ... 

Règles de la routine appelée


Avant d'appeler, le sous-programme doit:

  1. Enregistrez le pointeur de registre de base ebp de la trame précédente en l'écrivant dans la pile.
  2. Ajustez ebp de l'image précédente au courant (valeur esp actuelle).
  3. Allouez plus d'espace sur la pile pour les variables locales, si nécessaire, déplacez le pointeur esp . Au fur et à mesure que la pile se développe, vous devez soustraire la mémoire manquante de esp .
  4. Sauvegardez les registres de la routine appelée sur la pile. Ce sont ebx , edi et esi . Il n'est pas nécessaire de sauvegarder des registres dont la modification n'est pas prévue.

Pile d'appels après l'étape 1:



La pile d'appels après l'étape 2:



Pile d'appels après l'étape 4:



Dans ces diagrammes, une adresse de retour est indiquée dans chaque trame de pile. Il est automatiquement poussé sur la pile par une instruction d' call . L' ret récupère l'adresse du haut de la pile et y saute. Nous n'avons pas besoin de cette instruction, je viens de montrer pourquoi les variables locales de la fonction sont 4 octets au-dessus d' ebp , mais les arguments de la fonction sont 8 octets au-dessous d' ebp .

Dans le dernier diagramme, vous pouvez également remarquer que les variables locales de la fonction commencent toujours 4 octets au-dessus de ebp partir de l'adresse ebp-4 (soustraction ici, car nous ebp-4 la pile), et les arguments de la fonction commencent toujours 8 octets sous ebp partir de l'adresse ebp+8 (addition, car nous descendons la pile). Si vous suivez les règles de cette convention, il en sera de même pour les variables et les arguments de n'importe quelle fonction.

Lorsque la fonction est terminée et que vous souhaitez revenir, vous devez d'abord définir eax sur la valeur de retour de la fonction, si nécessaire. De plus, vous avez besoin de:

  1. Restaurez les registres enregistrés en les sortant de la pile dans l'ordre inverse.
  2. Libérez de l'espace sur la pile allouée par la variable locale à l'étape 3, si nécessaire: en installant simplement esp dans ebp
  3. Restaurez le pointeur de base ebp de l'image précédente en le ebp de la pile.
  4. Retour avec ret

Maintenant, nous implémentons la fonction _subtract de notre exemple:

 _subtract: push ebp ;      mov ebp, esp ;  ebp ;          ,      ;       ,     ;   ;    mov eax, [ebp+8] ;      eax.  ;       ebp+8 sub eax, [ebp+12] ;      ebp+12   ;  ;   , eax     ;     ,     ;       ,       pop ebp ;      ret 

Entrée et sortie


Dans l'exemple ci-dessus, vous pouvez remarquer que la fonction s'exécute toujours de la même manière: push ebp , mov ebp , esp et allocation de mémoire pour les variables locales. L'ensemble x86 a une instruction pratique qui fait tout cela: enter ab , où a est le nombre d'octets que vous souhaitez allouer aux variables locales, b est le "niveau d'imbrication", que nous allons toujours mettre à 0 . De plus, la fonction se termine toujours par les instructions pop ebp et mov esp , ebp (bien qu'elles ne soient nécessaires que lors de l'allocation de mémoire aux variables locales, mais en tout cas ne font pas de mal). Cela peut également être remplacé par une seule déclaration: leave . Nous apportons des modifications:

 _subtract: enter 0, 0 ;        ebp ;       ,     ;   ;    mov eax, [ebp+8] ;      eax.  ;       ebp+8 sub eax, [ebp+12] ;      ebp+12  ;   ;   , eax     ;     ,     leave ;      ret 

Écriture de certaines fonctions de base


Après avoir maîtrisé la convention d'appel, vous pouvez commencer à écrire certaines routines. Pourquoi ne pas généraliser le code qui affiche "Bonjour tout le monde!" Pour sortir des lignes: la fonction _print_msg .

Ici, nous avons besoin d'une autre fonction _strlen pour compter la longueur de la chaîne. En C, cela pourrait ressembler à ceci:

 size_t strlen(char *s) { size_t length = 0; while (*s != 0) { //   length++; s++; } //   return length; } 

En d'autres termes, dès le début de la ligne, nous ajoutons 1 à la valeur de retour pour chaque caractère sauf zéro. Dès que le caractère nul est remarqué, nous retournons la valeur accumulée dans la boucle. Dans l'assembleur, c'est aussi assez simple: vous pouvez utiliser la fonction _subtract précédemment écrite comme base:

 _strlen: enter 0, 0 ;        ebp ;       ,     ;   ;    mov eax, 0 ; length = 0 mov ecx, [ebp+8] ;    (   ;  )   ecx (   ; ,      ) _strlen_loop_start: ;  ,    cmp byte [ecx], 0 ;       .  ;     32  (4 ). ;    .    ;     ( ) je _strlen_loop_end ;       inc eax ;    ,  1    add ecx, 1 ;       jmp _strlen_loop_start ;      _strlen_loop_end: ;   , eax    ;     ,     leave ;      ret 

Déjà pas mal, non? L'écriture du code C en premier peut être utile, car la majeure partie est directement convertie en assembleur. Vous pouvez maintenant utiliser cette fonction dans _print_msg , où nous appliquons toutes les connaissances acquises:

 _print_msg: enter 0, 0 ;    mov eax, 0x04 ; 0x04 =   write() mov ebx, 0x1 ; 0x1 =   mov ecx, [ebp+8] ;       , ;   edx   .    _strlen push eax ;     (    edx) push ecx push dword [ebp+8] ;   _strlen  _print_msg.  NASM ; ,    ,  , . ;      dword (4 , 32 ) call _strlen ; eax     mov edx, eax ;     edx,     add esp, 4 ;  4    ( 4-  char*) pop ecx ;     pop eax ;      _strlen,     int 0x80 leave ret 

Et voyez les fruits de notre travail acharné, en utilisant cette fonction dans le programme complet "Hello, world!".

 _start: enter 0, 0 ;     (    ) push hello_world ;    _print_msg call _print_msg mov eax, 0x01 ; 0x01 = exit() mov ebx, 0 ; 0 =   int 0x80 

Croyez-le ou non, nous avons couvert tous les principaux sujets nécessaires pour écrire des programmes d'assembleur x86 de base! Nous avons maintenant tout le matériel d'introduction et la théorie, nous allons donc nous concentrer complètement sur le code et appliquer les connaissances acquises pour écrire notre calculatrice RPN. Les fonctions seront beaucoup plus longues et utiliseront même certaines variables locales. Si vous voulez voir immédiatement le programme terminé, le voici .

Pour ceux d'entre vous qui ne sont pas familiers avec la notation polonaise inversée (parfois appelée notation polonaise inversée ou notation postfixée), les expressions sont évaluées ici à l'aide de la pile. Par conséquent, vous devez créer une pile, ainsi que les fonctions _pop et _push pour manipuler cette pile. Vous aurez également _print_answer fonction _print_answer , qui affichera une représentation sous forme de chaîne du résultat numérique à la fin du calcul.

Création de pile


Tout d'abord, nous définissons l'espace en mémoire de notre pile, ainsi que la variable globale stack_size . Il est conseillé de modifier ces variables afin qu'elles ne tombent pas dans la section .rodata , mais dans .data .

 section .data stack_size: dd 0 ;   dword (4 )   0 stack: times 256 dd 0 ;    

Vous pouvez maintenant implémenter les fonctions _push et _pop :

 _push: enter 0, 0 ;    ,    push eax push edx mov eax, [stack_size] mov edx, [ebp+8] mov [stack + 4*eax], edx ;    .   ;       dword inc dword [stack_size] ;  1  stack_size ;     pop edx pop eax leave ret _pop: enter 0, 0 ;     dec dword [stack_size] ;   1  stack_size mov eax, [stack_size] mov eax, [stack + 4*eax] ;       eax ;     ,     leave ret 

Sortie numérique


_print_answer beaucoup plus compliqué: vous devez convertir des nombres en chaînes et utiliser plusieurs autres fonctions. Vous aurez _putc fonction _putc , qui _putc un caractère, la fonction mod pour calculer le reste de la division (module) des deux arguments et _pow_10 pour augmenter à la puissance de 10. Plus tard, vous comprendrez pourquoi ils sont nécessaires. C'est assez simple, voici le code:

 _pow_10: enter 0, 0 mov ecx, [ebp+8] ;  ecx (  )  ;  mov eax, 1 ;   10 (10**0 = 1) _pow_10_loop_start: ;  eax  10,  ecx   0 cmp ecx, 0 je _pow_10_loop_end imul eax, 10 sub ecx, 1 jmp _pow_10_loop_start _pow_10_loop_end: leave ret _mod: enter 0, 0 push ebx mov edx, 0 ;   mov eax, [ebp+8] mov ebx, [ebp+12] idiv ebx ;  64-  [edx:eax]  ebx.    ;  32-  eax,    edx  ; . ;    eax,   edx.  ,  ;       , ;    . mov eax, edx ;     () pop ebx leave ret _putc: enter 0, 0 mov eax, 0x04 ; write() mov ebx, 1 ;   lea ecx, [ebp+8] ;   mov edx, 1 ;   1  int 0x80 leave ret 

Alors, comment dériver des nombres individuels dans un nombre? Tout d'abord, notez que le dernier chiffre du nombre est le reste de la division par 10 (par exemple, 123 % 10 = 3 ), et le chiffre suivant est le reste de la division par 100, divisé par 10 (par exemple, (123 % 100)/10 = 2 ). En général, vous pouvez trouver un chiffre spécifique d'un nombre (de droite à gauche) en trouvant ( % 10**n) / 10**(n-1) , où le nombre d'unités sera n = 1 , le nombre de dizaines est n = 2 etc.

En utilisant cette connaissance, vous pouvez trouver tous les chiffres d'un nombre de n = 1 à n = 10 (c'est le nombre maximum de bits dans un entier signé de 4 octets). Mais il est beaucoup plus facile d'aller de gauche à droite - nous pouvons donc imprimer chaque caractère dès que nous le trouvons et nous débarrasser des zéros sur le côté gauche. Par conséquent, nous trions les nombres de n = 10 à n = 1 .

En C, le programme ressemblera Ă  ceci:

 #define MAX_DIGITS 10 void print_answer(int a) { if (a < 0) { //    putc('-'); //   «» a = -a; //     } int started = 0; for (int i = MAX_DIGITS; i > 0; i--) { int digit = (a % pow_10(i)) / pow_10(i-1); if (digit == 0 && started == 0) continue; //     started = 1; putc(digit + '0'); } } 

Vous comprenez maintenant pourquoi nous avons besoin de ces trois fonctions. Implémentons ceci dans l'assembleur:

 %define MAX_DIGITS 10 _print_answer: enter 1, 0 ;  1    "started"   C push ebx push edi push esi mov eax, [ebp+8] ;   "a" cmp eax, 0 ;    ,    ;  jge _print_answer_negate_end ; call putc for '-' push eax push 0x2d ;  '-' call _putc add esp, 4 pop eax neg eax ;     _print_answer_negate_end: mov byte [ebp-4], 0 ; started = 0 mov ecx, MAX_DIGITS ;  i _print_answer_loop_start: cmp ecx, 0 je _print_answer_loop_end ;  pow_10  ecx.   ebx   "digit"   C. ;    edx = pow_10(i-1),  ebx = pow_10(i) push eax push ecx dec ecx ; i-1 push ecx ;    _pow_10 call _pow_10 mov edx, eax ; edx = pow_10(i-1) add esp, 4 pop ecx ;   i  ecx pop eax ; end pow_10 call mov ebx, edx ; digit = ebx = pow_10(i-1) imul ebx, 10 ; digit = ebx = pow_10(i) ;  _mod  (a % pow_10(i)),   (eax mod ebx) push eax push ecx push edx push ebx ; arg2, ebx = digit = pow_10(i) push eax ; arg1, eax = a call _mod mov ebx, eax ; digit = ebx = a % pow_10(i+1), almost there add esp, 8 pop edx pop ecx pop eax ;   mod ;  ebx ( "digit" )  pow_10(i) (edx).    ; ,   idiv     edx, eax.  ; edx   ,    - ;   push esi mov esi, edx push eax mov eax, ebx mov edx, 0 idiv esi ; eax   () mov ebx, eax ; ebx = (a % pow_10(i)) / pow_10(i-1),  "digit"   C pop eax pop esi ; end division cmp ebx, 0 ;  digit == 0 jne _print_answer_trailing_zeroes_check_end cmp byte [ebp-4], 0 ;  started == 0 jne _print_answer_trailing_zeroes_check_end jmp _print_answer_loop_continue ; continue _print_answer_trailing_zeroes_check_end: mov byte [ebp-4], 1 ; started = 1 add ebx, 0x30 ; digit + '0' ;  putc push eax push ecx push edx push ebx call _putc add esp, 4 pop edx pop ecx pop eax ;   putc _print_answer_loop_continue: sub ecx, 1 jmp _print_answer_loop_start _print_answer_loop_end: pop esi pop edi pop ebx leave ret 

Ce fut un test difficile! J'espère que les commentaires aideront à le régler. Si vous pensez maintenant: "Pourquoi ne pouvez-vous pas simplement écrire printf("%d")?", Alors vous aimerez la fin de l'article, où nous remplacerons la fonction par juste cela!

Maintenant que nous avons toutes les fonctions nécessaires, il reste à implémenter la logique de base dans _start- et c'est tout!

Calcul de la notation polonaise inversée


Comme nous l'avons déjà dit, la notation polonaise inverse est calculée à l'aide de la pile. Lors de la lecture, le nombre est poussé sur la pile et lors de la lecture, l'opérateur est appliqué à deux objets en haut de la pile.

Par exemple, si nous voulons calculer 84/3+6*(cette expression peut également être écrite dans le formulaire 6384/+*), le processus est le suivant:

ÉtapeSymboleEmpiler avantPile après
18[][8]
24[8][8, 4]
3/[8, 4][2]
43[2][2, 3]
5+[2, 3][5]
66[5][5, 6]
7*[5, 6][30]

Si l'entrée est une expression de suffixe valide, à la fin des calculs, il ne reste qu'un élément sur la pile - c'est la réponse, le résultat des calculs. Dans notre cas, le nombre est 30.

Dans l'assembleur, vous devez implémenter quelque chose comme ce code en C:

 int stack[256]; // , 256      int stack_size = 0; int main(int argc, char *argv[]) { char *input = argv[0]; size_t input_length = strlen(input); for (int i = 0; i < input_length; i++) { char c = input[i]; if (c >= '0' && c <= '9') { //   —   push(c - '0'); //          } else { int b = pop(); int a = pop(); if (c == '+') { push(a+b); } else if (c == '-') { push(ab); } else if (c == '*') { push(a*b); } else if (c == '/') { push(a/b); } else { error("Invalid input\n"); exit(1); } } } if (stack_size != 1) { error("Invalid input\n"); exit(1); } print_answer(stack[0]); exit(0); } 

Maintenant que nous avons toutes les fonctions nécessaires pour l'implémenter, commençons.

 _start: ;  _start   ,    . ;   esp    argc ( ),  ; esp+4   argv. , esp+4    ; , esp+8 -       mov esi, [esp+8] ; esi = "input" = argv[0] ;  _strlen      push esi call _strlen mov ebx, eax ; ebx = input_length add esp, 4 ; end _strlen call mov ecx, 0 ; ecx = "i" _main_loop_start: cmp ecx, ebx ;  (i >= input_length) jge _main_loop_end mov edx, 0 mov dl, [esi + ecx] ;          ; edx.   edx . ; edx =  c = input[i] cmp edx, '0' jl _check_operator cmp edx, '9' jg _print_error sub edx, '0' mov eax, edx ; eax =  c - '0' (,  ) jmp _push_eax_and_continue _check_operator: ;   _pop    b  edi, a  b -  eax push ecx push ebx call _pop mov edi, eax ; edi = b call _pop ; eax = a pop ebx pop ecx ; end call _pop cmp edx, '+' jne _subtract add eax, edi ; eax = a+b jmp _push_eax_and_continue _subtract: cmp edx, '-' jne _multiply sub eax, edi ; eax = ab jmp _push_eax_and_continue _multiply: cmp edx, '*' jne _divide imul eax, edi ; eax = a*b jmp _push_eax_and_continue _divide: cmp edx, '/' jne _print_error push edx ;  edx,      idiv mov edx, 0 idiv edi ; eax = a/b pop edx ;   eax     _push_eax_and_continue: ;  _push push eax push ecx push edx push eax ;   call _push add esp, 4 pop edx pop ecx pop eax ;  call _push inc ecx jmp _main_loop_start _main_loop_end: cmp byte [stack_size], 1 ;  (stack_size != 1),   jne _print_error mov eax, [stack] push eax call _print_answer ; print a final newline push 0xA call _putc ; exit successfully mov eax, 0x01 ; 0x01 = exit() mov ebx, 0 ; 0 =   int 0x80 ;    _print_error: push error_msg call _print_msg mov eax, 0x01 mov ebx, 1 int 0x80 

Vous devrez également ajouter une ligne error_msgà la section .rodata:

 section .rodata ;     error_msg.  db  NASM ;    ,     ; . 0xA =  , 0x0 =    error_msg: db "Invalid input", 0xA, 0x0 

Et nous avons terminé! Surprenez tous vos amis si vous en avez. J'espère que vous allez maintenant réagir plus chaleureusement aux langages de haut niveau, surtout si vous vous souvenez que de nombreux anciens programmes ont été écrits complètement ou presque complètement en assembleur, par exemple, le RollerCoaster Tycoon original!

Tout le code est ici . Merci d'avoir lu! Je peux continuer si cela vous intéresse.

Actions supplémentaires


Vous pouvez vous entraîner en implémentant plusieurs fonctions supplémentaires:

  1. Renvoie un message d'erreur au lieu de segfault si le programme ne reçoit pas d'argument.
  2. Ajoutez la prise en charge des espaces supplémentaires entre les opérandes et les opérateurs dans l'entrée.
  3. Ajoutez la prise en charge des opérandes multi-bits.
  4. Autorisez les nombres négatifs.
  5. _strlen C , _print_answer printf .

Matériel supplémentaire


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


All Articles