Почему это делается для выхода из цикла на некоторых платформах, а не на других?

238

Недавно я начал изучать 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: i <= 11, код заканчивается только на моем ноутбуке под управлением Ubuntu. Он никогда не заканчивается в CentOS и Windows.

Может ли кто-нибудь объяснить, что происходит в памяти и почему разные ОС, работающие с одним и тем же кодом, дают разные результаты?

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

  • 145
    Поскольку вы заполняете массив, происходит неопределенное поведение. Неопределенное поведение означает, что может случиться что угодно, в том числе и работать. Таким образом, «код никогда не должен заканчиваться» не является допустимым ожиданием.
  • 37
    Точно, добро пожаловать в C. Ваш массив состоит из 10 элементов - от 0 до 9.
Показать ещё 36 комментариев
Теги:
debugging
buffer-overflow
undefined-behavior

14 ответов

372
Лучший ответ

На моем ноутбуке, работающем под Ubuntu 14.04, этот код не прерывает его выполнение до завершения. На моем школьном компьютере, работающем с CentOS 6.6, он также отлично работает. В Windows 8.1 цикл никогда не завершается.

Что более странно, когда я изменяю условное выражение цикла for на: i <= 11, код заканчивается только на моем ноутбуке под управлением Ubuntu. CentOS и Windows никогда не заканчиваются.

Вы только что обнаружили топание памяти. Подробнее об этом можно прочитать здесь: Что такое "память" ?

Когда вы выделяете int array[10],i;, эти переменные попадают в память (в частности, они выделяются в стеке, который является блоком памяти, связанным с этой функцией). array[] и i, вероятно, смежны друг с другом в памяти. Похоже, что в Windows 8.1 i находится в array[10]. На CentOS, 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; 
} 
  • 6
    Эй спасибо! Это действительно объяснило немного. В Windows говорится, что если смещение 10 от массива, то в CentOS и Ubuntu это значение равно -1. Что страннее, если я закомментирую ваш код отладчика, CentOS не сможет запустить код (он зависает), но с вашим кодом отладки он будет работать. C кажется очень языком до сих пор X_x
  • 1
    Я рад, что смог помочь. То, что я сказал о стеке, на самом деле просто мнемоническое устройство; это помогает программировать на С модель программирования ассемблера в вашей голове, но это не обязательно так из-за оптимизации. Это почти наверняка то, что вы видите с изменчивым поведением.
Показать ещё 16 комментариев
102

Ошибка между этими фрагментами кода:

int array[10],i;

for (i = 0; i <=10 ; i++)

array[i]=0;

Так как array имеет только 10 элементов, в последней итерации array[10] = 0; происходит переполнение буфера. Переполнение буфера UNDEFINED BEHAVIOR, что означает, что они могут отформатировать ваш жесткий диск или заставить демонов вылететь из вашего носа.

Это довольно часто, когда все переменные стека располагаются рядом друг с другом. Если i находится там, где array[10] записывается, тогда UB будет reset i до 0, что приведет к завершенному циклу.

Чтобы исправить, измените условие цикла на i < 10.

  • 6
    Nitpick: Вы не можете на самом деле отформатировать жесткий диск в любой нормальной операционной системе на рынке, если вы не работаете от имени пользователя root (или эквивалентного).
  • 26
    @Kevin, когда вы вызываете UB, вы отказываетесь от любых претензий на здравомыслие.
Показать ещё 6 комментариев
37

