Доступ до масиву поза межами не дає помилок, чому?


177

Я призначаю значення в програмі C ++ поза межами такого:

#include <iostream>
using namespace std;
int main()
{
    int array[2];
    array[0] = 1;
    array[1] = 2;
    array[3] = 3;
    array[4] = 4;
    cout << array[3] << endl;
    cout << array[4] << endl;
    return 0;
}

Програма друкує 3та 4. Це не повинно бути можливим. Я використовую g ++ 4.3.3

Ось команда компіляції та запуску

$ g++ -W -Wall errorRange.cpp -o errorRange
$ ./errorRange
3
4

Лише при призначенні array[3000]=3000це дає мені помилку в сегментації.

Якщо gcc не перевіряє межі масиву, то як я можу бути впевнений, що моя програма правильна, оскільки це може призвести до серйозних проблем пізніше?

Я замінив вищевказаний код на

vector<int> vint(2);
vint[0] = 0;
vint[1] = 1;
vint[2] = 2;
vint[5] = 5;
cout << vint[2] << endl;
cout << vint[5] << endl;

і ця також не створює помилок.


3
Пов'язані питання: stackoverflow.com/questions/671703 / ...
TSomKes

16
Код, звичайно, баггі, але він породжує невизначену поведінку. Невизначений означає, що він може або не може закінчитися. Гарантії аварії немає.
dmckee --- кошеня колишнього модератора

4
Ви можете бути впевнені, що ваша програма правильна, не накручуючи необроблені масиви. Програмісти на C ++ повинні використовувати замість них контейнерні класи, за винятком вбудованого / ОС програмування. Прочитайте це з міркувань користувачів контейнерів. parashift.com/c++-faq-lite/containers.html
jkeys

8
Майте на увазі, що вектори не обов'язково перевіряють дальність, використовуючи []. Використання .at () робить те ж саме, що і [], але робить перевірку діапазону.
Девід Торнлі

4
A vector не змінює автоматичний розмір під час доступу до елементів поза межами! Це просто УБ!
Павло Мінаєв

Відповіді:


364

Ласкаво просимо до кращого друга кожного програміста C / C ++: Невизначена поведінка .

Є багато, що не визначено мовним стандартом з різних причин. Це одна з них.

Загалом, коли б ви не стикалися з невизначеною поведінкою, все може статися. Додаток може вийти з ладу, він може замерзнути, він може вийняти привід CD-ROM або змусити демонів вийти з носа. Він може відформатувати ваш жорсткий диск або надіслати електронною поштою все ваше порно бабусі.

Можливо, навіть якщо вам справді не пощастило, може здатися, що ви працюєте правильно.

Мова просто говорить, що має статися, якщо ви отримуєте доступ до елементів у межах масиву. Невизначено, що станеться, якщо ви виходите за межі. Це може здатися, що сьогодні працює на вашому компіляторі, але це не є законним C або C ++, і немає гарантії, що він все одно працюватиме наступного разу, коли ви запустите програму. Або , що він не має перезаписані важливі дані навіть зараз, і ви просто не стикалися з проблемами, що це буде справа - поки.

Щодо того, чому немає перевірки меж, у відповіді є кілька аспектів:

  • Масив - це залишок від C. C масиви приблизно такі ж примітивні, як ви можете отримати. Просто послідовність елементів із суміжними адресами. Немає меж перевірки, тому що це просто розкриття необробленої пам'яті. Впровадити надійний механізм перевірки меж було б майже неможливим.
  • У C ++ можлива перевірка меж для типів класів. Але масив все ще є простим старим C-сумісним. Це не клас. Крім того, C ++ також будується на іншому правилі, яке робить перевірку меж неідеальною. Керівний принцип C ++ - "ви не платите за те, що не використовуєте". Якщо ваш код правильний, вам не потрібна перевірка меж, і ви не повинні бути змушені платити за накладні перевірки меж виконання.
  • Тож C ++ пропонує std::vectorшаблон класу, який дозволяє обидва. operator[]покликаний бути ефективним. Мовний стандарт не вимагає, щоб він здійснював перевірку меж (хоча це також не забороняє). Вектор також має функцію- at()член, який гарантовано здійснює перевірку меж. Тож у C ++ ви отримуєте найкраще з обох світів, якщо використовуєте вектор. Ви отримуєте схожість масиву без перевірки меж, і ви отримуєте можливість використовувати перевірений межами доступ, коли цього захочете.

