Почему это для цикла выхода на некоторых платформах, а не на других?
Я недавно начал изучать C и я беру класс с C в качестве субъекта. В настоящее время я играю с петлями, и я сталкиваюсь с каким-то странным поведением, которое я не знаю, как объяснить.
#include <stdio.h>
int main()
{
int array[10],i;
for (i = 0; i <=10 ; i++)
{
array[i]=0; /*code should never terminate*/
printf("test n");
}
printf("%d n", sizeof(array)/sizeof(int));
return 0;
}
на моем ноутбуке с Ubuntu 14.04, этот код не ломается. Он работает до завершения. На моем школьном компьютере под управлением CentOS 6.6, он также работает нормально. На Windows 8.1, цикл никогда не завершается.
что еще более странно, когда я редактирую состояние for
loop to:i <= 11
, код завершается только на моем ноутбуке с Ubuntu. Он никогда не заканчивается в CentOS и Windows.
может ли кто-нибудь объяснить, что происходит в памяти и почему разные ОС, работающие с одним и тем же кодом, дают разные результаты?
EDIT: я знаю, что цикл for выходит за рамки. Я делаю это намеренно. Я просто не могу понять, как поведение может быть разным в разных ОС и компьютерах.
14 ответов:
на моем ноутбуке под управлением Ubuntu 14.04, этот код не ломается он работает до завершения. На моем школьном компьютере под управлением CentOS 6.6, он также работает нормально. На Windows 8.1, цикл никогда не завершается.
что более странно, когда я редактирую условное
for
loop to:i <= 11
, код завершается только на моем ноутбуке с Ubuntu. CentOS и Windows никогда не прекращаются.вы только что обнаружили топот памяти. Вы можете прочитать подробнее об этом здесь:что такое "память топать"?
когда вы выделяете
int array[10],i;
, эти переменные попадают в память (в частности, они выделяются в стеке, который является блоком памяти, связанным с функцией).array[]
иi
наверное, рядом друг с другом в памяти. Кажется, что на Windows 8.1,i
находится вarray[10]
. На Сентос,i
находится вarray[11]
. И в Ubuntu, это ни в одном месте (может быть, этоarray[-1]
?).попробуйте добавить эти инструкции отладки в свой код. Вы должны заметить, что на итерации 10 или 11,
array[i]
указывает наi
.#include <stdio.h> int main() { int array[10],i; printf ("array: %p, &i: %p\n", array, &i); printf ("i is offset %d from array\n", &i - array); for (i = 0; i <=11 ; i++) { printf ("%d: Writing 0 to address %p\n", i, &array[i]); array[i]=0; /*code should never terminate*/ } return 0; }
ошибка лежит между этими кусками кода:
int array[10],i; for (i = 0; i <=10 ; i++) array[i]=0;
С
array
только 10 элементов, в последней итерацииarray[10] = 0;
переполнение буфера. Переполнение буфераНЕОПРЕДЕЛЕННОЕ ПОВЕДЕНИЕ, что означает, что они могут отформатировать ваш жесткий диск или заставить демонов вылететь из вашего носа.это довольно часто для всех переменных стека, которые должны быть выложены рядом друг с другом. Если
i
где находитсяarray[10]
пишет, затем UB сброситi
к0
, таким образом приводя к unterminated петле.чтобы исправить, измените условие цикла на
i < 10
.
в том, что должно быть последним запуском цикла, вы пишете в
array[10]
, но в массиве есть только 10 элементов, пронумерованных от 0 до 9. Спецификация языка C говорит, что это "неопределенное поведение". На практике это означает, что ваша программа попытается написать вint
-размер фрагмента памяти, который лежит сразу послеarray
в памяти. То, что происходит тогда, зависит от того, что на самом деле лежит там, и это зависит не только от операционной системы, но и в большей степени от компилятор, параметры компилятора (например, параметры оптимизации), архитектура процессора, окружающий код и т. д. Он может даже варьироваться от выполнения к выполнению, например, из-за рандомизация адресного пространства (вероятно, не на этом игрушечном примере, но это происходит в реальной жизни). Некоторые возможности включают в себя:
- место не использовалось. Цикл завершается нормально.
- место было использовано для чего-то, что случилось иметь значение 0. Цикл завершается нормально.
- расположение содержало обратный адрес функции. Цикл завершается нормально, но затем программа аварийно завершает работу, потому что он пытается перейти к адресу 0.
- расположение содержит переменную
i
. Цикл никогда не завершается, потому чтоi
начинается с 0.- расположение содержит некоторые другие переменные. Цикл завершается нормально, но затем происходят "интересные" вещи.
- расположение недопустимый адрес памяти, например, потому что
array
находится в самом конце страницы виртуальной памяти, а следующая страница не отображается.- демоны вылетают из носа. К счастью, большинство компьютеров не имеют необходимого оборудования.
то, что вы наблюдали в Windows, было то, что компилятор решил разместить переменную
i
сразу после массива в памяти, так чтоarray[10] = 0
в конечном итоге назначениеi
. На Ubuntu и CentOS компилятор не поместилi
там. Почти все реализации C группируют локальные переменные в памяти, на A стек памяти, с одним важным исключением: некоторые локальные переменные могут быть полностью помещены в регистры. Даже если переменная находится в стеке, порядок переменных определяется компилятором, и он может зависеть не только от порядка в исходном файле, но и от их типов (чтобы не тратить память на ограничения выравнивания, которые оставили бы дыры), от их имен, от некоторого хэша значение, используемое во внутренней структуре компилятора данных и т. д.если вы хотите узнать, что ваш компилятор решил сделать, вы можете сказать ему, чтобы показать вам ассемблерный код. Да, и научиться расшифровывать ассемблер (это проще, чем писать его). С помощью GCC (и некоторых других компиляторов, особенно в мире Unix), передайте опцию
-S
для получения ассемблерного кода вместо двоичного. Например, вот фрагмент ассемблера для цикла от компиляции с GCC на amd64 с оптимизацией вариант-O0
(без оптимизации), с комментариями, добавленными вручную:.L3: movl -52(%rbp), %eax ; load i to register eax cltq movl , -48(%rbp,%rax,4) ; set array[i] to 0 movl $.LC0, %edi call puts ; printf of a constant string was optimized to puts addl , -52(%rbp) ; add 1 to i .L2: cmpl , -52(%rbp) ; compare i to 10 jle .L3
здесь переменная
i
находится на 52 байта ниже верхней части стека, в то время как массив начинается на 48 байт ниже верхней части стека. Так что этот компилятор случайно поместилi
непосредственно перед массивом; вы бы перезаписатьi
если вы напишитеarray[-1]
. Если вы изменитеarray[i]=0
доarray[9-i]=0
, вы получите бесконечный цикл на этой конкретной платформе с этим конкретным компилятором опции.теперь давайте скомпилируем программу с
gcc -O1
.movl , %ebx .L3: movl $.LC0, %edi call puts subl , %ebx jne .L3
это короче! Компилятор не только отказался выделить расположение стека для
i
- он только когда-либо хранится в регистреebx
- но он не потрудился выделить какую-либо память дляarray
, или генерировать код для установки его элементов, потому что он заметил, что ни один из элементов никогда не используется.чтобы сделать этот пример более показательным, давайте убедимся, что массив назначения выполняются путем предоставления компилятору чего-то, что он не может оптимизировать. Простой способ сделать это-использовать массив из другого файла - из-за отдельной компиляции компилятор не знает, что происходит в другом файле (если он не оптимизирует время ссылки, которое
gcc -O0
илиgcc -O1
нет). Создайте исходный файлuse_array.c
Сvoid use_array(int *array) {}
и измените исходный код на
#include <stdio.h> void use_array(int *array); int main() { int array[10],i; for (i = 0; i <=10 ; i++) { array[i]=0; /*code should never terminate*/ printf("test \n"); } printf("%zd \n", sizeof(array)/sizeof(int)); use_array(array); return 0; }
компиляции с
gcc -c use_array.c gcc -O1 -S -o with_use_array1.c with_use_array.c use_array.o
в этот раз ассемблерный код выглядит так:
movq %rsp, %rbx leaq 44(%rsp), %rbp .L3: movl , (%rbx) movl $.LC0, %edi call puts addq , %rbx cmpq %rbp, %rbx jne .L3
теперь массив находится в стеке, 44 байта сверху. А как же
i
? Он нигде не появляется! Но счетчик циклов хранится в регистреrbx
. Это не совсемi
, но адресarray[i]
. Компилятор решил, что так как значениеi
никогда не использовался напрямую, не было смысла выполнять арифметику, чтобы вычислить, где хранить 0 во время каждого запуск цикла. Вместо этого этот адрес является переменной цикла, и арифметика для определения границ была выполнена частично во время компиляции (умножьте 11 итераций на 4 байта на элемент массива, чтобы получить 44) и частично во время выполнения, но раз и навсегда до начала цикла (выполните вычитание, чтобы получить начальное значение).даже на этом очень простом примере мы видели, как изменяются параметры компилятора (включите оптимизацию) или изменяется что-то незначительное (
array[i]
доarray[9-i]
) или даже изменение чего-то, по-видимому, не связанного (добавление вызова вuse_array
) может существенно повлиять на то, что делает исполняемая программа, сгенерированная компилятором. оптимизация компилятора может сделать много вещей, которые могут показаться неинтуитивными в программах, которые вызывают неопределенное поведение. Вот почему неопределенное поведение остается полностью неопределенным. Когда вы немного отклоняетесь от треков, в реальных программах может быть очень трудно понять отношения между тем, что делает код и что он должен был сделать, даже для опытных программистов.
в отличие от Java, C не выполняет проверку границ массива, т. е. нет
ArrayIndexOutOfBoundsException
, задача убедиться, что индекс массива является допустимым, остается программисту. Делать это нарочно приводит к неопределенному поведению, все может случиться.
для массива:
int array[10]
индексы действительны только в диапазоне
0
до9
. Тем не менее, вы пытаетесь:for (i = 0; i <=10 ; i++)
открыть
array[10]
здесь измените условие наi < 10
у вас есть нарушение границ, и на незаконченных платформах я считаю, что вы непреднамеренно устанавливаете
i
до нуля в конце цикла, так что он начинается снова.
array[10]
недопустимо; он содержит 10 элементов,array[0]
черезarray[9]
иarray[10]
11-й. Ваш цикл должен быть написан, чтобы остановить до10
следующим образом:for (i = 0; i < 10; i++)
здесь
array[10]
земли определяется реализацией, и забавно, на двух из ваших платформы, он приземляется наi
, которые эти платформы, по-видимому, выкладывают непосредственно послеarray
.i
устанавливается в ноль, и цикл продолжается вечно. Для других платформ,i
может быть расположен передarray
илиarray
может иметь некоторые дополнения после него.
вы объявляете
int array[10]
означаетarray
индекс0
до9
(итого10
целочисленные элементы, которые он может содержать). Но следующий цикл,for (i = 0; i <=10 ; i++)
цикл
0
до10
означает11
времени. Следовательно, когдаi = 10
он переполнит буфер и вызовет Неопределенное Поведение.так попробуйте это:
for (i = 0; i < 10 ; i++)
или
for (i = 0; i <= 9 ; i++)
он не определен в
array[10]
, и дает неопределенное поведение как описано раньше. Подумайте об этом так:у меня есть 10 пунктов в моей продуктовой корзине. Они таковы:
0: хлопья
1: Хлеб
2: Молоко
3: пирог
4: яйца
5: торт
6: 2 литра соды
7: Салат
8: котлеты
9: мороженое
cart[10]
не определено, и может дать вне границ исключение в некоторых случаях. Но, видимо, еще много не. Очевидный 11-й элемент-это элемент не на самом деле в корзине. 11-й пункт указывает на то, что я собираюсь назвать "полтергейст".- Ее никогда не было, но она была там.почему некоторые компиляторы дают
i
индексarray[10]
илиarray[11]
или дажеarray[-1]
это из-за вашей инициализации/декларации заявление. Некоторые компиляторы интерпретируют это как:
- " выделите 10 блоков
int
s дляarray[10]
и еще одинint
блок. чтобы было проще, поставить их рядом друг с другом."- так же, как и раньше, но отодвиньте его на расстояние одного или двух шагов, так что
array[10]
наi
.- сделайте то же самое, что и раньше, но выделите
i
atarray[-1]
(потому что индекс массива не может или не должен быть отрицательным), или выделить его в совершенно другом месте, потому что ОС может обрабатывать его, и это безопаснее.некоторые компиляторы хотят, чтобы все шло быстрее, а некоторые компиляторы предпочитают безопасность. Все дело в контексте. Если бы я разрабатывал приложение для древней ОС BREW (ОС базового телефона), например, он не заботился бы о безопасности. Если бы я разрабатывал для iPhone 6, то он мог бы работать быстро, несмотря ни на что, поэтому мне нужно было бы сделать акцент на безопасности. (Серьезно, вы читали руководство Apple по App Store или читали о разработке Swift и Swift 2.0?)
Так как вы создали массив размером 10, для цикла условие должно быть следующим:
int array[10],i; for (i = 0; i <10 ; i++) {
В настоящее время вы пытаетесь получить доступ к неназначенному местоположению из памяти с помощью
array[10]
и причинив неопределенное поведение. Неопределенное поведение означает, что ваша программа будет вести себя неопределенным образом, поэтому она может давать разные результаты в каждом выполнении.
Ну, компилятор C традиционно не проверяет границы. Вы можете получить ошибку сегментации, если вы ссылаетесь на местоположение, которое не "принадлежит" вашему процессу. Однако локальные переменные выделяются в стеке и в зависимости от способа выделения памяти область сразу за массивом (
array[10]
) могут принадлежать к сегменту памяти процесса. Таким образом, никакая ловушка ошибки сегментации не брошена, и это то, что вы, кажется, испытываете. Как указывали другие, это не определено поведение в C и вашем коде может считаться неустойчивым. Поскольку вы изучаете C, вам лучше привыкнуть проверять границы в своем коде.
вне возможности того, что память может быть выложена так, что попытка записи в
a[10]
на самом деле перезаписываетi
, также возможно, что оптимизирующий компилятор может определить, что тест цикла не может быть достигнут со значениемi
больше десяти без кода, который сначала обратился к несуществующему элементу массиваa[10]
.поскольку попытка доступа к этому элементу будет неопределенным поведением, компилятор не будет иметь никаких обязательств в отношении что программа может сделать после этого момента. Более конкретно, поскольку компилятор не будет обязан генерировать код для проверки индекса цикла в любом случае, когда он может быть больше десяти, он не будет обязан генерировать код для проверки его вообще; вместо этого он может предположить, что
<=10
тест всегда будет давать истину. Обратите внимание, что это будет верно, даже если код будет читатьa[10]
вместо того, чтобы писать его.
когда вы повторяете прошлое
i==9
вы присваиваете ноль "элементам массива", которые на самом деле расположены последние массиве, так что вы перезаписываете некоторые другие данные. Скорее всего, вы переписываетеi
переменная, которая находится послеa[]
. Таким образом, вы просто сброситьi
переменной к нулю и таким образом перезапустить цикл.вы могли бы обнаружить, что сами, если вы напечатали
i
в цикле:printf("test i=%d\n", i);
вместо просто
printf("test \n");
конечно, результат сильно зависит от выделения памяти для переменных, которые, в свою очередь, зависит от компилятора и его настроек, так это вообще Неопределенное Поведение - вот почему результаты на разных машинах или разных операционных системах или на разных компиляторах могут отличаться.
ошибка находится в части массива[10] w/c также адрес i (int array[10], i;). когда array[10] установлен в 0, то i будет 0 w / c сбрасывает весь цикл и вызывает бесконечный цикл. там будет бесконечный цикл если массив[10] между 0-10.правильный цикл должен быть для (Я = 0; я
Я предложу что-то, что я не нашел выше:
попробуйте назначить array[i] = 20;
Я думаю, это должно прекратить код везде.. (учитывая, что вы держите i
Если это работает, вы можете твердо решить, что ответы, указанные здесь, уже верны [ответ, связанный с памятью, топающей один для ex.]
здесь есть две вещи неправильно. Int i На самом деле является элементом массива, array[10], Как видно на стеке. Поскольку вы позволили индексированию фактически сделать array[10] = 0, индекс цикла, i, никогда не будет превышать 10. Сделай это
for(i=0; i<10; i+=1)
.i++ есть, как K&R назвал бы это, 'плохой стиль'. Он увеличивает i на размер i, а не 1. i++ - это математика указателей, а i+=1-алгебра. Хотя это зависит от компилятора, это не очень хорошее Соглашение для переносимости.