Подтвердить что ты не робот

Является ли доступ к глобальному массиву за пределами его привязанного поведения undefined?

У меня только что был экзамен в моем классе сегодня - чтение кода C и ввода, и требуемый ответ был тем, что будет отображаться на экране, если программа действительно работает. Один из вопросов, объявленных a[4][4] как глобальная переменная и в точке этой программы, пытается получить доступ к a[27][27], поэтому я ответил на что-то вроде " "Доступ к массиву за пределами его границ" это поведение undefined, но учитель сказал, что a[27][27] будет иметь значение 0.

Впоследствии я попробовал некоторый код, чтобы проверить, истинна ли или нет всякая неинициализированная golbal-переменная в значение 0. Ну, это правда.

Итак, теперь мой вопрос:

  • Похоже, какая-то дополнительная память была очищена и зарезервирована для запуска кода. Сколько памяти зарезервировано? Почему компилятор резервирует больше памяти, чем должен, и для чего он нужен?
  • Будет ли a[27][27] быть 0 для всей среды?

Изменить:

В этом коде a[4][4] объявлена ​​глобальная переменная только, а в main() есть еще несколько локальных.

Я попробовал этот код снова в DevС++. Все они 0. Но это не так в VSE, в котором наибольшее значение 0, но некоторые из них имеют случайное значение, как указал Vyktor.

4b9b3361

Ответ 1

Вы были правы: это поведение undefined, и вы не можете считать его всегда производящим 0.

Что касается того, почему вы видите нуль в этом случае: современные операционные системы выделяют память для процессов в относительно крупнозернистых кусках, называемых страницами, которые намного больше отдельных переменных (не менее 4 Кбайт на x86). Когда у вас есть одна глобальная переменная, она будет расположена где-то на странице. Предполагая, что a имеет тип int[][], а int - четыре байта в вашей системе, a[27][27] будет располагаться около 500 байт с начала a. До тех пор, пока a находится рядом с началом страницы, доступ к a[27][27] будет зависеть от фактической памяти, и чтение не приведет к нарушению страницы/доступу к странице.

Конечно, вы не можете рассчитывать на это. Если, например, a предшествует почти 4 Кбайт других глобальных переменных, то a[27][27] не будет поддерживаться памятью, и ваш процесс будет сбой при попытке прочитать его.

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

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

Кроме того, если за a следуют достаточно другие глобальные переменные, которые инициализируются ненулевыми значениями, тогда доступ к a[27][27] покажет вам, какое бы значение ни находилось там.

Ответ 2

Доступ к массиву за пределами границ - это поведение undefined, что означает, что результаты непредсказуемы, поэтому этот результат a[27][27], являющийся 0, не является надежным вообще.

clang расскажите об этом очень четко, если мы используем -fsanitize=undefined:

runtime error: index 27 out of bounds for type 'int [4][4]'

Когда у вас есть поведение undefined, компилятор действительно может что-то сделать вообще, мы даже видели примеры, когда gcc имеет превративший конечный цикл в бесконечный цикл на основе оптимизации вокруг поведения undefined. Оба clang и gcc в некоторых случаях могут генерировать и undefined код команды инструкции, если он обнаруживает поведение undefined.

Почему это поведение undefined, Почему арифметическое поведение указателя out-of-bounds undefined? дает хорошее резюме причин. Например, результирующий указатель может не быть допустимым адресом, теперь указатель может указывать за пределами выделенных страниц памяти, вы могли бы работать с аппаратурой с отображением памяти вместо RAM и т.д....

Скорее всего, сегмент, в котором хранятся статические переменные, намного больше, чем выделяемый вами массив, или сегмент, который вы топаете, хотя просто обнуляется, и поэтому вам просто повезло в этом случае, но опять же совершенно ненадежное поведение, Скорее всего ваш размер составляет 4k, и доступ к a[27][27] находится в пределах этой границы, что, вероятно, связано с тем, что вы не видите ошибку сегментации.

Что говорится в стандарте

черновик стандарта C99 говорят нам, что это поведение undefined в разделе 6.5.6 Аддитивные операторы, которые покрывают арифметику указателя, которая является тем, что доступ к массиву снижается. В нем говорится:

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

