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*"
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.
eax | ebx | ecx | edx |
---|
Numéro d'appel système | arg1 | arg2 | arg3 |
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:
- 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. - É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.
- Appelez le sous-programme.
Si possible, la fonction enregistrera le résultat dans
eax
. Immédiatement après l'
call
appelant doit:
- 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. - 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:
- Enregistrez le pointeur de registre de base
ebp
de la trame précédente en l'écrivant dans la pile. - Ajustez
ebp
de l'image précédente au courant (valeur esp
actuelle). - 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
. - 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:
- Restaurez les registres enregistrés en les sortant de la pile dans l'ordre inverse.
- 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 - Restaurez le pointeur de base
ebp
de l'image précédente en le ebp
de la pile. - 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) {
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:Étape | Symbole | Empiler avant | Pile après |
---|
1 | 8 | [] | [8] |
2 | 4 | [8] | [8, 4] |
3 | / | [8, 4] | [2] |
4 | 3 | [2] | [2, 3] |
5 | + | [2, 3] | [5] |
6 | 6 | [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];
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:- Renvoie un message d'erreur au lieu de segfault si le programme ne reçoit pas d'argument.
- Ajoutez la prise en charge des espaces supplémentaires entre les opérandes et les opérateurs dans l'entrée.
- Ajoutez la prise en charge des opérandes multi-bits.
- Autorisez les nombres négatifs.
_strlen
C , _print_answer
printf
.
Matériel supplémentaire