Konflikt między samouczkiem Stanford a GCC

82

Zgodnie z tym filmem (około 38 minuty), jeśli mam dwie funkcje z tymi samymi lokalnymi zmiennymi, będą one używać tej samej przestrzeni. Powinien więc wydrukować poniższy program 5. Kompilowanie go z gccwynikami -1218960859. czemu?

Program:

zgodnie z żądaniem, oto dane wyjściowe z deasemblera:

elyashiv
źródło
41
„dobrze wykorzystują tę samą przestrzeń” - to nieprawda. Mogą. A może nie. I nie możesz na tym polegać.
Mat
17
Zastanawiam się, jakie ma to zastosowanie jako ćwiczenie, gdyby ktoś użył tego w kodzie produkcyjnym, zostałby zastrzelony.
AndersK
12
@claptrap Może chcesz dowiedzieć się, jak działa stos wywołań i zrozumieć, co komputer robi pod maską? Ludzie traktują to zbyt poważnie.
Jonathon Reinhart
9
@claptrap Ponownie, jest to ćwiczenie edukacyjne . Wszystkie „obręcze, przez które musisz przeskoczyć” mają sens, jeśli rozumiesz, co się dzieje na poziomie zespołu. I poważnie wątpić PO ma się zamiaru korzystania coś takiego w programie „prawdziwego” (jeśli to zrobi, powinien zostać wyrzucony!)
Jonathon Reinhart
12
Przykład jest mylący dla niczego niepodejrzewających, ponieważ dwie zmienne lokalne mają taką samą nazwę; ale to nie ma znaczenia dla tego, co się dzieje: liczy się tylko liczba i typ zmiennych. Różne nazwy powinny działać dokładnie tak samo.
alexis

Odpowiedzi:

130

Tak, tak, jest to niezdefiniowane zachowanie , ponieważ używasz zmiennej uninitialized 1 .

Jednak na architekturze x86, 2 , eksperyment ten powinien działać . Wartość nie jest „usuwana” ze stosu, a ponieważ nie jest zainicjowana w B(), ta sama wartość powinna nadal występować, pod warunkiem, że ramki stosu są identyczne.

Zaryzykowałbym zgadywanie, że ponieważ int anie jest używany w programie void B(), kompilator zoptymalizował ten kod, a 5 nigdy nie zostało zapisane w tym miejscu na stosie. Spróbuj również dodać printfin B()- po prostu może działać.

Ponadto flagi kompilatora - a mianowicie poziom optymalizacji - prawdopodobnie również wpłyną na ten eksperyment. Spróbuj wyłączyć optymalizacje, przekazując -O0do gcc.

Edycja: właśnie skompilowałem twój kod z gcc -O0(64-bitowym) i rzeczywiście, program wypisuje 5, jak oczekiwałby ktoś zaznajomiony ze stosem wywołań. W rzeczywistości działało nawet bez -O0. Kompilacja 32-bitowa może zachowywać się inaczej.

Zastrzeżenie: Nigdy, przenigdy nie używaj czegoś takiego w „prawdziwym” kodzie!

1 - Jest to debata dzieje pod uwagę, czy nie jest to oficjalnie „UB”, lub po prostu nieprzewidywalne.

2 - Również x64 i prawdopodobnie każda inna architektura używająca stosu wywołań (przynajmniej tych z MMU)


Spójrzmy na powód, dla którego to nie zadziałało. Najlepiej widać to w wersji 32-bitowej, więc skompiluję z -m32.

$ gcc --version
gcc (GCC) 4.7.2 20120921 (Red Hat 4.7.2-2)

Skompilowałem z $ gcc -m32 -O0 test.c(Optymalizacje wyłączone). Kiedy to uruchamiam, drukuje śmieci.

Patrząc na $ objdump -Mintel -d ./a.out:

