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


240

Я недавно начал изучать 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.

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

РЕДАКТИРОВАТЬ: Я знаю, что цикл for выходит за пределы. Я делаю это намеренно. Я просто не могу понять, как поведение может отличаться в разных ОС и компьютерах.


147
Поскольку вы заполняете массив, происходит неопределенное поведение. Неопределенное поведение означает, что может случиться что угодно, в том числе и работать. Таким образом, «код никогда не должен заканчиваться» не является допустимым ожиданием.
Кайлум

37
Точно, добро пожаловать в C. Ваш массив состоит из 10 элементов - с номерами от 0 до 9.
Yetti99

14
@JonCav Вы взломали код. Вы получаете неопределенное поведение, которое является неисправным кодом.
Кайлум

50
Ну, суть в том, что неопределенное поведение - это именно так. Вы не можете надежно проверить это и доказать, что что-то определенное произойдет. Вероятно, на вашей машине с Windows происходит то, что переменная iсохраняется сразу после конца array, и вы перезаписываете ее array[10]=0;. Это может быть не так в оптимизированной сборке на той же платформе, которая может храниться iв регистре и вообще никогда не ссылаться на него в памяти.
падди

46
Потому что непредсказуемость является фундаментальным свойством неопределенного поведения. Вы должны это понять ... Абсолютно все ставки сняты.
Пэдди

Ответы:


356

На моем ноутбуке под управлением 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 указано, что если i смещено на 10 от массива, то в CentOS и Ubuntu это равно -1. Что страннее, если я закомментирую ваш код отладчика, CentOS не сможет запустить код (он зависает), но с вашим кодом отладки он будет работать. C, кажется, очень язык до сих пор X_x
JonCav

12
@JonCav "он зависает" может произойти, например, если запись в array[10]разрушает фрейм стека. Как может быть разница между кодом с выводом отладки или без него? Если адрес iникогда не нужен, компилятор может оптимизировать i. в регистр, таким образом изменяя расположение памяти в стеке ...
Хаген фон Айцен

2
Я не думаю, что он зависает, я думаю, что он находится в бесконечном цикле, потому что он перезагружает счетчик цикла из памяти (который только что был обнулен array[10]=0. Если вы скомпилировали свой код с оптимизацией, это, вероятно, не произойдет (потому что C имеет правила псевдонимов, которые ограничивают то, какие типы обращений к памяти должны перекрывать другую память. Как локальная переменная, адрес которой вы никогда не берете, я думаю, что компилятор должен быть в состоянии предположить, что ничто не псевдоним это. массива - неопределенное поведение. Всегда старайтесь избегать зависимости от этого
Питер Кордес,

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

9
@JonCav Я бы сказал, что в целом вам не нужно больше знать об управлении памятью, и вместо этого вы просто не должны писать неопределенный код, в частности, не пишите после конца массива ...
Т. Кили,

98

Ошибка лежит между этими частями кода:

int array[10],i;

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

array[i]=0;

Поскольку arrayвсего 10 элементов, на последней итерации array[10] = 0;происходит переполнение буфера. Переполнения буфера НЕПРАВИЛЬНОЕ ПОВЕДЕНИЕ , что означает, что они могут отформатировать ваш жесткий диск или заставить демонов вылететь из вашего носа.

Все переменные стека довольно распространены рядом друг с другом. Если iнаходится там , где array[10]записи на, то УБ будет сброшены iна 0, что приводит к несогласованной петле.

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


6
Nitpick: На самом деле вы не можете отформатировать жесткий диск в любой здравомыслящей ОС на рынке, если вы не работаете от имени пользователя root (или эквивалентного).
Кевин

26
@Kevin, когда вы вызываете UB, вы отказываетесь от любых претензий на здравомыслие.
o11c

7
Неважно, нормален ли твой код. ОС не позволит вам сделать это.
Кевин

2
@Kevin Пример с форматированием вашего жесткого диска возник задолго до этого. Даже юниксы того времени (откуда возник C) были очень рады, что позволили вам делать такие вещи, и даже сегодня многие дистрибутивы с радостью позволят вам начать удалять все, rm -rf /даже когда вы не root, не «форматирование» всего диска, конечно, но уничтожение всех ваших данных. Уч.
Луаан

5
@Kevin, но неопределенное поведение может использовать уязвимость ОС, а затем поднять себя, чтобы установить новый драйвер жесткого диска, а затем начать очистку диска.
чокнутый урод

38

В каком последнем запуске цикла вы пишете array[10], но в массиве всего 10 элементов, пронумерованных от 0 до 9. Спецификация языка C говорит, что это «неопределенное поведение». На практике это означает, что ваша программа будет пытаться записать в тот 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) может существенно изменить то, что генерировала исполняемая программа компилятором делает. Оптимизация компилятора может сделать много вещей, которые могут казаться неинтуитивными в программах, которые вызывают неопределенное поведение . Вот почему неопределенное поведение остается полностью неопределенным. Когда вы слегка отклоняетесь от треков, в реальных программах может быть очень трудно понять связь между тем, что делает код, и тем, что он должен был сделать, даже для опытных программистов.


25

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


Для массива:

int array[10]

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

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

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


6
Выполнение этого не нарочно приводит к неопределенному поведению - компилятор не может сказать! ;-)
Тоби Спейт

