Assembler-Kurs · Lektion 4
Schleifen und Zählen
Am Ende kannst du Schleifen mit dem Test am Anfang und am Ende bauen, mit einem Zähler eine Summe bilden und zwei Schleifen ineinander setzen. Voraussetzung: Lektion 3.
Die Idee: zwei Formen
In Lektion 3 hast du gesehen, dass ein bedingter Sprung zurück eine Schleife ergibt. Je nachdem, wo die Bedingung geprüft wird, gibt es zwei Grundformen:
| Form | Prüfung | Der Körper läuft | Aus Hochsprachen bekannt als |
|---|---|---|---|
| Test am Anfang | vor jedem Durchlauf | null- oder mehrmals | while (i <= 10) { … } |
| Test am Ende | nach jedem Durchlauf | mindestens einmal | do { … } while (i != 0) |
Eine Zählschleife (for) ist ein Sonderfall: Ein Register zählt die Durchläufe, der Körper rechnet damit.
Test am Anfang: die Summe von 1 bis 10
Das Programm addiert 1 + 2 + … + 10. Das Register für i zählt von 1 aufwärts, ein zweites sammelt die Summe.
; Lesson 4: a loop that tests at the top (like "while"). Adds 1 + 2 + ... + 10. .format pe console .arch i386 .import msvcrt.dll printf .code start: mov ecx, 1 ; i = 1 xor edx, edx ; sum = 0 top: cmp ecx, 10 jg done ; i > 10: leave the loop add edx, ecx ; sum = sum + i inc ecx ; i = i + 1 jmp top ; and test again done: push edx ; the sum is the argument for %d push dword fmt call [printf] add esp, 8 xor eax, eax ret .data fmt db 'The sum of 1 to 10 is %d', 10, 0
; Lesson 4: a loop that tests at the top (like "while"). Adds 1 + 2 + ... + 10. .format pe console .arch x64 .import msvcrt.dll printf .code start: sub rsp, 40 mov ecx, 1 ; i = 1 xor edx, edx ; sum = 0 (edx is also the second argument of printf) top: cmp ecx, 10 jg done ; i > 10: leave the loop add edx, ecx ; sum = sum + i inc ecx ; i = i + 1 jmp top ; and test again done: lea rcx, [fmt] ; the format is the first argument, the sum is already in edx call [printf] xor eax, eax add rsp, 40 ret .data fmt db 'The sum of 1 to 10 is %d', 10, 0
; Lesson 4: a loop that tests at the top (like "while"). Adds 1 + 2 + ... + 10. .format pe console .arch thumb .import msvcrt.dll printf .code start: push {r4, lr} movs r1, #1 ; i = 1 movs r2, #0 ; sum = 0 top: cmp r1, #10 bgt done ; i > 10: leave the loop adds r2, r2, r1 ; sum = sum + i adds r1, r1, #1 ; i = i + 1 b top ; and test again done: mov r1, r2 ; the sum is the argument for %d adr r0, fmt ldr r3, printf blx r3 movs r0, #0 pop {r4, pc} .data align 4 fmt db 'The sum of 1 to 10 is %d', 10, 0
; Lesson 4: a loop that tests at the top (like "while"). Adds 1 + 2 + ... + 10. .format pe console .arch arm64 .import msvcrt.dll printf .code start: stp x29, x30, [sp, #-16]! mov w1, #1 ; i = 1 mov w2, #0 ; sum = 0 top: cmp w1, #10 b.gt done ; i > 10: leave the loop add w2, w2, w1 ; sum = sum + i add w1, w1, #1 ; i = i + 1 b top ; and test again done: mov w1, w2 ; the sum is the argument for %d adrp x0, fmt add x0, x0, :lo12:fmt adrp x16, printf ldr x16, [x16, :lo12:printf] blr x16 mov x0, #0 ldp x29, x30, [sp], #16 ret .data fmt db 'The sum of 1 to 10 is %d', 10, 0
- Am Label
topsteht der Test:cmp ecx, 10undjg doneverlassen die Schleife, sobaldigrößer als 10 ist.cmp r1, #10undbgt doneverlassen die Schleife, sobaldigrößer als 10 ist.cmp w1, #10undb.gt doneverlassen die Schleife, sobaldigrößer als 10 ist. Istivon Anfang an zu groß, läuft der Körper kein einziges Mal. - Der Körper addiert
izur Summe und erhöhtium 1. Ein unbedingter Sprung (jmp top)(b top) geht zurück zum Test. ecxisti,edxdie Summe. Beide darf jede Funktion verändern; das ist hier kein Problem, weil die Schleife keine Funktion aufruft. Erst danach folgtprintf, und die Summe wird als Argument auf den Stack gelegt.ecxisti,edxdie Summe. Beide darf jede Funktion verändern; das ist hier kein Problem, weil die Schleife keine Funktion aufruft. Nach der Schleife steht die Summe schon inedx, dem zweiten Argument vonprintf; nur das Format muss noch nachrcx.r1isti,r2die Summe. Beide darf jede Funktion verändern; das ist hier kein Problem, weil die Schleife keine Funktion aufruft. Nach der Schleife kopiertmov r1, r2die Summe in das zweite Argument vonprintf.w1isti,w2die Summe. Beide darf jede Funktion verändern; das ist hier kein Problem, weil die Schleife keine Funktion aufruft. Nach der Schleife kopiertmov w1, w2die Summe in das zweite Argument vonprintf.
Test am Ende: kürzer
Beim Test am Ende läuft der Körper immer mindestens einmal, und der unbedingte Sprung entfällt: Der bedingte Sprung am Ende geht selbst zurück. Das spart einen Befehl pro Durchlauf. Für die Summe zählt man dazu am besten abwärts von 10 bis 1, denn dann braucht man keinen Vergleich: decsubs setzt das Zero-Flag selbst, wie in Lektion 3.
Asmonite schreibt das lesbarer: do markiert den Anfang der Schleife, while not zero am Ende springt zurück, solange das Ergebnis des Befehls davor nicht 0 war. Der Assembler macht daraus genau den Sprung jnz (bei ARM bne beziehungsweise b.ne). Mehr dazu steht auf der Seite if, else, do … while.
; Lesson 4: the same sum with the do ... while of Asmonite (i386). It counts down from 10 to 1. ; "do" is a label, "while not zero" is the jnz back to it (it uses the flags of the dec in front of it). .format pe console .arch i386 .import msvcrt.dll printf .code start: mov ecx, 10 ; i = 10 xor edx, edx ; sum = 0 do add edx, ecx ; sum = sum + i dec ecx ; i = i - 1 while not zero ; as long as i is not 0 push edx push dword fmt call [printf] add esp, 8 xor eax, eax ret .data fmt db 'The sum of 1 to 10 is %d', 10, 0
; Lesson 4: the same sum with the do ... while of Asmonite (x64). It counts down from 10 to 1. ; "do" is a label, "while not zero" is the jnz back to it (it uses the flags of the dec in front of it). .format pe console .arch x64 .import msvcrt.dll printf .code start: sub rsp, 40 mov ecx, 10 ; i = 10 xor edx, edx ; sum = 0 do add edx, ecx ; sum = sum + i dec ecx ; i = i - 1 while not zero ; as long as i is not 0 lea rcx, [fmt] ; the format is the first argument, the sum is already in edx call [printf] xor eax, eax add rsp, 40 ret .data fmt db 'The sum of 1 to 10 is %d', 10, 0
; Lesson 4: the same sum with the do ... while of Asmonite (ARM 32, Thumb-2). It counts down from 10 to 1. ; "do" is a label, "while not zero" is the bne back to it (it uses the flags of the subs in front of it). .format pe console .arch thumb .import msvcrt.dll printf .code start: push {r4, lr} movs r1, #10 ; i = 10 movs r2, #0 ; sum = 0 do adds r2, r2, r1 ; sum = sum + i subs r1, r1, #1 ; i = i - 1 while not zero ; as long as i is not 0 mov r1, r2 ; the sum is the argument for %d adr r0, fmt ldr r3, printf blx r3 movs r0, #0 pop {r4, pc} .data align 4 fmt db 'The sum of 1 to 10 is %d', 10, 0
; Lesson 4: the same sum with the do ... while of Asmonite (ARM 64). It counts down from 10 to 1. ; "do" is a label, "while not zero" is the b.ne back to it (it uses the flags of the subs in front of it). .format pe console .arch arm64 .import msvcrt.dll printf .code start: stp x29, x30, [sp, #-16]! mov w1, #10 ; i = 10 mov w2, #0 ; sum = 0 do add w2, w2, w1 ; sum = sum + i subs w1, w1, #1 ; i = i - 1 while not zero ; as long as i is not 0 mov w1, w2 ; the sum is the argument for %d adrp x0, fmt add x0, x0, :lo12:fmt adrp x16, printf ldr x16, [x16, :lo12:printf] blr x16 mov x0, #0 ldp x29, x30, [sp], #16 ret .data fmt db 'The sum of 1 to 10 is %d', 10, 0
Die Intel-Prozessoren haben dafür auch einen eigenen Befehl: loop ziel zählt ecx um 1 herunter und springt, solange es nicht 0 ist. Compiler benutzen ihn selten (dec und jnz sind auf modernen Prozessoren schneller), aber du triffst ihn in altem Code.
Eine Schleife in der Schleife
Der Körper einer Schleife darf selbst eine Schleife enthalten. Das Programm druckt das kleine Einmaleins von 1 bis 3: Die äußere Schleife läuft über die Zeilen, die innere über die Spalten.
; Lesson 4: a loop inside a loop. Prints the 3 x 3 multiplication table. .format pe console .arch i386 .import msvcrt.dll printf .code start: push ebx ; ebx and esi must come back unchanged: we save them push esi mov ebx, 1 ; row = 1 row: mov esi, 1 ; column = 1 col: mov eax, ebx imul eax, esi ; row * column push eax push dword f_cell call [printf] add esp, 8 inc esi ; next column cmp esi, 3 jle col ; column <= 3: next cell push dword f_nl ; end of the row: a line break call [printf] add esp, 4 inc ebx ; next row cmp ebx, 3 jle row ; row <= 3: next row pop esi pop ebx xor eax, eax ret .data f_cell db '%3d', 0 f_nl db 10, 0
; Lesson 4: a loop inside a loop. Prints the 3 x 3 multiplication table. .format pe console .arch x64 .import msvcrt.dll printf .code start: push rbx ; rbx and rsi must come back unchanged: we save them push rsi sub rsp, 40 mov ebx, 1 ; row = 1 row: mov esi, 1 ; column = 1 col: mov edx, ebx imul edx, esi ; row * column: the second argument lea rcx, [f_cell] call [printf] inc esi ; next column cmp esi, 3 jle col ; column <= 3: next cell lea rcx, [f_nl] ; end of the row: a line break call [printf] inc ebx ; next row cmp ebx, 3 jle row ; row <= 3: next row xor eax, eax add rsp, 40 pop rsi pop rbx ret .data f_cell db '%3d', 0 f_nl db 10, 0
; Lesson 4: a loop inside a loop. Prints the 3 x 3 multiplication table. .format pe console .arch thumb .import msvcrt.dll printf .code start: push {r4, r5, r6, lr} ; r4 to r6 must come back unchanged: we save them ldr r6, printf movs r4, #1 ; row = 1 row: movs r5, #1 ; column = 1 col: mul r1, r4, r5 ; row * column: the argument for %3d adr r0, f_cell blx r6 adds r5, r5, #1 ; next column cmp r5, #3 ble col ; column <= 3: next cell adr r0, f_nl ; end of the row: a line break blx r6 adds r4, r4, #1 ; next row cmp r4, #3 ble row ; row <= 3: next row movs r0, #0 pop {r4, r5, r6, pc} .data align 4 f_cell db '%3d', 0 align 4 f_nl db 10, 0
; Lesson 4: a loop inside a loop. Prints the 3 x 3 multiplication table. .format pe console .arch arm64 .import msvcrt.dll printf .code start: stp x29, x30, [sp, #-48]! stp x19, x20, [sp, #16] ; x19 to x21 must come back unchanged: we save them str x21, [sp, #32] adrp x21, printf ldr x21, [x21, :lo12:printf] mov w19, #1 ; row = 1 row: mov w20, #1 ; column = 1 col: mul w1, w19, w20 ; row * column: the argument for %3d adrp x0, f_cell add x0, x0, :lo12:f_cell blr x21 add w20, w20, #1 ; next column cmp w20, #3 b.le col ; column <= 3: next cell adrp x0, f_nl ; end of the row: a line break add x0, x0, :lo12:f_nl blr x21 add w19, w19, #1 ; next row cmp w19, #3 b.le row ; row <= 3: next row mov x0, #0 ldp x19, x20, [sp, #16] ldr x21, [sp, #32] ldp x29, x30, [sp], #48 ret .data f_cell db '%3d', 0 f_nl db 10, 0
- Jede Schleife hat ihren eigenen Zähler:
ebxfür die Zeile,esifür die Spalteebxfür die Zeile,esifür die Spalter4für die Zeile,r5für die Spaltew19für die Zeile,w20für die Spalte. Der innere Zähler wird bei jeder Zeile neu auf 1 gesetzt (das Labelcolsteht hinter dieser Zuweisung, die innere Schleife springt also nicht dorthin zurück). - Der
printf-Aufruf steckt in der inneren Schleife und darf die Zähler nicht zerstören. Deshalb liegen sie in Registern, die eine Funktion unverändert zurückgeben muss (ebx,esi) (r4bisr11) (x19bisx28). Dafür sichert das Programm sie am Anfang auf dem Stack und holt sie am Ende zurück – das hast du in Lektion 2 fürlrgesehen. imul eax, esimultipliziert; das Ergebnis ersetzt den ersten Operanden.mul r1, r4, r5multipliziert zwei Register und schreibt das Ergebnis in ein drittes.- Das Format
%3dschreibt die Zahl mindestens 3 Stellen breit, mit Leerzeichen aufgefüllt, damit die Spalten untereinander stehen. Am Ende jeder Zeile schreibt ein zweiterprintf-Aufruf einen Zeilenumbruch.
In Asmonite ausprobieren
- Öffne das Summenprogramm, assembliere mit Strg+F9 und setze mit F5 einen Haltepunkt auf den Vergleich
cmpam Labeltop. Starte mit F9. - Bei jedem F9 hält das Programm wieder am
cmp. Beobachte die beiden Register:ecxzählt 1, 2, 3 … undedxwächst 0, 1, 3, 6, 10, 15 …r1zählt 1, 2, 3 … undr2wächst 0, 1, 3, 6, 10, 15 …w1zählt 1, 2, 3 … undw2wächst 0, 1, 3, 6, 10, 15 … - Um nicht zehnmal zu drücken: Setze den Textcursor auf die Zeile mit dem Label
doneund drücke F4 (Run to Cursor). Das Programm läuft durch die ganze Schleife und hält dahinter. Die Summe steht jetzt im Register. - Experiment: Starte neu, halte am ersten
cmpund ändere das Register füriper Doppelklick auf den Wert (ecx)(r1)(x1) auf 11. Mit F8 springt das Programm sofort aus der Schleife: Der Körper läuft nicht, die Summe bleibt 0. Das ist die Besonderheit des Tests am Anfang.
Was du siehst
The sum of 1 to 10 is 55
1 2 3 2 4 6 3 6 9
Die Summe 55 passt zur Formel n·(n+1)/2 = 10·11/2.
Übungen
1. Bis 100
Ändere das Summenprogramm so, dass es 1 + 2 + … + 100 berechnet. Was kommt heraus? Prüfe es mit der Formel n·(n+1)/2.
Lösung
Die Grenze im Vergleich von 10 auf 100 ändern (und, damit der Text stimmt, auch die Zahlen im Formattext). Es kommt 5050 heraus: 100·101/2.
2. Null Durchläufe
Wie verhalten sich die beiden Schleifenformen, wenn die Summe von 1 bis 0 gebildet werden soll? Sage es vorher voraus. Beim Test am Anfang ändere dazu die Grenze auf 0.
Lösung
Beim Test am Anfang ist i = 1 schon größer als die Grenze 0: Der Körper läuft nie, die Summe bleibt 0 – das ist richtig. Beim Test am Ende (abwärts zählen, Anfangswert 0) läuft der Körper einmal, und das Herunterzählen ergibt −1, das nicht 0 ist: Die Schleife läuft weiter, bis der Zähler nach rund vier Milliarden Durchläufen wieder bei 0 ankommt. Das Ergebnis ist falsch und das Programm braucht eine Weile (im Debugger: Pause oder Strg+F2). Merke: Die Form mit dem Test am Ende nimmst du nur, wenn der Körper sicher mindestens einmal laufen muss.
3. Knobelaufgabe: Zweierpotenzen
Gib die Zahlen 1, 2, 4, 8, … bis 512 aus, jede in einer Zeile (zehn Zahlen). Du brauchst nur Befehle, die du schon kennst. Tipp: Eine Zahl zu verdoppeln heißt, sie zu sich selbst zu addieren.
Lösung
; Lesson 4, exercise 3: the powers of two, 1 2 4 ... 512. Doubling = adding a number to itself. .format pe console .arch i386 .import msvcrt.dll printf .code start: push ebx ; ebx and esi must come back unchanged: we save them push esi mov ebx, 10 ; ten numbers mov esi, 1 ; the first one again: push esi push dword fmt call [printf] add esp, 8 add esi, esi ; doubled dec ebx jnz again pop esi pop ebx xor eax, eax ret .data fmt db '%d', 10, 0
; Lesson 4, exercise 3: the powers of two, 1 2 4 ... 512. Doubling = adding a number to itself. .format pe console .arch x64 .import msvcrt.dll printf .code start: push rbx ; rbx and rsi must come back unchanged: we save them push rsi sub rsp, 40 mov ebx, 10 ; ten numbers mov esi, 1 ; the first one again: lea rcx, [fmt] mov edx, esi call [printf] add esi, esi ; doubled dec ebx jnz again xor eax, eax add rsp, 40 pop rsi pop rbx ret .data fmt db '%d', 10, 0
; Lesson 4, exercise 3: the powers of two, 1 2 4 ... 512. Doubling = adding a number to itself. .format pe console .arch thumb .import msvcrt.dll printf .code start: push {r4, r5, r6, lr} ; r4 to r6 must come back unchanged: we save them ldr r6, printf movs r4, #10 ; ten numbers movs r5, #1 ; the first one again: adr r0, fmt mov r1, r5 blx r6 adds r5, r5, r5 ; doubled subs r4, r4, #1 bne again movs r0, #0 pop {r4, r5, r6, pc} .data align 4 fmt db '%d', 10, 0
; Lesson 4, exercise 3: the powers of two, 1 2 4 ... 512. Doubling = adding a number to itself. .format pe console .arch arm64 .import msvcrt.dll printf .code start: stp x29, x30, [sp, #-48]! stp x19, x20, [sp, #16] ; x19 to x21 must come back unchanged: we save them str x21, [sp, #32] adrp x21, printf ldr x21, [x21, :lo12:printf] mov w19, #10 ; ten numbers mov w20, #1 ; the first one again: adrp x0, fmt add x0, x0, :lo12:fmt mov w1, w20 blr x21 add w20, w20, w20 ; doubled subs w19, w19, #1 b.ne again mov x0, #0 ldp x19, x20, [sp, #16] ldr x21, [sp, #32] ldp x29, x30, [sp], #48 ret .data fmt db '%d', 10, 0
Ein Register hält die aktuelle Zahl (sie wird in jedem Durchlauf mit sich selbst addiert), ein zweites zählt zehn Durchläufe herunter. Beide müssen den Aufruf von printf überleben und liegen darum in Registern, die eine Funktion unverändert zurückgeben muss.
Merke
- Eine Schleife ist ein bedingter Sprung zurück. Der Test steht am Anfang (null oder mehr Durchläufe) oder am Ende (mindestens einer).
- Der Test am Ende ist kürzer, passt aber nur, wenn der Körper sicher einmal laufen muss.
- Ein Zählregister wird in jedem Durchlauf geändert; zählt es abwärts bis 0, spart man den Vergleich, weil
decsubsdie Flags setzt. - Jede Schleife in einer Schleife braucht einen eigenen Zähler. Der innere Zähler wird bei jedem Durchlauf der äußeren neu gesetzt.
- Ruft die Schleife eine Funktion auf, müssen die Zähler in Registern liegen, die die Funktion unverändert zurückgibt (und die du dafür sicherst).