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

82

Согласно этому фильму (около 38-й минуты), если у меня есть две функции с одинаковыми локальными переменными, они будут использовать одно и то же пространство. Итак, следующая программа должна напечатать 5. Компиляция с gccрезультатами -1218960859. Почему?

Программа:

как просили, вот вывод дизассемблера:

Эльяшив
источник
41
«они хорошо используют одно и то же пространство» - это неверно. Они могли бы. Или нет. И ни в коем случае нельзя на это полагаться.
Мат,
17
Интересно, какое использование это имеет в качестве упражнения, если бы кто-то использовал это в производственном коде, он бы был застрелен.
AndersK
12
@claptrap Может быть, чтобы узнать, как работает стек вызовов, и понять, что делает компьютер под капотом? Люди относятся к этому слишком серьезно.
Джонатон Рейнхарт,
9
@claptrap Опять же, это обучающее упражнение . Все «обручи, через которые нужно перепрыгивать» имеют смысл, если вы понимаете, что происходит на уровне сборки. Я серьезно сомневаюсь, что ОП имеет какое-либо намерение использовать что-то подобное в «настоящей» программе (если он это делает, его следует выгнать!)
Джонатон Рейнхарт
12
Пример вводит в заблуждение ничего не подозревающих, потому что две локальные переменные имеют одинаковое имя; но это не имеет отношения к тому, что происходит: имеет значение только количество и тип переменных. Разные имена должны работать одинаково.
Alexis

Ответы:

130

Да, да, это неопределенное поведение , потому что вы используете неинициализированную переменную 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 <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.

Джонатон Рейнхарт
источник
5
Даже если он сработает один раз, он не будет надежным на некоторых архитектурах - преамбула прерывания позволяет сдуть все, что ниже указателя стека, в любое время.
Мартин Джеймс,
4
@JonathonReinhart - один из примеров - это любой случай, когда нет блока управления памятью, нет ограничений режима пользователя / уровня и поэтому нет аппаратного переключения на другой стек при прерывании. Вероятно, есть и другие, где некоторые данные должны быть помещены в стек прерванных задач, прежде чем произойдет переключение на стек прерываний ядра.
Мартин Джеймс,
6
Так много голосов за ответ, в котором даже не упоминается «неопределенное поведение». Кроме того, это тоже принято.
BЈовић
25
Кроме того, это принято, потому что оно действительно отвечает на вопрос .
slebetman
8
@ BЈовић Вы смотрели какое-нибудь видео? Послушайте, все и их брат знают, что вы не должны делать этого в реальном коде, и это вызывает неопределенное поведение . Не в этом дело. Дело в том, что компьютер - это четко определенная, предсказуемая машина. На компьютере x86 (и, вероятно, на большинстве других архитектур), с нормальным компилятором и, возможно, с некоторым кодом / флагом, это будет работать, как ожидалось. Этот код вместе с видео - просто демонстрация того, как работает стек вызовов. Если вас это так сильно беспокоит, я предлагаю вам пойти в другое место. Некоторым из нас, любопытных, нравится разбираться в вещах.
Джонатон Рейнхарт,
36

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

Какой-то чувак-программист
источник
6
Чтобы быть более точным, использование унифицированной переменной, адрес которой никогда не берется, является неопределенным поведением.
Йенс Густедт,
@JensGustedt Хороший комментарий. Что вы можете сказать о разделе «Следующий пример» на сайте blog.frama-c.com/index.php?post/2013/03/13/… ?
Паскаль Куок,
@ PascalCuoq, это даже, кажется, обсуждается в комитете по стандартам. Бывают ситуации, когда проверка памяти, которую вы получаете с помощью указателя, имеет смысл, даже если вы не можете знать, инициализирована она или нет. Просто сделать его неопределенным во всех случаях - это слишком ограничительно.
Йенс Густедт,
@JensGustedt: Как использование его адреса приводит к определенному поведению: { int uninit; &uninit; printf("%d\n", uninit); }все еще имеет неопределенное поведение. С другой стороны, вы можете рассматривать любой объект как массив unsigned char; это то, что вы имели в виду?
Кейт Томпсон,
@KeithThompson, нет, все наоборот. Наличие такой переменной, что ее адрес никогда не берется и не инициализируется, приводит к UB. Считывание неопределенного значения само по себе не является неопределенным поведением, просто его содержимое непредсказуемо. Из 6.3.2.1 p2: Если lvalue обозначает объект с автоматической продолжительностью хранения, который мог быть объявлен с классом хранения регистров (адрес никогда не был взят), и этот объект не инициализирован (не объявлен с инициализатором и не присваивается ему было выполнено до использования), поведение не определено.
Йенс Густедт
12

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