5
@Jaif: ми використовуємо цю річ масиву так довго, але все ж чому не існує тесту для перевірки такої простої помилки?
seg.server.fault

7
Принцип проектування C ++ полягав у тому, що він не повинен бути повільніше, ніж еквівалентний код C, і C не здійснює перевірку обмежених масивів. Принцип проектування C в основному був швидкісним, оскільки він був спрямований на системне програмування. Перевірка обмежених масивів потребує часу, і так не робиться. Для більшості застосувань в C ++ ви все одно повинні використовувати контейнер, а не масив, і ви можете мати вибір обмеженої перевірки або без обмеженої перевірки шляхом доступу до елемента через .at () або [] відповідно.
KTC

4
@seg Така перевірка чогось коштує. Якщо ви пишете правильний код, ви не хочете платити цю ціну. Сказавши це, я став повноцінним перетворювачем в метод std :: vector's at (), який перевіряється. Використовуючи його, було викрито досить багато помилок у тому, що я вважав «правильним» кодом.

10
Я вважаю, що старі версії GCC насправді запустили Emacs та імітували вежі Ханої в ньому, коли він стикався з певними типами невизначеної поведінки. Як я вже казав, все може статися. ;)
jalf

4
Все вже було сказано, тому це лише є невеликим доповненням. Налагодження налагодження може бути дуже прощальним у цих умовах у порівнянні з версіями випуску. Через те, що інформація про налагодження міститься у бінарних файлах на виправленнях, є менше шансів, що щось життєво важливе буде перезаписано. Ось чому іноді налагодження налагодження спрацьовують нормально, в той час як збій збірки версії.
Багатий

31

Використовуючи г ++, ви можете додати параметр командного рядка: -fstack-protector-all.

На вашому прикладі це призвело до наступного:

> g++ -o t -fstack-protector-all t.cc
> ./t
3
4
/bin/bash: line 1: 15450 Segmentation fault      ./t

Це насправді не допоможе вам знайти або вирішити проблему, але принаймні segfault дасть вам знати, що щось не так.


10
Я просто знайшов навіть кращий варіант: -fmudflap
Привіт-Ангел

1
@ Hi-Angel: Сучасний еквівалент - це те, -fsanitize=addressщо ловить цю помилку як під час компіляції (якщо оптимізується), так і під час виконання.
Нейт Елдредж

@NateEldredge +1, сьогодні я навіть використовую -fsanitize=undefined,address. Але варто зауважити, що трапляються рідкісні кутові випадки з бібліотекою std, коли санітарій не виявляє доступ поза межами . З цієї причини я рекомендую додатково використовувати -D_GLIBCXX_DEBUGопцію, яка додає ще більше перевірок.
Привіт-Ангел

12

g ++ не перевіряє межі масиву, і ви можете перезаписати щось з 3,4, але нічого дуже важливого, якщо ви спробуєте з більшими числами, ви отримаєте збій.

Ви просто перезаписуєте частини стека, які не використовуються, ви можете продовжувати, поки не дістанете до кінця виділеного місця для стека, і він врешті-решт завершиться збоєм.

EDIT: У вас немає ніякого способу впоратися з цим, можливо, аналізатор статичного коду міг би виявити ці збої, але це занадто просто, у вас можуть бути подібні (але більш складні) збої, не виявлені навіть для статичних аналізаторів


6
Звідки ви дістаєтесь, якщо з цієї адреси за адресою масиву [3] та масиву [4] немає нічого насправді важливого ??
namezero

7

Наскільки я не знаю, це невизначена поведінка. Запустіть більшу програму з цим, і вона зазнає краху десь на шляху. Перевірка меж не є частиною необроблених масивів (або навіть std :: vector).

