Сегментация приложения на си 


Мы поможем в написании ваших работ!



ЗНАЕТЕ ЛИ ВЫ?

Сегментация приложения на си



Дале обще описывается структура приложения, которую обычно можно встретить на практике. Стандарт языка си не определяет структуры приложения, поэтому неверно говорить, например, о том, что в приложении на си автоматические перемные располагаются на стеке, а статические в bss сегменте и т.п. Однако, реальные примеры помогают глубже понять поведение компилятора и программы.

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

 

Структура программы на си

 

Data cостоит из статических и глобальных переменных, которые явно инициализируются значениями. Этот сегмент может быть далее разбит на ro-data (read only data) – сегмент данных только для чтения, и rw-data (read write data) – сегмент данных для чтения и записи. Например, глобальные переменные

 

s[] = "hello world"

int debug=1

 

Будут храниться в rw-области. Для выражения типа

 

const char* string = "hello world"

 

указатель будет храниться в rw-области, а строкой литерал "hello world" в ro-области.

 

static int i = 10

int i = 10

 

оба будут расположены в сегменте данных.

 

BSS-сегмент (block started by symbol) содержит неинициализированные глобальные переменные, или статические переменные без явной инициализации. Этот сегмент начинается непосредственно за data-сегментом. Обычно загрузчик программ инициализирует bss область при загрузке приложения нулями. Дело в том, что в data области переменные инициализированы – то есть затирают своими значениями выделенную область памяти. Так как переменные в bss области не инициализированы явно, то они теоретически могли бы иметь значение, которое ранее хранилось в этой области, а это уязвимость, которая предоставляет доступ до, возможно, приватных данных. Поэтому загрузчик вынужден обнулять все значения. За счёт этого и неинициализированные глобальные переменные, и статические переменные по умолчанию равны нулю.

Куча – начинается за BSS сегментом и начиная оттуда растёт, соответственно с увеличением адреса. Этот участок используется для выделения на нём памяти с использованием функции malloc (и прочих) и для очисти с помощью функции free.

Стек вызовов обычно растёт "навстречу" куче, то есть с увеличением стека адрес вершины стека уменьшается. Набор значений, которые кладутся на стек одной функцией, называются фреймом. После выхода из функции стек освобождается от фрейма. Один фрейм хранит как минимум одно значение - адрес возврата. Каждый раз, когда мы создаём локальные переменные, они располагаются на стеке. Как только мы выходим из функции, стек очищается и переменные исчезают. Вызов функций и передача параметров также происходит через стек.

Сегмент кода, или текстовый сегмент, или просто текст, содержит исполняемые инструкции. У него фиксированный размер и обычно он используется только для чтения, если же в него можно писать, то архитектура поддерживает самомодификацию. Сегмент кода располагается после начала стека, поэтому в случае роста он [стек] не перекрывает сегмент кода.


Тема №10

Динамическое выделение памяти

Malloc

Итак, уже обсуждалось, что локальные переменные кладутся на стек и существуют до тех пор, пока мы не вышли из функции. С одной стороны, это позволяет автоматически очищать память, с другой стороны, существует необходимость в переменных, время жизни которых мы можем контролировать самостоятельно. Кроме того, нам необходимо динамическое выделение памяти, когда размер используемого пространства заранее не известен. Для этого используется выделение памяти на куче. Недостатков у такого подхода два:

· во-первых, память необходимо вручную очищать,

· во-вторых, выдеение памяти – достаточно дорогостоящая операция.

Для выделения памяти на куче в си используется функция malloc (memory allocation) из библиотеки stdlib.h

 

void * malloc(size_t size);

 

Функция выделяет size байтов памяти и возвращает указатель на неё. Если память выделить не удалось, то функция возвращает NULL. Так как malloc возвращает указатель типа void, то его необходимо явно приводить к нужному нам типу. Например, создадим указатель, после этого выделим память размером в 100 байт.

 

#include <conio.h>

#include <stdio.h>

#include <stdlib.h>

 

void main() {

int *p = NULL;

p = (int*) malloc(100); /* индексация через 4байта */

 

free(p);

}

 

После того, как мы поработали с памятью, необходимо освободить память функцией free.

Используя указатель, можно работать с выделенной памятью как с массивом. Пример: пользователь вводит число – размер массива, создаём массив этого размера и заполняем его квадратами чисел по порядку. После этого выводим и удаляем массив.

 

#include <conio.h>

#include <stdio.h>

#include <stdlib.h>

 

void main() {

const int maxNumber = 100;

int *p = NULL;

unsigned i, size;

 

do {

printf("Enter number from 0 to %d: ", maxNumber);

scanf("%d", &size);

if (size < maxNumber) {

break;

}

} while (1);

 

p = (int*) malloc(size * sizeof(int));

 

for (i = 0; i < size; i++) {

p[i] = i*i;

}

 

for (i = 0; i < size; i++) {

printf("%d ", p[i]);

}

 

_getch();

free(p);

}

 

Разбираем код

 

p = (int*) malloc(size * sizeof(int));

 

Здесь (int *) – приведение типов. Пишем такой же тип, как и у указателя.

size * sizeof(int) – сколько байт выделить. sizeof(int) – размер одного элемента массива.

После этого работаем с указателем точно также, как и с массивом. В конце не забываем удалять выделенную память.

Теперь представим на рисунке, что у нас происходило. Пусть мы ввели число 5.

Выделение памяти.

 

Выделение памяти.

 

Функция malloc выделила память на куче по определённому адресу, после чего вернула его. Теперь указатель p хранит этот адрес и может им пользоваться для работы. В принципе, он может пользоваться и любым другим адресом.

Когда функция malloc "выделяет память", то она резервирует место на куче и возвращает адрес этого участка. У нас будет гарантия, что компьютер не отдаст нашу память кому-то ещё. Когда мы вызываем функцию free, то мы освобождаем память, то есть говорим компьютеру, что эта память может быть использована кем-то другим. Он может использовать нашу память, а может и нет, но теперь у нас уже нет гарантии, что эта память наша. При этом сама переменная не зануляется, она продолжает хранить адрес, которым ранее пользовалась.

Это очень похоже на съём номера в отеле. Мы получаем дубликат ключа от номера, живём в нём, а потом сдаём комнату обратно. Но дубликат ключа у нас остаётся. Всегда можно зайти в этот номер, но в нём уже кто-то может жить. Так что наша обязанность – удалить дубликат.

Иногда думают, что происходит "создание" или "удаление" памяти. На самом деле происходит только перераспределение ресурсов.



Поделиться:


Последнее изменение этой страницы: 2017-01-20; просмотров: 403; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 3.18.220.243 (0.011 с.)