080483ec <A>:
 80483ec:   55                      push   ebp
 80483ed:   89 e5                   mov    ebp,esp
 80483ef:   83 ec 28                sub    esp,0x28
 80483f2:   8b 45 f4                mov    eax,DWORD PTR [ebp-0xc]
 80483f5:   89 44 24 04             mov    DWORD PTR [esp+0x4],eax
 80483f9:   c7 04 24 c4 84 04 08    mov    DWORD PTR [esp],0x80484c4
 8048400:   e8 cb fe ff ff          call   80482d0 <printf@plt>
 8048405:   c9                      leave  
 8048406:   c3                      ret    

08048407 <B>:
 8048407:   55                      push   ebp
 8048408:   89 e5                   mov    ebp,esp
 804840a:   83 ec 10                sub    esp,0x10
 804840d:   c7 45 fc 05 00 00 00    mov    DWORD PTR [ebp-0x4],0x5
 8048414:   c9                      leave  
 8048415:   c3                      ret    

Widzimy, że w Bprogramie kompilator zarezerwował 0x10 bajtów miejsca na stosie i zainicjował naszą int azmienną na [ebp-0x4]5.

W AJednak kompilator umieszczony int ana [ebp-0xc]. Więc w tym przypadku nasze zmienne lokalne nie kończyły się w tym samym miejscu! Dodanie printf()wywołania Arównież spowoduje, że ramki stosu dla Ai Bbędą identyczne i zostaną wydrukowane 55.

Jonathon Reinhart
źródło
5
Nawet jeśli zadziała raz, nie będzie niezawodny na niektórych architekturach - preambuła przerwania powoduje, że w dowolnym momencie zdmuchuję wszystko poniżej wskaźnika stosu.
Martin James
4
@JonathonReinhart - jednym z przykładów jest sytuacja, w której nie ma jednostki zarządzającej pamięcią, nie ma rozróżnień trybu użytkownika / jądra, a więc nie ma przełączania sprzętowego na inny stos po przerwaniu. Prawdopodobnie są inne, w których niektóre dane muszą zostać wypchnięte na przerwany stos zadań, zanim nastąpi przełączenie na stos przerwań jądra.
Martin James
6
Tyle pozytywnych głosów na odpowiedź, która nawet nie wspomina o „niezdefiniowanym zachowaniu”. Oprócz tego jest również akceptowany.
BЈовић
25
Jest również akceptowany, ponieważ faktycznie odpowiada na pytanie .
slebetman
8
@ BЈовић Czy obejrzałeś któryś z filmów? Słuchaj, wszyscy i ich brat wiedzą, że nie powinieneś tego robić w prawdziwym kodzie, a to wywołuje niezdefiniowane zachowanie . Nie o to chodzi. Chodzi o to, że komputer jest dobrze zdefiniowaną, przewidywalną maszyną. Na pudełku x86 (i prawdopodobnie większości innych architektur), z rozsądnym kompilatorem i potencjalnie masowaniem kodu / flag, będzie to działać zgodnie z oczekiwaniami. Ten kod, wraz z wideo, to jedynie demonstracja działania stosu wywołań. Jeśli tak bardzo ci to przeszkadza, proponuję pójść gdzie indziej. Niektórzy z nas, ciekawi, lubią rozumieć.
Jonathon Reinhart
36

To niezdefiniowane zachowanie . Niezainicjowana zmienna lokalna ma nieokreśloną wartość i użycie jej doprowadzi do niezdefiniowanego zachowania.

