webentwicklung-frage-antwort-db.com.de

Wie drucke ich eine Ganzzahl in der Assembly-Level-Programmierung ohne printf aus der c-Bibliothek?

Kann mir jemand den reinen Assembly - Code für die Anzeige des Wertes in einem Register im Dezimalformat mitteilen? Bitte schlagen Sie nicht vor, den printf-Hack zu verwenden und dann mit gcc zu kompilieren.

Beschreibung:

Nun, ich habe einige Nachforschungen und Experimente mit NASM angestellt und mir gedacht, ich könnte die printf-Funktion aus der c-Bibliothek verwenden, um eine Ganzzahl zu drucken. Ich habe dazu die Objektdatei mit dem GCC-Compiler kompiliert und alles funktioniert fair genug.

Was ich jedoch erreichen möchte, ist, den in einem beliebigen Register gespeicherten Wert in Dezimalform auszudrucken.

Ich habe ein bisschen recherchiert und herausgefunden, dass der Interrupt-Vektor 021h für DOS-Kommandozeilen Strings und Zeichen anzeigen kann, während sich entweder 2 oder 9 im ah-Register und die Daten im dx befinden.

Fazit:

Keines der gefundenen Beispiele zeigte, wie der Inhaltswert eines Registers in Dezimalform ohne Verwendung des printf der C-Bibliothek angezeigt werden kann. Weiß jemand, wie man das in der Montage macht?

14

Sie müssen eine Binär-Dezimal-Konvertierungsroutine schreiben und dann die Dezimalstellen verwenden, um "Ziffern" zum Drucken zu erzeugen.

Sie müssen davon ausgehen, dass irgendwo auf Ihrem Ausgabegerät ein Zeichen gedruckt wird. Rufen Sie dieses Unterprogramm "print_character" auf. Angenommen, es wird ein Zeichencode in EAX verwendet und alle Register werden beibehalten. (Wenn Sie keine solche Subroutine haben, haben Sie ein zusätzliches Problem, das die Grundlage für eine andere Frage sein sollte).

Wenn Sie den Binärcode für eine Ziffer (z. B. einen Wert von 0 bis 9) in einem Register (z. B. EAX) haben, können Sie diesen Wert in ein Zeichen für die Ziffer konvertieren, indem Sie den Code ASCII hinzufügen für das "Null" -Zeichen an das Register. Das ist so einfach wie:

       add     eax, 0x30    ; convert digit in EAX to corresponding character digit

Sie können dann print_character aufrufen, um den Zifferncode zu drucken.

Um einen beliebigen Wert auszugeben, müssen Sie Ziffern abholen und drucken.

Das Abholen von Ziffern erfordert grundsätzlich das Arbeiten mit Zehnerpotenzen. Es ist am einfachsten, mit einer Zehnerpotenz, z. B. 10, selbst zu arbeiten. Stellen Sie sich vor, wir haben eine Divide-by-10-Routine, die einen Wert in EAX angenommen hat und einen Quotienten in EDX und einen Rest in EAX erzeugt hat. Ich überlasse es Ihnen als Übung, herauszufinden, wie Sie eine solche Routine implementieren können.

Eine einfache Routine mit der richtigen Idee besteht dann darin, eine Ziffer für alle Ziffern des Werts zu erzeugen. Ein 32-Bit-Register speichert Werte in Höhe von 4 Milliarden, so dass Sie möglicherweise 10 Ziffern drucken. So:

         mov    eax, valuetoprint
         mov    ecx, 10        ;  digit count to produce
loop:    call   dividebyten
         add    eax, 0x30
         call   printcharacter
         mov    eax, edx
         dec    ecx
         jne    loop

Das funktioniert ... aber die Ziffern werden in umgekehrter Reihenfolge gedruckt. Hoppla! Nun, wir können den Pushdown-Stack nutzen, um die produzierten Ziffern zu speichern, und sie in umgekehrter Reihenfolge abnehmen:

         mov    eax, valuetoprint
         mov    ecx, 10        ;  digit count to generate
