Scenariusz na lab 7

ARM przez przykłady

Pracujemy pod emulatorem QEMU, używamy asemblera GNU

1. Pierwszy program (jeden.s)


/* Komentarz */
.global main
.func main   @ 'main' jest funkcją (też komentarz)
 
main:
    mov r0, #2
    bx lr      @ return z main

Testujemy:

as -o first.o first.s
gcc -o first first.o
./first
./first ; echo $?
2

2. Dodawanie stałych

.global main
.func main
 
main:
    mov r1, #3
    mov r2, #4
    add r0, r1, r2
    bx lr

3. Dane w pamięci

.data
 
/* Dane 4-bajtowe, wyrównane do 4 bajtów */
.balign 4
var1:
    .word 3
.balign 4
var2:
    .word 4

.text
.global main
 
.balign 4
main:
    ldr r1, addr_of_var1
    ldr r1, [r1]
    ldr r2, addr_of_var2
    ldr r2, [r2]
    add r0, r1, r2
    bx lr
 
/* Dostęp do danych przez mini-GOT */
addr_of_var1: .word var1
addr_of_var2: .word var2

Uwagi:

4. Zapisywanie do pamięci: zmienne

.data
 
/* Zmienne wyrównane do 4 bajtów */
.balign 4
var1:
    .word 0
var2:
    .word 0

.text
.global main
 
/* Kod też */
.balign 4
main:
    /* Inicjowanie zmiennych */
    ldr r1, addr_of_var1
    mov r3, #3
    str r3, [r1]
    ldr r2, addr_of_var2
    mov r3, #4
    str r3, [r2]

    /* Dalej jak poprzednio */
    ldr r1, addr_of_var1
    ldr r1, [r1]
    ldr r2, addr_of_var2
    ldr r2, [r2]
    add r0, r1, r2
    bx lr
 
/* Adresy zmiennych */
addr_of_var1: .word var1
addr_of_var2: .word var2

5. Debugger

,,Business as usual'' czyli gdb:

$ gdb ./store1
GNU gdb (GDB) 7.4.1-debian
Copyright (C) 2012 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later 
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "arm-linux-gnueabihf".
For bug reporting instructions, please see:
...
Reading symbols from /home/zbyszek/asm/store1...(no debugging symbols found)...done.
(gdb) start
Temporary breakpoint 1 at 0x8390
Starting program: /home/zbyszek/asm/store1 

Temporary breakpoint 1, 0x00008390 in main ()
(gdb) _

Debugger mówi prawdę:

