Heutzutage ist es selten notwendig, in einem reinen Assembler zu schreiben, aber ich empfehle dies definitiv jedem, der sich für Programmierung interessiert. Sie werden die Dinge aus einem anderen Blickwinkel sehen und Fähigkeiten werden nützlich sein, wenn Sie Code in anderen Sprachen debuggen.
In diesem Artikel werden wir einen
RPN- Rechner
(Reverse Polish Notation) in einem reinen x86-Assembler von Grund auf neu schreiben. Wenn wir fertig sind, können wir es so verwenden:
$ ./calc "32+6*"
Der gesamte Code für den Artikel ist
hier . Es ist reichlich auskommentiert und kann als Lehrmaterial für diejenigen dienen, die Assembler bereits kennen.
Beginnen wir mit dem Schreiben des grundlegenden
Hello World-Programms! um die Umgebungseinstellungen zu überprüfen. Fahren wir dann mit Systemaufrufen, dem Aufrufstapel, den Stapelrahmen und der x86-Aufrufkonvention fort. Zum Üben werden wir dann einige grundlegende Funktionen in x86 Assembler schreiben - und beginnen, einen RPN-Rechner zu schreiben.
Es wird davon ausgegangen, dass der Leser über Programmiererfahrung in C und Grundkenntnisse der Computerarchitektur verfügt (z. B. ein Prozessorregister). Da wir Linux verwenden, sollten Sie auch die Linux-Befehlszeile verwenden können.
Umgebungseinstellung
Wie bereits erwähnt, verwenden wir Linux (64-Bit oder 32-Bit). Der obige Code funktioniert nicht unter Windows oder Mac OS X.
Für die Installation benötigen Sie nur den GNU
ld
Linker von
binutils
, der auf den meisten Distributionen vorinstalliert ist, und den NASM-Assembler. Unter Ubuntu und Debian können Sie beide mit einem Befehl installieren:
$ sudo apt-get install binutils nasm
Ich würde auch empfehlen,
eine ASCII-Tabelle griffbereit zu halten.
Hallo Welt!
Speichern Sie den folgenden Code in der Datei
calc.asm
um die Umgebung zu überprüfen:
; _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
Kommentare erläutern die allgemeine Struktur. Eine Liste der Register und allgemeinen Anweisungen finden Sie im
x86 Assembler-Handbuch der
University of Virginia . Bei einer weiteren Diskussion der Systemaufrufe wird dies umso notwendiger.
Die folgenden Befehle sammeln die Assembler-Datei in einer Objektdatei und kompilieren dann die ausführbare Datei:
$ nasm -f elf_i386 calc.asm -o calc $ ld -m elf_i386 calc.o -o calc
Nach dem Start sollten Sie sehen:
$ ./calc Hello world!
Makefile
Dies ist ein optionaler Teil, aber Sie können ein
Makefile
erstellen, um die Erstellung und das Layout in Zukunft zu vereinfachen. Speichern Sie es im selben Verzeichnis wie
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
Führen Sie dann anstelle der obigen Anweisungen einfach make aus.
Systemaufrufe
Linux-Systemaufrufe weisen das Betriebssystem an, etwas für uns zu tun. In diesem Artikel verwenden wir nur zwei Systemaufrufe:
write()
, um eine Zeile in eine Datei oder einen Stream zu schreiben (in unserem Fall ist dies ein Standardausgabegerät und ein Standardfehler) und
exit()
, um das Programm zu beenden:
syscall 0x01: exit(int error_code) error_code - 0 ( 1) syscall 0x04: write(int fd, char *string, int length) fd — 1 , 2 string — length —
Systemaufrufe werden konfiguriert, indem die Systemaufrufnummer im
eax
Register und dann die Argumente in
ebx
,
ecx
,
edx
in dieser Reihenfolge
edx
werden. Möglicherweise stellen Sie fest, dass
exit()
nur ein Argument hat - in diesem Fall spielen ecx und edx keine Rolle.
eax | ebx | ecx | edx |
---|
Systemrufnummer | arg1 | arg2 | arg3 |
Stapel aufrufen

