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

Почему приведенный ниже код работает без сбоев во время выполнения?

А также размер полностью зависит от машины / платформы / компилятора !!. Я могу даже отдать до 200 на 64-битной машине. как ошибка сегментации в основной функции будет обнаружена в ОС?

int main(int argc, char* argv[])
{
    int arr[3];
    arr[4] = 99;
}

Откуда это буферное пространство? Это стек, выделенный процессу?


person vprajan    schedule 23.06.2011    source источник
comment
Переполнение стека происходит, когда из стека выделяется слишком много памяти. В этом случае, предполагая sizeof(int)==4, вы выделили ничтожные 12 байтов из стека. Ваш код пишет за пределами конца массива. Это не переполнение стека. Это неопределенное поведение.   -  person David Hammen    schedule 23.06.2011
comment
Происходит из того же места, что и остальная часть оперативной памяти, вероятно, от того, кто продал вам компьютер. arr[3] означает выделить 3 int пространства, доступного для моего использования, это не означает создать 3 int пространства из эфира, хотя это было бы законной реализацией, если бы это было физически возможно. Вы набрасываете любую память / адрес, который оказывается рядом с arr (ну, фактически, по соседству), который, как говорит Дэвид, является UB. Да, это часть вашего стека (стандарты C и C ++ не говорят о стеке, но на практике именно туда попадают автоматические переменные).   -  person Steve Jessop    schedule 23.06.2011
comment
@vprajan - Я обновил ваш заголовок, чтобы отразить вопрос, так как здесь есть хороший ответ, на который можно обратить внимание.   -  person Steve Townsend    schedule 23.06.2011
comment
Ошибка сегментации и доступ к памяти, к которой я не собирался обращаться, не эквивалентны. Первое - это подмножество симптомов выполнения второго.   -  person Lightness Races in Orbit    schedule 23.06.2011
comment
@Steve, спасибо за обновление ..   -  person vprajan    schedule 23.06.2011
comment
arr[3] = 99; уже хватило бы.   -  person Peter - Reinstate Monica    schedule 26.11.2018


Ответы (9)


Что-то, что я написал когда-то в образовательных целях ...

Рассмотрим следующую c-программу:

int q[200];

main(void) {
    int i;
    for(i=0;i<2000;i++) {
        q[i]=i;
    }
}

после его компиляции и выполнения создается дамп ядра:

$ gcc -ggdb3 segfault.c
$ ulimit -c unlimited
$ ./a.out
Segmentation fault (core dumped)

теперь использую GDB для выполнения посмертного анализа:

$ gdb -q ./a.out core
Program terminated with signal 11, Segmentation fault.
[New process 7221]
#0  0x080483b4 in main () at s.c:8
8       q[i]=i;
(gdb) p i
$1 = 1008
(gdb)

да, программа не выполняла segfault, когда кто-то писал за пределами выделенных 200 элементов, вместо этого она аварийно завершала работу, когда i = 1008, почему?

Введите страницы.

В UNIX / Linux можно определить размер страницы несколькими способами, один из них - использовать системную функцию sysconf () следующим образом:

#include <stdio.h>
#include <unistd.h> // sysconf(3)

int main(void) {
    printf("The page size for this system is %ld bytes.\n",
            sysconf(_SC_PAGESIZE));

    return 0;
}

что дает результат:

Размер страницы для этой системы составляет 4096 байт.

или можно использовать утилиту командной строки getconf следующим образом:

$ getconf PAGESIZE
4096

вскрытие

Оказывается, segfault возникает не при i = 200, а при i = 1008, давайте разберемся, почему. Запустите GDB, чтобы провести посмертный анализ:

$gdb -q ./a.out core

