Структура памяти процесса
Память, выделяемая каждому процессу, состоит из нескольких частей, которые обычно называют сегментами. К числу таких сегментов относятся следующие:
1. Текстовый сегмент (.text)
Cодержит машинный код, который принадлежит программе, запущенной процессом. Текстовый сегмент создается только для чтения, чтобы процесс не мог случайно изменить свои собственные инструкции (из-за read-only статуса, некоторые компиляторы могут записывать туда константы).
Поскольку многие процессы могут выполнять одну и ту же программу, текстовый сегмент создается с возможностью совместного использования - единственная копия кода программы может быть отображена на виртуальное адресное пространство всех процессов.
2. Сегмент инициализированных данных (.data)
Хранит глобальные и статические переменные, инициализированные явным образом. Значения этих переменных считываются из исполняемого файла при загрузке программы в память.
3. Сегмент неинициализированных данных (.bss - block started by symbol)
Содержит глобальные и статические переменные, не инициализированные явным образом. Перед запуском программы система определяет всю память в этом сегменте значением 0.
Основная причина помещения прошедших инициализацию переменных в отдельный от неинициализированных переменных сегмент заключается в том, что, когда программа сохраняется на диске, нет никакого смысла выделять пространство под неинициализированные данные.
Вместо этого исполняемой программе просто нужно записать местоположение и размер, требуемый для сегмента неинициализированных данных, и это пространство выделяется загрузчиком программы в ходе ее выполнения.
4. Динамически увеличивающийся и уменьшающийся сегмент стека (stack)
Содержит стековые фреймы: для каждой отдельной функции выделяется один стековый фрейм, в котором хранятся ее локальные переменные, аргументы и возвращаемое значение. По мере вызова функций и возврата из них стек расширяется и сжимается.
Текущая вершина стека отслеживается в специально предназначенном для этого регистре - указателе стека. Процесс сжатия стека называется расруткой.
5. Динамическая память (heap)
Область, которая предназначена для динамического выделения памяти в ходе выполнения программы (malloc, calloc, free, realloc - под капотом используют системные вызовы brk и sbrk).
6. Сегмент аргументов командной строки
Сегмент для хранения переменных, которые переданы программе в качестве аргументов (argc и argv[]), где argc хранит количество переданных аргументов, а argv хранит значение фактических значений вместе с именем файла:
Далее продемонстрированы различные типы переменных в коде и комментарии, указывающие на сегменты их размещения (в случае оптимизаций, часто используемые переменные могут быть помещены в регистры или вообще исключены):
В комменты я отправил несколько изображений для визуализации материала.
#linuxcore #linux #cpp #cppcore
Память, выделяемая каждому процессу, состоит из нескольких частей, которые обычно называют сегментами. К числу таких сегментов относятся следующие:
1. Текстовый сегмент (.text)
Cодержит машинный код, который принадлежит программе, запущенной процессом. Текстовый сегмент создается только для чтения, чтобы процесс не мог случайно изменить свои собственные инструкции (из-за read-only статуса, некоторые компиляторы могут записывать туда константы).
Поскольку многие процессы могут выполнять одну и ту же программу, текстовый сегмент создается с возможностью совместного использования - единственная копия кода программы может быть отображена на виртуальное адресное пространство всех процессов.
2. Сегмент инициализированных данных (.data)
Хранит глобальные и статические переменные, инициализированные явным образом. Значения этих переменных считываются из исполняемого файла при загрузке программы в память.
3. Сегмент неинициализированных данных (.bss - block started by symbol)
Содержит глобальные и статические переменные, не инициализированные явным образом. Перед запуском программы система определяет всю память в этом сегменте значением 0.
Основная причина помещения прошедших инициализацию переменных в отдельный от неинициализированных переменных сегмент заключается в том, что, когда программа сохраняется на диске, нет никакого смысла выделять пространство под неинициализированные данные.
Вместо этого исполняемой программе просто нужно записать местоположение и размер, требуемый для сегмента неинициализированных данных, и это пространство выделяется загрузчиком программы в ходе ее выполнения.
4. Динамически увеличивающийся и уменьшающийся сегмент стека (stack)
Содержит стековые фреймы: для каждой отдельной функции выделяется один стековый фрейм, в котором хранятся ее локальные переменные, аргументы и возвращаемое значение. По мере вызова функций и возврата из них стек расширяется и сжимается.
Текущая вершина стека отслеживается в специально предназначенном для этого регистре - указателе стека. Процесс сжатия стека называется расруткой.
5. Динамическая память (heap)
Область, которая предназначена для динамического выделения памяти в ходе выполнения программы (malloc, calloc, free, realloc - под капотом используют системные вызовы brk и sbrk).
6. Сегмент аргументов командной строки
Сегмент для хранения переменных, которые переданы программе в качестве аргументов (argc и argv[]), где argc хранит количество переданных аргументов, а argv хранит значение фактических значений вместе с именем файла:
~$ ./prog 100 23 43 69
Далее продемонстрированы различные типы переменных в коде и комментарии, указывающие на сегменты их размещения (в случае оптимизаций, часто используемые переменные могут быть помещены в регистры или вообще исключены):
/* Сегмент неинициализированных данных */
char globBuf[65536];
/* Сегмент инициализированных данных */
int primes[] = { 2, 3, 5, 7 };
/* Размещается в фрейме для square() */
static int square(int x)
{
/* Размещается в фрейме для square() */
int result;
result = x * x;
/* Возвращаемое значение передается через регистр */
return result;
}
/* Размещается в фрейме для doCalc() */
static void doCalc(int val)
{
square(val)
if (val < 1000) {
/* Размещается в фрейме для doCalc() */
int t;
t = val * val * val;
}
}
/* Размещается в фрейме для main() */
int main(int argc, char *argv[])
{
printf("File name = %s\n", argv[0]);
printf("Number of arguments = %d\n", argc - 1);
/* Сегмент инициализированных данных */
static int key = 9973;
/* Сегмент неинициализированных данных */
static char mbuf[10240000];
/* Размещается в фрейме для main() */
char *p;
/* Указывает на память в сегменте кучи */
p = malloc(1024);
doCalc(key);
}
В комменты я отправил несколько изображений для визуализации материала.
#linuxcore #linux #cpp #cppcore
👍19🔥7❤3✍2🥰1👾1
Системные и библиотечные вызовы [1]
Ну чтож, начинаем погружение в интересную тему - рассмотрим то, без чего не обходится ни одно системное приложение - системные и библиотечные вызовы.
Тут, на самом деле, можно довольно глубоко копнуть. Если проявится ваш интерес, сделаем ряд постов, где вдоль и поперек разберем принцип работы вызовов, практики их использования и т.д.
Что такое системный вызов?
Можно считать, что все пользовательские программы работают внутри некоторой песочницы, из которой нельзя так просто выполнять ряд действий: получать доступ к файлам, передавать данные по сети, выводить текст на экран, создавать процессы и т.д. Для этого нужно обратиться к внутренним сервисам ядра и попросить его сделать что требуется через системные вызовы.
Системный вызов изменяет состояние процессора, переводя его из пользовательского режима в режим ядра, позволяя ему таким образом получить доступ к защищенной памяти ядра.
(Системные вызовы Linux перечисляются на странице руководства syscalls(2))
Это может вас немного напугать, но, обычно, системные вызовы недоступны напрямую. Все, с чем вы работаете - это тонкие функции-обертки библиотки glibc. Да-да, не удивляйтесь: read, write, fork являются всего лишь понятным пользователю интерфейсом, через который отрабатывают системные вызовы.
И да, важно помнить то, что системные вызовы - это платформозависимая история. Чтение файлов, взаимодействие с процессами, работа с сокетами, управление памятью и прочие системные операции реализованы по разному на каждой системе и могут отличаться друг от друга по API.
Как сделать прямой системный вызов?
Иногда бывает полезно явно обраться к системному вызову в случае, если для него не реализована сишная функция-обертка. Сделать это можно через библиотечный вызов syscall() из unistd.h, который первым параметром принимает номер системного вызова из "sys/syscall.h", а далее - аргументы самого вызова:
Таким образом, системный вызов write() можно инициировать по разному:
Что такое библиотечный вызов?
Библиотечным вызовом можно считать вызов функции стандартной библиотеки C, в которой не содержится прямой реализации системного вызова: системного прерывания, переноса аргументов в определенные, необходимые ядру, регистры, копирования номера системного вызова в регистр (%eax) и т.д.
Эти функции предназначены для решения широкого круга разнообразных задач: открытия файлов, преобразования времени в формат, понятный человеку, сравнения двух символьных строк и т.д.
Многие библиотечные функции вообще не используют системные вызовы (например, функции для работы со сроками). С другой стороны, некоторые библиотечные функции являются надстройками над системными вызовами с платформозависимой реализацией.
Например, библиотечная функция fopen() использует для открытия файла системный вызов open().
Зачастую библиотечные функции разработаны для предоставления более удобного интерфейса вызова по сравнению с тем, что имеется у исходного системного вызова.
Например, функция printf() предоставляет форматирование вывода и буферизацию данных, а внутренний системный вызов write() просто выводит блок байтов.
Аналогично этому функции malloc() и free() выполняют различные вспомогательные задачи, существенно облегчающие выделение и высвобождение оперативной памяти по сравнению с использованием исходного системного вызова brk().
#linuxcore #linux #cpp #cppcore
Ну чтож, начинаем погружение в интересную тему - рассмотрим то, без чего не обходится ни одно системное приложение - системные и библиотечные вызовы.
Тут, на самом деле, можно довольно глубоко копнуть. Если проявится ваш интерес, сделаем ряд постов, где вдоль и поперек разберем принцип работы вызовов, практики их использования и т.д.
Что такое системный вызов?
Можно считать, что все пользовательские программы работают внутри некоторой песочницы, из которой нельзя так просто выполнять ряд действий: получать доступ к файлам, передавать данные по сети, выводить текст на экран, создавать процессы и т.д. Для этого нужно обратиться к внутренним сервисам ядра и попросить его сделать что требуется через системные вызовы.
Системный вызов изменяет состояние процессора, переводя его из пользовательского режима в режим ядра, позволяя ему таким образом получить доступ к защищенной памяти ядра.
(Системные вызовы Linux перечисляются на странице руководства syscalls(2))
Это может вас немного напугать, но, обычно, системные вызовы недоступны напрямую. Все, с чем вы работаете - это тонкие функции-обертки библиотки glibc. Да-да, не удивляйтесь: read, write, fork являются всего лишь понятным пользователю интерфейсом, через который отрабатывают системные вызовы.
И да, важно помнить то, что системные вызовы - это платформозависимая история. Чтение файлов, взаимодействие с процессами, работа с сокетами, управление памятью и прочие системные операции реализованы по разному на каждой системе и могут отличаться друг от друга по API.
Как сделать прямой системный вызов?
Иногда бывает полезно явно обраться к системному вызову в случае, если для него не реализована сишная функция-обертка. Сделать это можно через библиотечный вызов syscall() из unistd.h, который первым параметром принимает номер системного вызова из "sys/syscall.h", а далее - аргументы самого вызова:
extern long int syscall (long int __sysno, ...) __THROW;
Таким образом, системный вызов write() можно инициировать по разному:
#include <unistd.h>
#include <sys/syscall.h>
int main(void) {
write(1, "hello, world!\n", 14)
syscall(SYS_write, 1, "hello, world!\n", 14);
return 0;
}
Что такое библиотечный вызов?
Библиотечным вызовом можно считать вызов функции стандартной библиотеки C, в которой не содержится прямой реализации системного вызова: системного прерывания, переноса аргументов в определенные, необходимые ядру, регистры, копирования номера системного вызова в регистр (%eax) и т.д.
Эти функции предназначены для решения широкого круга разнообразных задач: открытия файлов, преобразования времени в формат, понятный человеку, сравнения двух символьных строк и т.д.
Многие библиотечные функции вообще не используют системные вызовы (например, функции для работы со сроками). С другой стороны, некоторые библиотечные функции являются надстройками над системными вызовами с платформозависимой реализацией.
Например, библиотечная функция fopen() использует для открытия файла системный вызов open().
Зачастую библиотечные функции разработаны для предоставления более удобного интерфейса вызова по сравнению с тем, что имеется у исходного системного вызова.
Например, функция printf() предоставляет форматирование вывода и буферизацию данных, а внутренний системный вызов write() просто выводит блок байтов.
Аналогично этому функции malloc() и free() выполняют различные вспомогательные задачи, существенно облегчающие выделение и высвобождение оперативной памяти по сравнению с использованием исходного системного вызова brk().
#linuxcore #linux #cpp #cppcore
👍32🔥6❤🔥5❤2🖕1👾1