Ein Aufrufstapel ist eine Datenstruktur, in der Informationen zu jedem Aufruf einer Funktion gespeichert werden. Jeder Aufruf hat einen eigenen Abschnitt im Stapel - den "Frame". Es speichert einige Informationen über den aktuellen Aufruf: die lokalen Variablen dieser Funktion und die Rücksprungadresse (wohin das Programm gehen soll, nachdem die Funktion ausgeführt wurde).
Sofort stelle ich eine nicht offensichtliche Sache fest: Der Stapel vergrößert
den Speicher. Wenn Sie etwas oben im Stapel hinzufügen, wird es an einer Speicheradresse eingefügt, die niedriger als die des vorherigen Elements ist. Mit anderen Worten, wenn der Stapel wächst, nimmt die Speicheradresse am oberen Rand des Stapels ab. Um Verwirrung zu vermeiden, werde ich Sie immer daran erinnern.
Die
push
Anweisung
push
etwas oben auf dem Stapel ab und die Daten von dort werden eingeblendet. Wenn Sie beispielsweise
push
wird ein Platz oben im Stapel zugewiesen und der Wert aus dem
eax
Register dort platziert.
pop
überträgt alle Daten vom oberen
eax
des Stapels an
eax
und gibt diesen Speicherbereich frei.
Der Zweck des
esp
Registers besteht darin, auf die Oberseite des Stapels zu zeigen. Alle Daten über
esp
als nicht auf dem Stapel, dies sind Mülldaten. Das Ausführen einer
push
(oder
pop
) Anweisung verschiebt sich
esp
. Sie können
esp
direkt manipulieren, wenn Sie Ihren Aktionen einen Bericht geben.
Das
ebp
Register ähnelt
esp
, nur zeigt es immer ungefähr in die Mitte des aktuellen Stapelrahmens, unmittelbar vor den lokalen Variablen der aktuellen Funktion (wir werden später darauf
ebp
). Das Aufrufen einer anderen Funktion verschiebt
ebp
jedoch nicht automatisch, sondern muss jedes Mal manuell erfolgen.
Aufrufkonvention für X86-Architektur
In x86 gibt es kein integriertes Funktionskonzept wie in Hochsprachen. Die
call
goto
Grunde nur
jmp
(
goto
) an eine andere Speicheradresse. Um Routinen als Funktionen in anderen Sprachen zu verwenden (die Argumente aufnehmen und Daten zurückgeben können), müssen Sie die aufrufende Konvention befolgen (es gibt viele Konventionen, aber wir verwenden CDECL, die beliebteste Konvention für x86 unter C-Compilern und Assembler-Programmierern). Es stellt auch sicher, dass Routine-Register beim Aufrufen einer anderen Funktion nicht verwechselt werden.
Anruferregeln
Vor dem Aufruf der Funktion muss der Aufrufer:
- Speichern Sie die Register, die der Aufrufer auf dem Stapel speichern muss. Die aufgerufene Funktion kann einige Register ändern: Um keine Daten zu verlieren, muss der Aufrufer diese im Speicher speichern, bis sie auf den Stapel verschoben werden. Dies sind die
edx
eax
, ecx
und edx
. Wenn Sie keine davon verwenden, können Sie sie nicht speichern. - Schreiben Sie Funktionsargumente in umgekehrter Reihenfolge in den Stapel (erstes letztes Argument, erstes erstes Argument am Ende). Diese Reihenfolge stellt sicher, dass die aufgerufene Funktion ihre Argumente vom Stapel in der richtigen Reihenfolge empfängt.
- Rufen Sie das Unterprogramm auf.
Wenn möglich, speichert die Funktion das Ergebnis in
eax
. Unmittelbar nach dem
call
Anrufer:
- Entfernen Sie Funktionsargumente vom Stapel. Dies geschieht normalerweise durch einfaches Hinzufügen der Anzahl der Bytes zu
esp
. Vergessen Sie nicht, dass der Stapel kleiner wird. Um ihn aus dem Stapel zu entfernen, müssen Sie Bytes hinzufügen. - Stellen Sie gespeicherte Register wieder her, indem Sie sie in umgekehrter Reihenfolge vom Stapel entfernen. Die aufgerufene Funktion ändert keine anderen Register.
Das folgende Beispiel zeigt, wie diese Regeln gelten. Angenommen, die Funktion
_subtract
verwendet zwei ganzzahlige Argumente (4 Byte) und gibt das erste Argument minus das zweite zurück.
_mysubroutine
Unterroutine
_subtract
mit den Argumenten
10
und
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 ; ...
Regeln der aufgerufenen Routine
Vor dem Aufruf muss das Unterprogramm:
- Speichern Sie den
ebp
Basisregisterzeiger des vorherigen Frames, indem Sie ihn in den Stapel schreiben. ebp
vom vorherigen Frame an den aktuellen an (aktueller esp
Wert).- Weisen Sie mehr Platz auf dem Stapel für lokale Variablen zu. Bewegen Sie gegebenenfalls den
esp
Zeiger. Wenn der Stapel kleiner wird, müssen Sie den fehlenden Speicher von esp
subtrahieren. - Speichern Sie die Register der aufgerufenen Routine auf dem Stapel. Dies sind
ebx
, edi
und esi
. Es ist nicht erforderlich, Register zu speichern, deren Änderung nicht geplant ist.
Aufrufstapel nach Schritt 1:

