Если в C я пишу:
int num;
Прежде чем присваивать значение num
, является ли значение num
неопределенным?
Если в C я пишу:
int num;
Прежде чем присваивать значение num
, является ли значение num
неопределенным?
Статические переменные (область содержимого и статическая функция) инициализируются до нуля:
int x; // zero
int y = 0; // also zero
void foo() {
static int x; // also zero
}
Нестатические переменные (локальные переменные) являются неопределенными. Чтение их перед назначением значения приводит к поведению undefined.
void foo() {
int x;
printf("%d", x); // the compiler is free to crash here
}
На практике у них обычно возникает некоторая бессмысленная ценность - некоторые компиляторы могут даже вводить определенные фиксированные значения, чтобы сделать их очевидными при поиске в отладчике - но, строго говоря, компилятор может свободно делать что-либо из сбой на вызов демонов через ваши носовые проходы.
Что касается причин, по которым это поведение undefined вместо просто "undefined/произвольное значение", существует множество архитектур ЦП, у которых есть дополнительные биты флагов в их представлении для разных типов. Современным примером может быть Itanium, в котором есть бит "Not Thing" в своих регистрах; конечно, стандартные разработчики C рассматривали некоторые старые архитектуры.
Попытка работать со значением с установленными этими битами флагов может привести к исключению CPU в операции, которая действительно не должна прерываться (например, целочисленное добавление или присвоение другой переменной). И если вы идете и оставляете переменную неинициализированной, компилятор может забрать некоторый случайный мусор с установленными этими битами флагов, что означает касание того, что неинициализированная переменная может быть смертельной.
C всегда был очень специфичен в отношении начальных значений объектов. Если глобальный или static
, они будут обнулены. Если auto
, значение неопределенно.
Это было в случае с компиляторами pre-C89 и было так указано K & R и в исходном отчете DMR C.
Это было в C89, см. раздел 6.5.7 Инициализация.
Если объект, который имеет автоматический длительность хранения не инициализируется явно, его значение неопределенный. Если объект, который имеет статическая продолжительность хранения не является инициализируется явно, это инициализируется неявно, как если бы каждый член, который имеет арифметический тип, был назначается 0 и каждый член, который имеет типа указателя были присвоены нулевые константа указателя.
Это было в C99, см. раздел 6.7.8 Инициализация.
Если объект, который имеет автоматический длительность хранения не инициализируется явно, его значение неопределенный. Если объект, который имеет статическая продолжительность хранения не является инициализируется явно, тогда:
- если это имеет тип указателя, он инициализируется нулевой указатель;
- если он имеет арифметику type, он инициализируется (положительный или без знака) ноль;
- если это агрегат, каждый элемент инициализируется (рекурсивно) в соответствии с этими правила;
- если это союз, первый Инициализированный элемент инициализирован (рекурсивно) в соответствии с этими правила.
Что касается того, что именно неопределенно означает, я не уверен в C89, C99 говорит:
3.17.2 неопределенное значение - либо неопределенное значение, либо ловушка представление
Но независимо от того, что говорят стандарты, в реальной жизни каждая страница стека действительно начинается с нуля, но когда ваша программа смотрит на любые значения класса хранения auto
, она видит все, что осталось позади вашей собственной программы, когда она последний использовал эти адреса стека. Если вы выделите много массивов auto
, вы увидите, что они в конечном итоге начнут аккуратно с нулями.
Вы можете удивиться, почему так? Другой ответ SO отвечает на этот вопрос: fooobar.com/questions/9663/...
Это зависит от продолжительности хранения переменной. Переменная со статической продолжительностью хранения всегда неявно инициализируется нулем.
Как и для автоматических (локальных) переменных, неинициализированная переменная имеет неопределенное значение. Неопределенное значение, между прочим, означает, что любое "значение", которое вы могли бы "видеть" в этой переменной, не только непредсказуемо, но даже не гарантировано быть стабильным. Например, на практике (т.е. Игнорируя UB на секунду) этот код
int num;
int a = num;
int b = num;
не гарантирует, что переменные a
и b
получат одинаковые значения. Интересно, что это не какая-то педантичная теоретическая концепция, это легко получается на практике как следствие оптимизации.
В общем, популярный ответ, что "он инициализирован каким бы мусором в памяти", даже не отдаленно корректен. Неинициализированное поведение переменных отличается от поведения переменной, инициализированной мусором.
Ubuntu 15.10, Kernel 4.2.0, x86-64, пример GCC 5.2.1
Достаточно стандартов, давайте посмотрим на реализацию: -)
Локальная переменная
Стандарты: поведение undefined.
Реализация: программа выделяет пространство стека и никогда ничего не перемещает на этот адрес, поэтому все, что было ранее, используется.
#include <stdio.h>
int main() {
int i;
printf("%d\n", i);
}
скомпилировать с помощью:
gcc -O0 -std=c99 a.c
выходы:
0
и декомпилирует с помощью:
objdump -dr a.out
в
0000000000400536 <main>:
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 <[email protected]>
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
находится на первой странице стека, выделенной ядром, поэтому, чтобы понять это значение, мы рассмотрели бы код ядра и выяснили, что он устанавливает для этого.
TODO устанавливает ли ядро что-то память перед тем, как повторно использовать его для других процессов, когда процесс умирает? Если нет, новый процесс сможет читать память других готовых программ, утечки данных. См.: Неинициализированные значения когда-либо подвержены угрозе безопасности?
Мы также можем играть с нашими собственными изменениями в стеке и писать такие забавные вещи, как:
#include <assert.h>
int f() {
int i = 13;
return i;
}
int g() {
int i;
return i;
}
int main() {
f();
assert(g() == 13);
}
Глобальные переменные
Стандарты: 0
Реализация: .bss
.
#include <stdio.h>
int i;
int main() {
printf("%d\n", i);
}
gcc -00 -std=c99 a.c
скомпилируется:
0000000000400536 <main>:
400536: 55 push %rbp
400537: 48 89 e5 mov %rsp,%rbp
40053a: 8b 05 04 0b 20 00 mov 0x200b04(%rip),%eax # 601044 <i>
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 <[email protected]>
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>
говорит, что i
находится по адресу 0x601044
и:
readelf -SW a.out
содержит:
[25] .bss NOBITS 0000000000601040 001040 000008 00 WA 0 0 4
который говорит, что 0x601044
находится справа в середине раздела .bss
, который начинается с 0x601040
и имеет длину 8 байтов.
ELF standard затем гарантирует, что раздел с именем .bss
полностью заполнен нулями:
.bss
В этом разделе содержатся неинициализированные данные, которые программный образ памяти. По определению система инициализирует данные с нулями при запуске программы. Раздел pies нет файлового пространства, как указано типом раздела,SHT_NOBITS
.
Кроме того, тип SHT_NOBITS
эффективен и не занимает места в исполняемом файле:
sh_size
Этот член дает размер секций в байтах. Если сек- типSHT_NOBITS
, секция занимаетsh_size
байтов в файле. Раздел типаSHT_NOBITS
может иметь ненулевой размер, но он не занимает места в файле.
Тогда ядро Linux должно обнулить область памяти при загрузке программы в память при ее запуске.
Это зависит. Если это определение глобально (вне любой функции), то num
будет инициализироваться до нуля. Если он локальный (внутри функции), то его значение является неопределенным. Теоретически даже попытка считывания значения имеет поведение undefined - C допускает возможность битов, которые не вносят вклад в значение, но должны быть установлены определенным образом, чтобы вы даже получили определенные результаты от чтения переменная.
Основной ответ: да, это undefined.
Если вы видите странное поведение из-за этого, оно может зависеть от того, где оно объявлено. Если внутри функции в стеке содержимое будет более чем вероятно, будет отличаться при каждом вызове функции. Если это статическая область или область модуля, она undefined, но не изменится.
Если класс хранения является статическим или глобальным, то во время загрузки BSS инициализирует переменную или ячейку памяти (ML) до 0, если для переменной изначально не назначено какое-либо значение. В случае локальных неинициализированных переменных представление ловушки назначается ячейке памяти. Поэтому, если какой-либо из ваших регистров, содержащих важную информацию, будет перезаписан компилятором, программа может потерпеть крах.
но некоторые компиляторы могут иметь механизм, позволяющий избежать такой проблемы.
Я работал с рядом неё v850, когда понял. Существует ловушечное представление, которое имеет битовые шаблоны, которые представляют значения undefined для типов данных, за исключением char. Когда я взял неинициализированный 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", и поскольку функции должны помещать их возвращаемое значение в
register 0, компилятор может назначить регистр 0 на q
. Если mode
равно 1 или
3, регистр 0 будет загружен 2 или 4, соответственно, но если это некоторая
другое значение, функция может вернуть все, что было в регистре 0 (т.е.
значение 1234567), хотя это значение не находится в диапазоне uint16_t.
Чтобы избежать компиляторов для выполнения дополнительной работы, чтобы гарантировать, что неинициализированные переменные никогда, кажется, не хранят ценности вне их домена, и избегают необходимости для указания неопределенного поведения в чрезмерной детализации, говорится в Стандарте что использование неинициализированных автоматических переменных Undefined Behavior. В в некоторых случаях последствия этого могут быть еще более неожиданными, чем значение вне диапазона его типа. Например, данный:
void moo(int mode)
{
if (mode < 5)
launch_nukes();
hey(0, mode);
}
компилятор мог бы сделать вывод, что, вызвав moo()
с режимом, который
больше 3, неизбежно приведет к вызову программы Undefined
Поведение, компилятор может опустить любой код, который будет иметь значение только
если mode
составляет 4 или больше, например код, который обычно предотвращает
запуск ядерных боеприпасов в таких случаях. Обратите внимание, что ни Стандарт, ни
современной философии компилятора, заботится о том, что возвращаемое значение
из "эй" игнорируется - действие попытки вернуть его дает компилятор
неограниченная лицензия на создание произвольного кода.
Значение num будет значением некоторого количества мусора из основной памяти (ОЗУ). это лучше, если вы инициализируете переменную сразу после создания.
Мы использовали демонстрацию отладчика в прошлом, рассматривая строку int a;
. Он содержал неопределенное значение (большое отрицательное число), которое казалось одинаковым каждый раз. Независимо от того, не предполагайте, что ваш int равен 0.
Насколько я ушел, он в основном зависит от компилятора, но в большинстве случаев это значение считается принятым как 0. Я получил значение мусора в случае VС++, в то время как TC дал значение как 0. I Распечатайте его, как показано ниже.
int i;
printf('%d',i);