Прежде всего, если предположить, что вы говорите о x86-64, мы можем увидеть, что карта виртуальной памяти для x86-64:
========================================================================================================================
Start addr | Offset | End addr | Size | VM area description
========================================================================================================================
| | | |
0000000000000000 | 0 | 00007fffffffffff | 128 TB | user-space virtual memory, different per mm
__________________|____________|__________________|_________|___________________________________________________________
... | ... | ... | ...
Адреса пользовательского пространства всегда имеют каноническую форму в x86-64, используя только младшие 48 бит. Видеть:
Это помещает конец виртуальной памяти пользовательского пространства в 0x7fffffffffff
. Здесь начинается стек новых программ: то есть 0x7ffffffff000
(за вычетом некоторого случайного смещения из-за ASLR) и увеличивается до нижних адресов.
Позвольте мне сначала ответить на простой вопрос:
Будет ли проблема, если я вручную mmap
страницы вне этих префиксов?
Вовсе нет, системный вызов mmap
всегда проверяет запрашиваемый адрес и отказывается отображать страницы, которые перекрывают уже отображенную область памяти, или страницы с полностью недопустимыми адресами (например, _ 6_ или addr > 0x7ffffffff000
).
Теперь ... погрузимся прямо в код ядра Linux, а именно в загрузчик ядра ELF (_ 8_), мы видим довольно длинный и характерный комментарий:
/*
* This logic is run once for the first LOAD Program
* Header for ET_DYN binaries to calculate the
* randomization (load_bias) for all the LOAD
* Program Headers, and to calculate the entire
* size of the ELF mapping (total_size). (Note that
* load_addr_set is set to true later once the
* initial mapping is performed.)
*
* There are effectively two types of ET_DYN
* binaries: programs (i.e. PIE: ET_DYN with INTERP)
* and loaders (ET_DYN without INTERP, since they
* _are_ the ELF interpreter). The loaders must
* be loaded away from programs since the program
* may otherwise collide with the loader (especially
* for ET_EXEC which does not have a randomized
* position). For example to handle invocations of
* "./ld.so someprog" to test out a new version of
* the loader, the subsequent program that the
* loader loads must avoid the loader itself, so
* they cannot share the same load range. Sufficient
* room for the brk must be allocated with the
* loader as well, since brk must be available with
* the loader.
*
* Therefore, programs are loaded offset from
* ELF_ET_DYN_BASE and loaders are loaded into the
* independently randomized mmap region (0 load_bias
* without MAP_FIXED).
*/
if (interpreter) {
load_bias = ELF_ET_DYN_BASE;
if (current->flags & PF_RANDOMIZE)
load_bias += arch_mmap_rnd();
elf_flags |= MAP_FIXED;
} else
load_bias = 0;
Короче говоря, существует два типа независимых от позиции исполняемых файлов ELF:
Нормальные программы: для их работы требуется загрузчик. Это составляет в основном 99,9% программ ELF в нормальной системе Linux. Путь к загрузчику указывается в заголовках программы ELF с заголовком программы типа PT_INTERP
.
Загрузчики: загрузчик - это ELF, в котором не указан PT_INTERP
заголовок программы, и который отвечает за загрузку и запуск обычных программ. Он также выполняет множество причудливых вещей за кулисами (разрешает перемещения, загружает необходимые библиотеки и т. Д.) Перед фактическим запуском загружаемой программы.
Когда ядро выполняет новый ELF через системный вызов execve
, ему необходимо отобразить в память саму программу и загрузчик. Затем управление будет передано загрузчику, который разрешит и отобразит все необходимые разделяемые библиотеки и, наконец, передаст управление программе. Поскольку и программа, и ее загрузчик должны быть сопоставлены, ядру необходимо убедиться, что эти сопоставления не перекрываются (а также что будущие запросы сопоставления загрузчиком не будут перекрываться).
Для этого загрузчик отображается рядом со стеком (по более низкому адресу, чем стек, но с некоторым допуском, поскольку стеку разрешено увеличиваться за счет добавления дополнительных страниц, если это необходимо), оставляя обязанность применять ASLR к mmap
сам. Затем программа отображается с использованием load_bias
(как показано в приведенном выше фрагменте), чтобы поместить ее достаточно далеко от загрузчика (по гораздо более низкому адресу).
Если мы посмотрим на ELF_ET_DYN_BASE
, мы видим, что он зависит от архитектуры и на x86-64 оценивается как:
((1ULL << 47) - (1 << 12)) / 3 * 2 == 0x555555554aaa
Обычно около 2/3 TASK_SIZE
. Затем этот load_bias
корректируется добавлением arch_mmap_rnd()
< / a> байтов, если ASLR включен, и, наконец, с выравниванием по страницам. В конце концов, по этой причине мы обычно видим адреса программ, начинающиеся с 0x55
.
Когда управление передается загрузчику, область виртуальной памяти для процесса уже определена, и последовательные mmap
системные вызовы, не указывающие адрес, будут возвращать уменьшающиеся адреса, начиная с загрузчика. Поскольку, как мы только что видели, загрузчик отображается рядом со стеком, а стек находится в самом конце адресного пространства пользователя, по этой причине мы обычно видим адреса, начинающиеся с 0x7f
для библиотек.
Из вышеизложенного есть общее исключение. В случае, если загрузчик вызывается напрямую, например:
/lib/x86_64-linux-gnu/ld-2.24.so ./myprog
В этом случае ядро не будет отображать ./mpyprog
и оставит это загрузчику. Как следствие, ./myprog
будет отображен загрузчиком на некоторый 0x7f...
адрес.
Вы можете спросить: почему тогда ядро всегда не позволяет загрузчику отображать программу или почему программа не отображается прямо перед / после загрузчика? У меня нет на это 100% однозначного ответа, но на ум приходит несколько причин:
Последовательность: заставить само ядро загружать ELF в память независимо от загрузчика, позволяет избежать проблем. Если бы это было не так, ядро полностью зависело бы от загрузчика пользовательского пространства, что вообще не рекомендуется (это также может частично быть проблемой безопасности).
Эффективность: мы уверены, что по крайней мере как исполняемый файл, так и его загрузчик должны быть сопоставлены (независимо от каких-либо связанных библиотек), может также сэкономить драгоценное время и сделать это сразу, а не ждать другого системного вызова. с соответствующим переключением контекста.
Безопасность: в сценарии по умолчанию отображение программы по другому случайному адресу, отличному от адреса загрузчика и других библиотек, обеспечивает своего рода «изоляцию» между самой программой и загруженными библиотеками. Другими словами, «утечка» любого адреса библиотеки не покажет позицию программы в памяти, и наоборот. Отображение программы с предопределенным смещением от загрузчика и других библиотек вместо этого частично нарушило бы назначение ASLR.
В идеальном сценарии, основанном на безопасности, каждая отдельная mmap
(то есть любая необходимая библиотека) также должна быть размещена по случайному адресу независимо от предыдущих сопоставлений, но это значительно снизит производительность. Группирование выделений приводит к более быстрому поиску в таблице страниц: см. Понимание Ядро Linux (3-е издание), стр. 606: Таблица 15-3.. Наивысший индекс и максимальный размер файла для каждой высоты дерева системы счисления. Это также вызовет гораздо большую фрагментацию виртуальной памяти, что станет реальной проблемой для программ, которым необходимо отображать большие файлы в память. Существенная часть изоляции между программным кодом и кодом библиотеки уже выполнена, и дальнейшее развитие имеет больше минусов, чем плюсов.
Легкость отладки: вид RIP=0x55...
vs RIP=0x7f...
мгновенно помогает понять, где искать (саму программу или код библиотеки).
person
Marco Bonelli
schedule
02.05.2020