Конфлікт між підручником у Стенфорді та GCC


82

Згідно з цим фільмом (близько хвилини 38), якщо я маю дві функції з однаковими місцевими варі, вони будуть використовувати той самий простір. Отже, наступну програму слід надрукувати 5. Складання його з gccрезультатами -1218960859. чому?

Програма:

#include <stdio.h>

void A()
{
    int a;
    printf("%i",a);
}

void B()
{
    int a;
    a = 5;
}

int main()
{
    B();
    A();
    return 0;
}

за запитом, ось вихідні дані від розбирача:

0804840c <A>:
 804840c:   55                      push   ebp
 804840d:   89 e5                   mov    ebp,esp
 804840f:   83 ec 28                sub    esp,0x28
 8048412:   8b 45 f4                mov    eax,DWORD PTR [ebp-0xc]
 8048415:   89 44 24 04             mov    DWORD PTR [esp+0x4],eax
 8048419:   c7 04 24 e8 84 04 08    mov    DWORD PTR [esp],0x80484e8
 8048420:   e8 cb fe ff ff          call   80482f0 <printf@plt>
 8048425:   c9                      leave  
 8048426:   c3                      ret    

08048427 <B>:
 8048427:   55                      push   ebp
 8048428:   89 e5                   mov    ebp,esp
 804842a:   83 ec 10                sub    esp,0x10
 804842d:   c7 45 fc 05 00 00 00    mov    DWORD PTR [ebp-0x4],0x5
 8048434:   c9                      leave  
 8048435:   c3                      ret    

08048436 <main>:
 8048436:   55                      push   ebp
 8048437:   89 e5                   mov    ebp,esp
 8048439:   83 e4 f0                and    esp,0xfffffff0
 804843c:   e8 e6 ff ff ff          call   8048427 <B>
 8048441:   e8 c6 ff ff ff          call   804840c <A>
 8048446:   b8 00 00 00 00          mov    eax,0x0
 804844b:   c9                      leave  
 804844c:   c3                      ret    
 804844d:   66 90                   xchg   ax,ax
 804844f:   90                      nop

41
"вони добре використовують один і той же простір" - це неправильно. Вони можуть. А може і ні. І ви не можете покластися на це в будь-якому випадку.
Mat

17
Мені цікаво, яке використання цього має як вправу, якщо хтось буде використовувати це у виробничому коді, то отримає постріл.
AndersK

12
@claptrap Може бути, щоб дізнатися, як працює стек викликів, і зрозуміти, що робить комп'ютер під капотом? Люди сприймають цей шлях занадто серйозно.
Джонатан Рейнхарт,

9
@claptrap Знову ж таки, це навчальна вправа . «Обручі, через які потрібно перестрибнути», мають сенс, якщо ви розумієте, що відбувається на рівні складання. Я серйозно сумніваюся, що ОП має намір використати щось подібне у "справжній" програмі (якщо він це зробить, його слід штовхнути!)
Джонатан Рейнхарт,

12
Приклад вводить в оману нічого не підозрюючого, оскільки дві локальні змінні мають однакову назву; але це не має значення для того, що відбувається: має значення лише кількість та тип змінних. Різні імена повинні працювати абсолютно однаково.
alexis

Відповіді:


130

Так, так, це невизначена поведінка , оскільки ви використовуєте змінну uninitialized 1 .

Однак, на архітектурі x86 2 , цей експеримент повинен працювати . Значення не "стирається" зі стека, і оскільки воно не ініціалізоване B(), це те саме значення все одно повинно бути там, за умови, що кадри стека ідентичні.

Я наважуся здогадатися, що, оскільки int aвін не використовується всередині void B(), компілятор оптимізував цей код, і 5 ніколи не писалося в цьому місці в стеку. Спробуйте також додати printfin B()- це може просто спрацювати.

Крім того, прапорці компілятора - а саме рівень оптимізації - також, ймовірно, вплинуть і на цей експеримент. Спробуйте відключити оптимізацію, перейшовши -O0до gcc.

Редагувати: Я щойно скомпілював ваш код за допомогою gcc -O0(64-розрядної версії), і справді, програма друкує 5, як очікував би знайомий зі стеком викликів. Насправді це працювало навіть без -O0. 32-розрядна збірка може поводитися інакше.

Застереження: Ніколи, ніколи не використовуйте щось подібне у "реальному" коді!

1 - Там є дискусія триває нижче про те чи ні , це офіційно «UB», або просто непередбачувані.

2 - Також x64, і, мабуть, будь-яка інша архітектура, яка використовує стек викликів (принаймні, з MMU)


Давайте розглянемо причину, чому це не спрацювало. Це найкраще видно в 32 бітах, тому я буду компілювати з -m32.

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