[...]

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

и определение стандартов поведения undefined говорит нам о том, что стандарт не налагает никаких требований на поведение и заметки, возможное поведение непредсказуемо:

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

ПРИМЕЧАНИЕ. Возможное поведение undefined варьируется от игнорирования ситуации полностью с непредсказуемыми результатами, [...]

Ответ 3

Вот цитата из стандарта, которая указывает, что такое поведение undefined.

J.2 undefined behavor

  • Индекс массива выходит за пределы диапазона, даже если объект, по-видимому, доступен с помощью (как в выражении lvalue a [1] [7], учитывая объявление int a [4] [5]) (6.5.6).

  • Добавление или вычитание указателя на объект массива или целочисленный тип дает результат, который указывает непосредственно за объект массива и используется как операнд унарного * оператора, который оценивается (6.5.6).

В вашем случае вы индексируете массив полностью вне массива. В зависимости от того, что значение будет равно нулю, будет полностью ненадежным.

Кроме того, поведение всей программы под вопросом.

Ответ 4

Если вы просто запустите свой код из visual studio 2012 и получите результат, подобный этому (каждый при каждом запуске):

Address of a: 00FB8130
Address of a[4][4]: 00FB8180
Address of a[27][27]: 00FB834C
Value of a[27][27]: 0
Address of a[1000][1000]: 00FBCF50
Value of a[1000][1000]: <<< Unhandled exception at 0x00FB3D8F in GlobalArray.exe:
                            0xC0000005: Access violation reading location 0x00FBCF50.

