Ich schreibe ein Tutorial über grundlegende Ein- und Ausgaben in der Baugruppe. Ich verwende eine Linux-Distribution (Ubuntu), die 64-Bit ist. Im ersten Teil meines Tutorials habe ich über die grundlegende Ausgabe gesprochen und ein einfaches Programm wie das folgende erstellt:

global      _start
section     .text
_start:
    mov         rax,1
    mov         rdi,1
    mov         rsi,message
    mov         rdx,13
    syscall
    mov         rax,60
    xor         rdi,rdi
    syscall

section     .data
    message:    db          "Hello, World", 10

Das funktioniert super. Das System druckt die Zeichenfolge und wird sauber beendet. Für den nächsten Teil meines Tutorials möchte ich einfach ein Zeichen von der Tastatur einlesen. Nach meinem Verständnis dieser Website ändern wir das rdi -Register für einen sys_read-Aufruf 0 sein.

Ich subtrahiere zuerst 8 vom aktuellen rsp und lade dann diese Adresse in das rsi -Register. (Dort möchte ich den Saibling aufbewahren). Wenn ich mein Programm kompiliere und ausführe, scheint es zu funktionieren ... aber das Terminal scheint die Eingabe nachzuahmen, die ich erneut eingebe.

Hier ist das Programm:

global      _start            
section     .text
_start:
    sub         rsp,8           ; allocate space on the stack to read
    mov         rdi,0           ; set rdi to 0 to indicate a system read
    mov         rsi,[rsp-8]
    mov         rdx,1
    syscall

    mov         rax,1
    mov         rdi,1
    mov         rsi,message
    mov         rdx,13
    syscall
    mov         rax,60
    xor         rdi,rdi
    syscall

section     .data
    message:    db          "Hello, World", 10

Und genau das passiert in meinem Terminal ...

matthew@matthew-Precision-WorkStation-690:~/Documents/Programming/RockPaperScissors$ nasm -felf64 rps.asm && ld rps.o && ./a.out
5
Hello, World
matthew@matthew-Precision-WorkStation-690:~/Documents/Programming/RockPaperScissors$ 5
5: command not found
matthew@matthew-Precision-WorkStation-690:~/Documents/Programming/RockPaperScissors$

Die Eingabe 5 wird nach dem Beenden des Programms zum Terminal zurück wiederholt. Was ist der richtige Weg, um ein einzelnes Zeichen mit NASM und Linux x64 einzulesen?

2
Matthew 19 Apr. 2018 im 19:20

3 Antworten

Beste Antwort

In Ihrem ersten Codeabschnitt müssen Sie SYS_CALL für SYS_READ auf 0 setzen (wie in der anderen Antwort rudimentär erwähnt).

Überprüfen Sie daher eine Linux x64 SYS_CALL-Liste auf die entsprechenden Parameter und versuchen Sie es

_start:
  mov         rax, 0          ; set SYS_READ as SYS_CALL value
  sub         rsp, 8          ; allocate 8-byte space on the stack as read buffer
  mov         rdi, 0          ; set rdi to 0 to indicate a STDIN file descriptor
  lea         rsi, [rsp]      ; set const char *buf to the 8-byte space on stack
  mov         rdx, 1          ; set size_t count to 1 for one char
  syscall
4
zx485 19 Apr. 2018 im 19:12

es scheint zu funktionieren ... aber das Terminal scheint die Eingabe nachzuahmen, die ich erneut eingebe.

Nein, die 5 + Newline, die bash liest, ist die, die Sie eingegeben haben. Ihr Programm hat auf die Eingabe gewartet, die Eingabe jedoch nicht gelesen und sie im Terminal-Eingabepuffer des Kernels belassen, damit bash nach dem Beenden Ihres Programms gelesen werden kann. (Und bash gibt die Terminal-Eingabe selbst wieder, da das Terminal vor dem Lesen in den No-Echo-Modus versetzt wird. Der normale Mechanismus, mit dem Zeichen während der Eingabe in der Befehlszeile angezeigt werden, besteht darin, dass bash das Gelesene druckt. )

