Assembler-Kurs · Lektion 2
Hello, World! mit printf und Formaten
Am Ende kannst du Text ausgeben, einer Funktion Argumente übergeben und Zahlen mit printf als Dezimalzahl, Hexzahl und Zeichen schreiben. Voraussetzung: Lektion 1.
Die Idee: eine fertige Funktion benutzen
Ein Programm, das etwas auf den Bildschirm schreibt, müsste selbst mit dem Betriebssystem sprechen. Einfacher ist es, eine fertige Funktion zu benutzen. Wir nehmen printf aus der C-Laufzeitbibliothek msvcrt.dll, die auf jedem Windows vorhanden ist. Mit .import msvcrt.dll printf sagst du dem Assembler, dass dein Programm diese Funktion braucht. Er legt eine Importtabelle an, in der Windows beim Start die Adresse von printf einträgt. Aufgerufen wird die Funktion über diesen Eintrag.
Zwei Dinge brauchst du dafür: den Text im Speicher und die Regeln, wie man einer Funktion Argumente gibt.
Text im Speicher
Im Abschnitt .data legst du Daten ab. msg db 'Hello, World!', 10, 0 heißt: db (define byte) legt Bytes hintereinander in den Speicher; der Text in Hochkommas wird zu seinen Zeichencodes (ein Byte pro Zeichen, H ist 72). 10 ist der Zeilenumbruch, 0 markiert das Ende des Textes – printf liest, bis es eine 0 findet. Das Label msg steht für die Adresse, an der der Text beginnt. An printf gibst du also keinen Text, sondern die Adresse, wo er steht.
Wohin mit den Argumenten?
Die Regel, welches Argument wo übergeben wird, heißt Aufrufkonvention. Sie gilt für jede Funktion, die du aufrufst, nicht nur für printf.
Auf dem i386 (cdecl) kommen die Argumente auf den Stack, das letzte zuerst: push legt einen Wert oben auf den Stack. Nach dem Aufruf muss der Aufrufer sie selbst wieder entfernen (add esp, 4 für ein Argument à 4 Byte). Das Ergebnis der Funktion steht in eax.
Auf dem x64 (Windows) kommen die ersten vier Argumente in rcx, rdx, r8 und r9, das erste in rcx. Weitere kämen auf den Stack. Zusätzlich muss der Aufrufer 32 Byte Platz auf dem Stack lassen (Shadow-Space) und den Stack auf 16 Byte ausgerichtet halten – deshalb das sub rsp, 40 (32 + 8: beim Start des Programms liegt schon die 8 Byte große Rücksprungadresse auf dem Stack, die 8 Byte machen ihn wieder durch 16 teilbar). Das Ergebnis steht in rax.
Auf ARM 32 kommen die ersten vier Argumente in r0, r1, r2 und r3, das erste in r0. Der Aufruf blx springt in die Funktion und legt die Rücksprungadresse in lr. Weil der Aufruf lr überschreibt, muss ein Programm, das selbst Funktionen aufruft, lr zuerst auf dem Stack sichern (push {r4, lr}) und am Ende in pc zurückholen (pop {r4, pc}). Das Ergebnis steht in r0. r0 bis r3 darf jede Funktion verändern; r4 bis r11 muss sie unverändert zurückgeben.
Auf ARM 64 kommen die ersten acht Argumente in x0 bis x7, das erste in x0. Der Aufruf blr springt in die Funktion und legt die Rücksprungadresse in x30. Weil der Aufruf x30 überschreibt, muss ein Programm, das selbst Funktionen aufruft, x30 zuerst auf dem Stack sichern (stp x29, x30, [sp, #-16]!) und am Ende zurückholen (ldp). Das Ergebnis steht in x0. x0 bis x17 darf jede Funktion verändern; x19 bis x28 muss sie unverändert zurückgeben.
Hello, World!
; Lesson 2: Hello, World! with printf (i386, cdecl: the arguments go on the stack). .format pe console .arch i386 .import msvcrt.dll printf .code start: push dword msg ; the one argument: the address of the text call [printf] ; through the import table add esp, 4 ; cdecl: the caller takes the argument off the stack xor eax, eax ; exit code 0 ret .data msg db 'Hello, World!', 10, 0 ; 10 = new line, 0 = end of the text
; Lesson 2: Hello, World! with printf (x64, Windows: the first arguments are in rcx, rdx, r8, r9). .format pe console .arch x64 .import msvcrt.dll printf .code start: sub rsp, 40 ; 32 bytes shadow space for printf + 8 to align the stack lea rcx, [msg] ; first argument: the address of the text call [printf] xor eax, eax ; exit code 0 add rsp, 40 ret .data msg db 'Hello, World!', 10, 0 ; 10 = new line, 0 = end of the text
; Lesson 2: Hello, World! with printf (ARM 32, Thumb-2: the first arguments are in r0 to r3). .format pe console .arch thumb .import msvcrt.dll printf .code start: push {r4, lr} ; save lr (the way back); r4 keeps the stack 8-byte aligned adr r0, msg ; first argument: the address of the text (relative to pc) ldr r3, printf ; the address of printf from the import table blx r3 ; call it (lr = the next instruction) movs r0, #0 ; exit code 0 pop {r4, pc} ; return: the saved lr goes into pc .data msg db 'Hello, World!', 10, 0 ; 10 = new line, 0 = end of the text
; Lesson 2: Hello, World! with printf (ARM 64: the first arguments are in x0 to x7). .format pe console .arch arm64 .import msvcrt.dll printf .code start: stp x29, x30, [sp, #-16]! ; save the way back (x30 = lr) adrp x0, msg ; the page of the text ... add x0, x0, :lo12:msg ; ... plus the position in the page: first argument adrp x16, printf ldr x16, [x16, :lo12:printf] ; the address of printf from the import table blr x16 mov x0, #0 ; exit code 0 ldp x29, x30, [sp], #16 ret .data msg db 'Hello, World!', 10, 0 ; 10 = new line, 0 = end of the text
push dword msglegt die Adresse des Textes als Argument auf den Stack.call [printf]ruft die Funktion über die Importtabelle auf; die eckigen Klammern bedeuten „der Wert an dieser Adresse“, also die Adresse vonprintf, die Windows dort eingetragen hat.add esp, 4räumt das Argument auf.xor eax, eaxsetzteaxauf 0 (kürzer alsmov eax, 0: „exklusiv oder“ ergibt 0, wenn beide Bits gleich sind, und ein Register ist mit sich selbst immer gleich): Exit-Code 0.sub rsp, 40schafft Platz auf dem Stack.lea rcx, [msg](load effective address) lädt die Adresse vonmsginrcx, das erste Argument.call [printf]ruft die Funktion über die Importtabelle auf.xor eax, eaxsetzteaxauf 0 (Exit-Code 0),add rsp, 40gibt den Platz zurück, bevorretzurückkehrt.push {r4, lr}sichertlr(undr4, damit der Stack ausgerichtet bleibt).adr r0, msglädt die Adresse des Textes nachr0, relativ zumpc– das Programm hat keine feste Adresse.ldr r3, printfholt die Adresse vonprintfaus der Importtabelle,blx r3ruft sie auf.movs r0, #0ist der Exit-Code,pop {r4, pc}stelltr4wieder her und springt zurück.stp x29, x30, [sp, #-16]!sichert zwei Register auf dem Stack. Die Adresse des Textes entsteht in zwei Schritten:adrplädt die Adresse der 4-KB-Seite, auf dermsgliegt,add ... :lo12:msgaddiert die Position innerhalb der Seite. Genauso kommt die Adresse vonprintfaus der Importtabelle.blr x16ruft sie auf.mov x0, #0ist der Exit-Code,ldpholt die gesicherten Register zurück,retkehrt zurück.
Formate
Das erste Argument von printf ist ein Formattext. Steht darin ein %, setzt printf dort das nächste Argument ein:
| Format | schreibt das Argument als | Beispiel für 42 |
|---|---|---|
%d | Dezimalzahl mit Vorzeichen | 42 |
%x | Hexzahl (%X: mit Großbuchstaben) | 2a |
%c | ein Zeichen (der Wert ist der Zeichencode) | * |
%s | Text; das Argument ist die Adresse des Textes | – |
%% | das Zeichen % selbst | % |
Das Programm rechnet 20 + 22 und gibt drei Zeilen aus. Achte darauf, wie die Werte in der jeweiligen Architektur zu printf kommen.
; Lesson 2: printf with formats (i386). %d = decimal, %x = hex, %c = character, %s = text. .format pe console .arch i386 .import msvcrt.dll printf .code start: mov eax, 20 add eax, 22 ; eax = 42 push eax ; the arguments from right to left: the last one first push 22 push 20 push dword f_sum ; and the format at the end (it is the first argument) call [printf] add esp, 16 ; 4 arguments of 4 bytes push 42 ; %c push 42 ; %x push 42 ; %d push dword f_same call [printf] add esp, 16 push 8 ; %d push dword name ; %s: the address of the text push dword f_text call [printf] add esp, 12 xor eax, eax ret .data f_sum db '%d + %d = %d', 10, 0 f_same db '%d is %x in hex and the character %c', 10, 0 f_text db 'The name %s has %d letters', 10, 0 name db 'Asmonite', 0
; Lesson 2: printf with formats (x64). %d = decimal, %x = hex, %c = character, %s = text. .format pe console .arch x64 .import msvcrt.dll printf .code start: sub rsp, 40 mov edx, 20 ; second argument: rdx (edx is its lower half) mov r8d, 22 ; third argument: r8 mov r9d, edx add r9d, r8d ; fourth argument: r9 = 20 + 22 = 42 lea rcx, [f_sum] ; first argument: the format call [printf] lea rcx, [f_same] mov edx, 42 ; %d mov r8d, 42 ; %x mov r9d, 42 ; %c call [printf] lea rcx, [f_text] lea rdx, [name] ; %s: the address of the text mov r8d, 8 ; %d call [printf] xor eax, eax add rsp, 40 ret .data f_sum db '%d + %d = %d', 10, 0 f_same db '%d is %x in hex and the character %c', 10, 0 f_text db 'The name %s has %d letters', 10, 0 name db 'Asmonite', 0
; Lesson 2: printf with formats (ARM 32, Thumb-2). %d = decimal, %x = hex, %c = character, %s = text. ; The arguments: r0 = format, r1, r2, r3 = the values after it. .format pe console .arch thumb .import msvcrt.dll printf .code start: push {r4, lr} ldr r4, printf ; r4 keeps the address of printf: r4 stays unchanged across calls movs r1, #20 movs r2, #22 adds r3, r1, r2 ; r3 = 20 + 22 = 42 adr r0, f_sum ; the format blx r4 adr r0, f_same movs r1, #42 ; %d movs r2, #42 ; %x movs r3, #42 ; %c blx r4 adr r0, f_text adr r1, name ; %s: the address of the text movs r2, #8 ; %d blx r4 movs r0, #0 pop {r4, pc} .data align 4 f_sum db '%d + %d = %d', 10, 0 align 4 f_same db '%d is %x in hex and the character %c', 10, 0 align 4 f_text db 'The name %s has %d letters', 10, 0 align 4 name db 'Asmonite', 0
; Lesson 2: printf with formats (ARM 64). %d = decimal, %x = hex, %c = character, %s = text. ; The arguments: x0 = format, x1, x2, x3, ... = the values after it (w1 is the lower half of x1). .format pe console .arch arm64 .import msvcrt.dll printf .code start: stp x29, x30, [sp, #-32]! str x19, [sp, #16] adrp x19, printf ldr x19, [x19, :lo12:printf] ; x19 keeps the address of printf: x19 stays unchanged across calls mov w1, #20 mov w2, #22 add w3, w1, w2 ; w3 = 20 + 22 = 42 adrp x0, f_sum add x0, x0, :lo12:f_sum ; the format blr x19 adrp x0, f_same add x0, x0, :lo12:f_same mov w1, #42 ; %d mov w2, #42 ; %x mov w3, #42 ; %c blr x19 adrp x0, f_text add x0, x0, :lo12:f_text adrp x1, name add x1, x1, :lo12:name ; %s: the address of the text mov w2, #8 ; %d blr x19 mov x0, #0 ldr x19, [sp, #16] ldp x29, x30, [sp], #32 ret .data f_sum db '%d + %d = %d', 10, 0 f_same db '%d is %x in hex and the character %c', 10, 0 f_text db 'The name %s has %d letters', 10, 0 name db 'Asmonite', 0
Die Argumente werden von rechts nach links auf den Stack gelegt: zuerst der letzte Wert, zuletzt das Format. So liegt das Format oben auf dem Stack und ist für printf das erste Argument. Danach räumt add esp, 16 vier Argumente (je 4 Byte) weg; bei der dritten Ausgabe sind es drei, also add esp, 12. Rechne die Zahl nach, wenn du etwas änderst – ein falsches add esp zerstört den Stack.
Für jeden Aufruf lädst du neu: Format nach rcx, die Werte nach rdx, r8, r9. Die Funktion darf diese Register verändern, also setzt du sie vor jedem Aufruf neu. Mit edx, r8d und r9d schreibst du die unteren 32 Bit; die oberen werden auf 0 gesetzt, das reicht für %d und %x. Bei %s steht in rdx eine volle 64-Bit-Adresse, darum lea rdx, [name].
Das Format steht in r0, die Werte in r1 bis r3. printf verändert diese Register, also setzt du sie vor jedem Aufruf neu. Die Adresse von printf steht in r4 – das Register überlebt den Aufruf unverändert. So muss sie nicht jedes Mal geladen werden. adds r3, r1, r2 addiert und schreibt das Ergebnis in ein drittes Register, ohne die Summanden zu verändern. align 4 sorgt dafür, dass jeder Text an einer durch 4 teilbaren Adresse beginnt; adr verlangt das.
Das Format steht in x0, die Werte in w1 bis w3 (die unteren 32 Bit von x1 bis x3). printf verändert diese Register, also setzt du sie vor jedem Aufruf neu. Die Adresse von printf steht in x19 – das Register überlebt den Aufruf unverändert. Dafür sichert das Programm das alte x19 am Anfang auf dem Stack und holt es am Ende zurück. add w3, w1, w2 addiert und schreibt das Ergebnis in ein drittes Register.
In Asmonite ausprobieren
- Öffne ein Programm (Hello, World! oder Formate) und assembliere es mit Strg+F9.
- Setze mit F5 einen Haltepunkt auf die Zeile mit
call [printf](bei ARM: aufblxbeziehungsweiseblr) und starte mit F9. Das Programm hält vor dem Aufruf. - Sieh dir den Stack rechts an: oben liegt die Adresse des Textes, darunter die Werte. Das ist genau, was du mit
pushdort abgelegt hast. - Sieh dir die Register
rcx,rdx,r8undr9an: dort stehen die Argumente. Im Speicherfenster siehst du den Text an der Adresse inrcx. - Sieh dir die Register
r0bisr3an: dort stehen die Argumente. Im Speicherfenster siehst du den Text an der Adresse inr0. - Sieh dir die Register
x0bisx3an: dort stehen die Argumente. Im Speicherfenster siehst du den Text an der Adresse inx0. - Drücke F8: der Aufruf läuft, und im Ausgabefenster steht der Text. Mit F8 gehst du weiter durch die nächsten Aufrufe.
- Drücke F1 auf der Zeile mit dem Aufruf: rechts erklärt die Hilfe
printfmit seinen Parametern.
Was du siehst
Hello, World!
20 + 22 = 42 42 is 2a in hex and the character * The name Asmonite has 8 letters
Die Zahl 42 kommt dreimal an printf; was daraus wird, bestimmt allein das Format: 42, 2a oder das Zeichen mit dem Code 42, ein Stern. Die CPU weiß nicht, was eine Zahl „bedeutet“ – das legt erst das Programm fest.
Übungen
1. Dein Name
Ändere im Formatbeispiel den Namen in deinen eigenen. Die Ausgabe nennt noch die alte Anzahl der Buchstaben. Woran liegt das, und was müsstest du tun, damit sie stimmt?
Lösung
Die Zahl steht als Konstante im Befehl (8), das Programm zählt nichts. Du musst die Zahl selbst anpassen. Selbst zählen kann das Programm mit einer Schleife, die den Text bis zur 0 durchläuft – das machen wir in einer späteren Lektion (Schleifen, Strings).
2. Negative Zahlen
Gib mit demselben Format %d und %x die Zahl −1 aus. Was erscheint, und warum?
Lösung
Mit %d erscheint -1, mit %x ffffffff. Im Register stehen in beiden Fällen dieselben 32 Bit, alle gleich 1. %d liest sie als Zahl mit Vorzeichen (−1), %x als Zahl ohne Vorzeichen (4294967295 = ffffffff). Wie negative Zahlen gespeichert werden, kommt in der Lektion über Bits.
3. Knobelaufgabe: führende Nullen
Gib die Zahl 255 in der Form 0x000000FF aus – mit einem einzigen Format, ohne die Nullen selbst zu schreiben. Das Format %x kennt dafür eine Breite und eine Auffüllung.
Lösung
Das Format 0x%08X: die Zeichen 0x werden unverändert geschrieben, %08X schreibt die Zahl in großen Hexziffern, mindestens 8 Stellen breit (8), links mit Nullen aufgefüllt (0).
Merke
- Text im Speicher sind Bytes; ein Label vor dem Text ist seine Adresse, eine 0 am Ende markiert das Ende.
- Eine Funktion bekommt ihre Argumente an festen Stellen – wo, bestimmt die Aufrufkonvention der Architektur (Stack beim i386, Register bei x64 und ARM).
- Der Aufrufer muss wissen, welche Register die Funktion verändern darf und welche nicht.
printfbestimmt mit dem Format, was ein Wert bedeutet:%d,%x,%c,%s.