Aby podać konkretny przykład sposobu, w jaki kompilator zarządza stosem i sposobu uzyskiwania dostępu do wartości na stosie, możemy spojrzeć na obrazy oraz kod wygenerowany przez GCC
środowisko Linux z i386 jako architekturą docelową.
1. Układaj ramki
Jak wiadomo, stos jest lokalizacją w przestrzeni adresowej uruchomionego procesu, która jest używana przez funkcje lub procedury , w tym sensie, że przestrzeń jest przydzielana na stos dla zmiennych deklarowanych lokalnie, a także argumentów przekazywanych do funkcji ( miejsce na zmienne zadeklarowane poza jakąkolwiek funkcją (tj. zmienne globalne) jest przydzielone w innym regionie w pamięci wirtualnej). Miejsce przydzielone na wszystkie dane funkcji odnosi się do ramki stosu . Oto wizualne przedstawienie wielu ramek stosu (z Computer Systems: A Programmer's Perspective ):
2. Zarządzanie ramkami stosu i lokalizacja zmiennych
Aby wartości zapisane na stosie w ramach konkretnej ramki stosu były zarządzane przez kompilator i odczytywane przez program, musi istnieć pewna metoda obliczania pozycji tych wartości i odzyskiwania ich adresu pamięci. Pomagają w tym rejestry w CPU nazywane wskaźnikiem stosu i wskaźnikiem bazowym.
Wskaźnik bazowy, ebp
zgodnie z konwencją, zawiera adres pamięci dna lub podstawy stosu. Pozycje wszystkich wartości w ramce stosu można obliczyć za pomocą adresu we wskaźniku podstawowym jako odniesienia. Jest to pokazane na powyższym obrazku: %ebp + 4
jest to adres pamięci zapisany na przykład we wskaźniku podstawowym plus 4.
3. Kod generowany przez kompilator
Ale nie rozumiem, w jaki sposób zmienne na stosie są następnie odczytywane przez aplikację - jeśli zadeklaruję i przypiszę x jako liczbę całkowitą, powiedzmy x = 3, a pamięć jest zarezerwowana na stosie, a następnie przechowywana jest jej wartość 3 tam, a następnie w tej samej funkcji deklaruję i przypisuję y jako, powiedzmy 4, a następnie używam x w innym wyrażeniu, (powiedzmy z = 5 + x) w jaki sposób program może odczytać x w celu oceny z, kiedy jest na stosie poniżej y?
Użyjmy prostego przykładowego programu napisanego w C, aby zobaczyć, jak to działa:
int main(void)
{
int x = 3;
int y = 4;
int z = 5 + x;
return 0;
}
Przeanalizujmy tekst asemblera wygenerowany przez GCC dla tego tekstu źródłowego w języku C (dla uproszczenia wyczyściłem go trochę):
main:
pushl %ebp # save previous frame's base address on stack
movl %esp, %ebp # use current address of stack pointer as new frame base address
subl $16, %esp # allocate 16 bytes of space on stack for function data
movl $3, -12(%ebp) # variable x at address %ebp - 12
movl $4, -8(%ebp) # variable y at address %ebp - 8
movl -12(%ebp), %eax # write x to register %eax
addl $5, %eax # x + 5 = 9
movl %eax, -4(%ebp) # write 9 to address %ebp - 4 - this is z
movl $0, %eax
leave
Co możemy zaobserwować, że zmienne X, Y i Z znajdują się pod adresami %ebp - 12
, %ebp -8
i %ebp - 4
, odpowiednio. Innymi słowy, lokalizacje zmiennych w ramce stosu main()
są obliczane przy użyciu adresu pamięci zapisanego w rejestrze CPU %ebp
.
4. Dane w pamięci poza wskaźnikiem stosu są poza zakresem
Wyraźnie czegoś mi brakuje. Czy to dlatego, że położenie na stosie dotyczy tylko czasu życia / zakresu zmiennej i że cały stos jest faktycznie dostępny dla programu przez cały czas? Jeśli tak, to czy oznacza to, że istnieje jakiś inny indeks, który przechowuje tylko adresy zmiennych na stosie, aby umożliwić pobieranie wartości? Ale potem pomyślałem, że cały sens stosu polegał na tym, że wartości były przechowywane w tym samym miejscu co adres zmiennej?
Stos to region w pamięci wirtualnej, którego użyciem zarządza kompilator. Kompilator generuje kod w taki sposób, że wartości poza wskaźnikiem stosu (wartości poza górną krawędzią stosu) nigdy nie są przywoływane. Po wywołaniu funkcji pozycja wskaźnika stosu zmienia się, aby utworzyć miejsce na stosie, który można powiedzieć, że nie jest „poza granicami”.
Gdy funkcje są wywoływane i zwracane, wskaźnik stosu jest zmniejszany i zwiększany. Dane zapisane na stosie nie znikają, gdy są poza zakresem, ale kompilator nie generuje instrukcji odwołujących się do tych danych, ponieważ nie ma możliwości, aby kompilator obliczył adresy tych danych za pomocą %ebp
lub %esp
.
5. Podsumowanie
Kod, który może być bezpośrednio wykonany przez CPU, jest generowany przez kompilator. Kompilator zarządza stosem, ramkami stosu dla funkcji i rejestrami procesora. Jedną strategią stosowaną przez GCC do śledzenia lokalizacji zmiennych w ramkach stosu w kodzie przeznaczonym do wykonania w architekturze i386 jest użycie adresu pamięci w podstawowym wskaźniku ramki stosu %ebp
, jako odniesienia i zapisanie wartości zmiennych do lokalizacji w ramkach stosu w przesunięciach do adresu w %ebp
.