X86 Assembler-Handbuch für Anfänger

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*" # "(3+2)*6"    30 

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.

eaxebxecxedx
Systemrufnummerarg1arg2arg3


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:

  1. 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.
  2. 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.
  3. Rufen Sie das Unterprogramm auf.

Wenn möglich, speichert die Funktion das Ergebnis in eax . Unmittelbar nach dem call Anrufer:

  1. 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.
  2. 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:

  1. Speichern Sie den ebp Basisregisterzeiger des vorherigen Frames, indem Sie ihn in den Stapel schreiben.
  2. ebp vom vorherigen Frame an den aktuellen an (aktueller esp Wert).
  3. 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.
  4. 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:

  1. Stellen Sie gespeicherte Register wieder her, indem Sie sie in umgekehrter Reihenfolge vom Stapel entfernen.
  2. 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
  3. ebp den ebp des vorherigen Frames wieder her, indem Sie ihn vom Stapel ebp .
  4. 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) { //   length++; s++; } //   return length; } 

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:

SchrittSymbolVorher stapelnStapel nach
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]

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]; // , 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); } 

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_msgdem 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:

  1. Geben Sie eine Fehlermeldung anstelle von segfault zurück, wenn das Programm kein Argument erhält.
  2. Unterstützung für zusätzliche Leerzeichen zwischen Operanden und Operatoren in der Eingabe hinzufügen.
  3. Unterstützung für Multi-Bit-Operanden hinzufügen.
  4. Negative Zahlen zulassen.
  5. Durch _strleneine Funktion aus der Standard-C-Bibliothek_print_answer ersetzen und durch einen Aufruf ersetzen printf.

Zusätzliche Materialien


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


All Articles