Jak już wspominałem, programy w asemblerze są nieprzenośne. Programu napisanego na procesor z jednej rodziny nie da się nawet zasemblować asemblerem dla procesora z innej rodziny. Pomimo różnic w językach asemblera różnych procesorów, większość z nich opiera się jednak na takich samych podstawach. Zazwyczaj jest pewna liczba rejestrów określonego rozmiaru i rozkazy wykonujące proste operacje na rejestrach i komórkach pamięci. Sprawia to, że po zapoznaniu się z asemblerem dla jednej rodziny procesorów można dość szybko zacząć programować w asemblerze innej rodziny. Jest to podobna sytuacja jak, przykładowo, z językami programowania obiektowego.
Celem tych zajęć jest zapoznanie się z asemblerem procesorów ARM.
Ogólnie o procesorach ARM poczytać m.in. w Wikipedii, w wersji angielskej i trochę uboższej polskiej. Procesory te są dość popularne w rozmaitych urządzeniach elektronicznych takich jak telefony komórkowe, przenośne konsole do gier (Gameboy Advance), odtwarzacze muzyczne (Ipod, Iriver iFP) i innych, niekiedy bardziej poważnych.
Podstawowym dokumentem opisującym tę rodzinę procesorów jest ARM Architecture Reference Manual, w skrócie ARM ARM. Nie jest on chyba dostępny na stronie producenta, można go jednak znaleźć na przykład na stronie firmy Altera. Tutaj opiszę tylko kilka cech, na które warto zwrócić uwagę.
Procesory ARM mają 15 32-bitowych rejestrów ogólnego przeznaczenia o nazwach od R0 do R14. Rejestr R15, zwany też PC, jest licznikiem programu. Jest jednak również zaliczany do rejestrów ogólnego przeznaczenia i może być używany prawie wszędzie tam, gdzie pozostałe. Zapis do R15 powoduje skok pod wpisywany adres, odczyt zwraca adres bieżącej instrukcji powiększony o 8.
Rozkazy realizujące operacje dwuargumentowe mają zazwyczaj trzy operandy: dwa źródłowe i jeden docelowy. Instrukcja:
ADD R0, R1, R2wykonuje operację R0 := R1 + R2. Oczywiście nie wszystkie operandy muszą być różne:
ADD R0, R0, R0podwaja zawartość R0.
W odróżnieniu od procesorów z rodziny IA-32, rozkazy wykonujące obliczenia nie mogą używać komórek pamięci jako operandów. Komunikacja z pamięcią odbywa się tylko za pomocą rozkazów wczytujących i zapisujących dane.
Wiele rozkazów oprócz właściwej operacji pozwala też na wykonanie przesunięcia bitowego jednego z argumentów. Rozkaz
ADD R0, R1, R2, LSL #3wykonuje operację R0 := R1 + R2 * 8.
Wszystkie rozkazy, poza pewnymi wyjątkami w nowszych procesorach, mogą być warunkowe. Rozkaz
ADDEQ R0, R1, R2, LSL #3wykona się tylko jeśli znacznik Z jest równy 1. W przeciwnym przypadku procesor po prostu przejdzie tylko do kolejnej instrukcji. Oczywiście rozkaz skoku (np. rozkaz B) również może być warunkowy.
STMFD SP!, {R4, R5, R6} ; odłóż na stos rejestry R4-R6 ... LDMFD SP!, {R4, R5, R6} ; zdejmij je ze stosu
Rozkaz służący do wykonywania skoków do procedur nazywa się BL. Skacze on pod podany adres, a do rejestru R14, zwanego również LR, wpisuje adres powrotu, czyli adres następnej instrukcji po BL. Nie ma specjalnej instrukcji powrotu z procedury. Zamiast niej można wykonać na przykład MOV PC, LR. Jeśli, będąc w pewnej procedurze, chcemy wykonać skok do procedury, to musimy zachować bieżącą zawartość LR. Można ją po prostu odłożyć na stos. Przy pomocy pewnej sztuczki można połączyć odtwarzanie zachowanych rejestrów z powrotem z procedury:
STMFD SP!, {R4, R5, R6, LR} ; odłóż na stos rejestry R4-R6 oraz ; adres powrotu ... LDMFD SP!, {R4, R5, R6, PC} ; zdejmij je ze stosu wpisując ; adres powrotu do PC
Konwencje dotyczące procedur można znaleźć w dokumencie Procedure Call Standard. Ogólnie, cztery pierwsze argumenty, o ile nie są większe niż 32-bitowe, umieszcza się w rejestrach R0-R3, a wynik zwraca się w R0 (jeśli nie jest za duży). Procedury muszą zachowywać rejestry R4-R11 i R13 (przypominam, że R13 to SP).
Wiele rozkazów pozwala na stałe liczbowe jako operandy. Ponieważ kody wszystkich rozkazów zajmują 32-bity to oczywiście nie da się w nim umieścić dowolnej 32-bitowej stałej tak, by było miejsce na jeszcze inne informacje. Można więc umieścić tylko niektóre stałe (patrz punkt 5.1.2 ARM ARM). Inne trzeba wczytywać z pamięci jako dane. Asembler GNU udostępnia specjalną składnię:
LDR R1, =123456umieści w pamięci stałą 123456 i wygeneruje rozkaz ładujący ją do R1.
Można popracować z asemblerem procesorów ARM nawet nie mając
maszyny opartej o taki procesor. Narzędzia programistyczne GNU można
skompilować tak, by działały na komputerze, który mamy (np. z
procesorem z rodziny IA-32) i generowały kod dla procesorów
ARM. Kompilator generujący kod na inną platformę niż ta, na której sam
działa nazywa się czasem
Potrzebne będą pakiety binutils, gcc, gdb oraz newlib. Newlib to taka uproszczona biblioteka C. Kompilacja wymaga kilkuset MB przestrzeni dyskowej, więc trudno ją przeprowadzić na kontach studenckich. Dla chętnych przedstawiam krótką instrukcję kompilacji. Najpierw należy ściągnąć pewne pliki:
tar xjf binutils-2.16.1.tar.bz2 tar xjf gcc-4.0.3.tar.bz2 tar xjf gdb-6.4.tar.bz2 tar xzf newlib-1.14.0.tar.gzZaleca się kompilację poza katalogami ze źródłami, więc trzeba stworzyć katalogi do kompilacji:
mkdir build cd build mkdir binutils mkdir gcc mkdir gdb mkdir newlibKompilujemy binutils. Opcja --target określa architekturę, na którą będzie generowany kod. Opcja --prefix określa katalog, w którym zostaną zainstalowane skompilowane programy. Pracując na własnym komputerze można tę opcję pominąć, wtedy instalacja zostanie dokonana w katalogu /usr/local/. W tym wypadku do instalacji będą potrzebne prawa roota.
cd binutils ../../binutils-2.16.1/configure --target=arm-elf --prefix=$HOME/arm make make installPrzed dalszą kompilacją należy zapewnić by nowe binutils były w ścieżce poszukiwania.
cd ../gcc export PATH=$HOME/arm/bin:$PATH ../../gcc-4.0.3/configure --target=arm-elf --prefix=$HOME/arm --enable-languages=c make make install cd ../gdb ../../gdb-6.4/configure --target=arm-elf --prefix=$HOME/arm make make install cd ../newlib ../../newlib-1.14.0/configure --target=arm-elf --prefix=$HOME/arm make make install
Przygotowane środowisko programistyczne jest też na moim koncie na serwerze students w katalogu /home/inf/t/tm201069/arm. Nie miałem niestety okazji, by się przekonać, czy jest dostępne z innych kont niż moje, ale mam nadzieję, że uprawnienia są dobrze ustawione. Aby z niego korzystać wystarczy wykonać polecenie:
export PATH=/home/inf/t/tm201069/arm/bin:$PATH
Napiszemy program w C korzystający z procedury asemblerowej. Będziemy używali asemblera GNU. W przypadku procesorów ARM używa on takiej samej składni jak proponowana przez producenta. O szczegółach składniowych niezależnych od procesora (dyrektywy, komentarze, itp.) można poczytać np. w info (info gas).
Plik a.c:
#include <stdio.h> int pomnoz(int a); int main(void) { printf("%d", pomnoz(5)); return 0; }Plik b.s:
.text .global pomnoz pomnoz: ldr r1, =1234567 mul r0, r1, r0 mov pc, lrProgram można skompilować jednym poleceniem:
arm-elf-gcc -o program a.c b.s
Aby go uruchomić należy skorzystać z GDB:
arm-elf-gdb programi dalej w GDB:
target sim load run
Przypominam, że GCC ma opcję -S pozwalającą obejrzeć wygenerowany kod.