Минимальное распределение стека

Минимальное распределение стека

Я изучаю спецификацию 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, особенно если он получен из пользовательского ввода.

Связанный контент