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:
bl <nazwa>
blx <rejestr>
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