Виртуальные и реальные адреса

Каждый процесс в многозадачной ОС выполняется в собственной области памяти. Эта область представляет собой виртуальное адресное пространство, которое в 32-битном защищенном режиме всегда имеет размер, равный 4 гигабайтам. Соответствие между виртуальным пространством и физической памятью описывается с помощью таблицы страниц. Ядро создает и заполняет таблицы, а процессор обращается к ним когда нужно выполнить "перевод" адреса. Каждый процесс работает со своим набором таблиц.

Концепция виртуальной адресации распространяется на все выполняемые программы, включая и само ядро. По этой причине для ядра резервируется часть виртуального адресного пространства (т.н. kernel space). При попытке обращения к этим страницам из кода в пользовательском режиме (user mode) кода генерируется page fault. В Linux kernel space всегда присутствует в памяти процесса, и разные процессы отображают kernel space в одну и ту же область физической памяти. Таким образом, код и данные ядра всегда доступны, если нужно обработать прерывание или системный вызов.

Память запущенной программы (процесса) разделена на ряд непрерывных блоков (сегментов). Кроме Kernel Space существуют следующие сегменты:

  1. Код (Text)
  2. Инициализированные данные (Data)
  3. Неинициализированные данные (BSS)
  4. Cтек (Stack)
  5. Куча (Heap)

Схема распределения памяти процесса

Сегмент кода

В нем находится исполняемый код программы. Содержимое сегмента доступно для чтения/исполнения. Благодаря тому, что этот сегмент защищен от записи, программа не может испортить свой собственный код во время выполнения.

Сегмент инициализированных данных

Содержит глобальные переменные, которые инициализированы программистом. Размер этого сегмента зависит от объема помещенных в него данных, определяется на этапе компиляции программы и не изменяется во время ее выполнения.

Сегмент неинициализированных данных

Или сегмент BSS (сокращение от Block Started by Symbol; название историческое и мы не будем углубляться в причины его появления). Начинается сразу после окончания сегмента инициализированных данных. Содержит глобальные и статические переменные, которые не были явным образом инициализированы в исходном коде, и которые будут при запуске программы инициализированы нулями.

Данные в сегменты кода и инициализированных данных загружаются прямо из исполняемого файла (то есть эти области памяти являются копией исполняемого файла). Хранить же нули, которыми заполняется BSS, в исполняемом файле не имеет смысла. Поэтому в файле хранится только количество переменных и их тип. При загрузке программы в память, в последней будет зарезервирована область под требуемое количество нулей.

Стек

Область памяти, используемая для хранения локальных переменных и аргументов, переданных в функцию. Вызов функции или метода приводит к помещению в стек т.н. кадра стека. Когда функция возвращает управление, кадр стека уничтожается. Данные в стеке обрабатываются в соответствии с принципом «последним пришел — первым обслужен» (LIFO). Поэтому, для отслеживания содержимого стека достаточно знать лишь положение указателя на вершину стека. Добавление данных в стек и их удаление – операция быстрая. Кроме того, многократное использование одних и тех же областей стека приводит к тому, что они помещаются в кеш процессора, что еще более ускоряет доступ к ним.

Куча

Подобно стеку, куча используется для выделения памяти во время выполнения программы. Но в отличие от стека, память, выделенная в куче, сохраняется и после того, как функция, вызвавшая выделение этой памяти, завершит работу. Язык С предоставляет программисту целый ряд средств управления памятью в куче. Например, функция malloc() выделяет память, а free() освобождает ее.

Если текущий размер кучи позволяет выделить запрошенный объем памяти, то выделение может быть осуществлено средствами одной лишь среды выполнения, без привлечения ядра. В противном случае, функция malloc() задействует системный вызов brk() для необходимого увеличения размера кучи. Управление памятью в куче – непростая задача, для решения которой используются сложные алгоритмы. Куча также подвержена фрагментированию.

Куча и стек "растут" по направлению друг к другу.

Параметры командной строки и переменные окружения

Хранятся в самых верхних адресах доступной памяти процесса.

Пример

Выведем адреса переменных, заданных в каждом из сегментов памяти и отсортируем их по мере убывания адресов