Кстати. - C и C ++ полны таких «функций», вот ОТЛИЧНОЕ слайд-шоу об этом: http://www.slideshare.net/olvemaudal/deep-c Итак, если вы хотите увидеть больше похожих «функций», поймите, что под капотом и как это работает, просто посмотрите это слайд-шоу - вы не пожалеете, и я уверен, что даже большинство опытных программистов на c / c ++ могут многому научиться из этого.

Cyriel
источник
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 Я не смотрел учебник, думал, что это пример из учебника, я удалю эту часть.
Ю Хао
@LloydCrawley Я удалил часть об учебнике. Я знаю, что речь идет об архитектуре стека, это то, что я имел в виду, когда они находились по тому же адресу, когда он печатал 5, но, очевидно, у Джонатона Рейнхарта есть гораздо лучшее объяснение.
Ю Хао
7

Скомпилируйте свой код с помощью. gcc -Wall filename.cВы увидите эти предупреждения.

В c Печать неинициализированной переменной приводит к неопределенному поведению.

Раздел 6.7.8 Инициализация стандарта C99 говорит

Если объект, который имеет автоматическую продолжительность хранения, не инициализирован явно, его значение не определено. Если объект со статической продолжительностью хранения не инициализирован явно, то:

Edit1

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

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

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


Edit2

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

Случай 1: без оптимизации

Возможно, у этого компилятора есть набор именованных переменных, которые он использует повторно. Например, переменная a была использована и выпущена B(), тогда, когда ей A()потребуются целочисленные имена, aона получит переменную, которая получит то же место в памяти. Если вы переименуете переменную B(), скажем b, в, то я не думаю, что вы получите 5.

Случай 2: с оптимизацией

Когда срабатывает оптимизатор, может произойти множество вещей. В этом случае я предполагаю, что вызов B()можно пропустить, поскольку он не имеет побочных эффектов. Кроме того, я не удивлюсь, если он A()будет встроен main(), то есть без вызова функции. (Но поскольку A ()есть видимость компоновщика, объектный код для функции все равно должен быть создан на тот случай, если другой объектный файл захочет связать с функцией). В любом случае, я подозреваю, что напечатанное значение будет чем-то другим, если вы оптимизируете код.

Фигня!

Гангадхар
источник
1
Ваша логика в Edit 2, Case 1 полностью неверна. Это не совсем , как это работает. Имя локальной переменной абсолютно ничего не значит.
Джонатон Рейнхарт
@JonathonReinhart Как упоминалось в ответе, я добавил это из слайдов deepc, пожалуйста, объясните, на каком основании это неверно.
Гангадхар
3
Нет никакой связи между пространством стека и именами переменных. Пример основан на том факте, что концептуально кадр стека во втором вызове функции будет просто перекрывать кадр стека второго вызова функции. Неважно, каковы их имена, если обе сигнатуры методов одинаковы, может произойти одно и то же. Как указывали другие, если бы это было во встроенной системе и аппаратное прерывание обслуживалось между вызовами A () и B (), стек содержал бы случайные значения. Старые инструменты, такие как Code Guard для Borland, позволяли записывать нули в стек перед каждым вызовом.
Дэн Хейнс
@DanHaynes Ваш комментарий убеждает меня. Фрейм стека во втором вызове функции может перекрывать фрейм стека первого вызова функции, поскольку тип переменной и прототип функции совпадают. Да, я тоже согласен, потому что нет ничего общего с именами переменных.
Гангадхар