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


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

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 at array[-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-алгебра. Хотя это зависит от компилятора, это не очень хорошее Соглашение для переносимости.