Pętle i warunki, naturalnie są z jednym z podstawowych elementów każdego programu. Języki wysokiego poziomu umożliwiają realizację tego za pomocą słów If, for, foreach,while. W językach niskiego poziomu, takich jak IL Assembly, wszystkie powyższe czynności wykonuje się za pomocą skoków warunkowych i bezwarunkowych. Działają one analogicznie do słowa kluczowego GOTO.
W IL, najprostsza instrukcja skoku to BR:
br jump1 // jakas logika tutaj jump1: //...
br nie zawiera żadnego warunku. Można ją porównać do goto – po prostu skacze do wskazanej lokalizacji.
Bardziej interesujące są brtrue oraz brfalse. W zależności czy wartość jest true lub false, skaczą do wskazanego miejsca. Rozważmy kod c#:
bool flag = true; if (flag) { Console.WriteLine("true"); } else { Console.WriteLine("false"); }
Wygenerowany IL to:
IL_0001: ldc.i4.1 IL_0002: stloc.0 IL_0003: ldloc.0 IL_0004: ldc.i4.0 IL_0005: ceq IL_0007: stloc.1 IL_0008: ldloc.1 IL_0009: brtrue.s IL_001a IL_000c: ldstr "true" IL_0011: call void [mscorlib]System.Console::WriteLine(string) IL_0018: br.s IL_0027 IL_001a: nop IL_001b: ldstr "false" IL_0020: call void [mscorlib]System.Console::WriteLine(string) IL_0027: ret
Jeśli któraś z instrukcji jest niezrozumiała, zapraszam do poprzednich wpisów o IL na moim blogu. Wszystkie z powyższych instrukcji powinny być już zrozumiałe. Tak czy inaczej, spróbujmy rozszyfrować tą wiązankę.
1. IL_0001 – załadowanie wartości jeden na stos.
2. IL_0002 – zdjęcie wartości (1 – true) ze stosu i umieszczenie jej w zmiennej lokalnej o indeksie zero (czyli flag).
3. IL_003 – załadowanie zmiennej lokalnej na stos.
4. IL_004 – umieszczenie wartości 0 (false) na stosie.
5. IL_005 – ceq to instrukcja opisana w poprzednim wpisie. Porównuje dwie wartości i jeśli są one równe, cyfra 1 jest umieszczana na stosie, w przeciwnym wypadku 0. W naszej sytuacji będzie to porównanie jeden z zero (1==0) co poskutkuje, że na stosie pojawi się 0.
6. IL_007 – zapisanie wartości ze stosu (w naszym przypadku zero) do zmiennej lokalnej o indeksie 1. Jest to automatycznie wygenerowana zmienna przez kompilator. Zaglądając do deklaracji przekonamy się o tym:
.locals init ( [0] bool flag, [1] bool CS$4$0000 )
7. IL_008 – analogiczna sytuacja – umieszczenie zmiennej na stosie.
8. IL_009 – wreszcie mamy naszą instrukcję warunkową brtrue.s IL_001a. Jeśli wartość na stosie jest true, skoczymy do IL_001a, czyli:
IL_001a: nop IL_001b: ldstr "false" IL_0020: call void [mscorlib]System.Console::WriteLine(string)
W naszym przypadku, na stosie jest zero, zatem nie zostanie wykonany skok w tym momencie, a wykonamy:
IL_000c: ldstr "true" IL_0011: call void [mscorlib]System.Console::WriteLine(string) IL_0018: br.s IL_0027
Na końcu jednak widzimy br. Oczywiście, chcemy ominąć fragment kodu, który wykonuje gałąź else, stąd skaczemy do IL_007.
Jedyną instrukcją o której nie wspomniałem to nop. Na razie wystarczy wiedzieć, że ona nic nie robi (dosłownie, Do Nothing). W tym przypadku, służy wyłącznie jako miejsce skoku. Więcej o niej napiszę kiedy indziej.
Pozostałe instrukcje skoku to: beq, bne, blt, ble, bgt, bge. Analogicznie, w zależności od wyniku porównania, skaczą do podanego miejsca. Przykład c#:
int five = 5; int three = 3; if (five>three) { Console.WriteLine("true"); } else { Console.WriteLine("false"); }
IL:
IL_0000: ldc.i4.5 IL_0001: stloc.0 IL_0002: ldc.i4.3 IL_0003: stloc.1 IL_0004: ldloc.0 IL_0005: ldloc.1 IL_0006: ble.s IL_0013 IL_0008: ldstr "true" IL_000d: call void [mscorlib]System.Console::WriteLine(string) IL_0012: ret IL_0013: ldstr "false" IL_0018: call void [mscorlib]System.Console::WriteLine(string) IL_001d: ret
Warto zauważyć, że przedstawione instrukcje właściwie kończą się sufiksem .s. Sufiks oznacza short i korzysta się takich instrukcji gdy wskaźnik skoku (przesunięcia) można zapisać za pomocą jednego bajta. Innymi słowy, jeśli nie trzeba skakać zbyt daleko w IL to lepiej zapisać przesunięcie w jednobajtowej liczbie, co jest oszczędnością i wpływa na wydajność.
IL, jako, że jest językiem niskopoziomowym nie posiada specjalnych poleceń dla pętli. Wykonuje się je po prostu za pomocą skoków. Jeśli należy wykonać drugą iterację, skaczę się z powrotem na początek.
Przykład c# while:
int i = 0; while (i<5) { i++; }
IL:
IL_0000: nop IL_0001: ldc.i4.0 IL_0002: stloc.0 IL_0003: br.s IL_000b // loop start (head: IL_000b) IL_0005: nop IL_0006: ldloc.0 IL_0007: ldc.i4.1 IL_0008: add IL_0009: stloc.0 IL_000a: nop IL_000b: ldloc.0 IL_000c: ldc.i4.5 IL_000d: clt IL_000f: stloc.1 IL_0010: ldloc.1 IL_0011: brtrue.s IL_0005 // end loop IL_0013: ret
Jeszcze kilka ważnych rzeczy z IL mam zamiar opisać w kolejnych postach. Bazując jednak wyłącznie na tym i ostatnich wpisach, wiele można zrozumieć analizując kod z Reflector czy ILSpy.