Jakiś koleś programista
źródło
6
Mówiąc dokładniej, użycie zjednostkowanej zmiennej, której adres nigdy nie jest pobierany, jest niezdefiniowanym zachowaniem.
Jens Gustedt
@JensGustedt Ładny komentarz. Czy masz coś do powiedzenia na temat sekcji „Następny przykład” w blog.frama-c.com/index.php?post/2013/03/13/… ?
Pascal Cuoq
@ PascalCuoq, wydaje się, że to nawet tocząca się dyskusja w komitecie normalizacyjnym. Są sytuacje, w których sprawdzanie pamięci, którą uzyskujesz przez wskaźnik, ma sens, nawet jeśli nie możesz wiedzieć, czy jest zainicjowana, czy nie. Samo uczynienie go niezdefiniowanym we wszystkich przypadkach jest zbyt restrykcyjne.
Jens Gustedt
@JensGustedt: W jaki sposób zajęcie adresu powoduje, że używanie go ma zdefiniowane zachowanie: { int uninit; &uninit; printf("%d\n", uninit); }nadal ma niezdefiniowane zachowanie. Z drugiej strony, możesz traktować dowolny obiekt jako tablicę unsigned char; czy to właśnie miałeś na myśli?
Keith Thompson
@KeithThompson, nie, jest odwrotnie. Posiadanie zmiennej takiej, że jej adres nigdy nie jest pobierana i nie jest inicjowana, prowadzi do UB. Samo odczytanie nieokreślonej wartości nie jest niezdefiniowanym zachowaniem, zawartość jest po prostu nieprzewidywalna. Od 6.3.2.1 p2: Jeśli l-wartość wyznacza obiekt o automatycznym czasie przechowywania, który mógłby być zadeklarowany w klasie pamięci rejestru (nigdy nie miał pobranego adresu) i ten obiekt jest niezainicjalizowany (nie zadeklarowany z inicjalizatorem i bez przypisania zostało wykonane przed użyciem), zachowanie jest nieokreślone.
Jens Gustedt
12

Jedna ważna rzecz do zapamiętania - nigdy nie polegaj na czymś takim i nigdy nie używaj tego w prawdziwym kodzie! To po prostu interesująca rzecz (co nawet nie zawsze jest prawdą), a nie funkcja ani coś w tym rodzaju. Wyobraź sobie, że próbujesz znaleźć błąd wywołany przez tego rodzaju „funkcję” - koszmar.

Przy okazji. - C i C ++ są pełne tego rodzaju "funkcji", oto ŚWIETNY pokaz slajdów na ten temat: http://www.slideshare.net/olvemaudal/deep-c Jeśli chcesz zobaczyć więcej podobnych "funkcji", zrozum pod maską i jak to działa po prostu obejrzyj ten pokaz slajdów - nie pożałujesz i jestem pewien, że nawet większość doświadczonych programistów c / c ++ może się z tego wiele nauczyć.

cyriel
źródło
7

W funkcji Azmienna anie jest inicjalizowana, wypisanie jej wartości prowadzi do nieokreślonego zachowania.

W niektórych kompilatorach zmienne ain Ai ain Bznajdują się pod tym samym adresem, więc może drukować 5, ale znowu nie można polegać na niezdefiniowanym zachowaniu.

