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

В соответствии с этим фильмом (около минуты 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 <[email protected]>
 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

Ответ 1

Да, да, это поведение undefined, потому что вы используете переменную uninitialized 1.

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

Я бы догадался, что, поскольку int a не используется внутри void B(), компилятор оптимизировал этот код, а 5 никогда не записывался в это место в стеке. Попробуйте добавить printf в 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 <[email protected]>
 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.

Ответ 2

Это поведение undefined. Неинициализированная локальная переменная имеет неопределенное значение, и ее использование приведет к поведению undefined.

Ответ 3

Одна важная вещь, которую нужно запомнить - не никогда не полагаться на что-то подобное и никогда использовать это в реальном коде! Это просто интересная вещь (которая даже не всегда верна), а не функция или что-то в этом роде. Представьте, что вы пытаетесь найти ошибку, вызванную такой "особенностью" - кошмаром.

Btw. - C и С++ полны таких "функций", вот слайд-шоу GREAT: http://www.slideshare.net/olvemaudal/deep-c Итак, если вы хотите увидеть более похожие "функции", поймите, что под капотом и как он работает, просто смотрите это слайд-шоу - вы не пожалеете и я Я уверен, что даже большинство опытных программистов c/С++ могут многому научиться из этого.

Ответ 4

В функции A переменная A не инициализируется, печать ее значения приводит к поведению undefined.

В некотором компиляторе переменная A в A и A в B находится в одном и том же адресе, поэтому она может печатать 5, но вы не можете полагаться на undefined поведение.

Ответ 5

Скомпилируйте свой код с помощью 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 Печать неинициализированной переменной приводит к поведению Undefined.

Раздел 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.

Edit1

Как @Jonathon Reinhart Если вы отключите оптимизацию, используя флаг -O gcc-O0, вы можете получить вывод 5.

Но это не совсем хорошая идея, никогда не используйте ее в производственном коде.

-Wuninitialized Это одно из ценных предупреждений. Вы должны учитывать это. Вы не должны либо отключать, либо пропускать это предупреждение, которое приводит к огромному ущербу в производстве, например, к сбоям во время работы демонов.


Edit2

Deep C слайды объясняют, почему результат 5/garbage. Добавление этой информации с этих слайдов с незначительными изменениями, чтобы сделать этот ответ более эффективным.

Случай 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  

Garbage!