Wie hat es Ihr Programm geschafft, auf Eingaben zu warten, ohne diese zu lesen? mov rsi, [rsp-8] lädt 8 Bytes von dieser Adresse. Sie sollten lea verwendet haben, um rsi so einzustellen, dass es auf diesen Speicherort verweist, anstatt zu laden, was sich in diesem Puffer befand. read schlägt also mit -EFAULT fehl, anstatt etwas zu lesen, aber interessanterweise wird dies erst überprüft, nachdem darauf gewartet wurde, dass eine Terminaleingabe erfolgt.

Ich habe strace ./foo verwendet, um Systemaufrufe Ihres Programms zu verfolgen:

execve("./foo", ["./foo"], 0x7ffe90b8e850 /* 51 vars */) = 0
read(0, 5
NULL, 1)                        = -1 EFAULT (Bad address)
write(1, "Hello, World\n", 13Hello, World
)          = 13
exit(0)                                 = ?
+++ exited with 0 +++

Der normale Eingang / Ausgang des Terminals wird mit dem Strace-Ausgang gemischt. Ich hätte -o foo.trace oder was auch immer verwenden können. Die bereinigte Version des read Systemaufruf-Trace (ohne das eingemischte 5\n) lautet:

read(0, NULL, 1)                        = -1 EFAULT (Bad address)

Daher wurde (wie für _start in einer statischen ausführbaren Datei unter Linux erwartet) der Speicher unter RSP auf Null gesetzt. Aber alles, was kein Zeiger auf beschreibbaren Speicher ist, hätte das gleiche Ergebnis erzielt.


Die Antwort von zx485 ist korrekt, aber ineffizient (große Codegröße und eine zusätzliche Anweisung). Sie müssen sich nicht sofort um die Effizienz kümmern, aber es ist einer der Hauptgründe, etwas mit asm zu tun, und es gibt interessante Dinge zu diesem Fall zu sagen.

Sie müssen RSP nicht ändern. Sie können die verwenden (Speicher unter RSP), da Sie keine Funktionsaufrufe durchführen müssen. Ich denke, das haben Sie mit rsp-8 versucht. (Oder Sie haben nicht bemerkt, dass es nur aufgrund besonderer Umstände sicher ist ...)

Sie müssen RSP nicht ändern. Sie können die verwenden (Speicher unter RSP), da Sie keine Funktionsaufrufe durchführen müssen. Ich denke, das haben Sie mit read versucht. (Oder Sie haben nicht bemerkt, dass es nur aufgrund besonderer Umstände sicher ist ...)

   ssize_t read(int fd, void *buf, size_t count);

fd ist also ein ganzzahliges Argument, also wird nur edi betrachtet, nicht rdi. Sie müssen nicht das vollständige rdi schreiben, sondern nur das reguläre 32-Bit edi. (32-Bit-Operandengröße ist unter x86-64 normalerweise die effizienteste Sache).

