Адресное пространство процесса на примере программы на C.
Используемые утилиты и файлы:
Все примеры выполняются на ОС:
[simterm]
$ cat /etc/os-release | grep NAME NAME="Arch Linux" PRETTY_NAME="Arch Linux"
[/simterm]
С ядром:
[simterm]
$ uname -mrs Linux 4.12.8-2-ARCH x86_64
[/simterm]
С использованием gcc
:
[simterm]
$ gcc --version gcc (GCC) 7.2.0
[/simterm]
Содержание
Обзор
Стандартное распределение памяти процесса в 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; }
Собираем:
[simterm]
$ gcc mem_lay.c -o mem_lay
[/simterm]
Проверим размер с помощью size
:
[simterm]
$ size mem_lay text data bss dec hex filename 1708 608 8 2324 914 mem_lay
[/simterm]
Запускаем, и проверяем данные файла maps
:
[simterm]
$ ./mem_lay Address of main: 0x560d08b2e6da My process ID : 19752
[/simterm]
maps
:
[simterm]
$ cat /proc/19752/maps | head -n 3 560d08b2e000-560d08b2f000 r-xp 00000000 fe:01 16384016 /home/setevoy/Scripts/C/MEM_LAY/mem_lay 560d08d2e000-560d08d2f000 r--p 00000000 fe:01 16384016 /home/setevoy/Scripts/C/MEM_LAY/mem_lay 560d08d2f000-560d08d30000 rw-p 00001000 fe:01 16384016 /home/setevoy/Scripts/C/MEM_LAY/mem_lay
[/simterm]
Первая строка, с правами r
(read) и x
(executable), считаем размер блока между 560d08b2e000 и 560d08b2f000:
[simterm]
$ echo 'ibase=16;560D08B2E000-560D08B2F000' | bc -4096
[/simterm]
(для bc
hex значения указывайте заглавными)
4096 байт, 1 страница памяти:
[simterm]
$ getconf PAGESIZE 4096
[/simterm]
Попробуем увеличить размер 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
:
[simterm]
$ size mem_lay text data bss dec hex filename 9949 608 8 10565 2945 mem_lay
[/simterm]
text
– 9949 байт.
Для более полной картины, что бы убедиться, что static всё-таки находится в .rodata
секции – добавляем -A
:
[simterm]
$ size -A mem_lay | grep 'text\|rodata' mem_lay : .text 450 1488 .rodata 8288 1952
[/simterm]
Запускаем:
[simterm]
$ ./mem_lay Address of main: 0x5648aaec06da My process ID : 29839
[/simterm]
Проверяем данные maps
:
[simterm]
$ cat /proc/29839/maps | head -n 1 5648aaec0000-5648aaec3000 r-xp 00000000 fe:01 16384016 /home/setevoy/Scripts/C/MEM_LAY/mem_lay
[/simterm]
Или, более наглядно – pmap
:
[simterm]
$ pmap 29839 | head -n 2 29839: ./mem_lay 00005648aaec0000 12K r-x-- mem_lay
[/simterm]
12kb, rx. 9949 text
в результатах size
– уже 3 страницы памяти под Code сегмент.
Просмотреть секции можно, например, с помощью objdump
:
[simterm]
$ objdump -s mem_lay mem_lay: file format elf64-x86-64 ... Contents of section .text: 05d0 31ed4989 d15e4889 e24883e4 f050544c 1.I..^H..H...PTL 05e0 8d05aa01 0000488d 0d330100 00488d3d ......H..3...H.= ... 0770 014839dd 75ea4883 c4085b5d 415c415d .H9.u.H...[]A\A] 0780 415e415f c390662e 0f1f8400 00000000 A^A_..f......... 0790 f3c3 .. ... Contents of section .rodata: 07a0 01000200 09416464 72657373 206f6620 .....Address of 07b0 6d61696e 3a202570 0a00094d 79207072 main: %p...My pr 07c0 6f636573 73204944 203a2025 640a00 ocess ID : %d..
[/simterm]
И readelf
, который отобразит размеры:
[simterm]
$ readelf --sections mem_lay | grep -A 1 'text\|rodata' [13] .text PROGBITS 00000000000005d0 000005d0 00000000000001c2 0000000000000000 AX 0 0 16 -- [15] .rodata PROGBITS 00000000000007a0 000007a0 0000000000002060 0000000000000000 A 0 0 4
[/simterm]
Например, .rodata
, которая содержит добавленную выше статик константу имеет размер:
[simterm]
$ echo "ibase=16; 0000000000002060"|bc 8288
[/simterm]
Ссылки по 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
, что бы увидеть как изменился сам исполняемый файл:
[simterm]
$ gcc mem_lay.c -o mem_lay $ size mem_lay text data bss dec hex filename 10013 612 4 10629 2985 mem_lay
[/simterm]
Сравним с предыдущим результатом:
[simterm]
$ size mem_lay text data bss dec hex filename 9949 608 8 10565 2945 mem_lay
[/simterm]
data
612 вместо 608.
Проверим размер int
на этой машине:
[simterm]
$ cpp -dD /dev/null | grep __SIZEOF_INT #define __SIZEOF_INT__ 4
[/simterm]
Или с помощью функции sizeof()
:
#include <inttypes.h> #include <stdio.h> int main() { printf( "Int size: %zd\n" , sizeof(int) ) ; return 0; }
Запускаем:
[simterm]
$ gcc getintsize.c -o getintsize $ ./getintsize Int size: 4
[/simterm]
Вернёмся к Data сегменту и global_in_init
переменной.
Можно ещё раз использовать objdump
, что бы посмотреть сам исполняемый файл:
[simterm]
$ objdump -x mem_lay | grep -w global_in_init 0000000000203040 g O .data 0000000000000004 global_in_init
[/simterm]
.data 0000000000000004 global_in_init
Запускаем программу:
[simterm]
$ ./mem_lay Address of main: 0x559a69bcd6da My process ID : 8830 The global var's address is: 0x559a69dd0040
[/simterm]
The global var’s address is: 0x559a69dd0040
Проверяем данные maps
:
[simterm]
$ cat /proc/8830/maps | grep 559a69dd 559a69dcf000-559a69dd0000 r--p 00002000 fe:01 16384017 /home/setevoy/Scripts/C/MEM_LAY/mem_lay 559a69dd0000-559a69dd1000 rw-p 00003000 fe:01 16384017 /home/setevoy/Scripts/C/MEM_LAY/mem_lay
[/simterm]
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
:
[simterm]
$ gcc mem_lay.c -o mem_lay $ size mem_lay text data bss dec hex filename 10061 612 12 10685 29bd mem_lay
[/simterm]
data
остался прежним – 612, а bss
вырос на 8 байт.
Почему bss
увеличился на 8, а не на 4 байта, как должно быть для int
типа? Проверим размер под переменную global_uninit
с помощью objdump
:
[simterm]
$ objdump -t mem_lay | grep -w global_uninit 0000000000203048 g O .bss 0000000000000004 global_uninit
[/simterm]
4 байта – всё верно, а ещё 4 байта выделяются под переменные подключаемых библиотек.
Проверить это можно исключив все подключаемые файлы. Создадим пустую программу:
#include <stdio.h> int main() { return 0; }
Собираем и проверяем size
:
[simterm]
$ gcc do_nothing.c -o do_nothing $ size do_nothing text data bss dec hex filename 1335 520 8 1863 747 do_nothing
[/simterm]
bss
8 байт.
Добавим опцию -nostartfiles
(Do not use the standard system startup files when linking) для gcc
:
[simterm]
$ gcc -nostartfiles do_nothing.c -o do_nothing /usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 00000000000002bb $ size do_nothing text data bss dec hex filename 214 224 0 438 1b6 do_nothing
[/simterm]
bss == 0.
Вернёмся к программе, запускаем:
[simterm]
$ ./mem_lay Address of main: 0x563d35d6f6da My process ID : 32226 The global var's address is: 0x563d35f72040 The uninit var's address is: 0x563d35f72048
[/simterm]
The uninit var’s address is: 0x563d35f72048, проверяем maps
:
[simterm]
$ cat /proc/32226/maps | grep 563d35f7 563d35f71000-563d35f72000 r--p 00002000 fe:01 16384017 /home/setevoy/Scripts/C/MEM_LAY/mem_lay 563d35f72000-563d35f73000 rw-p 00003000 fe:01 16384017 /home/setevoy/Scripts/C/MEM_LAY/mem_lay
[/simterm]
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; }
Собираем и запускаем:
[simterm]
$ gcc mem_lay.c -o mem_lay $ ./mem_lay Address of main: 0x55fbe4e296da My process ID : 5844 The global var's address is: 0x55fbe502c040 The uninit var's address is: 0x55fbe502c048 Initialized data (edata) 0x55fbe502c044 Uninitialized data (end) 0x55fbe502c050
[/simterm]
Initialized data (edata) 0x55fbe502c044
Uninitialized data (end) 0x55fbe502c050
И maps
:
[simterm]
$ cat /proc/5844/maps | grep 55fbe502c0 55fbe502b000-55fbe502c000 r--p 00002000 fe:01 16384017 /home/setevoy/Scripts/C/MEM_LAY/mem_lay 55fbe502c000-55fbe502d000 rw-p 00003000 fe:01 16384017 /home/setevoy/Scripts/C/MEM_LAY/mem_lay
[/simterm]
pmap
:
[simterm]
$ pmap 5844 | grep 55fbe502c0 000055fbe502c000 4K rw--- mem_lay
[/simterm]
Ссылки по 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) структурой: когда функция добавляет в него переменную – она размещается “наверху” стека, а по завершению выполнения функции – все её переменные удаляются из стека.
Лимит размера стека на процесс определяется ОС:
[simterm]
$ ulimit --help | grep -w "\-s" -s the maximum stack size
[/simterm]
И сам размер:
[simterm]
$ ulimit -s 8192
[/simterm]
Вернёмся к программе и добавим переменную в 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); }; ...
Собираем и запускаем:
[simterm]
$ gcc mem_lay.c -o mem_lay $ ./mem_lay Address of main: 0x5565772666da My process ID : 30614 The global var's address is: 0x556577469040 The uninit var's address is: 0x556577469048 Initialized data (edata) 0x556577469044 Uninitialized data (end) 0x556577469050 Local int from main() in stack address: 0x7ffcdcd4b934
[/simterm]
Проверяем maps
:
[simterm]
$ cat /proc/30614/maps | grep 7ffcdcd4 7ffcdcd2d000-7ffcdcd4e000 rw-p 00000000 00:00 0 [stack]
[/simterm]
Что бы продемонстрировать работу 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); ...
Запускаем:
[simterm]
$ ./mem_lay Address of main: 0x560dda8506da My process ID : 2055 The global var's address is: 0x560ddaa53040 The uninit var's address is: 0x560ddaa53048 Initialized data (edata) 0x560ddaa53044 Uninitialized data (end) 0x560ddaa53050 Local int from main() in stack address: 0x7fff0d203e90 Local int from main() in stack on top address: 0x7fff0d203e94
[/simterm]
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); }; ...
Запускаем:
[simterm]
$ ./mem_lay Address of main: 0x558fab4656da My process ID : 26841 The global var's address is: 0x558fab668040 The uninit var's address is: 0x558fab668048 Initialized data (edata) 0x558fab668044 Uninitialized data (end) 0x558fab668050 Local int from main() in stack address: 0x7ffcb175d728 Local int from main() in stack on top address: 0x7ffcb175d72c Pointer located in stack: 0x7ffcb175d730
[/simterm]
Проверяем:
[simterm]
$ cat /proc/26841/maps | grep 7ffcb175 7ffcb173d000-7ffcb175e000 rw-p 00000000 00:00 0 [stack]
[/simterm]
Ссылки по 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; }
Запускаем:
[simterm]
$ ./mem_lay Address of main: 0x560c6caba72a My process ID : 2536 The global var's address is: 0x560c6ccbd048 The uninit var's address is: 0x560c6ccbd050 Initialized data (edata) 0x560c6ccbd04c Uninitialized data (end) 0x560c6ccbd058 Local int from main() in stack address: 0x7ffc4add1370 Local int from main() in stack on top address: 0x7ffc4add1374 Pointer located in stack: 0x7ffc4add1378 Pointer to heap content: 0x560c6d6d8260
[/simterm]
Проверяем maps
:
[simterm]
$ cat /proc/2536/maps | grep 560c6d6d8 560c6d6d8000-560c6d6f9000 rw-p 00000000 00:00 0 [heap]
[/simterm]
В целом – на этом всё.
Ссылки по теме
Память
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?