(gdb) disassemble
Dump of assembler code for function main:
=> 0x00008390 :	ldr	r1, [pc, #40]	; 0x83c0 
   0x00008394 :	mov	r3, #3
   0x00008398 :	str	r3, [r1]
   0x0000839c :	ldr	r2, [pc, #32]	; 0x83c4 
   0x000083a0 :	mov	r3, #4
   0x000083a4 :	str	r3, [r2]
   0x000083a8 :	ldr	r1, [pc, #16]	; 0x83c0 
   0x000083ac :	ldr	r1, [r1]
   0x000083b0 :	ldr	r2, [pc, #12]	; 0x83c4 
   0x000083b4 :	ldr	r2, [r2]
   0x000083b8 :	add	r0, r1, r2
   0x000083bc :	bx	lr
End of assembler dump.

Adresy zostały zastąpione przesunięciami, dlatego dwie instrukcje ldr odwołujące się do tego samego miejsca addr_of_myvarX mają inną wartość argumentu.

Strzałka => wskazuje na następną instrukcję do wykonania. Zanim ruszymy dalej, warto obejrzeć rejestry.

(gdb) info registers r0 r1 r2 r3
r0             0x1	1
r1             0xbefff744	3204446020
r2             0xbefff74c	3204446028
r3             0x8390	33680

(gdb) p $r0 = 2
$1 = 2
(gdb) info registers r0 r1 r2 r3
r0             0x2	2
r1             0xbefff744	3204446020
r2             0xbefff74c	3204446028
r3             0x8390	33680

(gdb) p $1
$2 = 2

Ruszamy:

(gdb) stepi
0x00008394 in main ()

(gdb) info register r1
r1             0x10564	66916

(gdb) p &var1
$3 = ( *) 0x10564

Czyli w r1 jest adres naszej zmiennej var1.

6. Porównywanie

.text
.global main
main:
    mov r1, #2
    mov r2, #2
    cmp r1, r2       @ zmienia flagi warunków w cpsr
    beq gdy_rowne
gdy_inne:
    mov r0, #2
    b end
gdy_rowne:
    mov r0, #1
end:
    bx lr

7. Iteracja

Policzymy sumę liczb od 1 do 22.

.text
.global main
main:
    mov r1, #0       @ suma
    mov r2, #1       @ licznik
loop: 
    cmp r2, #22
    bgt end
    add r1, r1, r2
    add r2, r2, #1
    b loop
end:
    mov r0, r1
    bx lr

8. Tryby adresowe

Pierwszy argument instrukcji (wynikowy) powinien być rejestrem, wyjątek to np. instrukcja str, ale tam wynik idzie do pamięci. Ostatni argument może być dodatkowo obrócony lub przesunięty

Modyfikatory ostatniego argumentu:

LSL #n
Logical Shift Left. Przesuwa o n bitów w lewo, uzupełniając z prawej zerami.
LSL Rx
To samo, ale wielkość przesunięcia podana w rejestrze.
LSR #n
Logical Shift Right. Przesuwa o n bitów w prawo, uzupełniając z lewej zerami.
LSR Rx
To samo, ale dla rejestru.
ASR #n
Arithmetic Shift Right. Jak LSR, ale rozszerza bit znaku.
ASR Rx
To samo, ale dla rejestru.
ROR #n
Rotate Right.
ROR Rx

Przykłady:

  mov r1, r2, LSL #1
  add r1, r2, r2, LSL #1    /* r1 := r2 * 3 */
  add r1, r2, r2, LSL #2    /* r1 := r2 * 5 */

9. Tablice

.data
 
.balign 4
a: .skip 400  @ tablica 100 liczb

/* Ustawiamy jej elementy na 100 kolejnych liczb */

.text
 
.global main
main:
    ldr r1, addr_of_a       @ bazowy adres tablicy a 
    mov r2, #0              @ początkowy indeks 
loop:
    cmp r2, #100            @ koniec?
    beq end
    add r3, r1, r2, LSL #2  @ adres kolejnego elementu
    str r2, [r3]
    add r2, r2, #1
    b loop
end:
    bx lr
addr_of_a: .word a

10. Funkcje

Gdy chcemy używać funkcji dwa rejestry stają się specjalne: r13 i r14. Rejestr r14 to inaczej lr (link register) czyli rejestr łączący: w nim zapisuje się adres powrotny dla wywołania funkcji.

Rejestr r13 ma też nazwę sp (stack pointer) i jest to wskaźnik wierzchołka stosu.

Parametry przekazuje się konwencjonalnie w rejestrach r0, r1, r2 i r3. Jeśli więcej, to reszta na stosie.

Konwencje:

Dwa sposoby wywołania funkcji:

Powrót z wywołania funkcji przez

  bx lr

Oczywiście można użyć innego rejestru.

11. Witaj świecie

.data
 
greeting:
 .asciz "Witaj świecie"
 
.balign 4
return: .word 0     @ magazyn na adres powrotny dla main
 
.text
.global main
.global puts

main:
    ldr r1, address_of_return     @ adres powrotny dla main
    str lr, [r1]
 
    ldr r0, address_of_greeting   @ parametr dla puts
    bl puts
 
    ldr r1, address_of_return
    ldr lr, [r1]
    bx lr                         @ return z main
address_of_greeting: .word greeting
address_of_return: .word return

12. Witaj lepszy świecie

(w każdym razie dla leniwych: budowę GOT można zepchnąć na asembler)

main:
    ldr r1, =return     @ adres powrotny dla main
    str lr, [r1]
 
    ldr r0, =greeting   @ parametr dla puts
    bl puts
 
    ldr r1, =return
    ldr lr, [r1]
    bx lr                         @ return z main

13. Pełna interakcja: printf i scanf

.data
 
.balign 4
message1: .asciz "Podaj liczbę: "
 
.balign 4
message2: .asciz "Wczytałem liczbę %d\n"
 
.balign 4
scanf_pattern : .asciz "%d"
 
.balign 4
number_read: .word 0

return: .word 0

.text
.global main
.global printf
.global scanf

main:
    ldr r1, =return
    str lr, [r1]
 
    ldr r0, =message1          @ call to printf
    bl printf
 
    ldr r0, =scanf_pattern     @ call to scanf
    ldr r1, =number_read
    bl scanf
    ldr r0, =message2          @ call to printf
    ldr r1, =number_read
    ldr r1, [r1]
    bl printf
 
    ldr r0, =number_read
    ldr r0, [r0]
 
    ldr lr, =return
    ldr lr, [lr]
    bx lr                      @ return from main

14. Operacje na liczbach 64-bitowych

Trzy operacje: dodawanie, odejmowanie i mnożenie. Architektura naszego ARM jest 32-bitowa, więc będziemy reprezentować liczbę 64-bitową parą liczb 32-bitowych.

Możemy taką liczbę trzymać w parze sąsiednich rejestrów (np. r1 i r2) albo w pamięci, dolna część jako pierwsza (i wtedy adres wyrównany do 8 z myślą o przyszłości).

Dodawanie: dodajemy dolne części, a potem górne uwzględniając przeniesienie.

adds r0, r2, r4     @ sufiks s żeby ustawiło flagę przeniesienia
adc  r1, r3, r5

Odejmowanie: odejmowanie jest podobne do dodawania, przeniesienie nazywa się czasem ,,pożyczką''. Jeśli powstaje pożyczka, to flaga przeniesienia jest zerowana. Można się tym nie przejmować, bo mamy instrukcję sbc analogiczną do adc.

subs r0, r2, r4     /* Sufiks s jak poprzednio */
sbc  r1, r3, r5