Cyfroteka.pl

klikaj i czytaj online

Cyfro
Czytomierz
00434 007162 13461238 na godz. na dobę w sumie
Inżynieria odwrotna w praktyce. Narzędzia i techniki - ebook/pdf
Inżynieria odwrotna w praktyce. Narzędzia i techniki - ebook/pdf
Autor: , , , Liczba stron: 368
Wydawca: Helion Język publikacji: polski
ISBN: 978-83-283-0681-3 Data wydania:
Lektor:
Kategoria: ebooki >> komputery i informatyka >> hacking >> bezpieczeństwo systemów
Porównaj ceny (książka, ebook (-20%), audiobook).

Inżynieria odwrotna pozwoli Ci na walkę z hakerami i rootkitami.

Na użytkowników globalnej sieci czekają coraz wymyślniejsze pułapki. Każdego dnia grozi im zarażenie oprogramowaniem szpiegującym, rozsyłającym niechciane wiadomości lub wykorzystującym moc obliczeniową procesora do nieznanych celów. Wykrywanie tego typu zagrożeń i przeciwdziałanie im wymaga dogłębnej analizy niechcianego oprogramowania. Jak to zrobić? Na te i wiele innych pytań odpowiedzi dostarczy ta wspaniała książka!

Dzięki niej zrozumiesz, jak działają procesory x86, x64 oraz ARM, zgłębisz tajniki jądra systemu Windows oraz poznasz najlepsze narzędzia, które wspomogą Cię w Twoich działaniach. W trakcie lektury kolejnych stron dowiesz się, jak korzystać z debuggera, jaką strukturę mają sterowniki oraz czym są pakiety żądań wejścia-wyjścia. Następnie dowiesz się, po co zaciemnia się kod oraz jakie narzędzia są do tego potrzebne. Techniką odwrotną do zaciemniania jest rozjaśnianie kodu. Zastanawiasz się, które narzędzia są skuteczniejsze? Przekonaj się sam! Ta pasjonująca lektura dostarczy Ci mnóstwo wiedzy na temat działania oprogramowania.

Dowiedz się, jak działają exploity, wirusy, rootkity! Ich działanie można w pełni zrozumieć tylko poprzez inżynierię odwrotną...

Znajdź podobne książki Ostatnio czytane w tej kategorii

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)

Gdzie kupić całą publikację:

Inżynieria odwrotna w praktyce. Narzędzia i techniki
Autor:
, , ,

Opinie na temat publikacji:


Inne popularne pozycje z tej kategorii:


Czytaj również:


Prowadzisz stronę lub blog? Wstaw link do fragmentu tej książki i współpracuj z Cyfroteką: