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.

Architektur:
Video zu dieser Lektion folgt.

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:

FormPrüfungDer Körper läuftAus Hochsprachen bekannt als
Test am Anfangvor jedem Durchlaufnull- oder mehrmalswhile (i <= 10) { … }
Test am Endenach jedem Durchlaufmindestens einmaldo { … } 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

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

In Asmonite ausprobieren

  1. Öffne das Summenprogramm, assembliere mit Strg+F9 und setze mit F5 einen Haltepunkt auf den Vergleich cmp am Label top. Starte mit F9.
  2. Bei jedem F9 hält das Programm wieder am cmp. Beobachte die beiden Register: ecx zählt 1, 2, 3 … und edx wächst 0, 1, 3, 6, 10, 15 …r1 zählt 1, 2, 3 … und r2 wächst 0, 1, 3, 6, 10, 15 …w1 zählt 1, 2, 3 … und w2 wächst 0, 1, 3, 6, 10, 15 …
  3. Um nicht zehnmal zu drücken: Setze den Textcursor auf die Zeile mit dem Label done und drücke F4 (Run to Cursor). Das Programm läuft durch die ganze Schleife und hält dahinter. Die Summe steht jetzt im Register.
  4. Experiment: Starte neu, halte am ersten cmp und ändere das Register für i per 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