Core was generated by `./a.out'.
Program terminated with signal 11, Segmentation fault.
[New process 4605]
#0  0x080483b4 in main () at seg.c:6
6           q[i]=i;
(gdb) p i
$1 = 1008
(gdb) p &q
$2 = (int (*)[200]) 0x804a040
(gdb) p &q[199]
$3 = (int *) 0x804a35c

q заканчивался по адресу 0x804a35c, точнее, последний байт q [199] находился в этом месте. Размер страницы, как мы видели ранее, составляет 4096 байт, а 32-битный размер слова машины дает, что виртуальный адрес разбивается на 20-битный номер страницы и 12-битное смещение.

q [] оканчивается номером виртуальной страницы:

0x804a = 32842 смещение:

0x35c = 860, так что все еще было:

4096 - 864 = 3232 байта осталось на той странице памяти, на которой был выделен q []. Это пространство может содержать:

3232/4 = 808 целых чисел, и код обрабатывал его так, как если бы он содержал элементы q в позициях от 200 до 1008.

Все мы знаем, что этих элементов не существует, и компилятор не жаловался, как и hw, поскольку у нас есть права на запись на эту страницу. Только когда я = 1008, q [] ссылался на адрес на другой странице, для которой у нас не было разрешения на запись, виртуальная память hw обнаружила это и запустила segfault.

Целое число хранится в 4 байтах, что означает, что эта страница содержит 808 (3236/4) дополнительных поддельных элементов, что означает, что доступ к этим элементам от q [200], q [201] до элемента 199 по-прежнему совершенно законен. + 808 = 1007 (q [1007]) без активации ошибки сегмента. При доступе к q [1008] вы попадаете на новую страницу с другими разрешениями.

person Fredrik Pihl    schedule 23.06.2011
comment
Это было абсолютно захватывающе, один из лучших постов, которые я когда-либо читал на SO. - person pg1989; 23.06.2011
comment
Отличный ответ, за исключением той части, где вы говорите, что все еще совершенно законно доступ к этим элементам от q [200], q [201] вплоть до element - бывает, что для этой реализации компилятора доступ к этим элементам не вызывает никаких проблем, но технически доступ к этим элементам является неопределенным поведением, и другой компилятор может генерировать очень разные результаты. То есть доступ к этим элементам незаконен, но в этих обстоятельствах это может сойти с рук. Например, скорость 75 миль в час при максимальной скорости 65 миль в час. :) - person Edward Loper; 23.06.2011
comment
+1 Хотя я согласен с Эдвардом. Законность очень строго определена; давайте не будем искажать его смысл здесь! - person Lightness Races in Orbit; 23.06.2011
comment
отличный пост !! .. Обратите внимание, что если то же самое делается внутри функции, отличной от основной, обнаруживается ошибка сегментации (переполнение буфера) .. !! - person vprajan; 23.06.2011

Поскольку вы пишете за пределами своего массива, поведение вашего кода не определено.

Природа неопределенного поведения заключается в том, что может случиться все что угодно, включая отсутствие ошибок сегментации (компилятор не обязан выполнять проверку границ).

Вы пишете в память, которую не выделили, но она есть и, вероятно, не используется ни для чего другого. Ваш код может вести себя по-другому, если вы вносите изменения в кажущиеся несвязанными части кода, в свою ОС, компилятор, флаги оптимизации и т. Д.

Другими словами, как только вы окажетесь на этой территории, все ставки отключены.

person NPE    schedule 23.06.2011

Что касается того, когда / где происходит сбой переполнения буфера локальной переменной, зависит от нескольких факторов:

  1. Количество данных в стеке уже на момент вызова функции, которая содержит доступ к переполнению переменной.
  2. Общий объем данных, записанных в переполняющую переменную / массив

Помните, что стопки растут вниз. Т.е. выполнение процесса начинается с указателя стека рядом с концом памяти, которая будет использоваться в качестве стека. Однако он не начинается с последнего отображенного слова, и это потому, что код инициализации системы может решить передать некоторую «информацию о запуске» процессу во время создания, и часто это делается в стеке.

Это обычный режим отказа - сбой при возврате из функции, содержащей код переполнения.

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

Если эта сумма меньше, чем размер используемой части стека в это время, тогда он будет работать нормально и выйдет из строя позже - фактически, на платформах, которые хранят адреса возврата в стеке ( что верно для x86 / x64) при возврате из вашей функции. Это потому, что инструкция ЦП ret фактически берет слово из стека (адрес возврата) и перенаправляет выполнение туда. Если вместо ожидаемого места кода этот адрес содержит какой-либо мусор, возникает исключение и ваша программа умирает.

Чтобы проиллюстрировать это: когда вызывается main(), стек выглядит следующим образом (в 32-битной программе UNIX x86):

[ esp          ] <return addr to caller> (which exits/terminates process)
[ esp + 4      ] argc
[ esp + 8      ] argv
[ esp + 12     ] envp <third arg to main() on UNIX - environment variables>
[ ...          ]
[ ...          ] <other things - like actual strings in argv[], envp[]
[ END          ] PAGE_SIZE-aligned stack top - unmapped beyond

Когда main() запускается, он выделяет место в стеке для различных целей, в том числе для размещения вашего массива, который должен быть переполнен. Это будет выглядеть так:

[ esp          ] <current bottom end of stack>
[ ...          ] <possibly local vars of main()>
[ esp + X      ] arr[0]
[ esp + X + 4  ] arr[1]
[ esp + X + 8  ] arr[2]
[ esp + X + 12 ] <possibly other local vars of main()>
[ ...          ] <possibly other things (saved regs)>

[ old esp      ] <return addr to caller> (which exits/terminates process)
[ old esp + 4  ] argc
[ old esp + 8  ] argv
[ old esp + 12 ] envp <third arg to main() on UNIX - environment variables>
[ ...          ]
[ ...          ] <other things - like actual strings in argv[], envp[]
[ END          ] PAGE_SIZE-aligned stack top - unmapped beyond

Это означает, что вы можете получить доступ далеко за пределы arr[2].

Чтобы оценить различные сбои, возникающие в результате переполнения буфера, попробуйте следующее:

#include <stdlib.h>
#include <stdio.h>

int main(int argc, char **argv)
{
    int i, arr[3];

    for (i = 0; i < atoi(argv[1]); i++)
        arr[i] = i;

    do {
        printf("argv[%d] = %s\n", argc, argv[argc]);
    } while (--argc);

    return 0;
}

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

person FrankH.    schedule 23.06.2011

Используя тип массива, который C ++ унаследовал от C, вы неявно просите не проводить проверку диапазона.

Если вы попробуете это вместо

void main(int argc, char* argv[])
{     
    std::vector<int> arr(3);

    arr.at(4) = 99;
} 

вы получите исключение.

Таким образом, C ++ предлагает как проверенный, так и непроверенный интерфейс. Вам решать, какой из них вы хотите использовать.

person Bo Persson    schedule 23.06.2011

Это неопределенное поведение - вы просто не наблюдаете никаких проблем. Наиболее вероятная причина заключается в том, что вы перезаписываете область памяти, от которой ранее не зависело поведение программы - эта память технически доступна для записи (размер стека в большинстве случаев составляет около 1 мегабайта), и вы не видите индикации ошибки. Не стоит полагаться на это.

person sharptooth    schedule 23.06.2011

Чтобы ответить на ваш вопрос, почему он «необнаружен»: большинство компиляторов C не анализируют во время компиляции, что вы делаете с указателями и памятью, и поэтому во время компиляции никто не замечает, что вы написали что-то опасное. Во время выполнения также нет контролируемой, управляемой среды, которая присматривает за вашими ссылками на память, поэтому никто не мешает вам читать память, на которую вы не имеете права. В этот момент память выделяется вам (потому что это просто часть стека недалеко от вашей функции), поэтому у ОС тоже нет проблем с этим.

Если вы хотите, чтобы при доступе к памяти держали за руку, вам нужна управляемая среда, такая как Java или CLI, где вся ваша программа запускается другой управляющей программой, которая отслеживает эти нарушения.

person Kerrek SB    schedule 23.06.2011

В вашем коде есть неопределенное поведение. Это означает, что он может делать что угодно или ничего. В зависимости от вашего компилятора, ОС и т. Д. Возможен сбой.

Тем не менее, со многими, если не большинством компиляторов, ваш код даже не компилируется.

Это потому, что у вас есть void main, в то время как стандарт C и стандарт C ++ требуют int main.

Примерно единственный компилятор, который доволен void main, - это Microsoft, Visual C ++.

Это дефект компилятора, но поскольку у Microsoft есть множество примеров документации и даже инструментов генерации кода, которые генерируют void main, они, скорее всего, никогда не исправят его. Однако учтите, что написание специфичного для Microsoft void main требует ввода на один символ больше, чем стандартного int main. Так почему бы не пойти со стандартами?

Ура & hth.,

person Cheers and hth. - Alf    schedule 23.06.2011

Ошибка сегментации возникает, когда процесс пытается перезаписать страницу в памяти, которой он не владеет; Если вы не пройдете долгий путь до конца своего буфера, вы не вызовете ошибку сегмента.

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

person RobH    schedule 23.06.2011

Очевидно, когда вы запрашиваете у компьютера определенное количество байтов для выделения в памяти, скажем: char array [10], он дает нам несколько дополнительных байтов, чтобы не столкнуться с segfault, однако использовать их по-прежнему небезопасно. , и попытка получить доступ к дополнительной памяти в конечном итоге приведет к сбою программы.

person Duck Ling    schedule 24.01.2019