Подтвердить что ты не робот

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

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

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

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

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

4b9b3361

Ответ 1

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

Рассмотрим следующую 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 для выполнения анализа post mortem:

$ 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)

huh, программа не segfault, когда вы писали за пределами выделенных 200 элементов, вместо этого она разбилась, когда я = 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

post mortem

Оказывается, что segfault происходит не при я = 200, а при я = 1008, давайте выясним, почему. Запустите gdb, чтобы сделать ananlysis post mortem:

$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] вы вводите новую страницу, для которой разные разрешения.

Ответ 2

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

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

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

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

Ответ 3

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

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

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

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

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

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

Ответ 4

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

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

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

    arr.at(4) = 99;
} 

вы будете получать исключение.

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

Ответ 5

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

Ответ 6

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

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

Ответ 7

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

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

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

О единственном компиляторе, который довольны void main, является Microsoft & rsquo; s, Visual С++.

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

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

Ответ 8

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

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

Ответ 9

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