Когда вы смотрите на окно "Модули", вы видите, что диапазон памяти вашего модуля приложений 00FA0000-00FBC000. И если у вас нет CRT Checks включен ничего будет контролировать то, что вы делаете внутри своей памяти (до тех пор, пока вы не 't нарушить защита памяти).

Итак, вы получили 0 в a[27][27] чисто случайно. Когда вы открываете представление памяти из позиции 00FB8130 (a), вы, вероятно, увидите что-то вроде этого:

0x00FB8130  08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x00FB8140  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x00FB8150  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x00FB8160  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x00FB8170  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x00FB8180  01 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00  ................
0x00FB8190  c0 90 45 00 b0 e9 45 00 00 00 00 00 00 00 00 00  À.E.°éE.........
0x00FB81A0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x00FB81B0  00 00 00 00 80 5c af 0f 00 00 00 00 00 00 00 00  ....€\¯.........
0x00FB81C0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
.......... 
0x00FB8330  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x00FB8340  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................ <<<<
0x00FB8350  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
..........                                      ^^ ^^ ^^ ^^

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

Например, с памятью, показанной выше a[6][0], указывается адрес 0x00FB8190, который содержит целочисленное значение 4559040.

Ответ 5

Затем попросите своего учителя объяснить это.

Я не знаю, будет ли это работать в вашей системе, но будет играть с блатной памятью. После того, как массив a с ненулевыми байтами даст другой результат для a[27][27].

В моей системе, когда я печатал содержимое a[27][27], это было 0xFFFFFFFF. т.е. -1, преобразованный в unsigned, - это все биты, установленные в дополнение к двум.

#include <stdio.h>
#include <string.h>

#define printer(expr) { printf(#expr" = %u\n", expr); }

   unsigned int d[8096];
   int a[4][4];  /* assuming an int is 4 bytes, next 4 x 4 x 4 bytes will be initialised to zero */
   unsigned int b[8096];
   unsigned int c[8096];


int main() {

   /* make sure next bytes do not contain zero'd bytes */
   memset(b, -1, 8096*4);
   memset(c, -1, 8096*4);
   memset(d, -1, 8096*4);

   /* lets check normal access */
   printer(a[0][0]);
   printer(a[3][3]);

   /* Now we disrepect the machine - undefined behaviour shall result */
   printer(a[27][27]);

   return 0;
}

Это мой вывод:

a[0][0] = 0
a[3][3] = 0
a[27][27] = 4294967295

Я видел в комментариях о просмотре памяти в Visual Studio. Самый простой способ - добавить точку разрыва где-нибудь в вашем коде (чтобы остановить выполнение), а затем перейти в Debug... windows... Меню памяти, выберите, например, память 1. Затем вы найдете адрес памяти вашего массива a. В моем случае адрес был 0x0130EFC0. поэтому введите 0x0130EFC0 в адресной строке и нажмите Enter. Это показывает память в этом месте.

Например, в моем случае.

0x0130EFC0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ..................................
0x0130EFE2  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ff ff ff ff  ..............................ÿÿÿÿ
0x0130F004  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ 
0x0130F026  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
0x0130F048  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ

Нули имеют курс a, который имеет размер байта 4 x 4 x sizeof int (4 в моем случае) = 64 байта. Байты с адреса 0x0130EFC0 равны 0xFF каждый (из содержимого b, c или d).

Обратите внимание, что:

0x130EFC0 + 64 = 0x130EFC0 + 0x40 = 130F000

который является началом всех этих ff байтов, которые вы видите. Вероятно, массив b.

Ответ 6

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

int a[4][4];
int b[4][4];

Если нет проблемы с выравниванием, и вы не задаете ни агрессивной оптимизации, ни проверки на санитацию, a[6][1] должно быть в действительности b[2][1]. Но, пожалуйста, никогда не делайте этого в производственном коде!

Ответ 7

В конкретной системе ваш учитель может быть прав - это может быть как ваш конкретный компилятор и операционная система будет вести себя.

В общей системе (т.е. без знания "инсайдера" ) ваш ответ правильный: это UB.

Ответ 8

Прежде всего, язык C не имеет границ. По сути, он почти не проверяет почти все. Это радость и гибель C.

Теперь, возвращаясь к проблеме, если вы переполняете память, это не означает, что вы вызываете segfault. Давайте посмотрим, как это работает.

Когда вы запускаете программу или вводите подпрограмму, процессор сохраняет в стеке адрес, на который возвращается, когда заканчивается функция.

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

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

push ebp      ;save register on the stack
mov ebp,esp   ;get actual stack address
sub esp,4     ;displace the stack of 4 bytes that will be used to store a 4 chars array

учитывая, что стек растет в обратном направлении данных, макет памяти:

0x0.....1C   [Parameters (if any)]    ;former function
0x0.....18   [Return Address]
0x0.....14   EBP
0x0.....10   0x0......x               ;Local DWORD parameter
0x0.....0C   [Parameters (if any)]    ;our function
0x0.....08   [Return Address]
0x0.....04   EBP
0x0.....00   0, 'c', 'b', 'a'    ;our string of 3 chars plus final nul

Это известно как стек кадров.

Теперь рассмотрим строку из четырех байтов, начиная с 0x0.... 0 и заканчивая на 0x.... 3. Если мы напишем более трех символов в массиве, мы будем последовательно заменять: сохраненную копию EBP, адрес возврата, параметры, локальные переменные предыдущей функции, затем ее EBP, обратный адрес и т.д.

Самый сценический эффект, который мы получаем, заключается в том, что при возврате функции CPU пытается вернуться к неправильному адресу, создав segfault. Такое же поведение может быть достигнуто, если одна из локальных переменных является указателями, в этом случае мы попытаемся прочитать или записать неправильные местоположения, снова инициирующие segfault.

Когда segfault не может произойти: когда раздутая переменная не находится в стеке, или у вас так много локальных переменных, что вы перезаписываете их, не касаясь адреса возврата (и они не являются указателями). Другим случаем является то, что процессор резервирует защитное пространство между локальными переменными и обратным адресом, в этом случае переполнение буфера не достигает адреса. Другая возможность - случайный доступ к элементам массива, в этом случае избыточный массив может превышать пространство стека и переполнение других данных, но, к счастью, мы не касаемся тех элементов, которые отображаются, где сохранен обратный адрес (каждый может произойти...).

Когда мы можем иметь переменные раздувания segfault, которые не находятся в стеке? При переполнении массива или указателей.

Я надеюсь, что это полезная информация...