В чем должен быть последний прогон цикла, вы пишете в array[10], но в массиве всего 10 элементов, пронумерованных от 0 до 9. Спецификация языка C говорит, что это "поведение undefined", На практике это означает, что ваша программа попытается записать в int -размерную часть памяти, которая находится сразу после array в памяти. То, что происходит тогда, зависит от того, что на самом деле лежит там, и это зависит не только от операционной системы, но и от того, что такое компилятор, от параметров компилятора (таких как настройки оптимизации), архитектуры процессора, окружающего кода и т.д. Это может даже варьироваться от исполнения до исполнения, например из-за рандомизации адресного пространства (вероятно, не на этом примере игрушек, но это происходит в реальной жизни). Некоторые возможности включают в себя:

  • Местоположение не использовалось. Цикл нормально завершается.
  • Местоположение было использовано для чего-то, что имело значение 0. Цикл заканчивается нормально.
  • Местоположение содержит адрес возврата функции. Цикл завершается нормально, но затем программа выходит из строя, потому что пытается перейти к адресу 0.
  • Местоположение содержит переменную i. Цикл никогда не заканчивается, потому что i перезапускается с 0.
  • В этом месте находится другая переменная. Цикл заканчивается нормально, но затем происходят "интересные" события.
  • Местоположение является недопустимым адресом памяти, например. потому что array находится справа в конце страницы виртуальной памяти, а следующая страница не отображается.
  • Демоны вылетают из вашего носа. К счастью, большинству компьютеров не хватает необходимого оборудования.

То, что вы наблюдали в Windows, заключалось в том, что компилятор решил поместить переменную i сразу после массива в память, поэтому array[10] = 0 закончил присвоение i. На Ubuntu и CentOS компилятор не разместил там i. Почти все реализации C группируют локальные переменные в памяти, в стек памяти, с одним основным исключением: некоторые локальные переменные могут быть помещены целиком в регистры. Даже если переменная находится в стеке, порядок переменных определяется компилятором, и это может зависеть не только от порядка в исходном файле, но и от их типов (чтобы избежать потери памяти для ограничений выравнивания, которые оставят дыры), на их именах, на некоторое хэш-значение, используемое во внутренней структуре данных компилятора и т.д.

Если вы хотите узнать, что решил ваш компилятор, вы можете сказать ему, чтобы показать вам код ассемблера. О, и научитесь расшифровывать ассемблер (это проще, чем писать). С GCC (и некоторыми другими компиляторами, особенно в мире Unix) передайте опцию -S для создания кода ассемблера вместо двоичного. Например, здесь фрагмент ассемблера для цикла от компиляции с GCC на amd64 с опцией оптимизации -O0 (без оптимизации), с комментариями, добавленными вручную:

.L3:
    movl    -52(%rbp), %eax           ; load i to register eax
    cltq
    movl    $0, -48(%rbp,%rax,4)      ; set array[i] to 0
    movl    $.LC0, %edi
    call    puts                      ; printf of a constant string was optimized to puts
    addl    $1, -52(%rbp)             ; add 1 to i
.L2:
    cmpl    $10, -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    $11, %ebx
.L3:
    movl    $.LC0, %edi
    call    puts
    subl    $1, %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    $0, (%rbx)
    movl    $.LC0, %edi
    call    puts
    addq    $4, %rbx
    cmpq    %rbp, %rbx
    jne     .L3

Теперь массив находится в стеке, 44 байта сверху. Что насчет i? Он нигде не появляется! Но счетчик циклов хранится в регистре rbx. Это не точно i, а адрес array[i]. Компилятор решил, что, поскольку значение i никогда не использовалось напрямую, не было смысла выполнять арифметику, чтобы вычислить, где хранить 0 во время каждого цикла цикла. Вместо этого этот адрес является переменной цикла, а арифметика для определения границ выполнялась частично во время компиляции (умножить 11 итераций на 4 байта на элемент массива, чтобы получить 44) и частично во время выполнения, но раз и навсегда до начала цикла ( выполнить вычитание, чтобы получить начальное значение).

Даже в этом очень простом примере мы видели, как изменяются параметры компилятора (включайте оптимизацию) или изменяете что-то незначительное (array[i] до array[9-i]) или даже изменяете что-то явно не связанное (добавление вызова к use_array) может существенно повлиять на то, что делает исполняемая программа, сгенерированная компилятором. Оптимизация компилятора может многое сделать, что может показаться неинтуитивным для программ, вызывающих поведение undefined. Поэтому поведение undefined остается полностью undefined. Когда вы немного отклоняетесь от треков, в реальных программах, очень сложно понять взаимосвязь между тем, что делает код и что он должен был сделать, даже для опытных программистов.

24

В отличие от Java, C не выполняет проверку границ массива, т.е. там нет ArrayIndexOutOfBoundsException, задача убедиться, что индекс массива действителен, остается программисту. Выполнение этого с целью приводит к undefined поведению, все может случиться.


