Как работает распределение стека в Linux?

18

Резервирует ли ОС фиксированный объем действительного виртуального пространства для стека или чего-то еще? Могу ли я произвести переполнение стека, просто используя большие локальные переменные?

Я написал небольшую Cпрограмму, чтобы проверить мое предположение. Он работает на X86-64 CentOS 6.5.

#include <string.h>
#include <stdio.h>
int main()
{
    int n = 10240 * 1024;
    char a[n];
    memset(a, 'x', n);
    printf("%x\n%x\n", &a[0], &a[n-1]);
    getchar();
    return 0;
}

Запуск программы дает &a[0] = f0ceabe0и&a[n-1] = f16eabdf

Карта proc показывает стек: 7ffff0cea000-7ffff16ec000. (10248 * 1024B)

Тогда я попытался увеличить n = 11240 * 1024

Запуск программы дает &a[0] = b6b36690и&a[n-1] = b763068f

Карта proc показывает стек: 7fffb6b35000-7fffb7633000. (11256 * 1024B)

ulimit -sпечатает 10240на моем ПК.

Как видите, в обоих случаях размер стека больше, чем ulimit -sдает. И стек растет с большей локальной переменной. Вершина стека как-то на 3-5 КБ больше &a[0](AFAIK красная зона - 128B).

Так как же эта карта стека распределяется?

амосс
источник

Ответы:

14

Похоже, что предел памяти стека не выделен (во всяком случае, это не может быть с неограниченным стеком). https://www.kernel.org/doc/Documentation/vm/overcommit-accounting говорит:

Рост стека языка C делает неявное mremap. Если вы хотите получить абсолютные гарантии и работать близко к краю, вы ДОЛЖНЫ отобразить свой стек на самый большой размер, который, по вашему мнению, вам понадобится. Для типичного использования стека это не имеет большого значения, но это угловой случай, если вы действительно заботитесь

Однако отображение стека будет целью компилятора (если у него есть опция для этого).

РЕДАКТИРОВАТЬ: После некоторых испытаний на компьютере Debian x84_64, я обнаружил, что стек растет без каких-либо системных вызовов (в соответствии с strace). Таким образом, это означает, что ядро ​​увеличивает его автоматически (это означает «неявное» выше), т.е. без явного mmap/ mremapиз процесса.

Было довольно сложно найти подробную информацию, подтверждающую это. Я рекомендую Понимание Менеджера виртуальной памяти Linux Мела Гормана. Я предполагаю, что ответ находится в Разделе 4.6.1 Обработка сбоя страницы , за исключением «Область недопустима, но находится рядом с расширяемой областью, такой как стек», и соответствующим действием «Расширить область и выделить страницу». Смотрите также D.5.2 Расширение стека .

Другие ссылки на управление памятью в Linux (но почти без стека):

РЕДАКТИРОВАТЬ 2: Эта реализация имеет недостаток: в угловых случаях столкновение кучи стека может не обнаруживаться, даже в случае, когда стек будет больше, чем предел! Причина в том, что запись в переменную в стеке может оказаться в выделенной памяти кучи, и в этом случае нет ошибок страницы, и ядро ​​не может знать, что стек необходимо расширить. См. Мой пример в обсуждении Столкновение тихой кучи стека под GNU / Linux, которое я начал в списке gcc-help. Чтобы избежать этого, компилятору необходимо добавить некоторый код при вызове функции; это можно сделать -fstack-checkдля GCC (подробности см. в ответе Яна Лэнса Тейлора и на странице руководства GCC).

vinc17
источник
Это кажется правильным ответом на мой вопрос. Но это смущает меня больше. Когда будет запущен вызов mremap? Это будет системный вызов, встроенный в программу?
Амос
@amos Я предполагаю, что вызов mremap будет запущен при необходимости при вызове функции или при вызове alloca ().
vinc17
Вероятно, было бы неплохо упомянуть, что такое mmap, для людей, которые не знают.
Фахим Митха
@FaheemMitha Я добавил немного информации. Для тех, кто не знает, что такое mmap, смотрите FAQ по памяти, упомянутый выше. Здесь для стека это было бы «анонимное отображение», так что неиспользуемое пространство не занимало бы никакой физической памяти, но, как объяснил Мел Горман, ядро ​​выполняет отображение (виртуальную память) и физическое распределение одновременно. ,
vinc17
1
@max Я попробовал программу OP с ulimit -sвыдачей 10240, как при условиях OP, и я получаю SIGSEGV, как и ожидалось (это то, что требуется для POSIX: «Если этот предел превышен, для потока будет сгенерирован SIGSEGV. «). Я подозреваю ошибку в ядре OP.
vinc17
6

Ядро Linux 4.2

Минимальная тестовая программа

Затем мы можем протестировать его с помощью минимальной 64-битной программы NASM:

global _start
_start:
    sub rsp, 0x7FF000
    mov [rsp], rax
    mov rax, 60
    mov rdi, 0
    syscall

Убедитесь, что вы отключили ASLR и удалили переменные окружения, так как они будут помещаться в стек и занимают место:

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
env -i ./main.out

Предел где-то чуть ниже моего ulimit -s(8MiB для меня). Похоже, это из-за того, что дополнительные данные System V изначально помещаются в стек в дополнение к среде: параметры командной строки Linux 64 в Assembly | Переполнение стека

Если вы серьезно относитесь к этому, TODO создаст минимальный образ initrd, который начинает запись с вершины стека и спускается, а затем запускает его с помощью QEMU + GDB . Поместите dprintfв цикл печать адреса стека и точку останова в acct_stack_growth. Это будет великолепно.

Связанный:

Ciro Santilli 新疆 改造 中心 法轮功 六四 事件
источник
2

По умолчанию максимальный размер стека составляет 8 МБ на процесс,
но его можно изменить с помощью ulimit:

Отображение значения по умолчанию в кБ:

$ ulimit -s
8192

Установить на неограниченное количество:

ulimit -s unlimited

влияет на текущую оболочку и подоболочки и их дочерние процессы.
( ulimitэто встроенная команда оболочки)

Вы можете показать фактический диапазон адресов стека, используемый с:
cat /proc/$PID/maps | grep -F '[stack]'
в Linux.

Volker Siegel
источник
Поэтому, когда программа загружается текущей оболочкой, ОС сделает сегмент памяти в ulimit -sКБ действительным для программы. В моем случае это 10240KB. Но когда я объявляю локальный массив char a[10240*1024]и устанавливаю a[0]=1, программа завершается корректно. Почему?
Амос
Попробуйте установить последний элемент тоже. И убедитесь, что они не оптимизированы.
vinc17
@amos Я думаю, что vinc17 означает, что вы назвали область памяти, которая не помещается в стек в вашей программе , но, поскольку вы фактически не обращаетесь к ней в той части, которая не подходит , машина никогда не замечает этого - она не даже получить эту информацию .
Фолькер Сигел
@amos Попробуй int n = 10240*1024; char a[n]; memset(a,'x',n);... Сег .
Златовласка
2
@amos Итак, как вы видите, a[]в вашем стеке 10 МБ не было места. Компилятор мог видеть, что не может быть рекурсивного вызова, и сделал специальное распределение, или что-то еще, например прерывистый стек или некоторую косвенность.
vinc17