Я скомпілював за допомогою $ gcc -m32 -O0 test.c(Оптимізацію вимкнено). Коли я запускаю це, воно друкує сміття.

Дивлячись на $ 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    

Ми бачимо, що в Bкомпіляторі зарезервовано 0x10 байт простору стека та ініціалізовано нашу int aзмінну на [ebp-0x4]5.

У AТим НЕ менше, компілятор поміщений int aв [ebp-0xc]. Так що в цьому випадку наші локальні змінні НЕ в кінцевому підсумку в тому ж місці! Додаючи printf()виклик , Aа також викличе кадри стека для Aі Bбути ідентичними, і друку 55.


7
Гарне застереження!
Тобіас Верре

5
Навіть якщо це спрацює один раз, це не буде надійним для деяких архітектур - преамбула переривання в будь-який момент усуває все, що знаходиться нижче вказівника стека.
Martin James

6
Так багато голосів за відповідь, яка навіть не згадує про "невизначену поведінку". Крім того, це також прийнято.
BЈовић

25
Крім того, це прийнято, оскільки насправді відповідає на питання .
slebetman

8
@ BЈовић Ти дивився якесь відео? Подивіться, всі та їхній брат знають, що ви не повинні робити це у реальному коді, і це викликає невизначену поведінку . Справа не в цьому. Справа в тому, що комп’ютер - це чітко визначена, передбачувана машина. На коробці x86 (і, мабуть, на більшості інших архітектур), з розумним компілятором і, можливо, деяким кодом / прапором, що масажує, це буде працювати, як очікувалося. Цей код, разом із відео, є лише демонстрацією того, як працює стек дзвінків. Якщо це вас так сильно турбує, я пропоную вам піти в інше місце. Деякі з нас цікавих типів люблять розуміти речі.
Джонатан Рейнхарт,

36

Це невизначена поведінка . Неініціалізована локальна змінна має невизначене значення, і її використання призведе до невизначеної поведінки.


6
Якщо бути точнішим, використання неіціалізованої змінної, адреса якої ніколи не береться, є невизначеною поведінкою.
Jens Gustedt

@JensGustedt Приємний коментар. Чи маєте Ви щось сказати про розділ “Наступний приклад” на blog.frama-c.com/index.php?post/2013/03/13/… ?
Паскаль Куок,

@ PascalCuoq, це навіть здається постійним обговоренням у комітеті зі стандартів. Бувають ситуації, коли перевірка пам'яті, яку ви отримуєте через вказівник, має сенс, навіть якщо ви не можете знати, ініціалізована вона чи ні. Просто зробити його невизначеним у всіх випадках є занадто обмежувальним.
Jens Gustedt

@JensGustedt: Як сприйняття адреси спричиняє використання визначеної поведінки: { int uninit; &uninit; printf("%d\n", uninit); }досі має невизначену поведінку. З іншого боку, ви можете розглядати будь-який об'єкт як масив unsigned char; це те, що ти мав на увазі?
Кіт Томпсон,

@KeithThompson, ні, це навпаки. Наявність такої змінної, що її адреса ніколи не береться і вона не ініціалізується, веде до UB. Читання невизначеного значення саме по собі не є невизначеною поведінкою, вміст просто непередбачуваний. З 6.3.2.1 p2: Якщо lvalue позначає об'єкт тривалості автоматичного зберігання, який міг бути оголошений за допомогою класу сховища реєстру (ніколи не бралася його адреса), і цей об'єкт неініціалізований (не оголошений за допомогою ініціалізатора і не присвоєний йому було виконано до використання), поведінка невизначена.
Jens Gustedt

12

Важливо пам’ятати одне - ніколи не покладайтесь на щось подібне і ніколи не використовуйте це в реальному коді! Це просто цікава річ (що навіть не завжди відповідає дійсності), а не особливість чи щось подібне. Уявіть собі, що ви намагаєтеся знайти помилку, вироблену такою «особливістю» - кошмаром.

До речі. - C і C ++ наповнені такими "особливостями", ось ВЕЛИКИЙ слайд-шоу про це: http://www.slideshare.net/olvemaudal/deep-c Тож, якщо ви хочете побачити більше подібних "функцій", зрозумійте, що просто дивіться це слайд-шоу - ви не пошкодуєте, і я впевнений, що навіть більшість досвідчених програмістів на c / c ++ можуть багато чому навчитися.


7

У функції Aзмінна aне ініціалізується, друк її значення призводить до невизначеної поведінки.

У деякому компіляторі змінна ain Aі ain Bзнаходяться в одній адресі, тому вона може друкуватися 5, але знову ж таки, ви не можете покладатися на невизначену поведінку.


