Darmowy fragment publikacji:
Tytuł oryginału: Practical Reverse Engineering x86, x64, ARM, Windows® Kernel, Reversing Tools, and
Obfuscation
Tłumaczenie: Konrad Matuk
ISBN: 978-83-283-0678-3
Copyright © 2014 by Bruce Dang.
Published by John Wiley Sons, Inc., Indianapolis, Indiana.
All rights reserved. This translation published under license with the original publisher John Wiley Sons,
Inc.
Translation copyright © 2015 by Helion S.A.
No part of this publication may be reproduced, stored in a retrieval system or transmitted in any form or by
any means, electronic, mechanical, photocopying, recording, scanning or otherwise, without either the prior
written permission
of the Publisher
Wiley and the Wiley logo are trademarks or registered trademarks of John Wiley Sons, Inc. and/or its
affiliates, in the United States and other countries, and may not be used without written permission. All
other trademarks are the property of their respective owners. John Wiley Sons, Inc. is not associated with
any product or vendor mentioned in this book.
Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości lub fragmentu niniejszej
publikacji w jakiejkolwiek postaci jest zabronione. Wykonywanie kopii metodą kserograficzną,
fotograficzną, a także kopiowanie książki na nośniku filmowym, magnetycznym lub innym powoduje
naruszenie praw autorskich niniejszej publikacji.
Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi bądź towarowymi ich
właścicieli.
Autor oraz Wydawnictwo HELION dołożyli wszelkich starań, by zawarte w tej książce informacje były
kompletne i rzetelne. Nie biorą jednak żadnej odpowiedzialności ani za ich wykorzystanie, ani za związane
z tym ewentualne naruszenie praw patentowych lub autorskich. Autor oraz Wydawnictwo HELION nie
ponoszą również żadnej odpowiedzialności za ewentualne szkody wynikłe z wykorzystania informacji
zawartych w książce.
Wydawnictwo HELION
ul. Kościuszki 1c, 44-100 GLIWICE
tel. 32 231 22 19, 32 230 98 63
e-mail: helion@helion.pl
WWW: http://helion.pl (księgarnia internetowa, katalog książek)
Drogi Czytelniku!
Jeżeli chcesz ocenić tę książkę, zajrzyj pod adres
http://helion.pl/user/opinie/inodpr
Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję.
Printed in Poland.
• Kup książkę
• Poleć książkę
• Oceń książkę
• Księgarnia internetowa
• Lubię to! » Nasza społeczność
O autorach
O korektorze merytorycznym
Podziękowania
Wstęp
Rozdział 1. Architektura x86 i x64
Rejestry i typy danych
Zbiór instrukcji
Składnia
Przenoszenie danych
Ćwiczenie
Operacje arytmetyczne
Operacje stosu i wywoływanie funkcji
Ćwiczenia
Sterowanie wykonywanym programem
Mechanizm systemowy
Translacja adresów
Przerwania i wyjątki
Analiza krok po kroku
Ćwiczenia
x64
Rejestry i typy danych
Przenoszenie danych
Adresowanie kanoniczne
Wywołanie funkcji
Ćwiczenia
Spis treści
9
11
13
17
21
22
23
24
25
30
31
32
36
37
44
45
47
47
54
55
55
56
56
57
57
5
Kup książkęPoleć książkę6
Spis treści
Rozdział 2. Architektura ARM
Podstawowe funkcje
Typy danych i rejestry
Opcje systemu i ustawienia
Instrukcje — wprowadzenie
Ładowanie i zapisywanie danych
Instrukcje LDR i STR
Inne zastosowania instrukcji LDR
Instrukcje LDM i STM
Instrukcje PUSH i POP
Funkcje i wywoływanie funkcji
Operacje arytmetyczne
Rozgałęzianie i wykonywanie warunkowe
Tryb Thumb
Polecenia switch-case
Rozmaitości
Kompilacja just-in-time i samomodyfikujący się kod
Podstawy synchronizacji
Mechanizmy i usługi systemowe
Instrukcje
Analiza krok po kroku
Co dalej?
Ćwiczenia
Rozdział 3.
Jądro systemu Windows
Podstawy systemu Windows
Rozkład pamięci
Inicjalizacja procesora
Wywołania systemowe
Poziom żądań przerwania urządzenia
Pule pamięci
Listy deskryptorów pamięci
Procesy i wątki
Kontekst wykonywania
Podstawy synchronizacji jądra
Listy
Szczegóły implementacji
Analiza krok po kroku
Ćwiczenia
59
60
63
65
66
67
67
71
72
76
77
80
81
85
85
87
87
88
89
91
91
98
98
107
108
108
109
111
123
125
126
126
128
129
130
131
138
142
Kup książkęPoleć książkęSpis treści
7
Wykonywanie asynchroniczne i ad hoc
Wątki systemowe
Elementy robocze
Asynchroniczne wywoływanie procedur
Opóźnione wywoływanie procedur
Timery
Wywołania zwrotne procesów i wątków
Procedury zakończenia
Pakiety żądań wejścia-wyjścia
Struktura sterownika
Punkty rozpoczęcia
Obiekty sterownika i urządzenia
Obsługa pakietów IRP
Popularne mechanizmy zapewniające komunikację
pomiędzy kodem użytkownika a kodem jądra
Inne mechanizmy systemowe
Analiza krok po kroku
Rootkit w architekturze x86
Rootkit w architekturze x64
Dalszy rozwój
Ćwiczenia
Rozwijanie pewności siebie i utrwalanie wiadomości
Poszerzanie horyzontów
Analiza prawdziwych sterowników
Rozdział 4. Debugowanie i automatyzacja
Narzędzia i podstawowe polecenia służące do debugowania
Określanie ścieżki plików symboli
Okna debugera
Obliczanie wartości wyrażenia
Zarządzanie procesami i debugowanie zdarzeń
Rejestry, pamięć i symbole
Punkty wstrzymania
Kontrolowanie procesów i modułów
Inne polecenia
Skrypty i debugowanie
Pseudorejestry
Aliasy
Język
Pliki skryptów
Skrypty jako funkcje
Przykładowe skrypty przydatne podczas debugowania
146
147
148
150
154
158
159
161
162
164
165
166
167
168
170
173
173
188
195
196
197
198
201
203
204
205
205
206
210
214
223
226
229
230
231
233
240
251
255
260
Kup książkęPoleć książkę8
Spis treści
Korzystanie z narzędzi SDK
Pojęcia
Tworzenie rozszerzeń narzędzi debugujących
Praktyczne rozszerzenia, narzędzia i źródła wiedzy
Rozdział 5. Zaciemnianie kodu
Techniki zaciemniania kodu
Po co zaciemniać kod?
Zaciemnianie oparte na danych
Zaciemnianie oparte na sterowaniu
Jednoczesne zaciemnianie przepływu sterowania i przepływu danych
Zabezpieczanie przez zaciemnianie
Techniki rozjaśniania kodu
Natura rozjaśniania kodu: odwracanie przekształceń
Narzędzia przeznaczone do rozjaśniania kodu
Rozjaśnianie kodu w praktyce
Studium przypadku
Pierwsze wrażenie
Analiza semantyki procedury
Obliczanie symboliczne
Rozwiązanie zadania
Podsumowanie
Ćwiczenia
Bibliografia
Dodatek A
Sumy kontrolne SHA1
Skorowidz
267
268
272
274
277
279
279
282
287
293
297
297
298
303
319
335
335
337
339
340
342
343
343
347
349
Kup książkęPoleć książkęRozdział
2
Architektura ARM
Firma Acron Computers pod koniec lat 80. stworzyła 32-bitową architekturę RISC, która począt-
kowo nosiła nazwę Acron RISC Machine (później przemianowano ją na Advanced RISC Machine).
Architektura ta okazała się na tyle dobra, że zaczęto ją stosować również w produktach innych firm.
Z tego powodu powstała spółka o nazwie ARM Holdings. Sprzedawała ona licencje pozwalające na
zastosowanie architektury ARM w wielu różnych urządzeniach. Architektury tej używa się w licz-
nych systemach wbudowanych, takich jak telefony komórkowe, elektronika samochodowa, odtwa-
rzacze MP3, telewizory. Pierwsza wersja tej architektury została wprowadzona na rynek w roku
1985. Obecnie powstaje jej siódma generacja (ARMv7). Istnieje wiele serii procesorów ARM (np.
ARM7, ARM7TDMI, ARM926EJ-S, Cortex). Nie należy ich mylić z różnymi wersjami specyfikacji
architektury, oznaczanymi jako ARMv1 – ARMv7. Jest dostępnych kilka wersji wspomnianej ar-
chitektury, przy czym większość urządzeń korzysta z wersji ARMv4, 5, 6 lub 7. Wersje oznaczone
numerami 4 i 5 są już relatywnie dość „stare”, niemniej jednak procesory oparte na tych wersjach ar-
chitektury są najczęściej spotykane (według danych przedstawianych przez firmę ARM „powstało
ponad 10 miliardów” tego typu procesorów). W popularnych urządzeniach elektronicznych spoty-
ka się nowsze wersje tej architektury. Na przykład produkowany przez firmę Apple odtwarzacz
IPod Touch trzeciej generacji wyposażono w procesor o architekturze ARMv6, a wszystkie póź-
niejsze urządzenia, takie jak iPhone, iPad i Windows Phone 7, produkowano na bazie architektury
ARMv7.
Firmy Intel i AMD same projektują i produkują swoje procesory. Natomiast ARM podchodzi
do tego nieco inaczej. Firma ta projektuje architekturę, a następnie sprzedaje innym firmom licen-
cje na produkcję układów. Firmy te wytwarzają procesory i montują je w swoich urządzeniach. Na
przykład firmy Apple, NVIDIA, Qualcomm i Texas Instruments produkują własne procesory, takie
jak A, Tegra, Snapdragon i OMAP, lecz ich architektura jest licencjonowana przez ARM. We wszystkich
59
Kup książkęPoleć książkę60
Rozdział 2 (cid:31) Architektura ARM
tych układach zastosowano ten sam model zarządzania pamięcią, który zdefiniowano w doku-
mentacji architektury ARM. Do procesorów mogą być dodane oczywiście pewne funkcje zwięk-
szające ich możliwości; przykładowo rozszerzenie instrukcji sprzętowych Jazelle pozwala procesorowi
na bezpośrednie przetwarzanie kodu Java, a rozszerzenie Thumb zapewnia obsługę 16- i 32-bitowych
instrukcji, co umożliwia tworzenie gęstszego kodu (standardowe instrukcje ARM są 32-bitowe).
Rozszerzenie Debug pozwala inżynierom analizować pracę procesora za pomocą specjalnego urzą-
dzenia. Rozszerzenia są oznaczane za pomocą liter (np. J, T, D). Producenci, w zależności od swoich
wymagań, mogą kupić odpowiednią licencję. To właśnie dlatego w oznaczeniach procesorów zgod-
nych z architekturą ARMv6 (a także ze starszymi jej wersjami) zastosowano litery — np. ARM1156T2
oznacza procesor zgodny z architekturą ARMv6 z rozszerzeniem Thumb-2. Ta konwencja nazew-
nictwa procesorów nie jest stosowana w przypadku architektury ARMv7, gdzie podaje się nazwę
modelu oraz jeden z trzech sufiksów: A (aplikacje), R (czas rzeczywisty) i M (mikrokontroler). Na
przykład seria procesorów ARMv7 Cortex-A jest zoptymalizowana do wykonywania aplikacji, a układy
z rodziny Cortex-M są przeznaczone do pracy w mikrokontrolerach i obsługują tylko wykonywanie
instrukcji w trybie Thumb.
W niniejszym rozdziale opisano architekturę ARMv7 zdefiniowaną w ARM Architecture Reference
Manual: ARMv7-A and ARMv7-R edition (ARM DDI 0406B).
Podstawowe funkcje
Architektura ARM jest oparta na architekturze RISC, a więc istnieją pewne różnice pomiędzy ar-
chitekturą ARM a architekturą CISC (x86, x64). W nowych procesorach firmy Intel dodano pewne
funkcje znane wcześniej z architektury RISC (procesory te nie charakteryzują się „czystą” archi-
tekturą CISC). Po pierwsze, zestaw instrukcji ARM jest o wiele mniejszy od zestawu instrukcji ob-
sługiwanych przez procesory x86, z tym że w tej architekturze znalazło się więcej rejestrów ogólnego
przeznaczenia. Po drugie, w przypadku procesorów ARM instrukcje charakteryzują się stałą długo-
ścią (w zależności od trybu pracy są one 16- lub 32-bitowe). Po trzecie, w architekturze ARM do-
stęp do pamięci uzyskuje się za pomocą modelu load-store (pol. „załaduj z pamięci — zapisz w pa-
mięci”). Dane przed przetworzeniem muszą być przeniesione z pamięci do rejestrów. Dostęp do
pamięci odbywa się tylko za pomocą instrukcji załaduj (LDR) i zapisz (STR). Jeżeli chcesz dokonać
inkrementacji 32-bitowej wartości zapisanej pod danym adresem pamięci, to musisz najpierw za-
ładować wartość spod tego adresu do rejestru, wykonać operację inkrementowania, a następnie za-
pisać wartość z powrotem w pamięci. W architekturze x86 większość instrukcji mogła dokonywać
bezpośrednich operacji na danych zapisanych w pamięci. To, co w przypadku architektury x86
wymaga wykonania jednej instrukcji, w przypadku architektury ARM wymaga wykonania trzech
instrukcji (jednej instrukcji ładowania, jednej inkrementowania i jednej zapisu). Osobie zajmującej się
inżynierią odwrotną może się wydawać, że odczytanie takiego kodu będzie trwało dłużej, ale w praktyce
nie ma to znaczenia, gdy przyzwyczaisz się do tej architektury.
W architekturze ARM występuje także kilka różnych poziomów uprawnień przypisywanych
procesom. W przypadku architektury x86 przywileje były określane za pomocą czterech ringów.
Kup książkęPoleć książkęPodstawowe funkcje
61
Ring 0 charakteryzował się najwyższymi przywilejami, a ring 3 — najniższymi. W architekturze ARM
przywileje są określone przez osiem trybów pracy:
(cid:31) USR — ang. user — tryb użytkownika,
(cid:31) FIQ — ang. fast interrupt request — tryb obsługujący przerwania o wysokich priorytetach,
(cid:31) IRQ — ang. interrupt request — tryb obsługi przerwań o niskim priorytecie,
(cid:31) SVC — ang. supervisor — tryb superużytkownika,
(cid:31) MON — ang. monitor — tryb monitorowania,
(cid:31) ABT — ang. abort — tryb obsługi wyjątków związanych z pamięcią,
(cid:31) UND — ang. undefined — tryb obsługi nieznanych wyjątków,
(cid:31) SYS — ang. system — tryb wykorzystywany przez system operacyjny.
Kod uruchomiony w danym trybie charakteryzuje się pewnymi uprawnieniami i dostępem do
rejestrów, do których aplikacje działające w innych trybach nie mają dostępu. Na przykład kod
uruchomiony w trybie USR nie może modyfikować rejestrów systemowych, które zwykle mogą
być modyfikowane tylko w trybie SVC. Tryb USR charakteryzuje się najmniejszą liczbą uprawnień.
Istnieje wiele różnic natury technicznej, jednak w dużym uproszczeniu można powiedzieć, że tryb
USR przypomina ring 3 znany z architektury x86, a tryb SVC jest odpowiednikiem ringu 0. Więk-
szość systemów operacyjnych implementuje instrukcje jądra w trybie SVC, a aplikacje użytkowni-
ka — w trybie USR. Dzieje się tak w przypadku systemów Windows i Linux.
W rozdziale 1. pisaliśmy, że procesory x64 mogą obsługiwać aplikacje 32-bitowe, 64-bitowe lub
oba rodzaje aplikacji. Podobnie procesory ARM mogą pracować w dwóch stanach: ARM i Thumb.
Te dwa stany obsługują różny zestaw instrukcji i nie mają nic wspólnego z poziomami uprawnień.
Na przykład kod uruchomiony w trybie SVC może być wykonywany w trybie ARM lub Thumb.
W trybie ARM instrukcje są zawsze 32-bitowe, a w trybie Thumb mogą być 16- lub 32-bitowe. Tryb
pracy procesora zależy od dwóch rzeczy:
(cid:31) Podczas rozgałęziania instrukcji BX i BLX najmniej znaczący bit docelowego rejestru może
przybrać wartość 1 — wtedy procesor jest przełączany w tryb Thumb. (Instrukcje mogą
być 2- lub 4-bajtowe. Procesor będzie ignorował najmniej znaczące bity, a więc nie zaistnieją
problemy z wyrównywaniem).
(cid:31) Jeżeli bit T znajdujący się w rejestrze stanu aktualnie wykonywanego programu (CPSR)
przyjmuje wartość 1, oznacza to, że procesor działa w trybie Thumb. Składnia rejestru CPSR
zostanie wyjaśniona w kolejnym podrozdziale, ale na razie możesz rozumieć ten rejestr
jako nieco bardziej rozbudowaną wersję rejestru EFLAGS, znanego Ci z architektury x86.
Procesor ARM jest uruchamiany w trybie ARM i pozostaje w tym trybie, dopóki nie zostanie
jawnie lub niejawnie przełączony w tryb Thumb. W praktyce wiele współczesnych systemów ope-
racyjnych korzysta głównie z kodu wykonywanego w trybie Thumb, ponieważ zapewnia on więk-
szą gęstość kodu (mieszanka instrukcji 16- i 32-bitowych może zajmować mniej pamięci od ciągu
instrukcji 32-bitowych). Aplikacje mogą działać w dowolnym trybie. Większość instrukcji Thumb i ARM
charakteryzuje się identycznymi mnemonikami, 32-bitowe instrukcje Thumb oznaczono sufiksem .w.
Kup książkęPoleć książkę62
Rozdział 2 (cid:31) Architektura ARM
Do(cid:401)(cid:273) cz(cid:295)sto tryb Thumb mylnie porównuje si(cid:295) do rzeczywistego trybu pracy
procesora o architekturze x68 lub x64, a tryb ARM — do trybu chronionego wymienionych
architektur. Jest to b(cid:371)(cid:295)dne rozumowanie. Wi(cid:295)kszo(cid:401)(cid:273) systemów operacyjnych uruchomionych
na platformach x86 i x64 dzia(cid:371)a w trybie chronionym i bardzo rzadko, o ile w ogóle tak si(cid:295)
dzieje, prze(cid:371)(cid:268)cza procesor w tryb rzeczywisty. W przypadku platformy ARM instrukcje mog(cid:268)
by(cid:273) wykonywane naprzemiennie w trybach ARM i Thumb. Zwró(cid:273) uwag(cid:295) równie(cid:463) na to,
(cid:463)e te tryby pracy nie maj(cid:268) nic wspólnego z trybami okre(cid:401)laj(cid:268)cymi uprawnienia, które
opisali(cid:401)my we wcze(cid:401)niejszym akapicie (USR, SVC itd.).
Istniej(cid:268) dwie wersje trybu Thumb: Thumb-1 i Thumb-2. Thumb-1 wyst(cid:295)powa(cid:371)
w architekturze ARMv6 i jej starszych wersjach. Instrukcje w tym trybie by(cid:371)y zawsze
1-bitowe. W trybie Thumb-2 dodano obs(cid:371)ug(cid:295) wi(cid:295)kszej liczby instrukcji, które mog(cid:268) by(cid:273)
16- lub 32-bitowe. Architektura ARMv7 wymaga Thumb-2, a wi(cid:295)c ilekro(cid:273) piszemy o Thumb,
mamy tak naprawd(cid:295) na my(cid:401)li Thumb-2.
Istniej(cid:268) równie(cid:463) inne ró(cid:463)nice mi(cid:295)dzy prac(cid:268) w trybie ARM a prac(cid:268) w trybie Thumb, lecz
nie mo(cid:463)emy tu omawia(cid:273) ich wszystkich. Niektóre instrukcje mog(cid:268) by(cid:273) na przyk(cid:371)ad dost(cid:295)pne
w trybie ARM, a niedost(cid:295)pne w trybie Thumb (lub na odwrót). Takie szczegó(cid:371)owe informacje
znajdziesz w oficjalnej dokumentacji architektury ARM.
Poza różnymi stanami pracy procesory ARM charakteryzują się również obsługą wykonywania
warunkowego. Oznacza to, że instrukcje mogą zawierać pewne warunki arytmetyczne, które muszą
zostać spełnione przed uruchomieniem instrukcji. Na przykład instrukcja może zawierać warunek
określający to, że może zostać wykonana tylko w sytuacji, gdy w efekcie działania poprzedniej in-
strukcji uzyskano 0. W przypadku procesorów x86 niemalże żadne instrukcje nie były obwarowane
warunkami. (Procesory firmy Intel posiadają kilka instrukcji, które bezpośrednio obsługują wyko-
nywanie warunkowe: CMOV i SETNE). Wykonywanie warunkowe jest przydatną funkcją, ponieważ
pozwala skrócić rozgałęzienia instrukcji (których wykonywanie zajmuje wiele cykli pracy procesora)
i zmniejszyć liczbę wykonywanych instrukcji (co umożliwia zwiększenie gęstości kodu). Wszystkie
instrukcje występujące w trybie ARM obsługują wykonywanie warunkowe, ale domyślnie są wyko-
nywane bezwarunkowo. W trybie Thumb wykonywanie warunkowe należy umożliwić za pomocą
specjalnej instrukcji IT.
Kolejną wyjątkową cechą architektury ARM jest obsługa przesunięcia bitowego. Niektóre in-
strukcje mogą „zawierać” inne instrukcje arytmetyczne, które przestawiają lub obracają zawartość
rejestru. Jest to przydatne rozwiązanie, ponieważ umożliwia wykonanie niektórych operacji za pomocą
jednej instrukcji (pozwala uniknąć stosowania całego szeregu instrukcji). Możesz chcieć pomnożyć
przez 2 zawartość jakiegoś rejestru, a następnie zapisać wynik operacji w innym rejestrze. Normalnie
wymagałoby to wykonania dwóch instrukcji (mnożenia, a następnie zapisania wartości w innym
rejestrze), lecz dzięki przesunięciu bitowemu możesz zawrzeć działanie mnożenia (przesunięcie
bitów o jedną pozycję w lewo) w instrukcji MOV. Taka instrukcja wyglądałaby następująco:
MOV R1, R0, LSL #1 ; R1 = R0·2
Kup książkęPoleć książkęTypy danych i rejestry
63
Typy danych i rejestry
Podobnie jak języki wysokiego poziomu architektura ARM obsługuje operacje na różnych typach
danych. Obsługiwane są następujące typy danych: byte (8 bitów), half-word (16 bitów), word (32
bity) i double word (64 bity).
W architekturze ARM zdefiniowano 16 32-bitowych rejestrów ogólnego przeznaczenia, które
oznaczono R0, R1, R2, …, R15. Programiści tworzący aplikacje mogą korzystać z tych wszystkich re-
jestrów, ale w praktyce tylko 12 pierwszych może być naprawdę wykorzystywanych w roli reje-
strów ogólnego przeznaczenia (w przypadku architektury x86 wspomnianą rolę odgrywały rejestry
takie jak EAX i EBX). Trzy ostatnie rejestry architektury ARM pełnią pewne określone funkcje:
(cid:31) Rejestr R13 definiuje wskaźnik stosu (SP). W architekturach x86 i x64 taką samą rolę
odgrywały rejestry ESP i RSP. Wskaźnik stosu wskazuje wierzchołek stosu programu.
(cid:31) R14 można określić mianem rejestru łączącego (LR). Standardowo podczas uruchamiania
funkcji zapisywany jest w nim adres zwrotny. Niektóre instrukcje bezwarunkowo korzystają
z tego rejestru. Na przykład instrukcja BL zawsze zapisuje adres zwrotny w rejestrze LR przed
wykonaniem kolejnego rozgałęzienia. Rejestru o identycznej funkcji nie przewidziano
w architekturach x86 i x64, ponieważ w przypadku tych architektur adres zwrotny jest
zawsze przechowywany na stosie. Jeżeli jakiś kod nie zapisuje adresu zwrotnego w rejestrze
LR, to może korzystać z tego rejestru jak ze zwyczajnego rejestru ogólnego przeznaczenia.
(cid:31) Rejestr R15 można określić mianem licznika rozkazów (PC). Podczas pracy w trybie ARM
w rejestrze tym znajduje się adres obecnie przetwarzanej instrukcji plus 8 (2 instrukcje ARM
do przodu). W czasie pracy w trybie Thumb w rejestrze tym znajduje się adres obecnie
przetwarzanej instrukcji plus 4 (2 16-bitowe instrukcje Thumb do przodu). Rejestr ten
działa podobnie jak rejestry EIP i RIP , występujące w architekturach x86 i x64, z tym
wyjątkiem, że przytoczone rejestry zawsze zawierają adres kolejnej instrukcji, która ma
zostać wykonana. Kolejna ważna różnica jest taka, że kod może bezpośrednio zapisywać
dane w rejestrze PC i bezpośrednio je z niego odczytywać. Zapisanie adresu w rejestrze PC
spowoduje natychmiastowe uruchomienie instrukcji znajdującej się pod tym adresem.
Warto przeanalizować ten szczegół. Przyjrzyj się fragmentowi kodu, który ma zostać
wykonany w trybie Thumb:
1: 0x00008344 push {lr}
2: 0x00008346 mov r0, pc
3: 0x00008348 mov.w r2, r1, lsl #31
4: 0x0000834c pop {pc}
Po wykonaniu 2. linii kodu w rejestrze R0 będzie się znajdowała wartość 0x0000834a
(=0x00008346+4):
(gdb) br main
rozkaz przerwania 1 pod adresem 0x8348
...
rozkaz przerwania 1, 0x00008348 w funkcji main ()
(gdb) disas main
zrzut kodu asemblera funkcji main:
0x00008344 +0 : push {lr}
0x00008346 +2 : mov r0, pc
Kup książkęPoleć książkę64
Rozdział 2 (cid:31) Architektura ARM
= 0x00008348 +4 : mov.w r2, r1, lsl #31
0x0000834c +8 : pop {pc}
0x0000834e +10 : lsls r0, r0, #0
koniec zrzutu kodu asemblera
(gdb) info register pc
pc 0x8348 0x8348 main+4
(gdb) info register r0
r0 0x834a 33610
Pod adresem 0x00008348 ustawiono rozkaz przerwania. Przyjrzyjmy się zawartości rejestrów PC
i R0. Jak widać, zawartość rejestru PC wskazuje na 3. instrukcję (znajdującą się pod adresem
0x00008348), która ma za chwilę zostać wykonana — w rejestrze R0 znajduje się wartość wcześniej
wczytana z rejestru PC. Przykład ten ilustruje, że podczas bezpośredniego odczytu rejestru PC za-
chowuje się on zgodnie z definicją, przy czym w czasie debugowania rejestr PC zawiera wskaźnik
instrukcji, która ma zostać wykonana jako kolejna.
Dzieje się tak z powodu zachowania przez architekturę ARM wstecznej zgodności z wykony-
waniem potokowym w starszych procesorach, które pobierały 2 instrukcje do przodu względem
aktualnie przetwarzanej instrukcji. W obecnie produkowanych procesorach ARM przetwarzanie
potokowe jest o wiele bardziej skomplikowanym procesem, niemniej jednak zachowano również
wspomnianą definicję przetwarzania potokowego w celu zapewnienia zgodności z produkowanymi
wcześniej procesorami.
Podobnie jak ma to miejsce w przypadku innych architektur, procesory ARM przechowują in-
formacje o obecnie przetwarzanym procesie w rejestrze stanu aktualnie wykonywanego programu
(CPSR). Z punktu widzenia programisty rejestr CPSR działa podobnie jak rejestry EFLAGS i RFLAGS
w architekturach x86 i x64. W niektórych dokumentacjach znajdziesz również informacje na temat
rejestru statusu aktualnie wykonywanej aplikacji (APSR), który jest nazwą umowną zbioru niektórych
pól znajdujących się w rejestrze CPSR. Rejestr CPSR zawiera wiele flag. Niektóre z nich przedstawiono
na rysunku 2.1 (pozostałe flagi zostaną omówione w dalszej części tego rozdziału).
(cid:31) E (bit określający porządek bajtów) — Procesory ARM mogą obsługiwać dane zapisane
w formacie big-endian lub little-endian. Jeżeli temu bitowi przypisze się wartość 0, to
procesor będzie obsługiwał dane w formacie little-endian, a jeżeli przypisze mu się wartość 1,
to procesor będzie obsługiwał dane w formacie big-endian. Przez większość czasu korzysta
się z danych zapisanych w formacie little-endian, a więc bit ten będzie przyjmował zwykle
wartość 0.
(cid:31) T (bit trybu Thumb) — Po przypisaniu temu bitowi wartości 1 procesor przełączany jest
w tryb Thumb. W przeciwnym wypadku będzie on pracował w trybie ARM. Jednym ze
sposobów na jawne przełączanie między trybami Thumb i ARM jest modyfikacja tego bitu.
(cid:31) M (bity trybów) — Te bity określają aktualne uprawnienia (USR, SVC itd.).
Rysunek 2.1.
Kup książkęPoleć książkęOpcje systemu i ustawienia
65
Opcje systemu i ustawienia
W architekturze ARM można spotkać się z koprocesorami obsługującymi dodatkowe instrukcje
i funkcje na poziomie systemu. Na przykład w systemie obsługującym jednostkę zarządzania pamię-
cią (MMU) dostęp do jej ustawień musi uzyskać kod rozruchowy i kod jądra systemu operacyjnego.
W przypadku architektur x86 i x64 ustawienia te są zapisywane w rejestrze CR0 lub CR4. W archi-
tekturze ARM ustawienia te są przechowywane przez koprocesor numer 15. W architekturze ARM
przewidziano 16 koprocesorów. Są one identyfikowane za pomocą numerów: CP0, CP1, …, CP15.
(W kodzie oznacza się je nazwami: P0, …, P15). Pierwszych 13 koprocesorów jest opcjonalnych albo
zarezerwowanych przez architekturę ARM. Opcjonalne koprocesory mogą zostać użyte przez pro-
ducentów w celu implementacji przewidzianych przez nich specjalnych instrukcji lub funkcji. Na
przykład koprocesory CP10 i CP11 są zwykle używane podczas wykonywania operacji na liczbach
zmiennoprzecinkowych. Obsługują one również technologię NEON. Każdy koprocesor ma swoje
„kody operacyjne” i rejestry, które mogą być obsługiwane przez specjalne instrukcje ARM. Kopro-
cesory CP14 i CP15 są używane podczas debugowania, a także przechowują ustawienia systemowe. Ko-
procesor CP15 można określić mianem koprocesora sterującego pracą systemu — przechowywane są
w nim prawie wszystkie ustawienia systemu (ustawienia zapisywania w pamięci podręcznej, stro-
nicowania, obsługi wyjątków itp.).
Technologia NEON zapewnia obs(cid:371)ug(cid:295) instrukcji SIMD (pojedynczych instrukcji
przetwarzaj(cid:268)cych wiele danych). Rozwi(cid:268)zanie takie przydaje si(cid:295) w aplikacjach multimedialnych.
W architekturze x86 zestawy wspomnianych instrukcji s(cid:268) zapewniane przez technologie
SSE i MMX.
Każdy koprocesor posiada 16 rejestrów i 8 kodów operacyjnych. Składnia tych rejestrów i ko-
dów operacyjnych jest charakterystyczna dla danego koprocesora. Dostęp do koprocesora może
być uzyskany tylko za pomocą instrukcji MRC (odczyt) i MCR (zapis). Instrukcje te w roli argumentów
przyjmują numer koprocesora, numer rejestru i kod operacji. Na przykład w celu odczytania bazy
rejestru translacji (w architekturach x86 i x64 podobną funkcję pełnił rejestr CR3) i zapisania od-
czytanych danych w rejestrze R0 należy posłużyć się następującym kodem:
MRC p15, 0, r0, c2, c0, 0 ; zapisz TTBR w r0
Kod ten spowoduje „odczytanie rejestru C2/C0 koprocesora numer 15 za pomocą kodu operacji
0/0 i zapisanie odczytanej wartości w rejestrze ogólnego przeznaczenia R0”. W każdym koproceso-
rze znajduje się wiele rejestrów, a dodatkowo każdy koprocesor obsługuje wiele kodów operacji,
tak więc musisz zapoznać się z dokumentacją. Tylko to pozwoli Ci dokładnie zrozumieć znaczenie
każdej instrukcji. Niektóre rejestry (C13/C0) są zarezerwowane dla systemu operacyjnego — umiesz-
czane są tam dane dotyczące danego procesu lub wątku.
Instrukcje MRC i MCR nie wymagają wysokiego poziomu uprawnień (można je uruchomić w trybie
USR), przy czym niektóre rejestry koprocesorów i kody operacji mogą być obsługiwane tylko w trybie
SVC. Próba odczytania pewnych rejestrów bez odpowiednich uprawnień zakończy się wyjątkiem.
Kup książkęPoleć książkę66
Rozdział 2 (cid:31) Architektura ARM
W praktyce bardzo rzadko spotyka się instrukcje tego typu zastosowane w kodzie uruchamianym
przez użytkownika. Znacznie częściej można je znaleźć w programach rozruchowych, firmwarze,
niskopoziomowym kodzie zapisanym w pamięciach ROM lub kodzie jądra systemu.
Instrukcje — wprowadzenie
Teraz możesz już przystąpić do analizy ważnych instrukcji występujących w architekturze ARM.
Poza obsługą wykonywania warunkowego i przesuwania bitów architektura ARM obsługuje rów-
nież pewne instrukcje, które nie mają swoich odpowiedników w architekturze x86. Po pierwsze,
niektóre instrukcje mogą wykonać sekwencję operacji na podanym zakresie rejestrów. Przykłado-
wo, aby zapisać zawartość 5 rejestrów (np. R6 – R10) pod określonym adresem, zapisanym w reje-
strze R1, możliwe jest zastosowanie instrukcji STM R1, {R6-R10}. Zawartość rejestru R6 zostanie za-
pisana pod adresem R1, zawartość rejestru R7 — pod adresem R1+4, a zawartość adresu R8 — pod
adresem R1+8. Rejestry, które nie mają kolejnych numerów, należy oddzielać od siebie przecinkami
(np. {R1,R5,R8}). W składni języka asemblera ARM zakresy rejestrów podaje się zwykle w nawia-
sach klamrowych. Po drugie, niektóre instrukcje mogą opcjonalnie uaktualniać rejestr bazowy po
wykonaniu operacji zapisu lub odczytu. Aby skorzystać z tej funkcji, należy po nazwie rejestru do-
dać wykrzyknik (!). Na przykład gdybyś podaną wcześniej instrukcję zapisał w następujący sposób:
STM R1!,{R6-R10} i uruchomił ją, to zawartość rejestru R1 zostałaby uaktualniona — umieszczono
by w nim kolejny adres po adresie, pod którym zapisano zawartość rejestru R10. Poniższy przykład
wyjaśnia tę zasadę. Przeanalizuj go.
01: (gdb) disas main
02: zrzut kodu asemblera funkcji main:
03: = 0x00008344 +0 : mov r6, #10
04: 0x00008348 +4 : mov r7, #11
05: 0x0000834c +8 : mov r8, #12
06: 0x00008350 +12 : mov r9, #13
07: 0x00008354 +16 : mov r10, #14
08: 0x00008358 +20 : stmia sp!, {r6, r7, r8, r9, r10}
09: 0x0000835c +24 : bx lr
10: koniec zrzutu asemblera
11: (gdb) si
12: 0x00008348 w funkcji main ()
13: ...
14: 0x00008358 w funkcji main ()
15: (gdb) info reg sp
16: sp 0xbedf5848 0xbedf5848
17: (gdb) si
18: 0x0000835c w funkcji main ()
19: (gdb) info regsp
20: sp 0xbedf585c 0xbedf585c
21: (gdb) x/6x 0xbedf5848
22: 0xbedf5848: 0x0000000a 0x0000000b 0x0000000c
0x0000000d
23: 0xbedf5858: 0x0000000e 0x00000000
Kup książkęPoleć książkęŁadowanie i zapisywanie danych
67
W 15. linii wyświetlono zawartość rejestru SP (0xbedf5848) przed uruchomieniem instrukcji
STM. Instrukcję tę uruchomiono w wierszach 17. i 19. — w wierszu 19. znajduje się uaktualniona
zawartość rejestru SP. W 21. linii kodu wyświetlono sześć wartości typu word, zaczynając od starej
zawartości rejestru SP. Zwróć uwagę na to, że zawartość R6 była zapisana pod starym adresem rejestru
SP, zawartość R7 pod adresem SP+0x4, R8 pod adresem SP+0x8, R9 pod SP+0xc, a R10 pod SP+0x10.
Nowy adres przypisany rejestrowi SP (0xbedf585c) jest kolejnym adresem po adresie, pod którym
zapisano R10.
STMIA i STMEA s(cid:268) pseudoinstrukcjami instrukcji STM (daj(cid:268) one ten sam efekt).
Dezasemblery wy(cid:401)wietlaj(cid:268) jedn(cid:268) z nich. Niektóre b(cid:295)d(cid:268) wy(cid:401)wietla(cid:273) STMEA, je(cid:463)eli bazowym
rejestrem jest SP, a w kontek(cid:401)cie innych rejestrów b(cid:295)dzie wy(cid:401)wietlana pseudoinstrukcja
STMIA. Inne dezasemblery b(cid:295)d(cid:268) pos(cid:371)ugiwa(cid:273) si(cid:295) instrukcj(cid:268) STM, a jeszcze inne b(cid:295)d(cid:268) zawsze
wy(cid:401)wietla(cid:273) pseudoinstrukcj(cid:295) STMIA. Nie ma jednej, ogólnie przyj(cid:295)tej konwencji. Je(cid:463)eli
u(cid:463)ywasz wielu dezasemblerów, to musisz si(cid:295) do tego przyzwyczai(cid:273).
Ładowanie i zapisywanie danych
W jednym z poprzednich podrozdziałów stwierdziliśmy, że architektura ARM posługuje się mo-
delem load-store (pol. „załaduj z pamięci — zapisz w pamięci”) — przed wykonaniem operacji na
danych muszą one zostać umieszczone w rejestrze. Dostęp do pamięci mają tylko instrukcje od-
czytujące dane z pamięci i zapisujące w niej dane. Wszystkie pozostałe instrukcje mogą przetwarzać
wyłącznie zawartość rejestrów. Ładowanie danych z pamięci polega na odczytaniu ich i zapisaniu
w rejestrze. Natomiast zapisywanie danych w pamięci polega na odczytaniu ich z rejestru i umiesz-
czeniu pod określonym adresem. Pary instrukcji LDR-STR, LDM-STM i PUSH-POP służą do odczytywania
i zapisywania danych.
Instrukcje LDR i STR
Instrukcje te mogą odczytać z pamięci bądź zapisać w niej 1 bajt, 2 lub 4 bajty danych. Ich pełna
składnia jest dość złożona — istnieje kilka różnych sposobów określania przesunięcia, a także uaktual-
niania rejestru bazowego. Przeanalizuj najprostszy przypadek:
01: 03 68 LDR R3,[R0] ;R3 = *R0
02: 23 60 STR R3,[R4] ;*R4 = R3;
W instrukcji widocznej w 1. linii R0 jest rejestrem bazowym, a R3 — docelowym. Instrukcja ta
ładuje wartość typu word z adresu R0 do R3. W instrukcji widocznej w 2. linii R4 jest rejestrem ba-
zowym, a R3 — docelowym. Instrukcja ta zapisuje wartość przypisaną rejestrowi R3 pod adresem
wskazywanym przez rejestr R4. Jest to prosty przykład, ponieważ adres pamięci jest określany przez
rejestr bazowy.
Podstawowymi argumentami przyjmowanymi przez instrukcje LDR i STR są rejestr bazowy i prze-
sunięcie. Przesunięcie może być podane w trzech formach, a każda z tych form może być wyrażona
Kup książkęPoleć książkę68
Rozdział 2 (cid:31) Architektura ARM
w trzech trybach. Zaczniemy od omówienia trzech form przesunięcia, jakimi są: bezpośredni adres,
rejestr i rejestr skalowany.
W pierwszej formie przesunięcia w roli bezpośredniego adresu podawana jest po prostu war-
tość typu integer. Jest to wartość uzyskana w wyniku operacji dodawania wartości przesunięć do
rejestru bazowego lub odejmowania wartości przesunięć od tego rejestru — w ten sposób możliwe
jest uzyskanie dostępu do danych, których przesunięcie jest znane w momencie kompilacji pro-
gramu. Technika ta jest najczęściej stosowana w celu uzyskania dostępu do określonego pola struktury
lub tablicy metod wirtualnych. Ogólnie przyjęto następujący sposób jej zapisu:
(cid:31) STR Ra, [Rb, imm]
(cid:31) LDR Ra, [Rc, imm]
Rb jest adresem rejestru bazowego, a imm jest przesunięciem dodawanym do Rb.
Załóżmy, że na przykład w rejestrze R0 zapisano wskaźnik struktury KDPC. Przyjrzyj się przed-
stawionemu kodowi:
Definicja struktury
0:000 dt ntkrnlmp!_KDPC
+0x000 Type : UChar
+0x001 Importance : UChar
+0x002 Number : Uint2B
+0x004 DpcListEntry : _LIST_ENTRY
+0x00c DeferredRoutine : Ptr32 void
+0x010 DeferredContext : Ptr32 Void
+0x014 SystemArgument1 : Ptr32 Void
+0x018 SystemArgument2 : Ptr32 Void
+0x01c DpcData : Ptr32 Void
Kod
01: 13 23 MOVS R3, #0x13
02: 03 70 STRB R3, [R0]
03: 01 23 MOVS R3, #1
04: 43 70 STRB R3, [R0,#1]
05: 00 23 MOVS R3, #0
06: 43 80 STRH R3, [R0,#2]
07: C3 61 STR R3, [R0,#0x1C]
08: C1 60 STR R1, [R0,#0xC]
09: 02 61 STR R2, [R0,#0x10]
Tym razem R0 jest rejestrem bazowym, a wartościami informującymi o przesunięciu są: 0x1,
0x2, 0xC, 0x10 i 0x1C. Zaprezentowany fragment kodu można przedstawić za pomocą języka C w taki
sposób:
KDPC *obj = ...; /* R0 jest obiektem obj.*/
obj- Type = 0x13;
obj- Importance = 0x1;
obj- Number = 0x0;
obj- DpcData = NULL;
obj- DeferredRoutine = R1; /* Nie znamy R1 .*/
obj- DeferredContext = R2; /* Nie znamy R2. */
Kup książkęPoleć książkęŁadowanie i zapisywanie danych
69
Taka forma zapisu przesunięcia przypomina instrukcję MOV Reg, [Reg+Imm], znaną z architek-
tur x86 i x64.
Przesunięcie można również wyrazić za pomocą rejestru. Technikę tę stosuje się często w przy-
padku kodu, który musi uzyskać dostęp do tablicy o indeksie określanym w trakcie działania pro-
gramu. Zapis tego typu przesunięcia ma ogólny format:
(cid:31) STR Ra, [Rb, Rc]
(cid:31) LDR Ra, [Rb, Rc]
W zależności od kontekstu Rb i Rc mogą pełnić funkcję adresu bazowego lub przesunięcia. Przyjrzyj
się tym dwóm przykładom:
Przykład 1.
01: 03 F0 F2 FA BL strlen
02: 06 46 MOV R6,R0
; R0 jest warto(cid:286)ci(cid:261) zwracan(cid:261) przez funkcj(cid:266) strlen.
03: ...
04: BB 57 LDRSB R3, [R7,R6]
; W tym przypadku R6 definiuje przesuni(cid:266)cie.
Przykład 2.
01: B3 EB 05 08 SUBS.W R8, R3, R5
02: 2F 78 LDRB R7, [R5]
03: 18 F8 05 30 LDRB.W R3, [R8,R5]
; W tym przyk(cid:225)adzie R5 jest adresem bazowym, a R8 przesuni(cid:266)ciem.
04: 9F 42 CMP R7, R3
Taka forma zapisu przesunięcia przypomina instrukcję MOV Reg, [Reg+Reg], znaną z architektur
x86 i x64.
Trzecia forma określająca przesunięcie korzysta z rejestru skalowanego. Technika ta jest stoso-
wana często w kontekście pętli iterującej tablicę. Przesunięcie jest skalowane za pomocą operacji
przesuwania bitów. Ogólnie technikę tę można zapisać za pomocą następującego kodu:
(cid:31) LDR Ra, [Rb, Rc, element okre(cid:258)laj(cid:200)cy przesuni(cid:218)cie ]
(cid:31) STR Ra, [Rb, Rc, element okre(cid:258)laj(cid:200)cy przesuni(cid:218)cie ]
Rb jest rejestrem bazowym, Rc adresem bezpośrednim, a element okre(cid:258)laj(cid:200)cy przesuni(cid:218)cie
definiuje operację wykonywaną na Rc — zwykle są to operacje przesunięcia w lewo lub w prawo,
które mają na celu przeskalowanie przesunięcia. Na przykład:
01: 0E 4B LDR R3, =KeNumberNodes
02: ...
03: 00 24 MOVS R4, #0
04: 19 88 LDRH R1, [R3]
05: 09 48 LDR R0, =KeNodeBlock
06: 00 23 MOVS R3, #0
07: loop_start
08: 50 F8 23 20 LDR.W R2, [R0,R3,LSL#2]
09: 00 23 MOVS R3, #0
10: A2 F8 90 30 STRH.W R3, [R2,#0x90]
Kup książkęPoleć książkę70
Rozdział 2 (cid:31) Architektura ARM
11: 92 F8 89 30 LDRB.W R3, [R2,#0x89]
12: 53 F0 02 03 ORRS.W R3, R3,#2
13: 82 F8 89 30 STRB.W R3, [R2,#0x89]
14: 63 1C ADDS R3, R4, #1
15: 9C B2 UXTH R4, R3
16: 23 46 MOV R3, R4
17: 8C 42 CMP R4, R1
18: EF DB BLT loop_start
KeNumberNodes jest globalną zmienną typu integer, a KeNodeBlock — globalną tablicą wskaźni-
ków KNODE.
Zmienne te są ładowane do rejestrów przez kod zapisany w liniach numer 1 i 5 (składnię tych
linii wyjaśnimy później). Kod umieszczony w 8. wierszu iteruje tablicę KeNodeBlock (R0 jest bazą, R3
jest indeksem mnożonym przez 2, ponieważ mamy do czynienia z tablicą wskaźników, a na tej
platformie wskaźniki są 4-bajtowe). W wierszach oznaczonych numerami 10 – 13 inicjowane są
niektóre pola elementu KNODE. W 14. linii inkrementowany jest indeks. W 17. linii indeks jest po-
równywany z rozmiarem tablicy (R1 określa rozmiar tablicy — zobacz linię numer 4). Jeżeli indeks
jest mniejszy od rozmiaru tablicy, to pętla jest nadal wykonywana.
Kod ten w języku C można ogólnie wyrazić w następujący sposób:
int KeNumberNodes = …;
KNODE *KeNodeBlock[KeNumberNodes] = …;
for (int i=0; i KeNumberNodes; i++) {
KeNodeBlock[i].x = …;
KeNodeBlock[i].y= …;
…
}
Taka forma zapisu przesunięcia przypomina instrukcję MOV Reg, [reg+idx*scale], znaną z ar-
chitektur x86 i x64.
Omówiliśmy trzy formy przedstawiania przesunięcia. Teraz czas przyjrzeć się trybom adresowania:
bezpośredniemu, przedindeksowemu i poindeksowemu. (W niektórych publikacjach wspomniane
wcześniej tryby określane są mianem trybu przedindeksowego, przedindeksowego z buforowaniem
i postindeksowego. Zastosowana przez nas terminologia jest zgodna z terminologią użytą w ofi-
cjalnej dokumentacji architektury ARM). Wymienione tryby adresowania różnią się jedynie mo-
dyfikacją rejestru bazowego. We wszystkich wcześniejszych przykładach modyfikowano rejestr ba-
zowy, działając w trybie adresowania bezpośredniego. Tryb ten jest najczęściej stosowany. Łatwo
rozpoznać go po tym, że w kodzie asemblera nie ma nigdzie wykrzyknika (!), a bezpośredni adres ba-
zowy podany jest w nawiasach kwadratowych. Ogólna składnia tego trybu adresowania jest taka:
LDR Rd, [Rn, offset].
W trybie adresowania przedindeksowego rejestr bazowy będzie uaktualniony przed operacją,
w której zostanie użyty. Semantyka takiego wyrażenia przypomina stosowanie w jednoskładnikowych
operacjach prefiksów ++ i --, znanych z języka C. Omawiany tryb adresowania można przedstawić
za pomocą ogólnej składni: LDR Rd, [Rn, offset]!. Na przykład:
12 F9 01 3D LDRSB.W R3, [R2 ,#-1]! ; R3 = *(R2-1)
; R2 = R2-1
Kup książkęPoleć książkęŁadowanie i zapisywanie danych
71
W trybie adresowania poindeksowego rejestr bazowy jest używany w roli ostatecznego adresu,
a następnie jest uaktualniany — dodaje się do niego wartość przesunięcia. Przypomina to notację
przyrostkową języka C (++ i --), stosowaną w jednoskładnikowych operacjach. Omawiany tryb ad-
resowania można przedstawić za pomocą ogólnej składni: LDR Rd,[Rn], offset. Na przykład:
10 F9 01 6B LDRSB.W R6,[R0],#1 ; R6 = *R0
; R0 = R0+1
Formy adresowania przedindeksowego i poindeksowego są zwykle spotykane w kodzie, który
uzyskuje wielokrotnie dostęp do danych znajdujących się w tym samym buforze. Na przykład taki
kod może zawierać pętlę sprawdzającą, czy dany znak łańcucha jest jednym z pięciu poszukiwa-
nych znaków. Kompilator może wtedy zastosować technikę adresowania rejestru bazowego odpo-
wiednią dla instrukcji inkrementacji.
Oto wskazówka, która u(cid:371)atwi Ci rozpoznawanie ró(cid:463)nych trybów adresowania
stosowanych w instrukcjach LDR i STR. Je(cid:463)eli widzisz znak !, to znaczy, (cid:463)e jest to tryb
przedindeksowy. Natomiast je(cid:463)eli w nawiasach kwadratowych uj(cid:295)to wy(cid:371)(cid:268)cznie rejestr
bazowy, to znaczy, (cid:463)e jest to tryb poindeksowy. We wszystkich pozosta(cid:371)ych przypadkach
b(cid:295)dziesz mie(cid:273) do czynienia z trybem bezpo(cid:401)redniego okre(cid:401)lania przesuni(cid:295)cia.
Inne zastosowania instrukcji LDR
Wcześniej pisaliśmy, że instrukcja LDR służy do wczytywania danych z pamięci do rejestru — czasem
jednak można ją spotkać w następujących formach:
01: DF F8 50 82 LDR.W R8, =0x2932E00 ; LDR R8, [PC, x]
02: 80 4A LDR R2, =a04d ; 04d ; LDR R2, [PC, y]
03: 0E 4B LDR R3, =__imp_realloc ; LDR R3, [PC, z]
Zgodnie z uwagami zamieszczonymi wcześniej w tym podrozdziale taka składnia nie jest po-
prawna. Technicznie rzecz biorąc, są to pseudoinstrukcje — instrukcje tego typu są stosowane
przez dezasemblery w celu ułatwienia użytkownikowi przeglądania kodu. Wewnętrznie korzystają
one z formy bezpośredniej instrukcji LDR — w roli rejestru bazowego zastosowano rejestr PC.
Rozwiązanie takie można określić mianem adresowania PC-relative (jest to odpowiednik adreso-
wania RIP-relative w architekturze x64). W architekturze ARM zwykle spotyka się literał zlokali-
zowany w obszarze pamięci przeznaczonym do zapisu stałych, łańcuchów i informacji o przesunię-
ciach. Dostęp do tego obszaru pamięci można uzyskać w sposób niezależny od jego pozycji. (Literał
jest częścią kodu, a więc będzie znajdował się w tej samej sekcji). W podanym wcześniej fragmencie
kod odwołuje się do 32-bitowej stałej, łańcucha i danych dotyczących przesunięcia importowanej
funkcji zapisanej w literale. Zaprezentowana pseudoinstrukcja jest przydatna, ponieważ pozwala na
przeniesienie 32-bitowej stałej do rejestru za pomocą pojedynczej instrukcji. W zrozumieniu tego
może pomóc Ci kolejny fragment kodu:
Kup książkęPoleć książkę72
Rozdział 2 (cid:31) Architektura ARM
01: .text:0100B134 35 4B LDR R3, =0x68DB8BAD
; Jest to tak naprawd(cid:266) instrukcja LDR R3, [PC, #0xD4],
; teraz PC = 0x0100B138.
02: ...
03: .text:0100B20C AD 8B DB 68 dword_100B20C DCD 0x68DB8BA
Jak dezasembler skrócił pierwszą instrukcję z LDR R3, [PC, #0xD4] i przedstawił ją w alternatywnej
formie? Taka operacja mogła być wykonana, ponieważ kod działa w trybie Thumb, a w rejestrze PC za-
pisano adres obecnie wykonywanej instrukcji plus 4, czyli 0x0100B138. Kod korzysta z bezpośredniej
formy adresowania — odczytywane są dane typu word z adresu 0x0100B20C (=0x100B138+0xD4), a aku-
rat tam znajduje się stała, którą chcemy załadować.
Inną podobną instrukcją jest ADR, która uzyskuje adres etykiety lub funkcji i umieszcza go w reje-
strze. Na przykład:
01: 00009390 65 A5 ADR R5, dword_9528
02: 00009392 D5 E9 00 45 LDRD.W R4, R5, [R5]
03: ...
04: 00009528 00 CE 22 A9+dword_9528 DCD 0xA922CE00 , 0xC0A4
Ta instrukcja jest zwykle stosowana w celu implementacji tablic skoków lub wywołań zwrot-
nych — tam, gdzie niezbędne jest przekazanie adresu funkcji do innej funkcji. Procesor, wykonując
tę instrukcję, oblicza przesunięcie względem rejestru PC i zapisuje je w rejestrze docelowym.
Instrukcje LDM i STM
Instrukcje LDM i STM działają podobnie do instrukcji LDR i STR, ale mogą odczytywać lub zapisywać
wiele danych typu word znajdujących się pod adresem bazowym. Przydają się podczas przenoszenia
wielu bloków danych do i z pamięci. Mają one ogólną składnię:
(cid:120)
(cid:120)
LDM mode Rn[!], {Rm}
STM mode Rn[!], {Rm}
Rn jest rejestrem bazowym przechowującym adres, z którego dane będą odczytywane lub pod
którym będą zapisywane. Dodatkowy wykrzyknik (!) informuje o tym, że rejestr bazowy powinien
zostać uaktualniony przez nowy (zwrócony) adres. Rm jest zakresem rejestrów, do których dane zostaną
wczytane lub z których zostaną zapisane. Opisywane instrukcje mogą działać w czterech trybach:
(cid:31) IA (inkrementuj po) — Zapis danych rozpoczyna się od komórki pamięci wskazywanej
przez adres bazowy. Zwracany jest adres umiejscowiony 4 bajty nad ostatnio zwróconym
adresem. Jest to tryb domyślny, używany, gdy nie określono żadnego innego trybu.
(cid:31) IB (inkrementuj przed) — Zapis danych rozpoczyna się od komórki pamięci umiejscowionej
4 bajty nad adresem bazowym. Zwracany jest adres komórki pamięci, w której zapisano dane.
(cid:31) DA (dekrementuj po) — Zapis danych kończy się w komórce pamięci wskazywanej przez
adres bazowy. Zwracany jest adres 4 bajty poniżej najniższego adresu, pod którym zapisano
dane.
(cid:31) DB (dekrementuj przed) — Zapis danych kończy się w komórce pamięci położonej 4 bajty
poniżej adresu bazowego. Zwracany jest adres pierwszej komórki pamięci.
Kup książkęPoleć książkęŁadowanie i zapisywanie danych
73
Na początku może się to wydawać dość skomplikowane, a więc przeanalizujmy następujący
przykład na podstawie debugera:
01: (gdb) br main
02: punkt wstrzymania 1: 0x8344
03: (gdb) disas main
04: zrzut kodu asemblera funkcji main:
05: 0x00008344 +0 : ldr r6, =mem ; Nieco zmodyfikowano.
06: 0x00008348 +4 : mov r0, #10
07: 0x0000834c +8 : mov r1, #11
08: 0x00008350 +12 : mov r2, #12
09: 0x00008354 +16 : ldm r6, {r3, r4, r5}; tryb IA
10: 0x00008358 +20 : stm r6, {r0, r1, r2}; tryb IA
11: ...
12: (gdb) r
13: punkt wstrzymania 1, 0x00008344 w funkcji main ()
14: (gdb) si
15: 0x00008348 w funkcji main ()
16: (gdb) x/3x $r6
17: 0x1050c mem : 0x00000001 0x00000002 0x00000003
18: (gdb) si
19: 0x0000834c w funkcji main ()
20: ...
21: (gdb)
22: 0x00008358 w funkcji main ()
23: (gdb) info reg r3 r4 r5
24: r3 0x1 1
25: r4 0x2 2
26: r5 0x3 3
27: (gdb) si
28: 0x0000835c w funkcji main ()
29: (gdb) x/3x $r6
30: 0x1050c mem : 0x0000000a 0x0000000b 0x0000000
W 5. linii kodu adres pamięci zapisano w rejestrze R6. Pod tym adresem (0x1050c) znajdują się
trzy elementy typu word (zobacz linia oznaczona numerem 17). W liniach o numerach 6 – 8 reje-
strom R0 – R2 przypisano pewne stałe. W 9. wierszu kodu trzy elementy typu word są ładowane do
rejestrów R3 – R5, zaczynając od komórki pamięci określonej przez zawartość rejestru R6. W 29. wier-
szu kodu widzimy, że właściwe wartości zostały zapisane. Operacje te zilustrowano na rysunku 2.2.
Oto ten sam eksperyment, ale tym razem skorzystano z możliwości zwracania adresu:
01: (gdb) br main
02: punkt wstrzymania 1: 0x8344
03: (gdb) disas main
04: zrzut kodu asemblera funkcji main:
05: 0x00008344 +0 : ldr r6, =mem ; Nieco zmodyfikowano.
06: 0x00008348 +4 : mov r0, #10
07: 0x0000834c +8 : mov r1, #11
08: 0x00008350 +12 : mov r2, #12
09: 0x00008354 +16 : ldm r6!, {r3, r4, r5}; tryb IA ze zwracaniem adresu
10: 0x00008358 +20 : stmia r6!, {r0, r1, r2}; tryb IA ze zwracaniem adresu
11: ...
12: (gdb) r
Kup książkęPoleć książkę74
Rozdział 2 (cid:31) Architektura ARM
Rysunek 2.2.
13: punkt wstrzymania 1, 0x00008344 w funkcji main ()
14: (gdb) si
15: 0x00008348 w funkcji main ()
16: ...
17: (gdb)
18: 0x00008354 w funkcji main ()
19: (gdb) x/3x $r6
20: 0x1050c mem : 0x00000001 0x00000002 0x00000003
21: (gdb) si
22: 0x00008358 w funkcji main ()
23: (gdb) info reg r6
24: r6 0x10518 66840
25: (gdb) si
26: 0x0000835c w funkcji main ()
27: (gdb) info reg $r6
28: r6 0x10524 66852
29: (gdb) x/4x $r6-12
30: 0x10518 : 0x0000000a 0x0000000b 0x0000000c
0x00000000
W 9. linii zastosowano tryb IA ze zwracaniem adresu, a więc zawartość rejestru r6 jest nadpisy-
wana przez adres komórki pamięci leżącej 4 bajty nad ostatnio użytą komórką (zobacz linia ozna-
czona numerem 23). Tę samą technikę zastosowano w wierszach kodu oznaczonych numerami 10,
27 i 30. Efekt działania zaprezentowanego fragmentu kodu pokazano na rysunku 2.3.
Instrukcje LDM i STM mogą podczas jednego wywołania przenosić wiele elementów, a więc są
często używane w blokowych operacjach kopiowania lub przenoszenia. Mogą one być użyte na
przykład w celu implementacji funkcji memcpy, gdy ilość danych przeznaczonych do skopiowania
jest znana w momencie kompilacji programu. Wspomniane instrukcje działają podobnie do znanej
z architektury x86 instrukcji MOVS, poprzedzonej prefiksem REP. Przyjrzyj się dwóm fragmentom
kodu wygenerowanym przez dwa różne kompilatory na podstawie tego samego kodu źródłowego:
Kup książkęPoleć książkęŁadowanie i zapisywanie danych
75
Rysunek 2.3.
Kompilator A
01: A4 46 MOV R12, R4
02: 35 46 MOV R5, R6
03: BC E8 0F 00 LDMIA.W R12!, {R0-R3}
04: 0F C5 STMIA R5!, {R0-R3}
05: BC E8 0F 00 LDMIA.W R12!, {R0-R3}
06: 0F C5 STMIA R5!, {R0-R3}
07: 9C E8 0F 00 LDMIA.W R12, {R0-R3}
08: 85 E8 0F 00 STMIA.W R5, {R0-R3}
Kompilator B
01: 30 22 MOVS R2, #0x30
02: 21 46 MOV R1, R4
03: 30 46 MOV R0, R6
04: 23 F0 17 FA BL memcpy
Kod ten służy jedynie do skopiowania 48 bajtów z jednego bufora do drugiego. Pierwszy kom-
pilator posługiwał się instrukcjami LDM i STM oraz zwracaniem adresu. Odczytywał i zapisywał dane
w porcjach po 16 bajtów. Drugi kompilator po prostu wywoływał swoją implementację funkcji
memcpy. Osoba zajmująca się inżynierią odwrotną takiego kodu może rozpoznać zastosowanie
funkcji memcpy po tym, że niektóre wskaźniki źródeł i celów są używane przez instrukcje LDM i STM
wraz z pewnym zestawem rejestrów. Warto o tym pamiętać, ponieważ często stosuje się tego typu
rozwiązania.
Instrukcje LDM i STM spotyka się również często na początku i na końcu funkcji wykonywanych
w trybie ARM. W tym kontekście pełnią one funkcję prologu i epilogu. Na przykład:
01: F0 4F 2D E9 STMFD SP!, {R4-R11,LR} ; Zapisuje rejestry i adres zwrotny.
02: ...
03: F0 8F BD E8 LDMFD SP!, {R4-R11,PC} ; Przywraca rejestry i zwraca dane.
STMFD jest pseudoinstrukcją STMDB, a LDMFD jest pseudoinstrukcją LDMIA i LDM.
Kup książkęPoleć książkę76
Rozdział 2 (cid:31) Architektura ARM
Do instrukcji STM i LDM cz(cid:295)sto dodaje si(cid:295) sufiksy FD, FA, ED lub EA. Tworzy si(cid:295) w ten
sposób po prostu pseudoinstrukcje instrukcji LDM i STM, które dzia(cid:371)aj(cid:268) w ró(cid:463)nych trybach (IA,
IB itd.). Skojarzone funkcje to: STMFD i STMDB, STMFA i STMIB, STMED i STMDA, STMEA i STMIA, LDMFD
i LDMIA, LDMFA i LDMDA, a tak(cid:463)e LDMEA i LDMDB. Zapami(cid:295)tanie tego wszystkiego mo(cid:463)e by(cid:273) do(cid:401)(cid:273)
trudne. Najszybciej zrozumiesz to, tworz(cid:268)c rysunki ilustruj(cid:268)ce dzia(cid:371)anie ka(cid:463)dej instrukcji.
Instrukcje PUSH i POP
Ostatnimi instrukcjami należącymi do instrukcji ładujących dane do pamięci oraz odczytujących
dane z pamięci są instrukcje PUSH i POP. Działają one podobnie do instrukcji LDM i STM, z tym że:
(cid:31) stosują rejestr SP w roli adresu bazowego,
(cid:31) SP jest automatycznie uaktualniany.
Stos rośnie w dół tak, jak miało to miejsce w przypadku architektur x86 i x64. Ogólna składnia
tych instrukcji ma postać: PUSH/POP {Rn}, gdzie Rn może być zakresem rejestrów.
Instrukcja PUSH odkłada rejestry na stos (tak, że ostatnia lokalizacja znajduje się 4 bajty poniżej
obecnego wskaźnika stosu), a następnie aktualizuje rejestr SP — zapisuje w nim adres pierwszej lo-
kalizacji. Instrukcja POP ładuje do rejestru dane, zaczynając od pozycji wskazywanej przez bieżący
wskaźnik stosu, po czym wpisuje do rejestru SP adres pamięci znajdujący się 4 bajty nad ostatnią
lokalizacją. Instrukcje PUSH i POP funkcjonują tak samo jak działające w trybie zwracania adresu in-
strukcje STMDB i LDMIA, korzystające z rejestru SP jako wskaźnika bazowego. Oto krótki przykład
ilustrujący działanie tych instrukcji:
01: (gdb) disas main
02: zrzut kodu asemblera funkcji main:
03: 0x00008344 +0 : mov.w r0, #10
04: 0x00008348 +4 : mov.w r1, #11
05: 0x0000834c +8 : mov.w r2, #12
06: 0x00008350 +12 : push {r0, r1, r2}
07: 0x00008352 +14 : pop {r3, r4, r5}
08: ...
09: (gdb) br main
10: punkt wstrzymania 1: 0x8344
11: (gdb) r
12: punkt wstrzymania 1, 0x00008344 w funkcji main ()
13: (gdb) si
14: 0x00008348 w funkcji main ()
15: ...
16: (gdb)
17: 0x00008350 w funkcji main ()
18: (gdb) info reg sp ; bie(cid:298)(cid:261)cy wska(cid:296)nik stosu
19: sp 0xbee56848 0xbee56848
20: (gdb) si
21: 0x00008352 w funkcji main ()
22: (gdb) x/3x $sp ; spis uaktualniony po wykonaniu instrukcji push
23: 0xbee5683c: 0x0000000a 0x0000000 b0x0000000c
24: (gdb) si ; zdj(cid:266)cie danych ze stosu do rejestru
25: 0x00008354 w funkcji main ()
26: (gdb) info reg r3 r4 r5 ; nowe rejestry
Kup książkęPoleć książkęFunkcje i wywoływanie funkcji
77
27: r3 0xa 10
28: r4 0xb 11
29: r5 0xc 12
30: (gdb) info regsp ; nowa warto(cid:286)(cid:252) sp (4 bajty nad ostatni(cid:261) lokalizacj(cid:261))
31: sp 0xbee56848 0xbee56848
32: (gdb) x/3x $sp-12
33: 0xbee5683c: 0x0000000a 0x0000000b 0x0000000c
Działanie tego kodu pokazano na rysunku 2.4.
Rysunek 2.4.
Instrukcje PUSH i POP spotyka się najczęściej na początku i na końcu funkcji. W tym kontekście
odgrywają one rolę prologu i epilogu (tak jak instrukcje STMFD i LDMFD w trybie ARM). Na przykład:
01: 2D E9 F0 4F PUSH.W {R4-R11,LR} ; Zapisuje rejestry i adres zwrotny.
02: ...
03: BD E8 F0 8F POP.W {R4-R11,PC} ; Przywraca rejestry i zwraca dane.
Niektóre dezasemblery wykorzystują tę technikę w celu określenia granic funkcji.
Funkcje i wywoływanie funkcji
W przypadku architektur x86 i x64 funkcje były wywoływane tylko za pomocą instrukcji CALL i rozga-
łęziane wyłącznie przy użyciu instrukcji JMP. W architekturze ARM do wywoływania funkcji korzysta
sie z kilku różnych instrukcji, zależnie od sposobu kodowania wywoływanych funkcji. Podczas
wywołania funkcji procesor musi wiedzieć, jaki kod ma być dalej przetwarzany po zakończeniu jej
działania. Kod ten jest określany przez adres zwrotny. W przypadku architektury x86 instrukcja
CALL bezwarunkowo odkłada adres zwrotny na stos przed przejściem do funkcji docelowej. Gdy funkcja
docelowa zostanie wykonana, jej kod wznawia wykonywanie funkcji wywołującej, ściągając odło-
żony wcześniej adres ze stosu i ładując go do rejestru EIP.
Ogólnie rzecz biorąc, mechanizm ten działa podobnie w architekturze ARM, przy czym cha-
rakteryzują go pewne różnice względem mechanizmu znanego z architektury x86. Po pierwsze, ad-
res zwrotny może być przechowywany na stosie albo w rejestrze powiązań (LR). Po zakończeniu
Kup książkęPoleć książkę78
Rozdział 2 (cid:31) Architektura ARM
działania wywołanej funkcji adres zwrotny jest jawnie ściągany ze stosu i ładowany do rejestru PC,
o ile nie istnieje bezwarunkowe rozgałęzienie kierujące do rejestru LR. Po drugie, rozgałęzienie mo-
że przełączać między trybami ARM i Thumb w zależności od najmniej znaczącego bitu adresowa-
nego elementu. Po trzecie, w przypadku architektury ARM przyjęto standardową konwencję wy-
woływania, która mówi, że 4 pierwsze 32-bitowe parametry są przekazywane za pośrednictwem
rejestrów (R0 – R3), a pozostałe są odkładane na stos. Zwracana wartość jest przechowywana w re-
jestrze R0.
Podczas wywoływania funkcji stosowane są instrukcje B, BX, BL i BLX.
Co prawda instrukcja B jest rzadko używana w kontekście wywoływania funkcji, ale może być
zastosowana do przeniesienia kontroli. Jest to po prostu rozgałęzienie bezwarunkowe działające jak
znana z architektury x86 instrukcja JMP. Instrukcja B jest zwykle używana w pętlach i konstrukcjach
warunkowych w celu powrotu do początku danej konstrukcji lub przerwania jej. Może być użyta
również po to, aby wywołać funkcję, która niczego nie zwraca. Instrukcja B może określać cel tylko
na podstawie etykiety przesunięcia — nie może do realizacji tego zadania korzystać z rejestrów.
W takim kontekście instrukcja B ma następującą składnię: B imm, gdzie imm jest przesunięciem wzglę-
dem obecnie wykonywanej instrukcji. (Nie są tu brane pod uwagę flagi wykonywania warunkowe-
go, które zostaną omówione w podrozdziale „Rozgałęzianie i wykonywanie warunkowe”). Warto
tu zauważyć, że instrukcje ARM i Thumb charakteryzują się 4- i 2-bajtowym wyrównaniem, a więc
docelowa wartość przesunięcia musi być liczbą parzystą. Oto przykładowy fragment kodu ilustrujący
zastosowanie instrukcji B:
01: 0001C788 B loc_1C7A8
02: 0001C78A
03: 0001C78A loc_1C78A
04: 0001C78A LDRB R7, [R6,R2]
05: ...
06: 0001C7A4 STRB.W R7, [R3,#-1]
07: 0001C7A8
08: 0001C7A8 loc_1C7A8
09: 0001C7A8 MOV R7, R3
10: 0001C7AA ADDS R3, #2
11: 0001C7AC CMP R2, R4
12: 0001C7AE BLT loc_1C78A
W 1. linii kodu instrukcję B zastosowano w celu wykonania bezwarunkowego skoku uruchamiają-
cego pętlę. Na razie możesz zignorować pozostałe instrukcje.
Instrukcja BX jest instrukcją „skoku i zmiany” — podobnie jak instrukcja B przenosi sterowanie
wykonywanym kodem do docelowej funkcji, ale może również przełączać procesor między tryba-
mi ARM i Thumb. Adres docelowej instrukcji jest przechowywany w rejestrze. Instrukcje skoku,
których nazwy kończą się na literę „X”, mogą przełączać tryb pracy procesora. Jeżeli najmniej zna-
czący bit adresu docelowego przyjmuje wartość 1, to procesor jest automatycznie przełączany w tryb
Thumb. W przeciwnym wypadku będzie pracował w trybie ARM. Instrukcja ta ma składnię BX
rejestr , gdzie rejestr przechowuje adres docelowy. Instrukcja ta jest najczęściej używana w kon-
tekście kończenia wykonywania jakiejś funkcji i skoku do LR (to jest BX LR) w przypadku przejęcia
kontroli nad procesorem przez kod wymagający zmiany trybu pracy procesora (a więc przejścia
Kup książkęPoleć książkęFunkcje i wywoływanie funkcji
79
z trybu ARM w tryb Thumb lub na odwrót). W skompilowanym kodzie prawie zawsze zobaczysz
zapis BX LR, znajdujący się na końcu funkcji. Podobnie w architekturze x86 funkcje kończy in-
strukcja RET.
Instrukcja BL jest instrukcją „skoku i powiązania”. Działa ona podobnie jak instrukcja B, ale za-
pisuje adres zwrotny w rejestrze LR przed przeniesieniem kontroli nad procesorem do docelowego
kodu. Instrukcja ta jest najbliższym odpowiednikiem instrukcji CALL, znanej z architektury x86. Jest
ona często używana podczas wywoływania funkcji. Charakteryzuje się taką samą składnią jak in-
strukcja B (w roli argumentu przyjmuje tylko przesunięcie). Oto krótki fragment kodu pokazujący
wywołanie funkcji, a także jej zakończenie.
01: 00014350 BL foo ; LR = 0x00014354
02: 00014354 MOVS R4, #0x15
03: ...
04: 0001B224 foo
05: 0001B224 PUSH {R1-R3}
06: 0001B226 MOV R3, 0x61240
07: ...
08: 0001B24C BX LR ; Wraca do 0x00014354.
W 1. linii kodu funkcja foo jest wywoływana za pomocą instrukcji BL. Przed przeniesieniem
kontroli do docelowej funkcji instrukcja BL zapisuje adres zwrotny (0x00014354) w rejestrze LR. Funk-
cja foo wykonuje pewne zadania i wraca do funkcji, która ją wywołała (BX LR).
Instrukcję BLX można nazwać instrukcją „skoku, powiązania i zmiany”. Instrukcja ta jest po-
dobna do instrukcji BL, z tym że pozwala również zmienić tryb pracy procesora. Największa różni-
ca między tymi instrukcjami jest taka, że BLX może w charakterze rozgałęzienia przyjmować rejestr
lub przesunięcie. Gdy instrukcja BLX przyjmuje przesunięcie, wtedy zawsze dochodzi do przełącze-
nia trybu pracy procesora (z ARM na Thumb lub odwrotnie). Instrukcja ta ma podobną charakte-
rystykę do instrukcji BL, a więc można traktować ją jako odpowiednik instrukcji CALL, znanej z ar-
chitektury x86. W praktyce instrukcje BL i BLX są używane podczas wywoływania funkcji. Instrukcja
BL jest zazwyczaj używana, gdy funkcja mieści się w zakresie 32 MB, a instrukcja BLX jest stosowana,
gdy zakres wywoływanego elementu jest nieznany (do takiej sytuacji dochodzi na przykład w przypad-
ku wskaźnika funkcji). W trybie Thumb instrukcja BLX jest zwykle stosowana do wywoływania proce-
dur biblioteki. W trybie ARM w tym celu używana jest zwykle instrukcja BL.
Po przeanalizowaniu wszystkich instrukcji służących do obsługi bezwarunkowych rozgałęzień
i bezpośredniego wywoływania funkcji, a także zwracania danych przez funkcję (BX LR) jesteś gotowy
do tego, aby przeanalizować całą funkcję i utrwalić zdobytą wiedzę.
01: 0100C388 ; void *__cdecl mystery(int)
02: 0100C388 mystery
03: 0100C388 2D E9 30 48 PUSH.W {R4,R5,R11,LR}
04: 0100C38C 0D F2 08 0B ADDW R11, SP, #8
05: 0100C390 0C 4B LDR R3, =__imp_malloc
06: 0100C392 C5 1D ADDS R5, R0, #7
07: 0100C394 6F F3 02 05 BFC.W R5, #0, #3
08: 0100C398 1B 68 LDR R3, [R3]
09: 0100C39A 15 F1 08 00 ADDS.W R0, R5, #8
10: 0100C39E 98 47 BLX R3
11: 0100C3A0 04 46 MOV R4, R0
Kup książkęPoleć książkę80
Rozdział 2 (cid:31) Architektura ARM
12: 0100C3A2 24 B1 CBZ R4, loc_100C3AE
13: 0100C3A4 EB 17 ASRS R3,R5,#0x1F
14: 0100C3A6 63 60 STR R3, [R4,#4]
15: 0100C3A8 25 60 STR R5,[R4]
16: 0100C3AA 08 34 ADDS R4,#8
17: 0100C3AC 04 E0 B loc_100C3B8
18: 0100C3AE loc_100C3AE
19: 0100C3AE 04 49 LDR R1,=aFailed ; „niepowodzenie...”
20: 0100C3B0 2A 46 MOV R2, R5
21: 0100C3B2 07 20 MOVS R0,#7
22: 0100C3B4 01 F0 14 FC BL foo
23: 0100C3B8
24: 0100C3B8 loc_100C3B8
25: 0100C3B8 20 46 MOV R0, R4
26: 0100C3BA BD E8 30 88 POP.W {R4,R5,R11,PC}
27: 0100C3BA ; koniec funkc
Pobierz darmowy fragment (pdf)