Для массива:

int array[10]
Индексы

действительны только в диапазоне от 0 до 9. Однако вы пытаетесь:

for (i = 0; i <=10 ; i++)

доступ array[10] здесь, измените условие на i < 10

  • 6
    Выполнение этого не нарочно приводит к неопределенному поведению - компилятор не может сказать! ;-)
  • 1
    Просто используйте макрос, чтобы привести ваши ошибки в качестве предупреждений: #define UNINTENDED_MISTAKE (EXP) printf ("Предупреждение:" #EXP "ошибка \ n");
Показать ещё 2 комментария
18

У вас есть нарушение границ, а на платформах, не связанных с завершением, я считаю, что вы случайно не устанавливаете 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 может иметь некоторое дополнение после него.

  • 0
    Я не думаю, что valgrind может поймать это, так как это все еще допустимое местоположение, но ASAN может.
12

Вы объявляете int array[10] означает array имеет индекс 0 до 9 (всего 10 целых элементов, которые он может удерживать). Но следующий цикл,

for (i = 0; i <=10 ; i++)

будет цикл 0 до 10 означает 11 время. Следовательно, когда i = 10 он переполнит буфер и вызовет Undefined Behavior.

Итак, попробуйте следующее:

for (i = 0; i < 10 ; i++)

или,

for (i = 0; i <= 9 ; i++)
7

Это undefined в array[10] и дает поведение undefined, как описано выше. Подумайте об этом так:

У меня есть 10 предметов в моей продуктовой тележке. Это:

0: Ящик с зерном
1: Хлеб
2: Молоко
3: Pie
4: Яйца
5: Торт
6: 2 л соды
7: Салат
8: Бюргерса
9: Мороженое

cart[10] является undefined и может давать исключение за пределы в некоторых компиляторах. Но, по-видимому, нет. Очевидный 11-й элемент - это элемент не на самом деле в корзине. 11-й пункт указывает на то, что я собираюсь назвать, "объект полтергейста". Он никогда не существовал, но он был там.

Почему некоторые компиляторы дают i индекс array[10] или array[11] или даже array[-1] из-за вашего утверждения инициализации/объявления. Некоторые компиляторы интерпретируют это как:

  • "Выделите 10 блоков int для array[10] и еще один int блок. , чтобы упростить его, поставить их рядом друг с другом."
  • То же, что и раньше, но переместите его на два или два места, так что array[10] не указывает на i.
  • Сделайте то же самое, что и раньше, но выделите i в array[-1] (потому что индекс массива не может или не должен быть отрицательным), или выделите его в совершенно другом месте, потому что ОС может обработайте его, и он безопаснее.

Некоторые компиляторы хотят, чтобы все ускорилось, а некоторые компиляторы предпочитают безопасность. Все о контексте. Например, если я разрабатывал приложение для древней ОС BREW (ОС базового телефона), это не заботило бы о безопасности. Если бы я развивался для iPhone 6, то он мог быстро работать независимо от того, что мне нужно, и мне нужно сделать упор на безопасность. (Серьезно, вы читали руководства Apple App Store или читали о развитии Swift и Swift 2.0?)

  • 0
    Примечание. Я напечатал список так, чтобы он отображался как «0, 1, 2, 3, 4, 5, 6, 7, 8, 9», но язык разметки SO зафиксировал положение моего упорядоченного списка.
6

Поскольку вы создали массив размером 10, для условия цикла должно быть следующее:

int array[10],i;