loop1:   call   dividebyten
         add    eax, 0x30
         Push   eax
         mov    eax, edx
         dec    ecx
         jne    loop1
         mov    ecx, 10        ;  digit count to print
loop2:   pop    eax
         call   printcharacter
         dec    ecx
         jne    loop2

Links als Übung für den Leser: Führende Nullen unterdrücken. Da wir Ziffernzeichen in den Speicher schreiben, könnten wir sie, anstatt sie in den Stapel zu schreiben, in einen Puffer schreiben und dann den Pufferinhalt drucken. Auch als Übung dem Leser überlassen.

12
Ira Baxter

Die meisten Betriebssysteme/Umgebungen haben keinen Systemaufruf, der Ganzzahlen akzeptiert und für Sie in Dezimalzahlen konvertiert. Sie müssen dies selbst tun, bevor Sie die Bytes an das Betriebssystem senden, sie selbst in den Videospeicher kopieren oder die entsprechenden Schriftzeichen in den Videospeicher zeichnen ...

Bei weitem die effizienteste Methode ist es, einen einzigen Systemaufruf durchzuführen, der die gesamte Zeichenfolge auf einmal ausführt, da ein Systemaufruf, der 8 Byte schreibt, im Prinzip die gleichen Kosten verursacht wie das Schreiben von 1 Byte.

Das heißt, wir brauchen einen Puffer, aber das trägt nicht viel zu unserer Komplexität bei. 2 ^ 32-1 ist nur 4294967295, also nur 10 Dezimalstellen. Unser Puffer muss nicht groß sein, also können wir den Stapel einfach verwenden.

Der gewöhnliche Algorithmus erzeugt Ziffern LSD-first. Da die Druckreihenfolge MSD-first ist, können wir einfach am Ende des Puffers beginnen und rückwärts arbeiten. Behalten Sie beim Drucken oder Kopieren an anderer Stelle im Auge, wo es beginnt, und kümmern Sie sich nicht darum, es an den Anfang eines festen Puffers zu bringen. Sie müssen sich nicht mit Push/Pop herumschlagen, um etwas umzukehren. Produzieren Sie es einfach rückwärts.

char *itoa_end(unsigned long val, char *p_end) {
  const unsigned base = 10;
  char *p = p_end;
  do {
    *--p = (val % base) + '0';
    val /= base;
  } while(val);                  // runs at least once to print '0' for val=0.

  // write(1, p,  p_end-p);
  return p;  // let the caller know where the leading digit is
}

gcc/clang leisten hervorragende Arbeit, mit einem Multiplikator für magische Konstante anstelle von div, um effizient durch 10 zu teilen. ( Godbolt Compiler Explorer für die Ausgabe von asm).

Hier ist eine einfache kommentierte NASM-Version davon, die div (langsamer, aber kürzerer Code) für vorzeichenlose 32-Bit-Ganzzahlen und einen Linux-write-Systemaufruf verwendet. Es sollte leicht sein, diesen Code in den 32-Bit-Modus-Code zu portieren, indem Sie einfach die Register in ecx statt in rcx ändern. (Sie sollten auch esi für die üblichen 32-Bit-Aufrufkonventionen speichern/wiederherstellen, sofern Sie dies nicht zu einem Makro oder einer Funktion zum internen Gebrauch machen.)

Der Systemaufrufteil ist spezifisch für 64-Bit-Linux. Ersetzen Sie dies durch das, was für Ihr System geeignet ist, z. Rufen Sie die VDSO-Seite für effiziente Systemaufrufe unter 32-Bit-Linux auf oder verwenden Sie int 0x80 direkt für ineffiziente Systemaufrufe. Siehe Aufrufkonventionen für 32- und 64-Bit-Systemaufrufe unter Unix/Linux .