1
Підручник на 100% правильний, але чи s machine will be the same depends on the assembly generated by the compiler. As @JonathonReinhart pointed out the call to можна було оптимізувати результати на оригінальному плакаті B () `.
Ллойд Кроулі,

1
У мене проблема зі словником "що підручник неправильний". Ви насправді пішли дивитись підручник? Це не спроба навчити вас робити такі божевільні речі, а продемонструвати, як працює стек дзвінків. У цьому випадку підручник є повністю правильним.
Джонатан Рейнхарт,

@JonathonReinhart Я не дивився підручник, думав, що цей приклад з підручника, я видалю цю частину.
Yu Hao

@LloydCrawley Я видалив частину про підручник. Я знаю, що це стосується архітектури стеків, це те, що я мав на увазі, маючи на увазі, що вони знаходяться в одній адресі, коли вона надрукувала 5, але, очевидно, Джонатан Рейнхарт має набагато краще пояснення.
Yu Hao

7

Складіть свій код за допомогою gcc -Wall filename.cВи побачите ці попередження.

In function 'B':
11:9: warning: variable 'a' set but not used [-Wunused-but-set-variable]

In function 'A':
6:11: warning: 'a' is used uninitialized in this function [-Wuninitialized]  

В c Друк неініціалізованої змінної веде до невизначеної поведінки.

Розділ 6.7.8. Ініціалізація стандарту C99 говорить

Якщо об'єкт, що має автоматичну тривалість зберігання, не ініціалізується явно, його значення є невизначеним. Якщо об'єкт, який має статичну тривалість зберігання, не ініціалізований явно, тоді:

if it has pointer type, it is initialized to a null pointer;
— if it has arithmetic type, it is initialized to (positive or unsigned) zero;
— if it is an aggregate, every member is initialized (recursively) according to these rules;
— if it is a union, the first named member is initialized (recursively) according to these rules.

Редагувати1

Як @Jonathon Reinhart Якщо ви відключите оптимізацію за допомогою -Oпрапораgcc-O0 тоді ви можете отримати результат 5.

Але це зовсім не гарна ідея, ніколи не використовуйте це у виробничому коді.

-Wuninitialized це одне з цінних попереджень. Ви повинні розглянути це. Не слід ні вимикати, ні пропускати це попередження, яке призводить до величезних збитків у виробництві, таких як спричинення збоїв під час запуску демонів.


Редагувати2

Пояснення слайдів Deep C, чому результат 5 / сміття. Додавання цієї інформації з цих слайдів з незначними змінами, щоб зробити цю відповідь трохи ефективнішою.

Випадок 1: без оптимізації

$ gcc -O0 file.c && ./a.out  
5

Можливо, цей компілятор має пул іменованих змінних, які він використовує повторно. Наприклад, змінна a була використана і випущена B(), тоді, коли A()потрібні цілі імена, aвона отримає змінну і отримає те саме місце в пам'яті. Якщо ви перейменуєте змінну в B(), скажімо b, тоді я не думаю, що ви отримаєте 5.

Випадок 2: з оптимізацією

Багато речей може трапитися, коли ввімкнеться оптимізатор. У цьому випадку я гадаю, що виклик до B()можна пропустити, оскільки він не має побічних ефектів. Крім того, я не був би здивований, якщо A()вбудований символ main(), тобто відсутність виклику функції. (Але оскільки A ()має видимість компонувальника, об’єктний код функції все одно повинен бути створений на випадок, якщо інший об’єктний файл хоче пов’язати функцію). У будь-якому випадку, я підозрюю, що друковане значення буде чимось іншим, якщо ви оптимізуєте код.

gcc -O file.c && ./a.out
1606415608  

Сміття!


1
Ваша логіка в "Редагуванні 2", випадок 1, абсолютно неправильна. Це не зовсім , як це працює. Ім'я локальної змінної абсолютно нічого не означає.
Джонатан Рейнхарт,

@JonathonReinhart Як зазначалося у відповіді, я додав це із слайдів deepc, поясніть, будь ласка, на якій основі це неправильно.
Gangadhar

3
Не існує жодної асоціації між простором стеку та іменами змінних. Приклад спирається на той факт, що концептуально кадр стека у другому виклику функції просто буде накладати кадр стека другого виклику функції. Не має значення, які імена вони мають, якщо обидва підписи методів однакові, може статися одне і те ж. Як зазначали інші, якби воно було у вбудованій системі і апаратне переривання обслуговувалось між викликами A () і B (), стек містив би випадкові значення. Старі інструменти, такі як Code Guard для Borland, дозволяли писати нулі в стек перед кожним дзвінком.
Ден Хейнс

@DanHaynes Ваш коментар переконує мене. Кадр стека під час другого виклику функції може накладати кадр стека першого виклику функції, якщо тип змінної та прототип функції однакові.
Gangadhar
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.