Почему моя программа не падает, когда я пишу после конца массива? - PullRequest
15 голосов
/ 23 июня 2011

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

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

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

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

Ответы [ 9 ]

73 голосов
/ 23 июня 2011

То, что я написал некоторое время назад для образовательных целей ...

Рассмотрим следующую 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)

да, программа не работала с ошибкой, когда кто-то писал за пределами 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

вскрытие

Оказывается, что ошибка происходит не при 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, поскольку у нас есть права на запись для этой страницы. Только когда i = 1008, q [] ссылался на адрес на другой странице, для которого у нас не было разрешения на запись, виртуальная память обнаружила это и вызвала ошибку.

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

6 голосов
/ 23 июня 2011

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

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

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

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

4 голосов
/ 23 июня 2011

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

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

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

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

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

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

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

[ 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 в стек и код перезаписывает его во время цикла).

3 голосов
/ 23 июня 2011

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

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

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

    arr.at(4) = 99;
} 

you будет получено исключение.

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

2 голосов
/ 23 июня 2011

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

1 голос
/ 23 июня 2011

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

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

0 голосов
/ 24 января 2019

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

0 голосов
/ 23 июня 2011

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

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

0 голосов
/ 23 июня 2011

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

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

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

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

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

Приветствия и hth.,

...