Linux: C – адресное пространство процесса

Автор: | 17/09/2017
 

Адресное пространство процесса на примере программы на 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://reverseengineering.stackexchange.com/questions/4230/why-i-can-not-directly-get-the-content-of-bss-section

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)

maps, smaps and Memory Stats!

Pagemap Interface of Linux Explained (немного про трансляцию виртуальных адресов в физические)

Memory : Stack vs Heap

Different Segments of a C Program’s Address Space

C Dynamic Memory Allocation

Видео

Memory Layout

C Programming Tutorial 1 : Memory Layout of a C / C++ Program : Think Aloud Academy

Pointers and dynamic memory – stack vs heap

Исполняемые файлы

ELF Object File Format

How is a binary executable organized?

Compiling and Linking