Використовуйте std::vector::iteratorзамість std :: vector with 's, тому вам не доведеться турбуватися про це.

Редагувати:

Просто для розваги запустіть це і подивіться, скільки часу до аварії:

int main()
{
   int array[1];

   for (int i = 0; i != 100000; i++)
   {
       array[i] = i;
   }

   return 0; //will be lucky to ever reach this
}

Edit2:

Не запускайте цього.

Edit3:

ОК, ось короткий урок про масиви та їх зв’язки з покажчиками:

Коли ви використовуєте індексацію масиву, ви дійсно використовуєте покажчик у маскуванні (званий "посиланням"), який автоматично відновлюється. Ось чому замість * (масив [1]), масив [1] автоматично повертає значення при цьому значенні.

Коли у вас є вказівник на масив, наприклад:

int array[5];
int *ptr = array;

Тоді "масив" у другій декларації дійсно занепадає до вказівника на перший масив. Це поведінка рівнозначна цьому:

int *ptr = &array[0];

Коли ви намагаєтесь отримати доступ за межі того, що ви виділили, ви дійсно використовуєте вказівник на іншу пам'ять (на яку C ++ не буде скаржитися). Беручи мій приклад програми вище, це рівнозначно цьому:

int main()
{
   int array[1];
   int *ptr = array;

   for (int i = 0; i != 100000; i++, ptr++)
   {
       *ptr++ = i;
   }

   return 0; //will be lucky to ever reach this
}

Компілятор не поскаржиться, оскільки при програмуванні вам часто доводиться спілкуватися з іншими програмами, особливо з операційною системою. Це робиться за допомогою покажчиків зовсім небагато.


3
Я думаю, ви забули збільшити "ptr" у своєму останньому прикладі там. Ви випадково створили якийсь чітко визначений код.
Джефф Лейк

1
Ха-ха, дивіться, чому ви не повинні використовувати необроблені масиви?
jkeys

"Ось чому замість * (масив [1]), масив [1] автоматично повертає значення при цьому значенні." Ви впевнені, що * (масив [1]) буде працювати належним чином? Я думаю, це має бути * (масив + 1). ps: Lol, це як надіслати повідомлення минулому. Але, як би там не було:
мюйстан

5

Підказка

Якщо ви хочете мати масиви швидкого обмеження з перевіркою помилок діапазону, спробуйте використовувати boost :: array , (також std :: tr1 :: масив з <tr1/array>нього буде стандартним контейнером у наступній специфікації C ++). Це набагато швидше, ніж std :: vector. Він резервує пам'ять у купі або внутрішньому екземплярі класу, як int масив [].
Це простий код зразка:

#include <iostream>
#include <boost/array.hpp>
int main()
{
    boost::array<int,2> array;
    array.at(0) = 1; // checking index is inside range
    array[1] = 2;    // no error check, as fast as int array[2];
    try
    {
       // index is inside range
       std::cout << "array.at(0) = " << array.at(0) << std::endl;

       // index is outside range, throwing exception
       std::cout << "array.at(2) = " << array.at(2) << std::endl; 

       // never comes here
       std::cout << "array.at(1) = " << array.at(1) << std::endl;  
    }
    catch(const std::out_of_range& r)
    {
        std::cout << "Something goes wrong: " << r.what() << std::endl;
    }
    return 0;
}

Ця програма буде друкувати:

array.at(0) = 1
Something goes wrong: array<>: index out of range

4

C або C ++ не перевірять межі доступу до масиву.

Ви виділяєте масив у стеці. Індексація масиву через array[3]еквівалентна * (array + 3), де масив - вказівник на & масив [0]. Це призведе до невизначеної поведінки.

Один із способів зловити це іноді на C - це використання статичної перевірки, наприклад, шини . Якщо ви запускаєте:

splint +bounds array.c

на,

int main(void)
{
    int array[1];

    array[1] = 1;

    return 0;
}

тоді ви отримаєте попередження:

array.c: (у головній функції) array.c: 5: 9: Ймовірно, поза межами магазину: array [1] Неможливо вирішити обмеження: потрібно 0> = 1, необхідний для задоволення передумови: вимагає maxSet (array @ array .c: 5: 9)> = 1 Запис пам'яті може записуватись на адресу, що виходить за межі виділеного буфера.


Виправлення: його вже виділила ОС або інша програма. Він перезаписує іншу пам’ять.
jkeys

1
Сказати, що "C / C ++ не перевірятиме межі" не зовсім коректно - немає нічого, що не заважає певній сумісній реалізації виконувати це, як за замовчуванням, так і з деякими прапорами компіляції. Просто ніхто з них не турбує.
Павло Мінаєв

3

Ви, звичайно, перезаписуєте свій стек, але програма досить проста, що наслідки цього залишаються непоміченими.


2
Перезапис чи стек залежить від платформи.
Кріс Кліленд

3

Запустіть це через Valgrind, і ви побачите помилку.

Як зазначила Фалаїна, valgrind не виявляє багатьох випадків корупції стеків. Я просто спробував зразок під valgrind, і він справді повідомляє про нульові помилки. Однак Valgrind може допомогти в пошуку багатьох інших типів проблем з пам'яттю, він просто не особливо корисний у цьому випадку, якщо ви не модифікуєте пакет даних, щоб включити параметр --stack-check. Якщо ви будуєте та запускаєте зразок як

g++ --stack-check -W -Wall errorRange.cpp -o errorRange
valgrind ./errorRange

Valgrind буде повідомляти про помилку.


2
Насправді, Valgrind досить бідний у визначенні неправильних доступів масиву на стеку. (і це правильно, найкраще, що можна зробити, - це позначити весь стек як дійсне місце для запису)
Falaina

@Falaina - хороший момент, але Valgrind може виявити хоча б деякі помилки стеку.
Todd Stout

І valgrind не побачить нічого поганого в коді, тому що компілятор досить розумний, щоб оптимізувати масив і просто виводити буквальні 3 і 4. Така оптимізація відбувається до того, як gcc перевіряє межі масиву, тому попередження про GCC не виходить за межі не показано.
Госвін фон Бредерлоу

2

Невизначена поведінка, що працює на вашу користь. Яка б пам’ять ви не вгамували, мабуть, не містить нічого важливого. Зауважте, що C і C ++ не проводять перевірку меж масивів, тому подібні речі не збираються під час компіляції чи запуску.


5
Ні, не визначена поведінка "працює на вашу користь", коли вона чітко виходить з ладу. Коли це, здається, працює, це про найгірший можливий сценарій.
jalf

@JohnBode: Тоді було б краще, якщо ви виправите формулювання відповідно до коментаря jalf
Destructor

1

При ініціалізації масиву з int array[2]виділяється простір для 2 цілих чисел; але ідентифікатор arrayпросто вказує на початок цього простору. Коли ви звертаєтесь до array[3]і array[4], компілятор потім просто збільшує адресу, щоб вказати, де були б ці значення, якби масив був досить довгим; спробуйте отримати доступ до чогось подібного, array[42]не спершу ініціалізуючи його, ви отримаєте будь-яке значення, яке вже було в пам’яті в цьому місці.

Редагувати:

Більше інформації про вказівники / масиви: http://home.netcom.com/~tjensen/ptr/pointers.htm


0

коли ви оголошуєте масив int [2]; Ви резервуєте 2 місця пам’яті по 4 байти кожен (32-бітова програма). якщо ви введете array [4] у своєму коді, він все ще відповідає дійсному виклику, але лише під час виконання він видасть необроблений виняток. C ++ використовує ручне управління пам'яттю. Це фактично недолік безпеки, який використовувався для злому програм

це може допомогти зрозуміти:

int * somepointer;

somepointer [0] = somepointer [5];


0

Як я розумію, локальні змінні виділяються на стеці, тому вихід із меж власного стека може замінити лише якусь іншу локальну змінну, якщо ви не надто сильно перебуваєте в oob і не перевищуєте розмір стека. Оскільки у вас немає жодних інших змінних, оголошених у вашій функції - це не викликає побічних ефектів. Спробуйте оголосити іншу змінну / масив відразу після першої та подивіться, що буде з нею.