Der Aufrufstapel nach Schritt 2:

Aufrufstapel nach Schritt 4:

In diesen Diagrammen ist in jedem Stapelrahmen eine Rücksprungadresse angegeben. Es wird automatisch von einer Aufrufanweisung auf den Stapel geschoben. Die
ret
ruft die Adresse vom oberen Rand des Stapels ab und springt dorthin. Wir brauchen diese Anweisung nicht, ich habe nur gezeigt, warum die lokalen Variablen der Funktion 4 Bytes über
ebp
, aber die Argumente der Funktion sind 8 Bytes unter
ebp
.
Im letzten Diagramm können Sie auch feststellen, dass die lokalen Variablen der Funktion immer 4 Bytes über
ebp
von der
ebp-4
Adresse beginnen (Subtraktion hier, weil wir den Stapel nach oben verschieben) und die Argumente der Funktion immer 8 Bytes unter
ebp
von der
ebp+8
(zusätzlich, weil wir uns den Stapel hinunter bewegen). Wenn Sie die Regeln dieser Konvention befolgen, gilt dies auch für die Variablen und Argumente einer Funktion.
Wenn die Funktion abgeschlossen ist und Sie zurückkehren möchten, müssen Sie bei Bedarf zuerst
eax
auf den Rückgabewert der Funktion setzen. Darüber hinaus benötigen Sie:
- Stellen Sie gespeicherte Register wieder her, indem Sie sie in umgekehrter Reihenfolge vom Stapel entfernen.
- Geben Sie bei Bedarf Speicherplatz auf dem Stapel frei, der von der lokalen Variablen in Schritt 3 zugewiesen wurde: Installieren Sie einfach
esp
in ebp ebp
den ebp
des vorherigen Frames wieder her, indem Sie ihn vom Stapel ebp
.- Rückkehr mit
ret
Jetzt implementieren wir die
_subtract
Funktion aus unserem Beispiel:
_subtract: push ebp ; mov ebp, esp ; ebp ; , ; , ; ; mov eax, [ebp+8] ; eax. ; ebp+8 sub eax, [ebp+12] ; ebp+12 ; ; , eax ; , ; , pop ebp ; ret
Ein- und Ausstieg
Im obigen Beispiel können Sie feststellen, dass die Funktion immer auf die gleiche Weise ausgeführt wird:
push ebp
,
mov ebp
,
esp
und Speicherzuordnung für lokale Variablen. Der x86-Satz verfügt über eine praktische Anweisung, die all dies erledigt:
enter ab
, wobei
a
die Anzahl der Bytes ist, die Sie für lokale Variablen zuweisen möchten,
b
die "Verschachtelungsebene" ist, die wir immer auf
0
. Außerdem endet die Funktion immer mit den Anweisungen
pop ebp
und
mov esp
,
ebp
(obwohl sie nur erforderlich sind, wenn Speicher für lokale Variablen
ebp
wird, aber auf keinen Fall Schaden anrichten). Dies kann auch durch eine einzige Aussage ersetzt werden:
leave
. Wir nehmen Änderungen vor:
_subtract: enter 0, 0 ; ebp ; , ; ; mov eax, [ebp+8] ; eax. ; ebp+8 sub eax, [ebp+12] ; ebp+12 ; ; , eax ; , leave ; ret
Einige grundlegende Funktionen schreiben
Nachdem Sie die Aufrufkonvention beherrschen, können Sie mit dem Schreiben einiger Routinen beginnen. Verallgemeinern Sie den Code, der "Hallo Welt!"
_print_msg
. So geben Sie Zeilen aus: die Funktion
_print_msg
.
Hier benötigen wir eine weitere
_strlen
Funktion, um die Länge des Strings zu zählen. In C könnte es so aussehen:
size_t strlen(char *s) { size_t length = 0; while (*s != 0) {
Mit anderen Worten, vom Anfang der Zeile an addieren wir 1 zum Rückgabewert für jedes Zeichen außer Null. Sobald das Nullzeichen bemerkt wird, geben wir den in der Schleife akkumulierten Wert zurück. In Assembler ist dies ebenfalls recht einfach: Sie können die zuvor geschriebene
_subtract
Funktion als Basis verwenden:
_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
Schon nicht schlecht, oder? Das erste Schreiben von C-Code kann hilfreich sein, da das meiste davon direkt in Assembler konvertiert wird. Jetzt können Sie diese Funktion in
_print_msg
, wo wir alle gewonnenen Erkenntnisse anwenden:
_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
Und sehen Sie die Früchte unserer harten Arbeit, indem Sie diese Funktion im vollständigen Programm „Hallo Welt!“ Verwenden.
_start: enter 0, 0 ; ( ) push hello_world ; _print_msg call _print_msg mov eax, 0x01 ; 0x01 = exit() mov ebx, 0 ; 0 = int 0x80
Ob Sie es glauben oder nicht, wir haben alle Hauptthemen behandelt, die zum Schreiben grundlegender x86-Assembler-Programme erforderlich sind! Jetzt haben wir das gesamte Einführungsmaterial und die Theorie, sodass wir uns vollständig auf den Code konzentrieren und das erworbene Wissen anwenden, um unseren RPN-Rechner zu schreiben. Die Funktionen sind viel länger und verwenden sogar einige lokale Variablen. Wenn Sie das fertige Programm sofort sehen möchten,
finden Sie es hier .
Für diejenigen unter Ihnen, die mit der umgekehrten polnischen Notation (manchmal auch als umgekehrte polnische Notation oder Postfix-Notation bezeichnet) nicht vertraut sind, werden hier Ausdrücke mithilfe des Stapels ausgewertet. Daher müssen Sie einen Stapel sowie die
_push
_pop
und
_push
, um diesen Stapel zu
_push
. Sie
_print_answer
Funktion
_print_answer
, die am Ende der Berechnung eine Zeichenfolgendarstellung des numerischen Ergebnisses ausgibt.
Stapelerstellung
Zunächst definieren wir den Speicherplatz für unseren Stack sowie die globale Variable
stack_size
. Es ist ratsam, diese Variablen so zu ändern, dass sie nicht in den Abschnitt
.rodata
, sondern in
.data
.
section .data stack_size: dd 0 ; dword (4 ) 0 stack: times 256 dd 0 ;
Jetzt können Sie die
_pop
_push
und
_pop
implementieren:
_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
Zahlenausgabe
_print_answer
viel komplizierter: Sie müssen Zahlen in Zeichenfolgen konvertieren und mehrere andere Funktionen verwenden. Sie
_putc
Funktion
_putc
, die ein Zeichen ausgibt, die Funktion
mod
, um den Rest der Division (Modul) der beiden Argumente zu
_pow_10
, und
_pow_10
, um die Potenz von 10 zu erhöhen. Später werden Sie verstehen, warum sie benötigt werden. Das ist ziemlich einfach, hier ist der 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
Wie leiten wir also einzelne Zahlen in einer Zahl ab? Beachten Sie zunächst, dass die letzte Ziffer der Zahl der Rest der Division durch 10 ist (z. B.
123 % 10 = 3
), und die nächste Ziffer der Rest der Division durch 100, geteilt durch 10 (z. B.
(123 % 100)/10 = 2
). Im Allgemeinen können Sie eine bestimmte Ziffer einer Zahl (von rechts nach links) finden, indem Sie
( % 10**n) / 10**(n-1)
suchen, wobei die Anzahl der Einheiten
n = 1
, die Anzahl der Zehner
n = 2
und so weiter.
Mit diesem Wissen können Sie alle Ziffern einer Zahl von
n = 1
bis
n = 10
(dies ist die maximale Anzahl von Bits in einer vorzeichenbehafteten 4-Byte-Ganzzahl). Es ist jedoch viel einfacher, von links nach rechts zu wechseln. So können wir jedes Zeichen drucken, sobald wir es finden, und die Nullen auf der linken Seite entfernen. Daher sortieren wir die Zahlen von
n = 10
bis
n = 1
.
In C sieht das Programm ungefähr so aus:
#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'); } }
Jetzt verstehen Sie, warum wir diese drei Funktionen benötigen. Lassen Sie uns dies in Assembler implementieren: %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
Es war ein schwieriger Test! Hoffe, die Kommentare helfen, es zu klären. Wenn Sie jetzt denken: "Warum können Sie nicht einfach schreiben printf("%d")
?", Dann wird Ihnen das Ende des Artikels gefallen, in dem wir die Funktion durch genau das ersetzen werden!Jetzt haben wir alle notwendigen Funktionen, es bleibt die Grundlogik zu implementieren _start
- und das ist alles!Reverse polnische Notationsberechnung
Wie bereits erwähnt, wird die umgekehrte polnische Notation mithilfe des Stapels berechnet. Beim Lesen wird die Zahl auf den Stapel geschoben, und beim Lesen wird der Operator auf zwei Objekte oben im Stapel angewendet.Wenn wir beispielsweise berechnen möchten 84/3+6*
(dieser Ausdruck kann auch in der Form geschrieben werden 6384/+*
), ist der Prozess wie folgt:Schritt | Symbol | Vorher stapeln | Stapel nach |
---|
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] |
Wenn die Eingabe ein gültiger Postfix-Ausdruck ist, befindet sich am Ende der Berechnungen nur noch ein Element auf dem Stapel - dies ist die Antwort, das Ergebnis der Berechnungen. In unserem Fall ist die Zahl 30.In Assembler müssen Sie so etwas wie diesen Code in C implementieren: int stack[256];
Jetzt haben wir alle Funktionen, um dies zu implementieren. Beginnen wir. _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
Sie müssen error_msg
dem Abschnitt auch eine Zeile hinzufügen .rodata
: section .rodata ; error_msg. db NASM ; , ; . 0xA = , 0x0 = error_msg: db "Invalid input", 0xA, 0x0
Und wir sind fertig! Überraschen Sie alle Ihre Freunde, wenn Sie sie haben. Ich hoffe, dass Sie jetzt besser auf Hochsprachen reagieren, besonders wenn Sie sich daran erinnern, dass viele alte Programme vollständig oder fast vollständig in Assembler geschrieben wurden, zum Beispiel der ursprüngliche RollerCoaster Tycoon!Der gesamte Code ist hier . Danke fürs Lesen! Ich kann fortfahren, wenn Sie interessiert sind.Weitere Aktionen
Sie können üben, indem Sie mehrere zusätzliche Funktionen implementieren:- Geben Sie eine Fehlermeldung anstelle von segfault zurück, wenn das Programm kein Argument erhält.
- Unterstützung für zusätzliche Leerzeichen zwischen Operanden und Operatoren in der Eingabe hinzufügen.
- Unterstützung für Multi-Bit-Operanden hinzufügen.
- Negative Zahlen zulassen.
- Durch
_strlen
eine Funktion aus der Standard-C-Bibliothek_print_answer
ersetzen und durch einen Aufruf ersetzen printf
.
Zusätzliche Materialien