for (i = 0; i <10 ; i++)
{

В настоящее время вы пытаетесь получить доступ к неназначенному местоположению из памяти с помощью array[10] и это вызывает поведение undefined. Undefined поведение означает, что ваша программа будет вести себя неопределенно, поэтому она может давать разные выходы в каждом исполнении.

5

Ну, компилятор C традиционно не проверяет границы. Вы можете получить ошибку сегментации в случае, если вы ссылаетесь на местоположение, которое не "принадлежит" вашему процессу. Однако локальные переменные выделяются в стеке и в зависимости от способа выделения памяти область, находящаяся за пределами массива (array[10]), может принадлежать сегменту памяти процесса. Таким образом, не возникает ловушка отказов сегментации, и это то, что вы, похоже, испытываете. Как указывали другие, это поведение undefined в C, и ваш код может считаться неустойчивым. Поскольку вы изучаете C, вам лучше привыкнуть проверять границы вашего кода.

4

Помимо возможности того, что память может быть выложена так, чтобы попытка записи на a[10] фактически перезаписывала i, также было бы возможно, что оптимизирующий компилятор может определить, что тест цикла не может быть достигнут со значением i больше десяти без кода, который первым обратился к несуществующему элементу массива a[10].

Поскольку попытка доступа к этому элементу будет иметь поведение undefined, у компилятора не будет никаких обязательств в отношении того, что программа могла бы выполнить после этой точки. Более конкретно, поскольку компилятор не обязан генерировать код для проверки индекса цикла в любом случае, когда он может быть больше десяти, он не обязан генерировать код для его проверки; он мог вместо этого предположить, что тест <=10 всегда будет иметь значение true. Обратите внимание, что это было бы правдой, даже если код читал бы a[10], а не записывал его.

3

Когда вы повторяете прошлое i==9, вы назначаете ноль "элементам массива", которые фактически расположены после массива, поэтому вы перезаписываете некоторые другие данные. Скорее всего, вы перезаписываете переменную i, которая находится после a[]. Таким образом, вы просто reset переменной i равны нулю и, таким образом, перезапустите цикл.

Вы можете обнаружить это самостоятельно, если вы напечатали i в цикле:

      printf("test i=%d\n", i);

вместо

      printf("test \n");

Конечно, этот результат сильно зависит от распределения памяти для ваших переменных, что, в свою очередь, зависит от компилятора и его настроек, поэтому он обычно Undefined Behavior - почему результаты на разных машинах или разные операционные системы или разные компиляторы могут отличаться.

0

Я предлагаю что-то, что я нахожу выше:

Попробуйте назначить массив [i] = 20;

Я предполагаю, что это должно повредить код везде.. (если вы держите я <= 10 или ll)

Если это произойдет, вы можете твердо решить, что ответы, указанные здесь, уже верны [ответ, связанный с памятью, топающей один для примера.]

0

ошибка в частичном массиве [10] w/c также является адресом я (int array [10], i;). когда array [10] установлен в 0, тогда я будет 0 w/c сбрасывает весь цикл и вызывает бесконечный цикл.   будет бесконечный цикл, если массив [10] находится между 0-10. Правильный цикл должен быть для (i = 0; я < 10; я ++) {...}           int array [10], i;           для (i = 0; я <= 10; я ++)           массив [I] = 0;

-10

Здесь есть две вещи. Int я - это элемент массива, массив [10], как показано в стеке. Поскольку вы позволили индексированию фактически сделать массив [10] = 0, индекс цикла я не будет превышать 10. Сделайте его for(i=0; i<10; i+=1).

i ++ есть, поскольку K & R назвал бы его "плохим стилем". Он увеличивает я на размер i, а не 1. я ++ для математики указателя и я + = 1 для алгебры. Хотя это зависит от компилятора, это не является хорошим соглашением для переносимости.

  • 5
    -1 Совершенно неправильно. Переменная i является элементом массива a[10] , компилятор не обязан или даже не предлагает поместить ее в стек сразу после a[] - он также может быть расположен перед массивом или отделен некоторым дополнительным пространством. Он даже может быть размещен вне основной памяти, например, в регистре процессора. Также неверно, что ++ предназначен для указателей, а не для целых чисел. Совершенно неверно, что «i ++ увеличивает i на величину i» - прочитайте описание оператора в определении языка!
  • 0
    именно поэтому он работает на некоторых платформах, а не на других. это единственное логическое объяснение того, почему он навсегда зацикливается на окнах. в отношении I ++ это указатель математика, а не целое число. Прочитайте Писание ... «язык программирования C». Керниган и Ритч, если хотите, у меня есть копия с автографом, и я программировал в c с 1981 года.
Показать ещё 4 комментария

Ещё вопросы

Сообщество Overcoder
Наверх
Меню