Yu Hao
źródło
1
Samouczek jest w 100% poprawny, ale czy wyniki na oryginalnym plakacie s machine will be the same depends on the assembly generated by the compiler. As @JonathonReinhart pointed out the call to B () `mogły zostać zoptymalizowane.
Lloyd Crawley
1
Mam problem ze słowami „ten samouczek jest zły”. Czy faktycznie obejrzałeś samouczek? To nie jest próba nauczenia cię, jak robić takie szalone rzeczy, ale pokazanie, jak działa stos wywołań. W takim przypadku samouczek jest całkowicie poprawny.
Jonathon Reinhart
@JonathonReinhart Nie oglądałem samouczka, myślałem, że ten przykład pochodzi z samouczka, usunę tę część.
Yu Hao
@LloydCrawley Usunąłem część dotyczącą samouczka. Wiem, że chodzi o architekturę stosu, to właśnie miałem na myśli, mówiąc, że są pod tym samym adresem, kiedy drukowano 5, ale najwyraźniej Jonathon Reinhart ma o wiele lepsze wyjaśnienie.
Yu Hao
7

Skompiluj swój kod z gcc -Wall filename.cZobaczysz te ostrzeżenia.

In c Drukowanie niezainicjowanej zmiennej prowadzi do niezdefiniowanego zachowania.

Sekcja 6.7.8 Inicjalizacja standardu C99 mówi

Jeśli obiekt, który ma automatyczny czas trwania przechowywania, nie jest jawnie zainicjowany, jego wartość jest nieokreślona. Jeśli obiekt, który ma statyczny czas trwania nie jest inicjowany jawnie, to:

Edycja 1

As @Jonathon Reinhart Jeśli wyłączysz optymalizację za pomocą -Oflagi, gcc-O0 możesz otrzymać wynik 5.

Ale to wcale nie jest dobry pomysł, nigdy, przenigdy nie używaj tego w kodzie produkcyjnym.

-Wuninitialized jest to jedno z cennych ostrzeżeń. Powinieneś wziąć to pod uwagę. Nie powinieneś ani wyłączać, ani pomijać tego ostrzeżenia, które prowadzi do ogromnych szkód w produkcji, takich jak awarie podczas uruchamiania demonów.


Edycja 2

Wyjaśnienie slajdów z głębokiego C Dlaczego wynik to 5 / śmieci. Dodanie tych informacji z tych slajdów z niewielkimi modyfikacjami, aby ta odpowiedź była trochę bardziej skuteczna.

Przypadek 1: bez optymalizacji

Być może ten kompilator ma pulę nazwanych zmiennych, których używa ponownie. Np. Zmienna a została użyta i wypuszczona B(), wtedy gdy A()potrzebuje nazw całkowitych, ato otrzyma zmienną, która otrzyma to samo miejsce w pamięci. Jeśli zmienisz nazwę zmiennej B()na, powiedzmy b, nie sądzę, aby to dostałeś 5.

Przypadek 2: z optymalizacją

Wiele rzeczy może się wydarzyć, gdy zadziała optymalizator. W tym przypadku myślę, że wywołanie B()można pominąć, ponieważ nie ma żadnych skutków ubocznych. Nie zdziwiłbym się również, gdyby znak A()był wbudowany main(), tj. Nie ma wywołania funkcji. (Ale ponieważ A ()jest widoczny konsolidator, kod obiektowy funkcji musi być nadal utworzony na wypadek, gdyby inny plik obiektowy chciał się połączyć z funkcją). W każdym razie podejrzewam, że wydrukowana wartość będzie inna, jeśli zoptymalizujesz kod.

Śmieci!

Gangadhar
źródło
1
Twoja logika w Edycji 2, Przypadek 1 jest całkowicie błędna. To nie jest w ogóle jak to działa. Nazwa zmiennej lokalnej nic nie znaczy.
Jonathon Reinhart
@JonathonReinhart Jak wspomniano w odpowiedzi, dodałem to ze slajdów deepc, proszę wyjaśnić, na jakiej podstawie jest niepoprawne.
Gangadhar
3
Nie ma żadnego związku między przestrzenią stosu a nazwami zmiennych. Przykład opiera się na fakcie, że koncepcyjnie ramka stosu w drugim wywołaniu funkcji po prostu nałoży ramkę stosu drugiego wywołania funkcji. Nie ma znaczenia, jakie są nazwy, o ile sygnatury obu metod są takie same, może się zdarzyć to samo. Jak inni zauważyli, gdyby było to w systemie osadzonym i przerwanie sprzętowe zostało obsłużone między wywołaniami A () i B (), stos zawierałby losowe wartości. Stare narzędzia, takie jak Code Guard for Borland, pozwalały na zapisywanie zer do stosu przed każdym wywołaniem.
Dan Haynes
@DanHaynes Twój komentarz przekonuje me.stack frame w drugim wywołaniu funkcji może nałożyć ramkę stosu wywołania funkcji First, o ile typ zmiennej i prototyp funkcji są takie same. Tak, zgadzam się, że nie ma to nic wspólnego z nazwami zmiennych.
Gangadhar