IL Assembly: instrukcje warunkowe i pętle

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.

Leave a Reply

Your email address will not be published.