Прежде всего, если вы говорите о 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
syscall всегда проверяет запрашиваемый адрес и отказывается отображать страницы, которые перекрывают уже отображенную область памяти, или страницы по совершенно недействительным адресам (например, addr < mmap_min_addr
или addr > 0x7ffffffff000
).
Теперь ... погрузившись прямо в код ядра Linux, точно в загрузчик ELF ядра (fs/binfmt_elf.c:960
), мы можем увидеть довольно длинный и содержательный комментарий :
/*
* 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
заголовок программы, который отвечает за загрузку и запуск обычных программ. Он также делает кучу необычных вещей за кулисами (разрешение перемещений, загрузка необходимых библиотек и т. Д. c.) Перед тем, как запускать загружаемую программу.
Когда ядро выполняет новый 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()
байтов, если 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...
против RIP=0x7f...
мгновенно помогает понять где искать (саму программу или код библиотеки).