Структура виртуальной памяти процесса в Linux
Как распределены и по каким регистрам процессора указатели, задающие границы области стека, области динамических данных, области статических данных и области команд модуля программы?
Стоит различать модель памяти языка и организацию памяти непосредственно на целевой машине. Вопрос из заголовка и вопрос в тексте — разные.
1 ответ 1
Вопрос почти не связан с языком C или компилятором GCC, а связан больше с операционной системой, в которой выполняется программа. Конкретно, вас интересует карта виртуальной памяти процесса — именно она определяет, где по каким адресам расположен стек, куча и сегменты исполняемого файла (те самые .bss , .text , .data и прч.). Ниже приведен ответ для ОС с ядром Linux и архитектурой семейства x86.
Ядро Linux делит всё виртуальное адресное пространства процесса на две части: user-space memory и kernel memory. Конкретное деление различно, есть как минимум три варианта:
- В архитектуре i386 обычно все виртуальное адресное пространство имеет размер 4 GiB и ядро выделяет нижние 3 GiB для user-space и верхний 1 GiB для самого ядра * .
- В архитектуре x86_64 с 4-х уровневыми page tables виртуальное адресное пространство является 48-битным. User-space memory занимает 128 TB, начиная с адреса 0x0000000000000000 и заканчивая адресом 0x00007fffffffffff . Kernel memory также занимает 128 TB, начиная с адреса 0xffff800000000000 и заканчивая адресом 0xffffffffffffffff † .
- В архитектуре x86_64 с 5-ти уровневыми page tables виртуальное адресное пространство является 56-битным, а схема его разбиения похожа на (2) † .
Память ядра является одинаковой и разделяемой для всех программ, поэтому нас будет интересовать user-space memory.
Как ни странно, но найти конкретную информацию по разбиению адресного пространства процесса в Linux не так просто. Мне удалось найти достаточно подробную статью “Understanding the Memory Layout of Linux Executables”, где после довольно долго расследования выясняется примерно следующее распределение памяти процесса:
0 Nothing here, because it was just an arbitrary choice by the linker ELF and Program and Section Headers - 0x400000 on 64 bit Program Text (.text) - Entry Point as Reported by readelf Nothing Here either Some unknown assembly and data - 0x600000 Initialised Data (.data) - 0x601068 Uninitialised Data (.bss) - 0x601078 Heap | v Memory Mapped Region for Shared Libraries or Anything Else ^ | User Stack
Таким образом видим, что стек (англ. stack) располагается в самом конце адресного пространства и расет «вниз» ‡ , то есть по направлению к младшим адресам (к нулю). В свою очередь, куча (англ. heap) растет «вверх» и располагается сразу после секции .bss .
По-умолчанию размер стека равен 8 MiB. Изначально под стек выделяется его первая страница памяти 4 KiB. Если пользовательский код выходит за пределы этих 4 KiB, то происходит Page Fault, которое ловит ядро. Затем ядро проверяет, не вышел ли стек за границу в 8 MiB. Если не вышел — выделяет новую странцу для стека (стек растет), если вышел — убивает процесс.
Размер стека можно менять из пространства пользователя с помощью программы ulimit .
Также стоит отметить, что конкретные адреса кучи и стека всегда будут разными при каждом запуске из-за ASLR (Address Space Layer Randomization). Функции для их рандомизации находятся в файле linux/mm/util.c . Конкретно это функции randomize_stack_top и arch_randomize_brk .
Практический способ выяснения структуры памяти процесса
Другим способом выяснить разметку памяти процесса в Linux будет использование файла /proc//maps , который собственно и содержит информацию об адресном пространстве процесса. Если написать простейший hello world, можно увидеть примерно следующее:
$ cat /proc/$(pidof hello)/maps 558c1ca6f000-558c1ca70000 r--p 00000000 00:20 638 /tmp/hello 558c1ca70000-558c1ca71000 r-xp 00001000 00:20 638 /tmp/hello 558c1ca71000-558c1ca72000 r--p 00002000 00:20 638 /tmp/hello 558c1ca72000-558c1ca73000 r--p 00002000 00:20 638 /tmp/hello 558c1ca73000-558c1ca74000 rw-p 00003000 00:20 638 /tmp/hello 558c1e82c000-558c1e84d000 rw-p 00000000 00:00 0 [heap] 7ff9a4a01000-7ff9a4a03000 rw-p 00000000 00:00 0 7ff9a4a03000-7ff9a4a29000 r--p 00000000 103:04 789879 /usr/lib/libc-2.33.so 7ff9a4a29000-7ff9a4b74000 r-xp 00026000 103:04 789879 /usr/lib/libc-2.33.so 7ff9a4b74000-7ff9a4bc0000 r--p 00171000 103:04 789879 /usr/lib/libc-2.33.so 7ff9a4bc0000-7ff9a4bc3000 r--p 001bc000 103:04 789879 /usr/lib/libc-2.33.so 7ff9a4bc3000-7ff9a4bc6000 rw-p 001bf000 103:04 789879 /usr/lib/libc-2.33.so 7ff9a4bc6000-7ff9a4bd1000 rw-p 00000000 00:00 0 7ff9a4be3000-7ff9a4be4000 r--p 00000000 103:04 789868 /usr/lib/ld-2.33.so 7ff9a4be4000-7ff9a4c08000 r-xp 00001000 103:04 789868 /usr/lib/ld-2.33.so 7ff9a4c08000-7ff9a4c11000 r--p 00025000 103:04 789868 /usr/lib/ld-2.33.so 7ff9a4c11000-7ff9a4c13000 r--p 0002d000 103:04 789868 /usr/lib/ld-2.33.so 7ff9a4c13000-7ff9a4c15000 rw-p 0002f000 103:04 789868 /usr/lib/ld-2.33.so 7ffecbeaf000-7ffecbed0000 rw-p 00000000 00:00 0 [stack] 7ffecbf31000-7ffecbf35000 r--p 00000000 00:00 0 [vvar] 7ffecbf35000-7ffecbf37000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
Здесь видно, что первые пять строк файла означают секции исполняемого файла /tmp/hello . Конкретные названия не указаны, но о значении некоторых секций можно догадаться по выставленным разрешениям (англ. permissions).
Так же можно использовать программу-фронтенд pmap :
$ pmap $(pidof hello) 35111: ./hello 000055eef0906000 4K r---- hello 000055eef0907000 4K r-x-- hello 000055eef0908000 4K r---- hello 000055eef0909000 4K r---- hello 000055eef090a000 4K rw--- hello 000055eef116b000 132K rw--- [ anon ] 00007f2b7a11f000 8K rw--- [ anon ] 00007f2b7a121000 152K r---- libc-2.33.so 00007f2b7a147000 1324K r-x-- libc-2.33.so 00007f2b7a292000 304K r---- libc-2.33.so 00007f2b7a2de000 12K r---- libc-2.33.so 00007f2b7a2e1000 12K rw--- libc-2.33.so 00007f2b7a2e4000 44K rw--- [ anon ] 00007f2b7a301000 4K r---- ld-2.33.so 00007f2b7a302000 144K r-x-- ld-2.33.so 00007f2b7a326000 36K r---- ld-2.33.so 00007f2b7a32f000 8K r---- ld-2.33.so 00007f2b7a331000 8K rw--- ld-2.33.so 00007ffd6e1a9000 132K rw--- [ stack ] 00007ffd6e1cb000 16K r---- [ anon ] 00007ffd6e1cf000 8K r-x-- [ anon ] ffffffffff600000 4K --x-- [ anon ] total 2368K
* В комментарии под одним из вопросов на Unix SE пользователь с ником @phuclv утверждает, что это не совсем верная информация. Согласно комментарию, «в зависимости от версии ядра отношение разбиения может отличаться. Старые версии могут использовать разбиения 1/3, 2/2 или 3/1, что указывается опциями CONFIG_VMSPLIT_ ; а с 2007 года можно выбрать дробные разбиения типа 5/16-тых и 15/32-ых. Если в ручную поменять некоторые #define -ы, можно добиться произвольных разбиений. Сегодня системы, подверженные уязвимости Meltdown обычно используют разбиение 4/4, то есть полностью отдельные адресные пространства для ядра и пользовательского режима».
† Детальное описание карты виртуальной памяти для x86_64 можно найти в документации к ядру.
‡ Направление роста стека на самом деле платформо-зависимо. См. соответствующий вопрос на enSO.
Русские Блоги
Linux делит адресное пространство процесса на две части: область ядра и область пользователя.
- Код и данные ядра операционной системы отображаются в области ядра.
- Исполняемый образ (код и данные) процесса отображается в пользовательскую область виртуальной памяти.
Примечание. Это адресное пространство является логическим адресным пространством, определяемым принципами операционной системы. Зона пользователя Максимальное пространство Это 0 для 3G, а область ядра — это пространство над 3G.
В режиме ядра и в пользовательском режиме память распределяется по-разному.
- Состояние ядра может напрямую получать динамическую память.
- Когда пользовательский режим запрашивает динамическую память, он не сразу получает фактический физический кадр страницы (также называемый физическим блоком), а только получает право использовать новый линейный диапазон адресов. Этот линейный диапазон адресов станет частью адресного пространства процесса, называемого Линейная область , Каждая линейная область состоит из начального линейного адреса, конечного адреса и некоторых описаний полномочий доступа. Адресное пространство процесса состоит из всех линейных адресов, к которым процесс может получить доступ.
На следующем рисунке показано описание пространства пользовательского процесса.
В дескрипторе процесса task_struct есть поле mm, которое является указателем на структуру дескриптора памяти mm_struct. Линейная область — это область виртуальной памяти, описываемая структурой vm_area_struct. Начало и конец линейной области должны быть выровнены с 4 КБ. Процесс может получить доступ только к допустимой линейной области. Если процесс пытается получить доступ к адресу за пределами допустимой области или использует неправильный адрес Чтобы получить доступ к допустимой области, ядро завершит процесс из-за ошибки сегментации. Поле mmap дескриптора памяти используется для поиска линейной области, а поле mmap указывает на первый дескриптор линейной области в связанном списке. mmap_cache используется для кэширования последней использованной линейной области.
Дескриптор памяти mm_struct
- mm_users: количество легких процессов, совместно использующих mm_struct
- mm_count: если дескриптор ядра временно передан потоку ядра, mm_count увеличивается. Основной счетчик использования дескриптора памяти, mm_users, используется как единица измерения в mm_count для всех пользователей, использующих счетчик. Всякий раз, когда mm_users уменьшается, ядро должно проверить, становится ли оно равным 0, и если да, оно освобождает дескриптор памяти.
Процесс получения новой линейной области
- Новый процесс только что создан
- использовать exec Системный вызов загружает новую программу для запуска
- Сопоставьте файл (или часть) с адресным пространством процесса
- Когда пользовательского стека недостаточно, расширьте линейную область, соответствующую стеку
Связанный список и красно-черная древовидная структура линейной области
- Связанный список линейных областей, на который указывает mmap, используется для обхода адресного пространства всего процесса.
- Красно-черное дерево mm_rb используется для определения того, в какую линейную область в адресном пространстве процесса попадает данный линейный адрес.
Неправильная страница отсутствует
Ядро просто выделяет процессу некоторое линейное адресное пространство с помощью mmap () и других вызовов и не выделяет процессу фактический физический страничный фрейм. Когда процесс пытается получить доступ к выделенному ему адресному пространству, физического соответствия страничного фрейма нет. Для этих линейных адресов будет инициировано исключение ошибки страницы.
Линейный адрес, к которому обращается процесс, не находится в линейной области пользовательского пространства. В этом случае необходимо определить, инициировано ли исключение ошибки страницы из-за того, что пространство стека пользовательского процесса исчерпано. Если это так, область стека расширяется в пользовательском пространстве, и Выделите соответствующую физическую страницу, если это не так, это будет рассматриваться как доступ по недопустимому адресу, и ядро завершит процесс.
Примечание: адрес несмежной памяти на рисунке относится к адресному пространству выше 3G; стек не зафиксирован из-за операции стека; доступ к пространству ядра нет В случае доступа к пользовательскому пространству, если есть прерывание, программный терминал, критическая секция и т. Д., Он может только указать на ошибку адреса и убить процесс. Чтобы
Копирование при записи
Копирование всего адресного пространства родительского процесса дочерним процессом занимает очень много времени, и часто это бессмысленно. Родительский процесс и дочерний процесс совместно используют фрейм страницы вместо копирования фрейма страницы. Когда родительский процесс и дочерний процесс пытаются записать совместно используемый страничный фрейм, генерируется исключение.В это время ядро копирует страницу в новый страничный фрейм и отмечает его как доступный для записи.