Чому це для виходу з циклу на одних платформах, а не на інших?


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.

Чи може хтось пояснити, що відбувається в пам'яті, і чому різні ОС, що працюють з одним кодом, дають різні результати?

EDIT: Я знаю, що цикл 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 не може запустити код (він висить), але з вашим кодом налагодження він працює. Здається, це поки що дуже мова 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]записується, то UB буде скинутий iна 0, тим самим ведучи до неперерваного циклу.

Щоб виправити, змініть умова циклу на i < 10.


6
Nitpick: Ви не можете фактично відформатувати жорсткий диск на будь-якій здоровій ОС на ринку, якщо ви не працюєте як root (або еквівалент).
Кевін

26
@Kevin, коли ви посилаєтесь на UB, ви відмовляєтесь від будь-якої претензії на розсудливість.
o11c

7
Не має значення, чи правильно ваш код. ОС не дозволить вам це зробити.
Кевін

2
@Kevin Приклад із форматуванням вашого жорсткого диска виник задовго до цього. Навіть унікси часу (звідки походить С) були дуже щасливі, що дозволяють вам робити такі речі - і навіть сьогодні багато дистрибуцій із задоволенням дозволять вам почати видаляти все, rm -rf /навіть коли ви не користуєтесь коренем, не "форматування" всього диска, звичайно, але все ж руйнуючи всі ваші дані. Ой.
Луань

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
Це робити не навмисно, також призводить до невизначеної поведінки - компілятор не може сказати! ;-)
Toby Speight

1
Просто використовуйте макрос, щоб подати свої помилки як попередження: #define UNINTENDED_MISTAKE (EXP) printf ("Увага:" #EXP "помилка \ n");
lkraider

1
Я маю на увазі, якщо ви робите помилку
навмисно,

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можуть мати деякі накладки після неї.


Я не думаю, що валдрінд зможе це зрозуміти, оскільки це все-таки дійсне місце, але 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-й предмет - це предмет, який фактично не знаходиться у візку. 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[10]) може належати до сегмента пам'яті процесу. Таким чином, не закидається жодна помилка сегментації, і це, здається, ви відчуваєте. Як зазначали інші, це невизначена поведінка на C, і ваш код може вважатися хаотичним. Оскільки ви вивчаєте С, вам краще ввійти в звичку перевіряти межі у своєму коді.


4

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

Оскільки спроба отримати доступ до цього елемента була б невизначеною поведінкою, компілятор не матиме жодних зобов'язань щодо того, що програма може робити після цього моменту. Більш конкретно, оскільки компілятор не буде зобов'язаний генерувати код для перевірки індексу циклу в будь-якому випадку, коли він може бути більшим десяти, він не матиме жодного зобов'язання генерувати код, щоб перевірити його взагалі; замість цього можна припустити, що <=10тест завжди буде справжній. Зауважте, що це було б істинно, навіть якщо код читав, 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 w / c скидає весь цикл і викликає нескінченний цикл. буде нескінченний цикл, якщо масив [10] знаходиться між 0-10. правильний цикл повинен бути для (i = 0; i <10; i ++) {...} int масив [10], i; для (i = 0; i <= 10; i ++) масиву [i] = 0;


0

Я підкажу щось, що я знайду вище:

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

Я думаю, що це має припинити код скрізь .. (якщо ви збережете i <= 10 або ll)

Якщо це працює, ви можете твердо вирішити, що відповіді, вказані тут, вже є правильними [відповідь, пов’язана із тупою пам'яттю, наприклад].


-9

Тут помиляються дві речі. Int i - це фактично елемент масиву, масив [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

саме тому він працює на деяких платформах, а не на інших. це єдине логічне пояснення того, чому він навічно зациклюється на Windows. щодо I ++ - це математика вказівника не ціла. читайте Святе Письмо ... "мову програмування C". від Керніган та Рітче, якщо ви хочете, щоб у мене була копія з автографом, і я програмував на c з 1981 року.
SkipBerne,

1
Прочитайте вихідний код за допомогою ОП та знайдіть декларацію змінної i- вона intтипу. Це ціле число , а не покажчик; ціле число, яке використовується в якості індексу до array,.
CiaPan

1
Я це зробив, і тому я коментував так, як це робив. можливо, ви повинні усвідомити, що якщо компілятор не включає стеки перевірок, і в цьому випадку це не має значення, як посилання стека, коли I = 10 насправді буде посилатися, в деяких компіляторах, на індекс масиву, і це знаходиться в межах області стека. компілятори не можуть виправити дурне. компіляції можуть зробити виправлення, як видається, що це робиться, але чисте тлумачення мови програмування c не підтримало б цю конвенцію і, як зазначає ОП, призведе до не портативних результатів.
SkipBerne

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