0

Коли ви пишете "масив [індекс]" на C, це перекладається на машинні інструкції.

Переклад іде приблизно так:

  1. 'отримати адресу масиву'
  2. 'отримати розмір типу масиву об'єктів складається'
  3. "помножити розмір типу на індекс"
  4. 'додати результат до адреси масиву'
  5. 'читайте, що за адресою'

Результат вирішує щось, що може бути, а може і не бути частиною масиву. В обмін на швидкодію швидкісних інструкцій машини ви втрачаєте захисну мережу комп'ютера, яка перевіряє речі для вас. Якщо ви прискіпливі та обережні, це не проблема. Якщо ви неохайні або помилитесь, ви згоріли. Іноді це може генерувати недійсну інструкцію, яка спричиняє виняток, іноді ні.


0

Гарний підхід, який я часто бачив, і який я використовував насправді, - це ввести який-небудь елемент типу NULL (або створений, як uint THIS_IS_INFINITY = 82862863263;) в кінці масиву.

Тоді, при перевірці умови циклу, TYPE *pagesWordsє якийсь масив вказівника:

int pagesWordsLength = sizeof(pagesWords) / sizeof(pagesWords[0]);

realloc (pagesWords, sizeof(pagesWords[0]) * (pagesWordsLength + 1);

pagesWords[pagesWordsLength] = MY_NULL;

for (uint i = 0; i < 1000; i++)
{
  if (pagesWords[i] == MY_NULL)
  {
    break;
  }
}

Це рішення не вкаже слово, якщо масив заповнений structтипами.


0

Як уже згадувалося зараз у запитанні, використовуючи std :: vector :: at вирішить проблему та зробить обмежену перевірку перед доступом.

Якщо вам потрібен масив постійного розміру, який знаходиться в стеці в якості першого коду, використовуйте C ++ 11 новий контейнер std :: array; як вектор є std :: array :: у функції. Насправді функція існує у всіх стандартних контейнерах, у яких вона має значення, тобто там, де визначено оператор [] :( deque, map, unororder_map) за винятком std :: bitset, в якому він називається std :: bitset: : тест.


0

libstdc ++, що входить до складу gcc, має спеціальний режим налагодження для перевірки помилок. Це увімкнено прапором компілятора -D_GLIBCXX_DEBUG. Крім усього іншого, він перевіряє межі за std::vectorціною виконання. Ось онлайн демонстрація з останньою версією gcc.

Тож фактично ви можете робити перевірку меж у режимі налагодження libstdc ++, але робити це потрібно лише під час тестування, оскільки це коштує помітної продуктивності порівняно зі звичайним режимом libstdc ++.


0

Якщо ви трохи змінили програму:

#include <iostream>
using namespace std;
int main()
{
    int array[2];
    INT NOTHING;
    CHAR FOO[4];
    STRCPY(FOO, "BAR");
    array[0] = 1;
    array[1] = 2;
    array[3] = 3;
    array[4] = 4;
    cout << array[3] << endl;
    cout << array[4] << endl;
    COUT << FOO << ENDL;
    return 0;
}

(Зміни великих літер - замініть їх на малі регістри, якщо ви збираєтеся спробувати це.)

Ви побачите, що змінна foo була пошкоджена. Ваш код буде зберігати значення у неіснуючому масиві [3] та масиві [4], а також зможе належним чином їх отримати, але фактичне використовуване сховище буде з foo .

Таким чином, ви можете "піти", перевищивши межі масиву у своєму оригінальному прикладі, але ціною заподіяння шкоди в іншому місці - шкода, яку може виявити дуже важко діагностувати.

Щодо того, чому немає автоматичної перевірки меж - правильно написана програма її не потребує. Після того, як це було зроблено, немає ніяких причин перевіряти межі часу виконання, і це просто сповільнить програму. Найкраще це зрозуміти під час проектування та кодування.

C ++ базується на C, який був розроблений максимально наближено до мови монтажу.

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