/* memsegments.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct mem
{
    char text[100];
    int *p;
} mem;

int cmp_by_address(const void *, const void *);
void print_struct_array(mem *, size_t);

int init_global_var = 10;        /* Initialized global variable */
int global_var;                  /* Uninitialized global variable */
static int init_static_var = 20; /* Initialized static variable in global scope */
static int static_var;           /* Uninitialized static variable in global scope */


int main(int argc, char **argv, char **envp)
{
    static int init_static_local_var = 30;   /* Initialized static local variable */
    static int static_local_var;             /* Uninitialized static local variable */
    int init_local_var = 40;                 /* Initialized local variable */
    int local_var;                           /* Uninitialized local variable */
    int *dynamic_var = (int*)malloc(sizeof(int));  /* Dynamic variable */

    mem structs[] =
    {
        {"Global variable (initialized)", &init_global_var},
        {"Global variable (uninitialized)", &global_var},
        {"Static variable (in global scope, initialized)", &init_static_var},
        {"Static variable (in global scope, uninitialized)", &static_var},
        {"Static variable (in local scope, initialized)", &init_static_local_var},
        {"Static variable (in local scope, uninitialized)", &static_local_var },
        {"Function (code)", (int*)&main },
        {"Environment variable", (int*)&envp[0] },
        {"Local variable (initialized)", &init_local_var },
        {"Local variable (uninitialized)", &local_var },
        {"Dynamic variable", dynamic_var },
    };

    size_t len = sizeof(structs) / sizeof(mem);

    qsort(structs, len, sizeof(mem), cmp_by_address);

    print_struct_array(structs, len);

    free(dynamic_var);

    return 0;
}

int cmp_by_address(const void *a, const void *b)
{
    mem *ma = (mem *)a;
    mem *mb = (mem *)b;

    if ((unsigned)ma->p > (unsigned)mb->p)
        return -1;
    else if ((unsigned)ma->p < (unsigned)mb->p)
        return 1;
    else
        return 0;
}

/* Example struct array printing function */
void print_struct_array(mem *array, size_t len)
{
    size_t i;

    for(i=0; i<len; i++)
        printf("%-50s:\t%p\n", array[i].text, array[i].p);
}

В результате получим:

Environment variable                              :     0xbff52ee0
Local variable (uninitialized)                    :     0xbff529ac
Local variable (initialized)                      :     0xbff529a8
Dynamic variable                                  :     0x871c008
Global variable (uninitialized)                   :     0x804a044
Static variable (in local scope, uninitialized)   :     0x804a040
Static variable (in global scope, uninitialized)  :     0x804a03c
Static variable (in local scope, initialized)     :     0x804a034
Static variable (in global scope, initialized)    :     0x804a030
Global variable (initialized)                     :     0x804a02c
Function (code)                                   :     0x80484ad

Инструменты

Утилита size показывает размер разделов и общий размер для объектных файлов или архивов. Так, для memsegments.o получим:

$ size memsegments.o
   text    data     bss     dec     hex filename
    745      12       8     765     2fd memsegments.o

Таким образом мы можем определить размеры сегментов Text, Data и BSS. В колонках "dec" и "hex" приведен суммарный размер указанных сегментов в десятичном и 16-ричном форматах соответственно.

Поскольку стек им куча формируются во время выполнения программы, size не может показать их размер.

Для просмотра стека и кучи нужно остановить выполнение программы, не завершая ее работы. Например, добавить в нее ожидание пользовательского ввода

char c = getchar();

а затем выполнить в терминале

ps aux | grep memsegments
cat /proc/<pid найденный_grep>/maps 

В итоге вы увидите стек, кучу и не только их.

Для просмотра содержимого бинарных исполняемых образов — символов, их адресов, сегментов и т. п. — можно использовать утилиты nm и objdump. В частности, objdump с опцией -d — это дизассемблер.

size, nm и objdump входят в состав комплекта утилит обработки двоичных файлов GNU binutils.

Не рассмотрено

Приведенная схема описывает распределение памяти процесса в первом приближении, игнорируя некоторые детали. В частности, между стеком и кучей располагается сегмент, предназначенный для memory mapping. Желающие познакомится с этими деталями могут посмотреть статью Густаво Дуарте.



Комментарии

comments powered by Disqus