-15% na nową książkę sekuraka: Wprowadzenie do bezpieczeństwa IT. Przy zamówieniu podaj kod: 10000

Jak się w końcu zabrać za tego assemblera? Czyli MIPS w kontekście bezpieczeństwa – lekcja 2.

26 lutego 2020, 16:34 | W biegu | komentarze 3
Tagi:

W kolejnym odcinku naszej serii przeanalizujemy pętlę for(), a także sprawdzimy, co odróżnia MIPSa od MIPSela. Będzie też bonus w postaci pracy domowej. Zacznijmy jednak od zadania domowego z lekcji 1.

Praca domowa z lekcji 1.

Rozwiązanie zadania 1 całkowicie pozostawiam Twojej ciekawości. Co do zadania 2 to przeanalizujmy sobie kod po kawałku rozpoczynając od linii +28:

<+28>: li v0, 5
<+32>: sw v0, 24(s8)
<+36>: lw v0, 24(s8)

Ładujemy liczbę 5 do rejestru „v0”. Oznaczmy ją sobie jako „x”. Wartość ta jest później ładowana do pamięci i znowu umieszczona w rejestrze „v0”.

<+40>: slti v0, v0, 7

I teraz tak – małym kluczem do zagadki (ostatni hint zadania 2) była właśnie instrukcja „slti” ;) – wyjaśnijmy sobie najpierw co ona robi:

slti v0, v0, 7 – (set on less than immediate (signed)) jeśli wartość w rejestrze „v0” jest mniejsza od 7, zmień wartość rejestru na liczbę 1. W przeciwnym wypadku wrzuć do rejestru „v0” liczbę 0. Dzięki instrukcji „slti” masz okazję poznać rodzinę kolejnych instrukcji arytmetyczno-logicznych takich jak „slt”, „sltu”, „slti” czy „sltui”.

<+44>: beqz v0, 0x4003f8 <main+104>

No i mamy pierwszy warunek: „beqz v0, 0x4003f8” -> Branch Equal to Zero -> Jeśli rejestr „v0” równa się zero, to skocz pod adres „0x4003f8” (+104).

Reasumując – w naszym przypadku instrukcja „slti” sprawdzi, czy liczba 7 jest większa od wartości rejestru „v0” (czyli naszej wcześniej zapisanej 5) i rzeczywiście jest to prawda, zatem wstawi ona liczbę 1 do rejestru „v0”. W dalszej kolejności instrukcja „beqz” zbada, czy wartość „v0” jest równa 0 – a nie jest – tak więc skoku do adresu 0x4003f8 nie będzie.

Dla ułatwienia zapiszmy sobie na początek te dwie powyżej omówione operacje w pseudokodzie:

if ( 7 > 5 )
{
	0x004003e4 <+84>:		bal	0x400430 <printf>	(wypisz: Warunek spełniony)
}
else
{
	0x004003f8 <+104>:		...
	0x004003fc <+108>:		...
	0x00400400 <+112>:		...
	0x00400404 <+116>:		...
	0x00400408 <+120>:		bal	0x400430 <printf>	(wypisz: Warunek nie spełniony)
}

roboczy_kod.c

Idźmy dalej. Poczynając od linii +52 mamy taki oto kodzik:

0x004003c4 <+52>: lw v0, 24(s8)
0x004003c8 <+56>: slti v0, v0, 5
0x004003cc <+60>: bnez v0, 0x4003f8 <main+104>

Mogłeś się tutaj zastanawiać, jaka wartość będzie się kryła pod rejestrem „v0” po wykonaniu instrukcji „lw”. Przyjmijmy, że będzie to wartość „y”, która dalej będzie sprawdzana, czy nie jest mniejsza od liczby 5 (znowu mamy „slti”).

Potem jedzie instrukcja „bnez” – Branch If Not Equal Zero – sprawdza, czy rejestr „v0” nie jest równy liczbie 0. Jeśli tak, to następuje skok pod adres 0x4003f8 – linię 140. Znowu.

Podsumowując: jeśli jeden z warunków – ten powyżej lub omawiany wcześniej (od linii +28) – nie zostanie spełniony, program skoczy pod adres 0x004003f8. Jeśli oba warunki zostaną spełnione, program będzie dalej wykonywać instrukcje aż:

wykona funkcję „printf” wypisując: Warunek spełniony (linia: +84),
wykona skok do linii +132, aby zakończyć działanie.

