Что происходит с объявленной, неинициализированной переменной в C? Имеет ли это значение?

Если в CI напишите:

int num; 

Прежде чем присваивать значение num , значение num равно неопределенному?

Статические переменные (область видимости файла и функция static) инициализируются до нуля:

 int x; // zero int y = 0; // also zero void foo() { static int x; // also zero } 

Нестатические переменные (локальные переменные) являются неопределенными . Чтение их перед назначением значения приводит к неопределенному поведению.

 void foo() { int x; printf("%d", x); // the compiler is free to crash here } 

На практике у них обычно есть какая-то бессмысленная ценность, изначально – некоторые компиляторы могут даже вводить определенные фиксированные значения, чтобы сделать их очевидными при поиске в отладчике, – но, строго говоря, компилятор может делать что угодно: от сбоев до вызова деmonoв через ваши носовые проходы .

Что касается того, почему это неопределенное поведение, а не просто «неопределенное / произвольное значение», существует множество архитектур процессора, у которых есть дополнительные биты флагов в их представлении для разных типов. Современным примером будет Itanium, в котором есть бит «Not Thing» в своих регистрах ; конечно, стандартные разработчики C рассматривали некоторые более старые архитектуры.

Попытка работать со значением с установленными этими битами флагов может привести к исключению CPU в операции, которая действительно не должна прерываться (например, целочисленное добавление или присвоение другой переменной). И если вы идете и оставляете переменную неинициализированной, компилятор может забрать некоторый случайный мусор с установленными этими битами флагов, то есть трогать, что неинициализированная переменная может быть смертельной.

0, если статический или глобальный, неопределенный, если class хранения авто

C всегда был очень специфичен в отношении начальных значений объектов. Если глобальные или static , они будут обнулены. Если значение auto , значение неопределенно .

Это имело место в компиляторах pre-C89 и было так указано K & R и в оригинальном отчете C DMR.

Это было в C89, см. Раздел 6.5.7 «Инициализация» .

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

Это было в C99, см. Раздел 6.7.8 Инициализация .

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

Что касается того, что именно неопределенно , я не уверен в C89, C99 говорит:

3.17.2
неопределенное значение
либо неопределенное значение, либо ловушечное представление

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

Вы можете удивиться, почему так? Другой ответ SO отвечает на этот вопрос: https://stackoverflow.com/a/2091505/140740

Это зависит от продолжительности хранения переменной. Переменная со статической продолжительностью хранения всегда неявно инициализируется нулем.

Что касается автоматических (локальных) переменных, то неинициализированная переменная имеет неопределенное значение . Неопределенное значение, между прочим, означает, что любое «значение», которое вы могли бы «видеть» в этой переменной, не только непредсказуемо, но даже не гарантировано быть стабильным . Например, на практике (т.е. игнорируя UB на секунду) этот код

 int num; int a = num; int b = num; 

не гарантирует, что переменные a и b получат одинаковые значения. Интересно, что это не какая-то педантичная теоретическая концепция, это легко происходит на практике как следствие оптимизации.

Так что в общем, популярный ответ, что «он инициализирован каким бы мусором в памяти», даже не отдаленно корректен. Поведение неинициализированной переменной отличается от поведения переменной, инициализированной мусором.

Ubuntu 15.10, Kernel 4.2.0, x86-64, пример GCC 5.2.1

Достаточно стандартов, давайте посмотрим на реализацию 🙂

Локальное значение

Стандарты: неопределенное поведение.

Реализация: программа выделяет пространство стека и никогда ничего не перемещает на этот адрес, поэтому все, что было ранее, используется.

 #include  int main() { int i; printf("%d\n", i); } 

скомпилировать с помощью:

 gcc -O0 -std=c99 ac 

выходы:

 0 

и декомпилирует с:

 objdump -dr a.out 

чтобы:

 0000000000400536 
: 400536: 55 push %rbp 400537: 48 89 e5 mov %rsp,%rbp 40053a: 48 83 ec 10 sub $0x10,%rsp 40053e: 8b 45 fc mov -0x4(%rbp),%eax 400541: 89 c6 mov %eax,%esi 400543: bf e4 05 40 00 mov $0x4005e4,%edi 400548: b8 00 00 00 00 mov $0x0,%eax 40054d: e8 be fe ff ff callq 400410 400552: b8 00 00 00 00 mov $0x0,%eax 400557: c9 leaveq 400558: c3 retq

Из наших знаний о соглашениях вызова x86-64:

  • %rdi – первый аргумент printf, поэтому строка "%d\n" по адресу 0x4005e4

  • %rsi – второй аргумент printf, таким образом, i .

    Он исходит от -0x4(%rbp) , который является первой 4-байтовой локальной переменной.

    На данный момент rbp находится на первой странице стека, который был выделен kernelм, поэтому, чтобы понять это значение, мы рассмотрели бы код ядра и выяснили, для чего он это устанавливает.

    TODO заставляет kernel ​​установить что-то память перед повторным использованием для других процессов, когда процесс умирает? Если нет, новый процесс сможет читать память других готовых программ, утечки данных. См .: Неинициализированные значения когда-либо представляют угрозу безопасности?

Затем мы также можем играть с нашими собственными изменениями в стеке и писать такие забавные вещи, как:

 #include  int f() { int i = 13; return i; } int g() { int i; return i; } int main() { f(); assert(g() == 13); } 

Глобальные переменные

Стандарты: 0

Реализация: .bss .

 #include  int i; int main() { printf("%d\n", i); } gcc -00 -std=c99 ac 

компилируется в:

 0000000000400536 
: 400536: 55 push %rbp 400537: 48 89 e5 mov %rsp,%rbp 40053a: 8b 05 04 0b 20 00 mov 0x200b04(%rip),%eax # 601044 400540: 89 c6 mov %eax,%esi 400542: bf e4 05 40 00 mov $0x4005e4,%edi 400547: b8 00 00 00 00 mov $0x0,%eax 40054c: e8 bf fe ff ff callq 400410 400551: b8 00 00 00 00 mov $0x0,%eax 400556: 5d pop %rbp 400557: c3 retq 400558: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1) 40055f: 00

# 601044 говорит, что i находится по адресу 0x601044 и:

 readelf -SW a.out 

содержит:

 [25] .bss NOBITS 0000000000601040 001040 000008 00 WA 0 0 4 

который говорит, что 0x601044 находится прямо в середине секции .bss , которая начинается с 0x601040 и имеет длину 8 байтов.

Затем стандарт ELF гарантирует, что раздел с именем .bss полностью заполнен нулями:

.bss этом разделе содержатся неинициализированные данные, которые вносят вклад в образ памяти программы. По определению система инициализирует данные нулями при запуске программы. Раздел не содержит файлового пространства, как указано типом раздела, SHT_NOBITS .

Кроме того, тип SHT_NOBITS эффективен и не занимает места в исполняемом файле:

sh_size Этот член дает размер раздела в байтах. Если тип SHT_NOBITS , секция занимает sh_size байты в файле. Раздел типа SHT_NOBITS может иметь ненулевой размер, но он не занимает места в файле.

Тогда kernel ​​Linux должно обнулить область памяти при загрузке программы в память при ее запуске.

Это зависит. Если это определение глобально (вне любой функции), то num будет инициализирован до нуля. Если он локальный (внутри функции), то его значение является неопределенным. Теоретически даже попытка считывания значения имеет неопределенное поведение – C допускает возможность битов, которые не вносят вклад в значение, но должны быть заданы определенными способами, чтобы вы даже получили определенные результаты от чтения переменной.

Основной ответ: да, это не определено.

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

Если class хранения является статическим или глобальным, то во время загрузки BSS инициализирует переменную или ячейку памяти (ML) до 0, если для переменной изначально не назначено какое-либо значение. В случае локальных неинициализированных переменных представление ловушки назначается ячейке памяти. Поэтому, если какой-либо из ваших регистров, содержащих важную информацию, будет перезаписан компилятором, программа может потерпеть крах.

но некоторые компиляторы могут иметь механизм, позволяющий избежать такой проблемы.

Я работал с некрополями v850, когда понял. Существует ловушечное представление, которое имеет битовые шаблоны, которые представляют неопределенные значения для типов данных, за исключением char. Когда я взял uninitialized char, я получил нулевое значение по умолчанию из-за представления ловушки. Это может быть полезно для any1, используя necv850es

Поскольку компьютеры имеют ограниченную емкость хранилища, автоматические переменные обычно будут храниться в элементах хранения (будь то регистры или ОЗУ), которые ранее использовались для какой-либо другой произвольной цели. Если такая переменная используется до того, как ей присвоено значение, это хранилище может содержать все, что было ранее, и поэтому содержимое переменной будет непредсказуемым.

В качестве дополнительной морщинки многие компиляторы могут хранить переменные в регистрах, которые больше, чем связанные типы. Хотя компилятор должен будет гарантировать, что любое значение, которое записывается в переменную и прочитанное, будет усечено и / или будет расширено до его надлежащего размера, многие компиляторы будут выполнять такое усечение при написании переменных и ожидать, что он будет иметь выполнялись до считывания переменной. На таких компиляторах что-то вроде:

 uint16_t hey(uint32_t x, uint32_t mode) { uint16_t q; if (mode==1) q=2; if (mode==3) q=4; return q; } uint32_t wow(uint32_t mode) { return hey(1234567, mode); } 

вполне может привести к тому, что wow() сохранит значения 1234567 в регистры 0 и 1 соответственно и вызовет foo() . Поскольку x не требуется внутри «foo», и поскольку функции должны помещать их возвращаемое значение в регистр 0, компилятор может распределить регистр 0 на q . Если mode равен 1 или 3, регистр 0 будет загружен 2 или 4, соответственно, но если это какое-то другое значение, функция может вернуть все, что было в регистре 0 (то есть значение 1234567), даже если это значение не находится в пределах диапазон uint16_t.

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

 void moo(int mode) { if (mode < 5) launch_nukes(); hey(0, mode); } 

компилятор мог бы сделать вывод, что поскольку вызов moo() с режимом, который больше 3, неизбежно приведет к вызову программы Undefined Behavior, компилятор может опустить любой код, который будет иметь значение только в том случае, если mode 4 или больше, например код что обычно предотвращает запуск ядерных боеприпасов в таких случаях. Обратите внимание, что ни стандартная, ни современная философия компилятора не заботятся о том, что возвращаемое значение из «эй» игнорируется - действие попытки вернуть его дает компилятору неограниченную лицензию на создание произвольного кода.

Значение num будет значением некоторого количества мусора из основной памяти (ОЗУ). это лучше, если вы инициализируете переменную сразу после создания.

Насколько я ушел, он в основном зависит от компилятора, но в большинстве случаев это значение считается принятым как 0.
Я получил значение мусора в случае VC ++, в то время как TC дал значение как 0. Я печатаю его, как показано ниже

 int i; printf('%d',i); 
  • Размещение звездочки в объявлениях указателей
  • Давайте будем гением компьютера.