Почему Linux предпочитает сопоставления 0x7f?

Запустив простой less /proc/self/maps, я вижу, что большинство сопоставлений начинаются с 55 и 7F. Я также заметил, что эти диапазоны используются при отладке любого двоичного файла.

Кроме того, этот комментарий здесь предполагает, что ядро ​​действительно имеет некоторые предпочтения диапазона .

Это почему? Есть ли более глубокая техническая причина для вышеуказанных диапазонов? Будет ли проблема, если я вручную mmap страницы вне этих префиксов?


person Anastasios Andronidis    schedule 02.05.2020    source источник


Ответы (1)


Прежде всего, если предположить, что вы говорите о 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:

  1. Нормальные программы: для их работы требуется загрузчик. Это составляет в основном 99,9% программ ELF в нормальной системе Linux. Путь к загрузчику указывается в заголовках программы ELF с заголовком программы типа PT_INTERP.

  2. Загрузчики: загрузчик - это 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% однозначного ответа, но на ум приходит несколько причин:

  1. Последовательность: заставить само ядро ​​загружать ELF в память независимо от загрузчика, позволяет избежать проблем. Если бы это было не так, ядро ​​полностью зависело бы от загрузчика пользовательского пространства, что вообще не рекомендуется (это также может частично быть проблемой безопасности).

  2. Эффективность: мы уверены, что по крайней мере как исполняемый файл, так и его загрузчик должны быть сопоставлены (независимо от каких-либо связанных библиотек), может также сэкономить драгоценное время и сделать это сразу, а не ждать другого системного вызова. с соответствующим переключением контекста.

  3. Безопасность: в сценарии по умолчанию отображение программы по другому случайному адресу, отличному от адреса загрузчика и других библиотек, обеспечивает своего рода «изоляцию» между самой программой и загруженными библиотеками. Другими словами, «утечка» любого адреса библиотеки не покажет позицию программы в памяти, и наоборот. Отображение программы с предопределенным смещением от загрузчика и других библиотек вместо этого частично нарушило бы назначение ASLR.

    В идеальном сценарии, основанном на безопасности, каждая отдельная mmap (то есть любая необходимая библиотека) также должна быть размещена по случайному адресу независимо от предыдущих сопоставлений, но это значительно снизит производительность. Группирование выделений приводит к более быстрому поиску в таблице страниц: см. Понимание Ядро Linux (3-е издание), стр. 606: Таблица 15-3.. Наивысший индекс и максимальный размер файла для каждой высоты дерева системы счисления. Это также вызовет гораздо большую фрагментацию виртуальной памяти, что станет реальной проблемой для программ, которым необходимо отображать большие файлы в память. Существенная часть изоляции между программным кодом и кодом библиотеки уже выполнена, и дальнейшее развитие имеет больше минусов, чем плюсов.

  4. Легкость отладки: вид RIP=0x55... vs RIP=0x7f... мгновенно помогает понять, где искать (саму программу или код библиотеки).

person Marco Bonelli    schedule 02.05.2020
comment
Следует отметить, что 00007fffffffffff - это самая верхняя часть нижней половины канонического диапазона используемых 48-битных виртуальных адресов. x86-64 48-битные виртуальные адреса - вот почему Linux выбрал (1ULL<<47) - 1. канонический адрес x86-64? и канонический адрес арифметика формы и указателя имеет диаграмму. - person Peter Cordes; 03.05.2020
comment
Почему ядро ​​отображает исполняемый файл? Также может быть эффективность; системный вызов execve уже нашел исполняемый файл ELF в файловой системе и проанализировал его заголовки программы. Можно также создать для него сопоставления вместо того, чтобы заставлять ld.so найти его по имени пути, а затем проанализировать его. Также означает, что нет состояния гонки, при котором исполняемый файл отключается, как только execve возвращается, а затем ld.so не может его найти. Или любая проблема с разрешениями. - person Peter Cordes; 03.05.2020
comment
Также обратите внимание, что до того, как PIE стали реальностью, обычные исполняемые файлы были сопоставлены почти с нижней частью виртуального адресного пространства (весь статический код и данные в нижнем 2 ГБ, ld по умолчанию 401000 в качестве начала текстового раздела, то есть только 4 МБ выше нуля) . Это оставило почти все виртуальное адресное пространство открытым для потенциально огромного непрерывного распределения между статическим кодом / данными внизу и значениями по умолчанию стек + mmap вверху, на случай, если кто-то захочет это сделать. - person Peter Cordes; 03.05.2020
comment
Также обратите внимание, что хранение ваших выделений, сгруппированных рядом друг с другом (а не разреженными), хорошо для эффективности таблицы страниц. Это дерево с основанием системы счисления, поэтому хранение ваших выделений сгруппированными в блоки размером 1 ГБ означает, что они находятся в одном поддереве, оставляя больше частей полного дерева, которые не должны существовать (не присутствующие в каталоге страниц верхнего уровня). - person Peter Cordes; 03.05.2020
comment
@PeterCordes Спасибо за ссылки! Кроме того, да, делать ядро ​​зависимым от загрузчика для каждого execve в любом случае не имеет большого смысла. Я не думал о производительности, это тоже хороший момент, но держу пари, что основная причина не зависит от загрузчика пользовательского пространства. - person Marco Bonelli; 03.05.2020
comment
Меня смущает описание адресного пространства процесса: вы говорите, что программа находится над загрузчиком, то есть над стеком. Затем этот mmap будет отображать 0x7f вниз, потому что он начинает отображать выше загрузчика. Но 0x55 (программа) - это ‹0x7f (загрузчик / mmap). - person Margaret Bloom; 03.05.2020
comment
@MargaretBloom, когда я говорю программа отображается [...] где-то над загрузчиком, я имею в виду выше, как в более низком адресе. То же самое, когда я говорю последовательные системные вызовы mmap, которые не указывают адрес, будут возвращать уменьшающиеся адреса, начиная прямо над загрузчиком, я имею в виду, что mmap будет отображать с 0x7f... вверх (где вверх означает меньшие адреса, чем 0x7f...). Я предполагаю, что макет виртуальной машины похож на тот, который изображен в документации ядра, которую я связал, сверху = 0x0, снизу = 0xffffffffffffffff. Это не понятно? Я мог бы перефразировать это, если у вас есть предложение, как лучше сформулировать это. - person Marco Bonelli; 03.05.2020
comment
@MarcoBonelli Я не являюсь носителем английского языка, так что это может быть только я, но я думаю, что вы используете «вверх» для обозначения более низких адресов и «вниз» для обозначения более высоких адресов. Примерно так, как вы используете карту адресного пространства, где адреса с более высокими числовыми значениями отображаются ниже адресов с более низкими значениями (например, как адрес из gdb) и используются стрелки вверх и вниз для обозначения положения на карте , а не в адресном пространстве. . Например. кажется, что для вас 0x55 находится * выше 0x7f (потому что это будет, например, в том, что выводит gdb), но для меня порядок осуществляется между адресами (поэтому 0x55 ниже 0x7f) - person Margaret Bloom; 03.05.2020
comment
@MargaretBloom Я тоже не говорю по-английски. Я всегда использую выше и ниже, думая о позиции в памяти, как если бы она была упорядочена с более высокими адресами внизу. Я понимаю, что эти предложения могут означать две разные, противоположные вещи, если интерпретировать их по-разному. Я перефразирую ответ, чтобы не запутаться. - person Marco Bonelli; 03.05.2020