Zatem modyfikując nasz pseudokod możemy tak oto skonstruować naszą instrukcję warunkową:

pseudokod rozwiązanego zadania

if ( 7 > 5 && 5 < 5 )
{
	0x004003e4 <+84>:		bal	0x400430 <printf>	(wypisz: Warunek spełniony)
}
else
{
	0x004003f8 <+104>:		...
	0x004003fc <+108>:		...
	0x00400400 <+112>:		...
	0x00400404 <+116>:		...
	0x00400408 <+120>:		bal	0x400430 <printf>	(wypisz: Warunek nie spełniony)

}

MIPS vs MIPSel

Jaka jest różnica pomiędzy pojęciami MIPS a MIPSel, z którymi pewnie już się kiedyś zetknąłeś? MIPS ma takie dwa tryby zapisu danych w komórkach pamięci w odpowiedniej kolejności, które nazywają się odpowiednio „little endian” oraz „big endian”.

O szczegółach poczytasz sobie na przykład tutaj, ale zrób to później, bo teraz chciałbym, abyś zobaczył w praktyce, o co tutaj chodzi ;)

Powiedzmy, że w naszym programie trzymamy sobie w zmiennej 4-bajtową wartość, np. „0x02040608”.

Procesor musi teraz wiedzieć, w jakiej kolejności zapisać te 4 bajty („0x02”, „0x04”, „0x06” i „0x08”) do komórek pamięci, które mają tak dla przykładu adresy: „0x4000”, ”0x4001”, ”0x4002” oraz „0x4003”. Używając trybów wspomnianych wcześniej ma tylko dwie możliwości:

  • big endian

Zapis bajtów rozpoczyna się od komórki pamięci z najniższym adresem:

0x4000 -> 0x02
0x4001 -> 0x04
0x4002 -> 0x06
0x4003 -> 0x08

  • little endian

Zapis bajtów rozpoczyna się od komórki pamięci z najwyższym adresem:

0x4000 -> 0x08
0x4001 -> 0x06
0x4002 -> 0x04
0x4003 -> 0x02

Weźmy sobie taki kod, który posłuży nam za przykład:

#include <stdint.h>

int main() {
int32_t m = 0x02040608;

return 0;
}

Kod endian.c, kompilacja: mipsel-linux-gcc -static -o endian endian.c -ggdb

Odpalmy gdb i zobaczmy, jak to wygląda pod maską:

Zapis bajtów zmiennej „m” w pamięci

Trochę o tym, co tutaj się wydarzyło:

  • Uruchomiliśmy gdb w trybie cichym (-q).
  • Polecenie „show endian” pokazuje tryb kolejności zapisu bajtów – w naszym przypadku jest to „little endian”, czyli MIPSel.
  • Za pomocą polecenia „list” gdb pokazał nasz kod w C.
  • Ustawiamy „breakpoint”* na koniec programu tak, ażeby zmienna „m” trafiła nam do pamięci.
  • Polecenie „x/4bx &m” pokazuje nam, jak jest zapisana wartość zmiennej „m” (więcej poleceń znajdziesz w dokumentacji albo cheat sheet gdb).

Teraz możesz zobaczyć, jak są zapisane bajty wartości „0x02040608” w pamięci pod adresem 0x7fffec80.

Kompilator udostępnił dodatkowe informacje dostępne podczas procesu debugowania specjalnie dla gdb. Było to możliwe przez ustawienie opcji „-ggdb” podczas kompilacji programu. Dzięki temu mogliśmy wylistować sobie kod za pomocą polecenia „list”, ustawić „breakpointa” na konkretną linię w programie (6) czy przeanalizować zmienną „m” pod kątem jej wartości („x/4bx &m”).

* Program po uruchomieniu wykonuje instrukcje jedna po drugiej. Breakpoint jest miejscem w kodzie programu, w którym program ma się zatrzymać, aby możliwa była dalsza jego analiza w trakcie działania, jak np. stan pamięci, rejestrów i inne istotne rzeczy. Breakpoint ustawiasz Ty w swoim debuggerze.

Pętla for()

Temat przewodni dzisiejszej lekcji, czyli pętle. Wrzucamy na warsztat taki kod:

#include <stdio.h>

int main() {

	for(int cnt = 0; cnt < 8; cnt++)
	{
		printf("pik %d\n", cnt);
	}

	return 0;
}

kod lesson02.c

Zobacz teraz, jak sobie radzi z takim kodem MIPS:

debug kodu lesson02.c