ALIGN 16
; void print_uint32(uint32_t edi)
; x86-64 System V calling convention.  Clobbers RSI, RCX, RDX, RAX.
global print_uint32
print_uint32:
    mov    eax, edi              ; function arg

    mov    ecx, 0xa              ; base 10
    Push   rcx                   ; newline = 0xa = base
    mov    rsi, rsp
    sub    rsp, 16               ; not needed on 64-bit Linux, the red-zone is big enough.  Change the LEA below if you remove this.

;;; rsi is pointing at '\n' on the stack, with 16B of "allocated" space below that.
.toascii_digit:                ; do {
    xor    edx, edx
    div    ecx                   ; edx=remainder = low digit = 0..9.  eax/=10
                                 ;; DIV IS SLOW.  use a multiplicative inverse if performance is relevant.
    add    edx, '0'
    dec    rsi                 ; store digits in MSD-first printing order, working backwards from the end of the string
    mov    [rsi], dl

    test   eax,eax             ; } while(x);
    jnz  .toascii_digit
;;; rsi points to the first digit


    mov    eax, 1               ; __NR_write from /usr/include/asm/unistd_64.h
    mov    edi, 1               ; fd = STDOUT_FILENO
    lea    edx, [rsp+16 + 1]    ; yes, it's safe to truncate pointers before subtracting to find length.
    sub    edx, esi             ; length, including the \n
    syscall                     ; write(1, string,  digits + 1)

    add  rsp, 24                ; undo the Push and the buffer reservation
    ret

Public Domain Fühlen Sie sich frei, dies zu kopieren oder in das zu kopieren, woran Sie gerade arbeiten. Wenn es kaputt geht, darfst du beide Teile behalten.

Und hier ist der Code, um ihn in einer Schleife aufzurufen, die auf 0 heruntergezählt wird (einschließlich 0). Das Einfügen in dieselbe Datei ist praktisch.

ALIGN 16
global _start
_start:
    mov    ebx, 100
.repeat:
    lea    edi, [rbx + 0]      ; put whatever constant you want here.
    call   print_uint32
    dec    ebx
    jge   .repeat


    xor    edi, edi
    mov    eax, 231
    syscall                             ; sys_exit_group(0)

Montieren und verbinden mit

yasm -felf64 -Worphan-labels -gdwarf2 print-integer.asm &&
ld -o print-integer print-integer.o

./print_integer
100
99
...
1
0

Verwenden Sie strace, um zu sehen, dass das einzige System, das dieses Programm aufruft, write() und exit() ist. (Siehe auch die gdb/debugging-Tipps unten im x86 -Tag-Wiki und die anderen Links dort.)


Ich habe eine AT & T-Syntaxversion für 64-Bit-Ganzzahlen als Antwort auf Drucken einer Ganzzahl als Zeichenfolge mit AT & T-Syntax mit Linux-Systemaufrufen anstelle von printf veröffentlicht. Weitere Informationen zur Leistung sowie ein Benchmarking von div gegenüber vom Compiler generierten Code mit mul finden Sie hier.

2
Peter Cordes

Ich kann dies nicht kommentieren, so dass ich auf diese Weise antworten kann. @ Ira Baxter, perfekte Antwort Ich möchte nur hinzufügen, dass Sie nicht 10 Mal teilen müssen, während Sie gepostet haben, dass Sie register cx auf den Wert 10 setzen Zahl in Axt bis "Axt == 0" teilen

loop1: call dividebyten
       ...
       cmp ax,0
       jnz loop1

Sie müssen auch speichern, wie viele Ziffern in der Originalnummer vorhanden waren.

       mov cx,0
loop1: call dividebyten
       inc cx

Wie auch immer, Ira Baxter hat mir geholfen, es gibt nur ein paar Möglichkeiten, um Code zu optimieren :)

Hierbei geht es nicht nur um die Optimierung, sondern auch um die Formatierung. Wenn Sie die Nummer 54 drucken möchten, möchten Sie den Druck 54 nicht 0000000054 :)

1
Gondil