Aber für Null oder positive ganze Zahlen setzt das Setzen von edi ohnehin auch rdi. (Alles, was Sie an edi schreiben, wird auf das volle rdi ausgedehnt. Und natürlich das Nullstellen eines Registers erfolgt am besten mit xor same,same ; Dies ist wahrscheinlich der bekannteste x86-Gucklochoptimierungstrick.

Wie das OP später kommentierte, lässt das Lesen von nur 1 Byte die neue Zeile ungelesen, wenn die Eingabe 5\n ist, und dies würde Bash dazu bringen, sie zu lesen und eine zusätzliche Eingabeaufforderung zu drucken. Wir können die Größe des gelesenen und den Speicherplatz für den Puffer auf 2 Bytes erhöhen. (Es wäre kein Nachteil, lea rsi, [rsp-8] zu verwenden und eine Lücke zu hinterlassen. Ich verwende lea rsi, [rsp-2], um den Puffer direkt unter argc auf dem Stapel oder unter dem Rückgabewert zu packen, wenn dies der Fall ist war eine Funktion anstelle eines Prozesseinstiegspunkts. Meistens, um genau zu zeigen, wie viel Platz benötigt wird.)

 ; One read of up to 2 characters
 ; giving the user room to type a digit + newline
_start:
  ;mov      eax, 0          ; set SYS_READ as SYS_CALL value
  xor      eax, eax        ; rax = __NR_read = 0  from unistd_64.h
  lea      rsi, [rsp-2]    ; rsi = buf = rsp-2
  xor      edi, edi        ; edi = fd = 0 (stdin)
  mov      edx, 2          ; rdx = count = 2 char
  syscall                     ; sys_read(0, rsp-2, 2)
 ; total = 16 bytes

Dies setzt sich wie folgt zusammen:

+ yasm -felf64 -Worphan-labels -gdwarf2 foo.asm
+ ld -o foo foo.o
ld: warning: cannot find entry symbol _start; defaulting to 0000000000400080

$ objdump -drwC -Mintel    
0000000000400080 <_start>:
  400080:       31 c0                   xor    eax,eax
  400082:       48 8d 74 24 ff          lea    rsi,[rsp-0x1]
  400087:       31 ff                   xor    edi,edi
  400089:       ba 01 00 00 00          mov    edx,0x1
  40008e:       0f 05                   syscall 
  ; next address = ...90

 ; I left out the rest of the program so you can't actually *run* foo
 ; but I used a script that assembles + links, and disassembles the result
 ; The linking step is irrelevant for just looking at the code here.

Im Vergleich dazu besteht die Antwort von zx485 aus 31 Bytes. Die Codegröße ist nicht das Wichtigste, aber wenn alles andere gleich ist, ist kleiner für die L1i-Cache-Dichte besser und dekodiert manchmal die Effizienz . (Und meine Version enthält auch weniger Anweisungen.)

0000000000400080 <_start>:
  400080:       48 c7 c0 00 00 00 00    mov    rax,0x0
  400087:       48 83 ec 08             sub    rsp,0x8
  40008b:       48 c7 c7 00 00 00 00    mov    rdi,0x0
  400092:       48 8d 34 24             lea    rsi,[rsp]
  400096:       48 c7 c2 01 00 00 00    mov    rdx,0x1
  40009d:       0f 05                   syscall 
  ; total = 31 bytes

Beachten Sie, wie diese mov reg,constant Anweisungen die 7-Byte-mov r64, sign_extended_imm32 -Codierung verwenden. (NASM optimiert diese auf 5 Byte mov r32, imm32 für insgesamt 25 Byte, kann jedoch mov nicht auf xor optimieren, da xor Flags beeinflusst. Sie müssen dies tun diese Optimierung selbst.)

Wenn Sie RSP ändern möchten, um Speicherplatz zu reservieren, benötigen Sie nur mov rsi, rsp und nicht lea. Verwenden Sie lea reg1, [rsp] (ohne Verschiebung) nur, wenn Sie Füllen Sie Ihren Code mit längeren Anweisungen, anstatt ein NOP für die Ausrichtung zu verwenden. Bei anderen Quellregistern als rsp oder rbp ist lea nicht länger, aber immer noch langsamer als mov. (Aber verwenden Sie auf jeden Fall lea zum Kopieren und Hinzufügen. Ich sage nur, es ist sinnlos, wenn Sie es durch ein mov ersetzen können.)

Sie können noch mehr Platz sparen, indem Sie lea edx, [rax+1] anstelle von mov edx,1 im Wesentlichen ohne Leistungskosten, aber das tun Compiler normalerweise nicht. (Obwohl sie es vielleicht sollten.)

4
Peter Cordes 26 Apr. 2018 im 16:34

Sie müssen eax zum Lesen auf die Systemrufnummer setzen.

0
prl 19 Apr. 2018 im 16:27