Wprowadzenie do evaluation stack

Bardzo często na blogu poruszam tematykę c# internals. Bez nich, praktycznie niemożliwe jest pisanie optymalnego kodu. Jeśli ktoś np. nie wie jak async\await jest zaimplementowany wewnętrznie, bardzo łatwo może popełnić błędy podczas pisania kodu c#. Niedawno ktoś zasugerował mi, abym wyjaśnił bardziej IL. Bardzo często wklejam fragmentu kodu z Reflector’ora i nie wyjaśniam szczegółów.

Z tego względu, przez kilka kolejnych wpisów zajmiemy się CLR internals oraz IL.

Na początek podstawowe pytanie, co to jest IL? Jest to skrót od  Intermediate Language. Kompilując kod C# czy VB, nie otrzymujemy kodu maszynowego, specyficznego dla konkretnej architektury. IL to język pośredni, który zostanie dopiero zamieniony w konkretny kod maszynowy przez JIT. Umożliwia to pisanie kodu, który nie jest zależny od konkretnej platformy\architektury. Dzięki temu również, mogą być dokonywane różne optymalizacje zależne np. od konkretnego CPU.

Bardzo często mylony jest IL z IL Assembly. IL to tak naprawdę kod binarny, którego człowiek nie jest w stanie przeczytać\zrozumieć. Z kolei IL Assembly to język niskiego poziomu (assembler). W skrócie będę pisał na blogu IL pokazując różne fragmenty kodu. Ale należy mieć na uwadze, że tak naprawdę chodzi IL Assembly… Nie da się wykonać bezpośrednio IL Assembly, musi on być potem z powrotem zamieniony do IL.

Dzisiaj zajmiemy jednym z ważniejszych elementów IL, a mianowicie stosem (konkretniej evaluation stack). IL, w przeciwieństwie do wielu innych jeżyków jest oparty na stosie a nie na rejestrach. Wszystkie operacje wykonujemy, umieszczając pewne dane na stosie, a potem wykonując różne instrukcje.

W skrócie, IL nie posiada rejestrów, ale stos jak i m.in. zmienne lokalne. Nie lubię zbyt wiele teorii, więc zacznijmy od przykładu c#:

static  void Main(string[] args)
{
  int a = 5;
  int b = 6;
  int c = a + b;
}

W tym prostym programie, mamy 3 zmienne lokalne. Pierwsza z nich przechowuje stałe a kolejna („c”) będzie zawierać sumę dwóch wcześniejszych zmiennych. Wygenerowany IL Assembly, będzie wyglądać, więc następująco:

.locals init (
   [0] int32 num,
   [1] int32 num2,
   [2] int32 num3)
L_0001: ldc.i4.5 
L_0002: stloc.0 
L_0003: ldc.i4.6 
L_0004: stloc.1 
L_0005: ldloc.0 
L_0006: ldloc.1 
L_0007: add 
L_0008: stloc.2 

Najpierw widzimy deklaracje zmiennych lokalnych:

.locals init (
   [0] int32 num,
   [1] int32 num2,
   [2] int32 num3)

To było proste do wywnioskowania. Kolejne instrukcje, bez znajomości internali są ciężkie prawdopodobnie do rozszyfrowania. Stos służy do wykonywania operacji. Aby dodać dwie liczby, należy je najpierw umieścić na stosie. Analogicznie, nie operuje się bezpośrednio na zmiennych lokalnych. Jeśli chcemy przechować liczbę w zmiennej lokalnej, najpierw należy ją umieścić na stosie, a potem zdjąć ją, umieszczając w konkretnej zmiennej lokalnej.

Spróbujmy przedstawić działanie powyższego kodu. W celu załadowania jakiejkolwiek wartości do zmiennej lokalnej, należy najpierw umieścić ją na stosie. Z tego względu stos na początku będzie wyglądać następująco:

clip_image002

Zmienne lokalne na tym etapie są puste. Powyższą operację, wykonuje się instrukcją ldc.i4 – przechowuje ona liczbę 4-bajtową (Int32) na stosie.

Kolejna instrukcja to stloc.0. Zdejmie ona wartość (5) ze stosu i umieści ją w pierwszej zmiennej lokalnej. Następnie mamy dwie kolejne, analogiczne instrukcje dla cyfry 6. Najpierw umieszczamy ją na stosie, a potem zdejmujemy, wrzucając do zmiennej lokalnej o indeksie jeden:

L_0003: ldc.i4.6 
L_0004: stloc.1 

Stos jest pusty na tym etapie, a dane znajdują się w zmiennych lokalnych (a,b). W celu ich dodania, musimy znów umieścić je na stosie:

L_0005: ldloc.0 
L_0006: ldloc.1 

Ldloc to instrukcja analogiczna do stloc. Jak wiemy, stloc zdejmuje wartość ze stosu, umieszczając ją w zmiennej lokalnej, a z kolei ldloc załaduje zmienną lokalną na stos. Oznacza to, że stos po tych dwóch instrukcjach będzie wyglądać następująco:

clip_image002[5]

Następnie wykonujemy add, która doda dwie wartości ze stosu i przechowa wynik również na nim:

L_0007: add

Co w wyniku da:

clip_image002[7]

Ostatnia linia już nie powinna być zaskoczeniem:

L_0008: stloc.2

Spowoduje, to zdjęcie wartości ze stosu i umieszczenie jej w lokalnej zmiennej o indeksie 2 (czyli zmienna o nazwie c).

Na pierwszy wpis wystarczy… Podsumowując jednak instrukcje z dzisiaj:

  1. stloc – zdejmuje wartość ze stosu i umieszcza ją we wskazanej przez indeks zmiennej lokalnej.
  2. ldloc – załaduje zmienną lokalną o wskazanym indeksie do stosu (push).
  3. add – sumuje dwie liczby (operuje na stosie).
  4. ldc.i4 – ładuje liczbę 4 bajtową (Int32) na stos.

2 thoughts on “Wprowadzenie do evaluation stack”

Leave a Reply

Your email address will not be published.