Я изучаю спецификацию ELF (http://www.skyfree.org/linux/references/ELF_Format.pdf), и один момент, который мне не ясен в процессе загрузки программы, это то, как инициализируется стек, и каков начальный размер страницы. Вот тест (на Ubuntu x86-64):
$ cat test.s
.text
.global _start
_start:
mov $0x3c,%eax
mov $0,%edi
syscall
$ as test.s -o test.o && ld test.o
$ gdb a.out -q
Reading symbols from a.out...(no debugging symbols found)...done.
(gdb) b _start
Breakpoint 1 at 0x400078
(gdb) run
Starting program: ~/a.out
Breakpoint 1, 0x0000000000400078 in _start ()
(gdb) print $sp
$1 = (void *) 0x7fffffffdf00
(gdb) info proc map
process 20062
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x400000 0x401000 0x1000 0x0 ~/a.out
0x7ffff7ffa000 0x7ffff7ffd000 0x3000 0x0 [vvar]
0x7ffff7ffd000 0x7ffff7fff000 0x2000 0x0 [vdso]
0x7ffffffde000 0x7ffffffff000 0x21000 0x0 [stack]
0xffffffffff600000 0xffffffffff601000 0x1000 0x0 [vsyscall]
Спецификация ELF очень мало говорит о том, как или почему эта страница стека существует в первую очередь, но я могу найти ссылки, в которых говорится, что стек должен быть инициализирован с SP, указывающим на argc, с argv, envp и вспомогательным вектором прямо над ним, и я подтвердил это. Но сколько места доступно под SP? В моей системе есть 0x1FF00
байты, отображенные под SP, но, по-видимому, это отсчет сверху стека в 0x7ffffffff000
, и есть 0x21000
байты в полном отображении. Что влияет на это число?
Я знаю, что страница, расположенная сразу под стеком, является «защитной страницей», которая автоматически становится доступной для записи и «растет вниз по стеку», если я записываю в нее (вероятно, для того, чтобы наивная обработка стека «просто работала»), но если я выделю большой фрейм стека, то я могу выйти за пределы защитной страницы и получить ошибку сегментации, поэтому я хочу определить, сколько места уже правильно выделено мне прямо при запуске процесса.
РЕДАКТИРОВАТЬ: Еще некоторые данные заставляют меня еще больше сомневаться в том, что происходит. Тест следующий:
.text
.global _start
_start:
subq $0x7fe000,%rsp
movq $1,(%rsp)
mov $0x3c,%eax
mov $0,%edi
syscall
Я играл с разными значениями константы 0x7fe000
здесь, чтобы посмотреть, что произойдет, и для этого значения недетерминировано, получу ли я segfault или нет. Согласно GDB, инструкция subq
сама по себе увеличит размер mmap, что для меня загадка (откуда Linux знает, что находится в моем регистре?), но эта программа обычно по какой-то причине приводит к сбою GDB при выходе. Это не может быть ASLR, вызывающая недетерминизм, потому что я не использую GOT или какой-либо раздел PLT; исполняемый файл всегда загружается в одни и те же места в виртуальной памяти каждый раз. Так это какая-то случайность PID или просачивания физической памяти? В общем, я очень запутался в том, какой объем стека на самом деле легально доступен для произвольного доступа, и какой объем запрашивается при изменении RSP или при записи в области «сразу за пределами диапазона» легальной памяти.
решение1
Я не думаю, что этот вопрос действительно связан с ELF. Насколько я знаю, ELF определяет способ "в разобранном виде"образ программы в файлы, а затем повторно собрать его для первого выполнения. Определение того, что такое стек и как он реализован, находится где-то между специфичным для ЦП и специфичным для ОС, если поведение ОС не было повышено до POSIX. Хотя, без сомнения, спецификация ELF предъявляет некоторые требования к тому, что ей необходимо в стеке.
Минимальное распределение стека
Из вашего вопроса:
Я знаю, что страница, расположенная сразу под стеком, является «защитной страницей», которая автоматически становится доступной для записи и «растет вниз по стеку», если я записываю в нее (вероятно, для того, чтобы наивная обработка стека «просто работала»), но если я выделю большой фрейм стека, то я могу выйти за пределы защитной страницы и получить ошибку сегментации, поэтому я хочу определить, сколько места уже правильно выделено мне прямо при запуске процесса.
Я пытаюсь найти авторитетную ссылку на это. Но я нашел достаточно большое количество неавторитетных ссылок, чтобы предположить, что это неверно.
Из того, что я читал, защитная страница используется для перехвата доступа за пределами максимального выделения стека, а не для "нормального" роста стека. Фактическое выделение памяти (отображение страниц в адреса памяти) выполняется по требованию. То есть: когда осуществляется доступ к неотображенным адресам в памяти, которые находятся между stack-base и stack-base - max-stack-size + 1, исключение может быть вызвано процессором, но ядро обработает исключение, отобразив страницу памяти, а не каскадируя ошибку сегментации.
Таким образом, доступ к стеку внутри максимального выделения не должен вызывать ошибку сегментации. Как вы обнаружили
Максимальное распределение стека
При изучении документации следует следовать строкам документации Linux по созданию потоков и загрузке образов (вилка(2),клон(2),execve(2)). Документация execveупоминает нечто интересное:
Ограничения на размер аргументов и окружения
...отрезать...
В ядре 2.6.23 и более поздних версиях большинство архитектур поддерживают ограничение размера, полученное из программногоRLIMIT_STACKограничение ресурсов (см.getrlimit(2))
...отрезать...
Это подтверждает, что ограничение требует поддержки архитектуры, а также указывает, где оно ограничено (getrlimit(2)).
RLIMIT_STACK
Это максимальный размер стека процесса в байтах. При достижении этого предела генерируется сигнал SIGSEGV. Для обработки этого сигнала процесс должен использовать альтернативный стек сигналов (sigaltstack(2)).
Начиная с Linux 2.6.23, этот предел также определяет объем пространства, используемого для аргументов командной строки процесса и переменных окружения; подробности см. в execve(2).
Увеличение стека путем изменения регистра RSP
Я не знаю ассемблер x86. Но я хочу обратить ваше внимание на «исключение Stack Fault», которое может быть вызвано процессорами x86 при изменении регистра SS. Пожалуйста, поправьте меня, если я ошибаюсь., но я считаю, что на x86-64 SS:SP просто стал "RSP". Так что, если я правильно понимаю, исключение Stack Fault может быть вызвано декрементированным RSP ( subq $0x7fe000,%rsp
).
Смотрите страницу 222 здесь:https://xem.github.io/minix86/manual/intel-x86-and-64-manual-vol3/o_fe12b1e2a880e0ce.html
решение2
Каждая область памяти процесса (например, код, статические данные, куча, стек и т. д.) имеет границы, и доступ к памяти за пределами любой области или доступ к записи в область только для чтения генерирует исключение ЦП. Ядро поддерживает эти области памяти. Доступ за пределами области распространяется в пользовательское пространство в виде сигнала ошибки сегментации.
Не все исключения генерируются при доступе к памяти за пределами регионов. Доступ внутри региона также может генерировать исключение. Например, если страница не сопоставлена с физической памятью, обработчик ошибок страниц обрабатывает это прозрачно для запущенного процесса.
Область основного стека процесса изначально имеет только небольшое количество фреймов страниц, сопоставленных с ней, но автоматически увеличивается, когда в нее помещается больше данных через указатель стека. Обработчик исключений проверяет, что доступ все еще находится в пределах области, зарезервированной для стека, и выделяет новый фрейм страницы, если это так. Это происходит автоматически с точки зрения кода уровня пользователя.
Страница защиты размещается сразу после конца области стека, чтобы обнаружить переполнение области стека. Недавно (в 2017 году) некоторые люди поняли, что одной страницы защиты недостаточно, потому что потенциально программу можно обмануть, чтобы она уменьшила указатель стека на большую величину, что может заставить указатель стека указывать на какой-то другой регион, который разрешает запись. «Решением» этой проблемы была замена страницы защиты размером 4 КБ на область защиты размером 1 МБ. Смотрите этоСтатья в LWN.
Следует отметить, что эксплуатация этой уязвимости не совсем тривиальна, для этого требуется, например, чтобы пользователь мог контролировать объем памяти, выделяемой программой через вызов alloca
. Надежные программы должны проверять параметр, передаваемый alloca
, особенно если он получен из пользовательского ввода.