Адресное пространство процесса на примере программы на C.
Используемые утилиты и файлы:
Все примеры выполняются на ОС:
С ядром:
С использованием gcc
:
Содержание
Обзор
Стандартное распределение памяти процесса в Linux выглядит следующим образом, от меньшего адреса к более высокому (или “снизу вверх”):
- Text или Code segment: содержит исполняемые инструкции
- Data segment: различные переменные, в свою очередь делится на две части:
- Initialized data segment: инициализированные данные
- Uninitialized data segment: неициализированные данные – не содержит данных, а только указание на выделение памяти при запуске
- Heap: участок динамического выделения памяти
- Stack: участок памяти, содержащий временные данные
Ниже будут использоваться понятия секция и сегмент. Основное различие между ними, это то что секции используются ld
во время линковки (т.е. поиска необходимых функций, или символов), а сегмент – во время исполнения файла:
– symbols are like function names, and are used to answer “If I call
printf()
and it’s defined somewhere else, how do I find it?”
– symbols are organized into sections – code lives in one section (.text
), and data in another (.data
,.rodata
)
– sections are organized into segments
Больше деталей – тут>>> и тут>>>.
Text или Code сегмент
Часть исполняемого файла или соответсвующего участка виртуального адресного пространства процесса этой программы, которая содержит исполняемые инструкции этой программы.
Как правило является read only участком памяти и имеет фиксированный размер.
Начнём с простой программы, которая просто висит в памяти в бесконечном цикле, что бы иметь возможность поиграть с ней:
include <stdio.h> #include <unistd.h> int main() { while (1) { printf("\tAddress of main: %p\n", &main); printf("\tMy process ID : %d\n", getpid()); sleep(10); }; return 0; }
Собираем:
Проверим размер с помощью size
:
Запускаем, и проверяем данные файла maps
:
maps
:
Первая строка, с правами r
(read) и x
(executable), считаем размер блока между 560d08b2e000 и 560d08b2f000:
(для bc
hex значения указывайте заглавными)
4096 байт, 1 страница памяти:
Попробуем увеличить размер text
, что бы он занимал более одной страницы.
Для этого добавим static константу с именем somedata
– массив размером 8192 байт (которую компилятор разместит в секции .rodata
исполняемого файла, и size
в виде по умолчанию отобразит её в .text
секции, который как и .rodata
будет read-only, после чего обе эти read-only секции будут размещены в Code сегменте памяти запущенной программы):
... int main() { static const char somedata[8192] = "somedata"; while (1) { printf("\tAddress of main: %p\n", &main); ...
Проверяем результаты size
:
text
– 9949 байт.
Для более полной картины, что бы убедиться, что static всё-таки находится в .rodata
секции – добавляем -A
:
Запускаем:
Проверяем данные maps
:
Или, более наглядно – pmap
:
12kb, rx. 9949 text
в результатах size
– уже 3 страницы памяти под Code сегмент.
Просмотреть секции можно, например, с помощью objdump
:
И readelf
, который отобразит размеры:
Например, .rodata
, которая содержит добавленную выше статик константу имеет размер:
Ссылки по Code segment, секциям и сегментам:
https://en.wikipedia.org/wiki/Code_segment
https://jvns.ca/blog/2014/09/06/how-to-read-an-executable/
Initialized data segment
После Code сегмента следует Data сегмент, который делится на две части – Initialized и Uninitialized.
Initialized data segment содержит все глобальные, статичные и внешние (объявленные через extern
) переменные, которые проинициализированы до запуска выполнения программы.
Data сегмент является read-write сегментом, т.к. переменные в нём могут изменяться в процессе выполнения.
Проверить это можно добавив ещё одну переменную, например глобальную (вне функций), типа int
, с именем global_in_init
:
#include <stdio.h> #include <unistd.h> int global_in_init = 10; int main() { static const char somedata[8192] = "somedata"; while (1) { printf("\tAddress of main: %p\n", &main); printf("\tMy process ID : %d\n", getpid()); printf("\tThe global var's address is: %p\n", &global_in_init); sleep(10); }; return 0; }
Собираем и проверим результаты size
, что бы увидеть как изменился сам исполняемый файл:
Сравним с предыдущим результатом:
data
612 вместо 608.
Проверим размер int
на этой машине:
Или с помощью функции sizeof()
:
#include <inttypes.h> #include <stdio.h> int main() { printf( "Int size: %zd\n" , sizeof(int) ) ; return 0; }
Запускаем:
Вернёмся к Data сегменту и global_in_init
переменной.
Можно ещё раз использовать objdump
, что бы посмотреть сам исполняемый файл:
.data 0000000000000004 global_in_init
Запускаем программу:
The global var’s address is: 0x559a69dd0040
Проверяем данные maps
:
559a69dd0000-559a69dd1000 rw – наш initialized data сегмент.
Ссылки:
https://en.wikipedia.org/wiki/Data_segment#Data
Uninitialized Data Segment (bss)
Сегмент неициализированных данных содержит глобальные и static переменные, которым не задано явно значение в коде, инициализируются со значением 0.
Например – объявим переменную global_uninit
типа int
:
#include <stdio.h> #include <unistd.h> int global_in_init = 10; int global_uninit; int main() { static const char somedata[8192] = "somedata"; while (1) { printf("\tAddress of main: %p\n", &main); printf("\tMy process ID : %d\n", getpid()); printf("\tThe global var's address is: %p\n", &global_in_init); printf("\tThe uninit var's address is: %p\n", &global_uninit); sleep(10); }; return 0; }
Собираем, проверяем с помощью size
:
data
остался прежним – 612, а bss
вырос на 8 байт.
Почему bss
увеличился на 8, а не на 4 байта, как должно быть для int
типа? Проверим размер под переменную global_uninit
с помощью objdump
:
4 байта – всё верно, а ещё 4 байта выделяются под переменные подключаемых библиотек.
Проверить это можно исключив все подключаемые файлы. Создадим пустую программу:
#include <stdio.h> int main() { return 0; }
Собираем и проверяем size
:
bss
8 байт.
Добавим опцию -nostartfiles
(Do not use the standard system startup files when linking) для gcc
:
bss == 0.
Вернёмся к программе, запускаем:
The uninit var’s address is: 0x563d35f72048, проверяем maps
:
0x563d35f72048 находится в блоке 563d35f72000-563d35f73000 rw – нашем Data сегменте.
Ещё один вариант получить границы Data сегмента – использовать edata
и end
символы:
edata This is the first address past the end of the initialized data
segment.end This is the first address past the end of the uninitialized
data segment (also known as the BSS segment).
http://man7.org/linux/man-pages/man3/end.3.html
Попробуем:
#include <stdio.h> #include <unistd.h> int global_in_init = 10; int global_uninit; extern char edata, end; int main() { static const char somedata[8192] = "somedata"; while (1) { printf("\tAddress of main: %p\n", &main); printf("\tMy process ID : %d\n", getpid()); printf("\tThe global var's address is: %p\n", &global_in_init); printf("\tThe uninit var's address is: %p\n", &global_uninit); printf("\tInitialized data (edata) %10p\n", &edata); printf("\tUninitialized data (end) %10p\n", &end); sleep(10); }; return 0; }
Собираем и запускаем:
Initialized data (edata) 0x55fbe502c044
Uninitialized data (end) 0x55fbe502c050
И maps
:
pmap
:
Ссылки по bss
:
https://stackoverflow.com/questions/28962014/why-bss-segment-is-16-by-default
https://stackoverflow.com/questions/1765969/where-are-the-symbols-etext-edata-and-end-defined
https://en.wikipedia.org/wiki/.bss
http://man7.org/linux/man-pages/man3/end.3.html
Stack
Хотя следующим по адресации следует Heap – сначала рассмотрим Stack, т.к. для heap
потребуются указатели (pointers), которые располагаются в стеке програмы.
Stack (“куча“) – участок памяти, который хранит локальные переменные функций, в т.ч. main()
. Является LIFO (last in – first out) структурой: когда функция добавляет в него переменную – она размещается “наверху” стека, а по завершению выполнения функции – все её переменные удаляются из стека.
Лимит размера стека на процесс определяется ОС:
И сам размер:
Вернёмся к программе и добавим переменную в main()
:
... int main() { static const char somedata[8192] = "somedata"; int local_int_in_stack = 10; while (1) { printf("\tAddress of main: %p\n", &main); printf("\tMy process ID : %d\n", getpid()); printf("\tThe global var's address is: %p\n", &global_in_init); printf("\tThe uninit var's address is: %p\n", &global_uninit); printf("\tInitialized data (edata) %10p\n", &edata); printf("\tUninitialized data (end) %10p\n", &end); printf("\tLocal int from main() in stack address: %p\n", &local_int_in_stack); sleep(10); }; ...
Собираем и запускаем:
Проверяем maps
:
Что бы продемонстрировать работу LIFO – добавим ещё одну переменную:
... int main() { static const char somedata[8192] = "somedata"; int local_int_in_stack = 10; int local_int_in_stack_on_top = 10; while (1) { printf("\tAddress of main: %p\n", &main); printf("\tMy process ID : %d\n", getpid()); printf("\tThe global var's address is: %p\n", &global_in_init); printf("\tThe uninit var's address is: %p\n", &global_uninit); printf("\tInitialized data (edata) %10p\n", &edata); printf("\tUninitialized data (end) %10p\n", &end); printf("\tLocal int from main() in stack address: %p\n", &local_int_in_stack); printf("\tLocal int from main() in stack on top address: %p\n", &local_int_in_stack_on_top); sleep(10); ...
Запускаем:
Local int from main() in stack address: 0x7fff0d203e90
Local int from main() in stack on top address: 0x7fff0d203e94 – на 4 байта “выше”, чем переменная local_int_in_stack
.
Как говорилось – указатели так же помещаются в стеке.
Добавим указатель с именем pointer
, который указыавет на переменную local_int_in_stack
:
... int local_int_in_stack = 10; int local_int_in_stack_on_top = 10; int * pointer = &local_int_in_stack; while (1) { printf("\tAddress of main: %p\n", &main); printf("\tMy process ID : %d\n", getpid()); printf("\tThe global var's address is: %p\n", &global_in_init); printf("\tThe uninit var's address is: %p\n", &global_uninit); printf("\tInitialized data (edata) %10p\n", &edata); printf("\tUninitialized data (end) %10p\n", &end); printf("\tLocal int from main() in stack address: %p\n", &local_int_in_stack); printf("\tLocal int from main() in stack on top address: %p\n", &local_int_in_stack_on_top); printf("\tPointer located in stack: %p\n", &pointer); sleep(10); }; ...
Запускаем:
Проверяем:
Ссылки по Stack:
http://gribblelab.org/CBootCamp/7_Memory_Stack_vs_Heap.html#orga366562
Heap
И, наконец-то – Heap.
Сегмент памяти используемый под динамически выделяемые участки. Он начинается от BSS сегмента и растёт вверх, и используется такими функциями как malloc()
, calloc()
и realloc()
, а для удаления данных из него – используется free()
.
Их описание можно найти тут>>>.
Рассмотрим пример с malloc()
. Добавим #include <stdlib.h>
, и обратите внимание на вывод:
printf("\tPointer content located in heap: %p\n", pointer_to_heap);
Нам требуется значение указателя, а не его адрес.
Обновим код:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> int global_in_init = 10; int global_uninit; extern char edata, end; int main() { static const char somedata[8192] = "somedata"; int local_int_in_stack = 10; int local_int_in_stack_on_top = 10; int * pointer = &local_int_in_stack; int * pointer_to_heap = malloc(sizeof(int)); while (1) { printf("\tAddress of main: %p\n", &main); printf("\tMy process ID : %d\n", getpid()); printf("\tThe global var's address is: %p\n", &global_in_init); printf("\tThe uninit var's address is: %p\n", &global_uninit); printf("\tInitialized data (edata) %10p\n", &edata); printf("\tUninitialized data (end) %10p\n", &end); printf("\tLocal int from main() in stack address: %p\n", &local_int_in_stack); printf("\tLocal int from main() in stack on top address: %p\n", &local_int_in_stack_on_top); printf("\tPointer located in stack: %p\n", &pointer); printf("\tPointer to heap content: %p\n", pointer_to_heap); sleep(10); }; return 0; }
Запускаем:
Проверяем maps
:
В целом – на этом всё.
Ссылки по теме
Память
Memory Layout of C Programs (с этого поста всё началось)
Why are constants stored in the text segment in a C memory map?
Why BSS segment is “16” by default?
Memory – Part 1: Memory Types (где-то у меня даже перевод этой части был)
Memory – Part 2: Understanding Process memory
In-Memory Layout of a Program (Process)
Pagemap Interface of Linux Explained (немного про трансляцию виртуальных адресов в физические)
Different Segments of a C Program’s Address Space
Видео
C Programming Tutorial 1 : Memory Layout of a C / C++ Program : Think Aloud Academy
Pointers and dynamic memory – stack vs heap
Исполняемые файлы
How is a binary executable organized?