2011-12-24 10 views
13

Próbuję zrozumieć kod wykonywalny generowany przez GCC (4.4.3) dla maszyny x86_64 działającej pod systemem Ubuntu Linux. W szczególności nie rozumiem, w jaki sposób kod śledzi ramki stosów. W dawnych czasach, w kodzie 32-bitowym, byłem przyzwyczajony do tego „prolog” w niemal każdej funkcji:x86_64 wywoływanie konwencji i ramek stosów

push %ebp 
movl %esp, %ebp 

Następnie, na końcu funkcji, nie przyjdzie to „epilog” albo

sub $xx, %esp # Where xx is a number based on GCC's accounting. 
pop %ebp 
ret 

lub po prostu

leave 
ret 

który realizuje to samo:

  • Ustaw wskaźnik stosu na szczycie aktualnej ramki, tuż pod adresem zwrotnym . Przywróć starą wartość wskaźnika ramki.

W kodzie 64-bitowym, jak widzę ją przez objdump demontażu, wiele funkcji nie przestrzegają tej konwencji - nie naciskać% RBP, a następnie zapisać% RSP do% RBP, w jaki sposób jak debugger GDB tworzy ślad zwrotny?

Moim prawdziwym celem jest próba znalezienia rozsądnego adresu, który mógłby zostać uznany za najwyższy (najwyższy adres) stosu użytkownika, gdy wykonanie osiągnie początek dowolnej funkcji w programie, gdzie być może wskaźnik stosu przesunął się w dół. Na przykład "początkowy" adres pierwotny argv byłby idealny - ale nie mam do niego dostępu z dowolnej funkcji, która wywołuje główne wywołania. Początkowo myślałem, że mogę użyć starej metody śledzenia wstecznego: ściganie zapisanych wartości wskaźnika ramki do momentu, gdy zapisana wartość będzie równa 0 - wtedy następny następny może być liczony jako najwyższa wartość praktyczna. (To nie jest to samo co uzyskanie adresu argv, ale będzie to robić - powiedzmy, aby znaleźć wartość wskaźnika stosu na _start lub jakikolwiek _start wywołuje [np. __libc_start_main].) Teraz nie wiem jak uzyskać równoważny adres w 64-bitowym kodzie.

Dzięki.

+0

Hm rzeczywiście. I nie chodzi tylko o '-fomit-frame-pointer'. –

+1

Czy próbowałeś -bar-omit-frame-wskaźnik? Czy możesz skompilować ten inny kod z tą flagą? –

+0

Kod źródłowy do 'libunwind' może być przydatny. – Nemo

Odpowiedz

5

Myślę, że różnica polega na tym, że pomijanie wskaźnika ramki jest po prostu bardziej zalecane w amd64. Przypis na stronie 16 abi mówi

konwencjonalnego zastosowania% RBP jako wskaźnik ramki dla ramki stosu można uniknąć stosując % RSP (wskaźnik stosu) do wskaźnika w ramce stosu. Technika ta zapisuje dwie instrukcje w prologu i epilogu i udostępnia jeden dodatkowy rejestr ogólnego przeznaczenia (% rbp).

Nie wiem, co robi GDB. Zakładam, że po kompilacji z -g obiekty mają magiczne informacje debugowania, które pozwalają GDB na odtworzenie tego, czego potrzebuje. Myślę, że nie próbowałem GDB na 64-bitowym komputerze bez debugowania informacji.

+0

Moje doświadczenie z x86-64 pokazało, że debugger wykorzystuje dodatkowe informacje, aby poznać rozmiar ramki stosu, który zapisuje instrukcje, ale sprawia, że ​​debugowanie i rozwijanie bólu. – Brian

+0

Tak, jak podejrzewałem. I czy to wszystko zadziwi, gdy plik wykonywalny zostanie skompilowany bez debugowania informacji? –

+0

Dziękuję. To zalecenie w ABI wyjaśnia, co się dzieje - ale wciąż zastanawiam się, jak mogę rozwiązać mój problem. Muszę uzyskać - z grubsza rzecz biorąc - wartość wskaźnika stosu, gdy wykonanie zostało wykonane jako główne, z dowolnej funkcji, która pojawia się po głównym wykresie wywołania. Wartość może być wyższa niż rzeczywista wartość górnej części stosu głównego, o ile znajduje się w stosie procesu, ale im bliżej górnej części stosu głównego, tym lepiej. –

1

Jeśli adres argv jest tym, czego potrzebujesz, dlaczego nie zapisać w nim wskaźnika?
Próba rozwinięcia stosu byłaby wysoce nieopłacalna, nawet gdyby działało.
Nawet jeśli uda ci się wrócić do stosu, nie jest oczywiste, że wskaźnik ramki pierwszej funkcji miałby wartość NULL. Pierwsza funkcja na stosie nie zwraca, ale wywołuje wywołanie systemowe, aby zakończyć, a zatem jego wskaźnik ramki nigdy nie jest używany.Nie ma dobrego powodu, dla którego byłby inicjalizowany na NULL.

+0

Dzięki. Niestety, nie, nie mogę zapisać wskaźnika w głównym. Piszę bibliotekę na poziomie użytkownika, aby połączyć się z dowolnym kodem, więc nie mogę dotknąć oryginalnego kodu (z wyjątkiem dodania #include) - lub wolałbym tego uniknąć, jeśli w ogóle jest to możliwe. Co do drugiej kwestii, miałem wrażenie, że jądra takie jak Linux podążają za konwencją ustawiania wskaźnika ramki na NULL przed przekazaniem kontroli do procesu użytkownika, właśnie w tym celu. Ale może to tylko starsza konwencja, której nie wszystkie systemy już podążają. –

1

Zakładając, że mam powiązania z glibc (co robię), wygląda na to, czy uda mi się rozwiązać ten problem dla celów praktycznych z glibc globalnym symbolem __libc_stack_end:

extern void * __libc_stack_end; 

void myfunction(void) { 
    /* ... */ 
    off_t stack_hi = (off_t)__libc_stack_end; 
    /* ... */ 
} 
3

GDB używa CFI karzeł dla odwijanie. W przypadku plików binarnych niezakrytych skompilowanych z opcją -g, będzie to sekcja .debug_info. W przypadku usuniętych plików binarnych x86-64 informacje o rozwijaniu są dostępne w sekcji .eh_frame. Jest to zdefiniowane w sekcji x86-64 ABI, sekcja 3.7, strona 56. Samo przetwarzanie tych informacji jest dość trudne, ponieważ parsowanie DWARF jest bardzo zaangażowane, ale uważam, że libunwind zawiera obsługę tego.

+0

Jestem prawie pewny, że zawsze znajduje się w sekcji '.eh_frame', co oznacza * dlaczego * nadal istnieje po usunięciu. Sposób w jaki to opisujesz, 'strip' musiałby znaleźć te informacje w' .debug_info' i skopiować je do '.eh_frame', a unwind musiałby sprawdzić obie lokalizacje ... –

+0

' .debug_info' zawiera * dodatkowe * informacje o zmiennych lokalnych wewnątrz klatek stosu, ale '.eh_frame' zawsze zawiera wystarczające informacje, aby rozwinąć stos. (tj. rozmiar każdej ramki stosu i gdzie zapisywane są rejestry zapisane w pamięci, ale nie to, która zmienna jest przechowywana w miejscu). –

Powiązane problemy