1
Просто используйте макрос, чтобы привести ваши ошибки в качестве предупреждений: #define UNINTENDED_MISTAKE (EXP) printf ("Предупреждение:" #EXP "ошибка \ n");
lkraider

1
Я имею в виду, что если вы делаете ошибку преднамеренно, вы можете также идентифицировать ее как таковую и обезопасить, чтобы избежать неопределенного поведения; D
lkraider

19

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


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

13

Вы объявляете 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++)

7

Не определено в array[10]и дает неопределенное поведение как описано ранее. Думайте об этом так:

У меня есть 10 товаров в моей продуктовой корзине. Они есть:

0: коробка с хлопьями
1: хлеб
2: молоко
3: пирог
4: яйца
5: торт
6: 2 литра соды
7: салат
8: гамбургеры
9: мороженое

cart[10]не определено, и может давать исключение вне границ в некоторых компиляторах. Но, очевидно, многие этого не делают. Кажущийся 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, 1, 2, 3, 4, 5, 6, 7, 8, 9», но язык разметки SO зафиксировал положение моего упорядоченного списка.
DDPWNAGE

6

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

int array[10],i;

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

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


5

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


4

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

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


3

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

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

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

вместо просто

      printf("test \n");

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


0

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


0

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

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

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

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


-9

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

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


5
-1 Совершенно неправильно. Переменный iявляется Notan элемента массива a[10], нет никаких обязательств или даже предложения для компилятора , чтобы положить его в стеке сразу после a[] - он может также быть расположен перед массивом, или отделяться с некоторым дополнительным пространством. Он может быть даже размещен вне основной памяти, например, в регистре процессора. Это также неверно ++для указателей, а не для целых чисел. Совершенно неверно, что «i ++ увеличивает i на величину i» - прочтите описание оператора в определении языка!
CiaPan

именно поэтому он работает на некоторых платформах, а не на других. это единственное логическое объяснение того, почему он навсегда зацикливается на окнах. в отношении I ++ это указатель математика, а не целое число. Прочитайте Писание ... «язык программирования C». Керниган и Ритче, если хотите, у меня есть копия с автографом, и я программирую в c с 1981 года.
SkipBerne

1
Прочтите исходный код по OP и найдите объявление переменной i- это intтип. Это целое число , а не указатель; целое число, используемое в качестве индекса к array,.
CiaPan

1
Я сделал, и именно поэтому я прокомментировал, как я сделал. возможно, вам следует понимать, что если компилятор не включает проверки стека, и в этом случае не имеет значения, что ссылка на стек, когда I = 10, фактически ссылалась бы на некоторые компиляции на индекс массива, который находится в пределах области стека. компиляторы не могут исправить глупо. компиляция может исправить ситуацию, как это происходит, но чистая интерпретация языка программирования c не будет поддерживать это соглашение и, как сказал OP, приведет к непереносимым результатам.
SkipBerne

@SkipBerne: Подумайте об удалении своего ответа, прежде чем вы будете "награждены" с большим количеством отрицательных баллов.
Петр ВАРГА
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.