Доступ к массиву вне границ не дает ошибок, почему?


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 ++ должны использовать вместо этого контейнерные классы, кроме как во встроенном / OS программировании. Прочитайте это по причинам для пользовательских контейнеров. parashift.com/c++-faq-lite/containers.html
jkeys

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

4
A vector не автоматически изменяет размер при доступе к элементам за пределами! Это просто UB!
Павел Минаев

Ответы:


364

Добро пожаловать к лучшему другу каждого программиста на C / C ++: неопределенному поведению .

Существует много того, что не указано в языковом стандарте по разным причинам. Это одна из них.

В общем, всякий раз, когда вы сталкиваетесь с неопределенным поведением, может произойти все что угодно . Приложение может зависнуть, оно может зависнуть, оно может вытолкнуть ваш привод CD-ROM или заставить демонов выйти из носа. Он может отформатировать жесткий диск или по электронной почте всех ваших порно к своей бабушке.

Даже если вам действительно не повезло, может показаться, что он работает правильно.

Язык просто говорит, что должно произойти, если вы обращаетесь к элементам в пределах массива. Не определено, что произойдет, если вы выйдете за пределы. Может показаться, что он работает сегодня на вашем компиляторе, но это не является допустимым C или 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 Такая проверка чего-то стоит. Если вы пишете правильный код, вы не хотите платить эту цену. Сказав это, я стал полностью преобразованным в метод st (::) at (), который проверен. Его использование выявило немало ошибок в том, что я считал «правильным» кодом.

10
Я полагаю, что старые версии GCC фактически запустили Emacs и симулятор Towers of Hanoi, когда он столкнулся с определенными типами неопределенного поведения. Как я уже сказал, все может случиться. ;)
Джалф

4
Все уже сказано, так что это только небольшое дополнение. Отладочные сборки могут быть очень простыми в этих обстоятельствах по сравнению с релизными сборками. Из-за того, что отладочная информация включена в отладочные файлы, вероятность того, что что-то важное будет перезаписано, меньше. Вот почему иногда кажется, что отладочные сборки работают нормально, в то время как релизная сборка вылетает.
Богатый

31

Используя g ++, вы можете добавить параметр командной строки: -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, когда доступ за пределы не обнаруживается sanitizer . По этой причине я бы рекомендовал дополнительно использовать -D_GLIBCXX_DEBUGопцию, которая добавляет еще больше проверок.
Привет-ангел

12

g ++ не проверяет границы массивов, и вы, возможно, что-то перезаписываете с помощью 3,4, но ничего особенно важного, если вы попытаетесь использовать большее число, вы получите сбой.

Вы просто перезаписываете части стека, которые не используются, вы можете продолжать, пока не достигнете конца выделенного пространства для стека, и в конечном итоге он потерпит крах

РЕДАКТИРОВАТЬ: у вас нет способа справиться с этим, может быть, статический анализатор кода может выявить эти сбои, но это слишком просто, у вас могут быть подобные (но более сложные) сбои, необнаруженные даже для статических анализаторов


6
Где взять, если по адресу массива [3] и массива [4] нет «ничего действительно важного» ??
Namezero

7

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

Используйте std :: vector с std::vector::iteratorВместо этого '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:

Хорошо, вот небольшой урок о массивах и их отношениях с указателями:

Когда вы используете индексирование массива, вы действительно используете скрытый указатель (называемый «ссылкой»), который автоматически разыменовывается. Вот почему вместо * (array [1]) array [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

«Вот почему вместо * (array [1]) array [1] автоматически возвращает значение при этом значении». Вы уверены, что * (array [1]) будет работать правильно? Я думаю, что это должно быть * (массив + 1). PS: LOL, это похоже на отправку сообщения в прошлое. Но, в любом случае:
мая

5

намек

Если вы хотите иметь быстрые массивы размеров ограничений с проверкой ошибок диапазона, попробуйте использовать boost :: array (также std :: tr1 :: array из <tr1/array>него будет стандартным контейнером в следующей спецификации C ++). Это намного быстрее, чем std :: vector. Он резервирует память в куче или внутри экземпляра класса, так же, как int array [].
Это простой пример кода:

#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) , где массив является указателем на & array [0]. Это приведет к неопределенному поведению.

Один из способов отловить это иногда в C - использовать статическую проверку, такую ​​как splint . Если вы запускаете:

splint +bounds array.c

на,

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

    array[1] = 1;

    return 0;
}

тогда вы получите предупреждение:

array.c: (в функции main) 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 может быть полезен при обнаружении многих других типов проблем с памятью, в этом случае он просто не особенно полезен, если вы не измените свой bulid для включения опции --stack-check. Если вы создаете и запускаете образец как

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

Valgrind будет сообщать об ошибке.


2
На самом деле, Valgrind довольно плохо определяет неправильный доступ к массиву в стеке. (и это правильно, лучшее, что он может сделать - пометить весь стек как правильное место записи)
Фалаина

@Falaina - хороший момент, но Valgrind может обнаружить хотя бы некоторые ошибки стека.
Тодд Стаут

И valgrind не увидит ничего плохого в коде, потому что компилятор достаточно умен, чтобы оптимизировать массив и просто вывести литерал 3 и 4. Эта оптимизация происходит до того, как gcc проверяет границы массива, поэтому предупреждение gcc за пределами допустимых значений делает есть не показано.
Госвин фон Бредерлоу

2

Неопределенное поведение работает в вашу пользу. Независимо от того, что память вы бьете, очевидно, не имеет ничего важного. Обратите внимание, что C и C ++ не выполняют проверку границ для массивов, поэтому подобные вещи не будут обнаружены при компиляции или во время выполнения.


5
Нет, неопределенное поведение «работает в вашу пользу», когда оно падает точно. Когда кажется, что это работает, это о худшем возможном сценарии.
Джалф

@JohnBode: Тогда было бы лучше, если бы вы исправили формулировку в соответствии с комментарием Джальфа
Деструктор

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-битная программа). если вы введете массив [4] в своем коде, он по-прежнему будет соответствовать допустимому вызову, но только во время выполнения вызовет необработанное исключение. C ++ использует ручное управление памятью. Это на самом деле недостаток безопасности, который был использован для взлома программ

это может помочь понять:

int * somepointer;

somepointer [0] = somepointer [5];


0

Как я понимаю, локальные переменные размещаются в стеке, поэтому выход за пределы собственного стека может перезаписать только некоторые другие локальные переменные, если вы не слишком перегружены и не превышаете размер стека. Поскольку у вас нет других переменных, объявленных в вашей функции - это не вызывает никаких побочных эффектов. Попробуйте объявить другую переменную / массив сразу после вашей первой и посмотрите, что будет с ней.


0

Когда вы пишете 'array [index]' в 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 :: at. Фактически функция существует во всех стандартных контейнерах, в которых она имеет значение, т. Е. Где определен operator [] :( deque, map, unordered_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.