Сегментация памяти программы
Сегментация памяти программы.
Память программы делится на пять сегментов: text (текст), data (данные), bss (bulk storage system - массовое ЗУ), heap (куча) и stack (стек). Каждый сегмент представляет специальный участок памяти, отведенный для определенной цели.
Сегмент текста иногда называют также сегментом кода. В нем располагаются ассемблированные машинные команды. Выполнение команд, находящихся в этом сегменте, происходит нелинейным образом благодаря упомянутым ранее управляющим структурам и функциям высокого уровня, которые компилируются в команды ветвления, перехода и вызова функций (branch, jump и call) на языке ассемблера. Когда выполняется программа, в EIP записывается адрес первой команды в сегменте текста. Затем процессор осуществляет следующий цикл выполнения:
1) прочесть команду, на которую указывает EIP;
2) прибавить к содержимому EIP длину команды в байтах;
3) выполнить команду, прочитанную на шаге 1;
4) перейти к шагу 1.
Иногда прочтенной командой оказывается команда перехода или вызова, которая изменяет значение EIP, помещая в него другой адрес памяти. Процессор не обращает внимания на такие изменения, будучи готовым к нелинейному характеру выполнения. Поэтому если на шаге 3 изменить EIP, то процессор вернется к шагу 1 и прочтет ту команду, которая находится по адресу, записанному в EIP.
В сегменте текста запрещена запись, поскольку он используется только для хранения кода, а не переменных. Это не позволяет модифицировать код программы, и попытки записать что-либо в этот сегмент памяти приводят к извещению пользователя об аварии и прекращению выполнения программы. Другим преимуществом того, что в этом сегменте разрешено только чтение, является возможность совместного использования его несколькими экземплярами программы, которые могут выполняться одновременно, не мешая друг другу. Следует также заметить, что этот сегмент памяти имеет фиксированный размер, потому что в нем не происходит никаких изменений.
Сегменты data и bss используются для хранения глобальных и статических переменных программы. В сегмент data записываются инициализированные глобальные переменные, строкой, и другие константы, используемые в программе. В сегмент bss записываются неинициализированные переменные. Хотя эти сегменты доступны для записи, их размер также фиксирован. Сегмент кучи отводится для остальных переменных программы. Важно заметить, что размер кучи не фиксирован: она может уменьшаться или увеличиваться по мере необходимости. Всей памятью кучи управляют алгоритмы
выделения и освобождения памяти, которые резервируют в куче участок памяти для использования, а затем отменяют резервирование и разрешают повторно использовать эту память для последующего резервирования. Куча растет или сокращается в зависимости от того, сколько памяти зарезервировано для нее. Рост кучи происходит вниз в направлении больших адресов памяти.
Сегмент стека тоже имеет переменный размер и используется как временная память для хранения контекста во время вызова функций. Когда программа использует функцию, последняя получает собственный комплект передаваемых ей переменных, а код функции находится в другом участке памяти в сегменте текста (или кода). Поскольку при вызове функции надо изменить контекст и EIP, в стек записываются все передаваемые переменные и адрес возврата, который должен быть записан в EIP после выполнения функции.
В общих понятиях вычислительной техники стеком называют часто используемую абстрактную структуру данных. Он обрабатывается в порядке «первым пришел – последним ушел» (FILO), означающем, что элемент, первым помещенный в стек, будет извлечен оттуда последним. Это напоминает нанизывание бусин на нитку с большим узлом, когда невозможно снять первую бусину, пока не будут сняты все остальные. Помещение элемента в стек называют проталкиванием (pushing), а извлечение из стека – выталкиванием (popping).
В соответствии с названием сегмент стека в памяти фактически является структурой данных типа стека. Адрес вершины (конца) стека хранится в регистре ESP и постоянно меняется по мере проталкивания элементов в стек или выталкивания из него. Это очень динамичный режим, и понятно поэтому, что размер стека также не фиксирован. В противоположность куче стек при увеличении размера растет в сторону младших адресов памяти.
Порядок хранения данных в стеке (FILO) может показаться непривычным, но для хранения контекста стек, оказывается, очень удобен. При вызове функции в стек проталкивается группа данных в виде структуры, называемой кадром стека. Для ссылки на переменные в текущем кадре стека служит регистр ЕВР (иногда называемый указателем кадра (FP) или локальным указателем базы (LB)). В каждом кадре стека содержатся параметры функции, ее локальные переменные и два указателя, необходимых, чтобы вернуться в исходное состояние: сохраненный указатель кадра стека (SFP) и адрес возврата. Указатель кадра стека нужен для восстановления предшествующего значения ЕВР, а адрес возврата - для восстановления в EIP адреса команды, следующей после вызова функции.
Вот пример тестовой функции и главной функции программы:
void test_function(int a, int b, int с, int d) {
char flag;
char buffer[10];
}
void main() {
test_function(1. 2, 3, 4);
}
В этом маленьком фрагменте кода сначала объявляется функция test__function с четырьмя аргументами, объявленными как целые числа: а, b, с и d. Локальные переменные функции состоят из одиночного символа flag и 10-символьного буфера с именем buffer. Функция main выполняется при запуске программы и просто вызывает тестовую функцию.
При вызове тестовой функции из функции main в стек помещаются различные значения, образуя кадр стека следующим образом. Когда вызывается test_function(), в стек помещаются ее аргументы в обратном порядке (потому что это FILO). Аргументами функции являются 1, 2, 3 и 4, поэтому последовательные команды push проталкивают на стек 4, 3, 2 и, наконец, 1. Эти значения соответствуют переменным d, с, b и а в функции.
При выполнении команды ассемблера «call», чтобы изменить контекст выполнения на test_function(), в стек проталкивается адрес возврата. Это значение будет адресом команды, следующей за текущим EIP, а именно значением, запомненным на шаге 3 цикла выполнения, который мы рассматривали выше. За записью адреса возврата следует то, что называют прологом процедуры. На этом этапе в стек проталкивается текущее значение ЕВР. Оно называется сохраненным указателем кадра (SFP) и позднее пригодится, чтобы восстановить исходное состояние ЕВР. Текущее значение ESP копируется затем в ЕВР
и устанавливает новый указатель кадра. Наконец, в стеке отводится память для локальных переменных функции (flag и buffer) путем вычитания из ESP. Память, отводимая для этих локальных переменных, не проталкивается в стек, поэтому переменные располагаются в естественном порядке. В итоге кадр стека выглядит примерно так:
Кадр стека:
Вершина стека;
Младшие адреса;
Buffer;
Указатель кадра стека (SFP);
Указатель кадра (ЕВ Р);
Адрес возврата (ret);
Старшие адреса;
Это кадр стека. Обращение к локальным переменным осуществляется путем вычитания из указателя кадра ЕВР, а к аргументам функции - путем сложения с ним.
Когда функция вызывается для выполнения, значение EIP изменяется на адрес начала этой функции в сегменте текста (или кода). Память в стеке служит для хранения локальных переменных функции и ее аргументов. По завершении выполнения весь кадр стека выталкивается из стека, и в EIP записывается адрес возврата, благодаря чему программа может продолжить выполнение. Если внутри этой функции вызвать другую функцию, в стек будет помещен еще один кадр стека и т. д. По завершении каждой функции ее кадр стека выталкивается из стека и выполнение возвращается к предыдущей функции. Такое поведение объясняет, почему этот сегмент памяти организован в виде структуры данных типа FILO.
Различные сегменты памяти организованы в том порядке, в котором они были показаны, - от младших адресов памяти к старшим. Поскольку большинству людей привычнее списки, нумеруемые сверху вниз, мы показали младшие адреса памяти сверху.
Куча растет в направлении от старших адресов к младшим адресам.
Адресация:
Вершина стека;
Младшие адреса;
Сегмент data;
Сегмент bss;
Сегмент heap.
По сути, куча и стек - это динамические сегменты; они увеличиваются в противоположных направлениях в сторону друг друга. Этим минимизируются непроизводительный расход памяти и возможность взаимопроникновения сегментов.
Петухов Олег, юрист в области международного права и защиты персональных данных, специалист в области информационной безопасности, защиты информации и персональных данных.
Телеграм-канал: https://t.me/zashchitainformacii
Группа в Телеграм: https://t.me/zashchitainformacii1
Сайт: https://legascom.ru
Электронная почта: online@legascom.ru
#защитаинформации #информационнаябезопасность
Segmentation of program memory.
The program's memory is divided into five segments: text, data, bss (bulk storage system), heap, and stack. Each segment represents a special area of memory allocated for a specific purpose.
A text segment is sometimes also called a code segment. It contains assembled machine commands. The execution of commands in this segment occurs in a non-linear manner due to the previously mentioned high-level control structures and functions, which are compiled into commands for branching, jumping, and calling functions (branch, jump, and call) in assembly language. When the program is running, the address of the first command in the text segment is recorded in the EIP. The processor then performs the next execution cycle:
1) read the command indicated by the EIP;
2) add the length of the command in bytes to the contents of the EIP;
3) execute the command read in step 1;
4) Go to step 1.
Sometimes the read command turns out to be a jump or call command that changes the EIP value by putting a different memory address in it. The processor does not pay attention to such changes, being prepared for the non-linear nature of execution. Therefore, if you change the EIP in step 3, the processor will return to step 1 and read the command that is located at the address written in the EIP.
Writing is prohibited in the text segment because it is used only to store code, not variables. This does not allow modification of the program code, and attempts to write anything to this memory segment result in the user being notified of an accident and the program being terminated. Another advantage of the fact that this segment is read-only is the ability to share it with multiple instances of the program that can run simultaneously without interfering with each other. It should also be noted that this memory segment has a fixed size, because no changes occur in it.
The data and bss segments are used to store global and static program variables. The data segment contains initialized global variables, a string, and other constants used in the program. Uninitialized variables are written to the bss segment. Although these segments are writable, their size is also fixed. The heap segment is reserved for the rest of the program variables. It is important to note that the size of the heap is not fixed: it can decrease or increase as needed. The entire memory of the heap is controlled by algorithms
memory allocation and deallocation, which reserve a portion of memory in the heap for use, and then cancel the reservation and allow this memory to be reused for subsequent redundancy. The heap grows or shrinks depending on how much memory is reserved for it. The heap grows downward in the direction of larger memory addresses.
The stack segment is also of variable size and is used as temporary memory to store the context during function calls. When a program uses a function, the latter receives its own set of variables passed to it, and the function code is located in another memory location in a segment of text (or code). Since the context and EIP need to be changed when calling the function, all passed variables and the return address are written to the stack, which should be written to the EIP after the function is executed.
In general computing terms, a stack is a commonly used abstract data structure. It is processed in the first–in-last-out (FILO) order, which means that the element first placed on the stack will be the last to be removed from there. It's like stringing beads on a string with a large knot, when it's impossible to remove the first bead until all the others are removed. Putting an item on the stack is called pushing, and removing it from the stack is called popping.
According to the name, a stack segment in memory is actually a stack-type data structure. The address of the top (end) of the stack is stored in the ESP register and is constantly changing as items are pushed onto or out of the stack. This is a very dynamic mode, and therefore it is understandable that the stack size is also not fixed. In contrast to the heap, the stack grows towards lower memory addresses as the size increases.
The order of storing data in a stack (FILO) may seem unusual, but the stack turns out to be very convenient for storing context. When a function is called, a group of data is pushed onto the stack in the form of a structure called a stack frame. The EBP register (sometimes called the frame pointer (FP) or the local base pointer (LB)) is used to refer to variables in the current stack frame. Each stack frame contains the parameters of the function, its local variables, and two pointers necessary to return to the initial state: the saved stack frame pointer (SFP) and the return address. The stack frame pointer is needed to restore the previous EBP value, and the return address is needed to restore the EIP address of the command following the function call.
Here is an example of a test function and the main function of the program:
void test_function(int a, int b, int c, int d) {
char flag;
char buffer[10];
}
void main() {
test_function(1. 2, 3, 4);
}
In this small piece of code, the test__function is first declared with four arguments declared as integers: a, b, c, and D. The local variables of the function consist of a single flag character and a 10-character buffer named buffer. The main function is executed when the program is started and simply calls the test function.
When calling the test function from the main function, various values are placed on the stack, forming a stack frame as follows. When test_function() is called, its arguments are placed on the stack in reverse order (because it is a FILO). The arguments of the function are 1, 2, 3, and 4, so consecutive push commands push 4, 3, 2, and finally 1 onto the stack. These values correspond to the variables d, c, b, and a in the function.
When executing the "call" assembler command, the return address is pushed onto the stack to change the execution context to test_function(). This value will be the address of the command following the current EIP, namely the value stored in step 3 of the execution cycle, which we discussed above. The recording of the return address is followed by what is called the procedure prologue. At this stage, the current EBP value is pushed onto the stack. It is called a saved frame pointer (SFP) and will be useful later to restore the original frame state. The current ESP value is then copied to HEB
and sets a new frame pointer. Finally, the stack allocates memory for the function's local variables (flag and buffer) by subtracting from ESP. The memory allocated for these local variables is not pushed onto the stack, so the variables are arranged in a natural order. As a result, the stack frame looks something like this:
Stack frame:
Top of the stack;
Junior addresses;
Buffer;
Stack Frame Pointer (SFP);
Frame Pointer (EV R);
Return Address (ret);
Senior addresses;
This is a stack frame. Access to local variables is carried out by subtracting the HEB frame from the pointer, and to the arguments of the function by adding it.
When a function is called for execution, the EIP value changes to the address of the beginning of this function in a segment of text (or code). The memory on the stack is used to store the local variables of the function and its arguments. Upon completion of execution, the entire stack frame is pushed off the stack, and the return address is written to the EIP, so that the program can continue execution. If another function is called inside this function, another stack frame will be placed on the stack, and so on. At the end of each function, its stack frame is pushed off the stack and execution returns to the previous function. This behavior explains why this memory segment is organized as a FILO-type data structure.
The various memory segments are organized in the order in which they were shown, from the lowest memory addresses to the highest. Since most people are more familiar with lists numbered from top to bottom, we have shown the lower memory addresses from above.
The pile grows in the direction from the higher addresses to the lower addresses.
Addressing:
Top of the stack;
Junior addresses;
The data segment;
The bss segment;
The heap segment.
In fact, the heap and stack are dynamic segments; they increase in opposite directions towards each other. This minimizes unproductive memory consumption and the possibility of segment interpenetration.
Oleg Petukhov, lawyer in the field of international law and personal data protection, information security specialist security, protection of information and personal data.
Telegram channel: https://t.me/protectioninformation
Telegram Group: https://t.me/informationprotection1
Website: https://legascom.ru
Email: online@legascom.ru
#informationprotection #informationsecurity