Omówimy sobie najistotniejsze elementy naszego programu poczynając od linii +28:

sw zero, 24(s8) – program wrzuca instrukcją „sw” wartość 0 do pamięci

b 0x4003e4 <main+84> – po czym wykonuje bezwarunkowy skok za pomocą instrukcji „Branch” do miejsca znajdującego się w linii +84

lw v0, 24(s8) – żeby w tym miejscu załadować to 0 z pamięci do rejestru „v0”

Super, tak więc rejestr „v0” będzie taką naszą zmienną „cnt” z kodu C. Co mamy dalej?

<+88>: slti v0, v0, 8
<+92>: bnez v0, 0x4003b8 <main+40>

Dalej widzimy instrukcje dobrze już nam znane z poprzedniej lekcji.

Te dwie instrukcje tworzą warunek sprawdzający (z naszej pętli for() w kodzie C), czy zmienna „cnt” jest mniejsza od liczby 8. Jeśli wartość rejestru „v0” jest mniejsza od liczby 8, to instrukcja „slti” wstawi do rejestru „v0” wartość 1, a co za tym idzie – „bnez” (sprawdzając „v0”) zadecyduje, że zostanie wykonany skok pod adres 0x4003b8.

Od adresu 0x4003b8 (czyli od linii +40) zobaczysz kod odpowiadający za wywołanie funkcji „printf()”, która wypisze nam słowo „pik” z aktualną wartością zmiennej „cnt”. O funkcjach już niedługo, stay tuned ;)

Po wywołaniu funkcji „printf()” kod wykonuje się spokojnie dalej bez żadnych dzikich skoków do innych sekcji, aż dochodzi do kolejnej dla nas, kluczowej instrukcji. Teraz zatrzymaj się na moment na linii +76:

<+76>: addiu v0, v0, 1 – Add immediate unsigned (no overflow) – dodaj do wartości rejestru „v0” liczbę 1, a następnie wynik tej sumy umieść w rejestrze „v0”. Tak dokładnie, to jest ostatnia składowa naszej pętli for(), czyli inkrementacja zmiennej „cnt” (cnt++).

Idąc dalej program znów natrafia na warunek, który omówiliśmy sobie wcześniej. W momencie, kiedy zmienna „cnt” nie będzie już mniejsza od liczby 8, program zamiast skoku do linii +40 (pod adres 0x4003b8) wykona kolejne instrukcje i zakończy swoje działanie.

Zadania domowe

Od tej pory będą już tylko dla komandosów. Co robi ten kod poniżej? ;)

Małe hinty:

przyjmij, że od linii +72 do +92 jest wywołanie funkcji, która wypisuje po prostu duże „X”,
instrukcja „move” kopiuje wartość z lewej („zero”) do prawej („v0”).

Kombinuj i nie poddawaj się ;)

Mateusz Wójcik, grupa Squadron31

Spodobał Ci się wpis? Podziel się nim ze znajomymi:



Komentarze

  1. Wójt

    Hej :D Super poradnik, w sumie nigdy nie wywoływałem funkcji bibliotecznych z mipsa.. dużo straciłem :)

    Ale co do zadania domowego, chyba wkradł się błąd..?
    ##UWAGA SPOILERS, CZYTASZ NA WŁASNĄ ODPOWIEDZIALNOŚĆ##
    Kod zapewne miał wypisać dwa 'iksy’ (bo iteracja od 1 i „if(i==3) break;”, ale z powodu zakrytych na czerwono adresów +100 i +104 nie widać kodu, który ustawiałby $v0 na 24(s8) przed zwiększeniem rejestru o 1
    W związku z tym można by wnioskować, że $v0 ma wartość „-32720(gp)”, przy pominięciu faktu, że printf ma prawo rejestr zmienić (chociaż nie wiem jakie są konwencje w MIPSie..)

    Odpowiedz
    • Wójt,

      dzięki! Słuszna uwaga ;) poprawiamy ;)

      Tam w linii +104 rzeczywiście kryje się instrukcja „lw” odpowiedzialna za załadowanie wartości z pamięci do rejestru „v0”.

      BTW
      Wójt, mega, że doczytałeś lekcję do końca ;)

      Odpowiedz
  2. PEX

    Czy aby na pewno w rozwiązaniu zadania domowego z części pierwszej nie ma błędu ? Wg mnie warunek będzie spełniony dla (a >=7 OR a<5)

    Odpowiedz

Odpowiedz na PEX