Memory Segments and Low-Level Data Management
Created On 19. Aug 2021
Updated: 2021-08-19 02:36:30.890995000 +0000
Created By: acidghost
A compiled program's memory in C is divided into five segments: text, data, bss, heap and stack.
----------------------- Low addresses
| Text (code) segment | / \
----------------------- |
| Data Segment | |
----------------------- |
| bss segment | |
----------------------- ---------------------
| Heap | | Heap | |
----------------------- ---------------> | grows downward | |
| Stack | | \ / |
----------------------- High addresses ---------------------
| / \ |
| grows upward | |
| Stack | |
-----------------------
The heap grows downward toward the higher memory addresses while the stack grows upward toward the lower ones. Both will grow using a single thread in the respective address space. Also see https://courses.engr.illinois.edu/cs225/sp2021/resources/stack-heap/
The Text Segment
also known as the code segment, this is where the assembled machine language instructions of the program are located. Here is where the program program executes the call instructions in assembly language. As the execution takes place, the IP is set to the first instruction in the code segment. This is what happens:
- Instruction where IP is pointing to is read
- byte length of the instruction is added to EIP
- Execute the instruction in step 1
- Go back to step 1
The IP can change to different address in memory depending what the program is instructed to do. This action is executed directly by the processor itself. The write permissions are disabled here, it has a fixed size and no variables are stored in the program, but just code. If the program detects that write is happening, it will alert the user and be killed. The read-only property of this segment also allows executing multiple instances of the same program without any issues that write could inflict.
Data Segment
here are the initialized global and static variables. It's a writable segment, but also has a fixed size. The global variables will always persist.
bss Segment
it is similar to the data segment, with one difference that it keeps the uninitialized global and static variables.
The Heap
The heap can be directly controlled by the programmer and it doesn't have a fixed size. The memory in the heap is managed by allocator and deallocator algorithms that reserve a region of memory in the heap that can be reused later on. The programmer will use allocation functions to reserve and free the memory.
The Stack
similar to heap, the stack does not have a fixed size and is used for temporarily storing local function variables and context during function calls. In general computer science terms, a stack is an abstract data structure also known as FILO - first in, last out. In the stack segment itself, things are pushed and popped in a stack frame. (see Data Access in Intro to Reverse Engineering) The EBP register also known as the frame pointer (FP) or local base pointer (LB), references the local function variables in the stack frame. Each stack frame has parameters of a function, its local variables and two pointers that put things back as they were - the saved frame pointer (SFP) and the return address. With SFP, EBP is returned to its previous value and the ret
is used to restore IP to the next instruction after the function call. This will restore the functional context of the previous stack frame.
Let's check the following example:
void test function(int a, int b, int c, int d) {
int flag;
char buffer[10];
flag = 31337;
buffer[0] = 'A';
}
int main() {
test_function(1, 2, 3, 4);
}
In this program the arguments are declared as integers a, b, c, d and there are local variables "flag" and a 10 character buffer. The memory of these variables will be in the stack segment while the machine instructions for the functions code will be in the text segment.
This can be analyzed in gdb for instance:
set disassembly-flavor intel intel
disass main
Dump of assembler code for function main:
0x00000000004005ab <+0>: push rbp
0x00000000004005ac <+1>: mov rbp,rsp
0x00000000004005af <+4>: mov ecx,0x4
0x00000000004005b4 <+9>: mov edx,0x3
0x00000000004005b9 <+14>: mov esi,0x2
0x00000000004005be <+19>: mov edi,0x1
0x00000000004005c3 <+24>: call 0x400566 <test_function>
0x00000000004005c8 <+29>: mov eax,0x0
0x00000000004005cd <+34>: pop rbp
0x00000000004005ce <+35>: ret
End of assembler dump.
We can see in the first two instructions the prologue ( also see Modules and Functions
in Intro to Reverse Engineering) that sets up the stack frame, after which the arguments are copied into the corresponding registers. After that, the return address is pushed and control flow jumps to the test_function()
.
(gdb) disass test_function
Dump of assembler code for function test_function:
0x0000000000400566 <+0>: push rbp
0x0000000000400567 <+1>: mov rbp,rsp
0x000000000040056a <+4>: sub rsp,0x30
0x000000000040056e <+8>: mov DWORD PTR [rbp-0x24],edi
0x0000000000400571 <+11>: mov DWORD PTR [rbp-0x28],esi
0x0000000000400574 <+14>: mov DWORD PTR [rbp-0x2c],edx
0x0000000000400577 <+17>: mov DWORD PTR [rbp-0x30],ecx
0x000000000040057a <+20>: mov rax,QWORD PTR fs:0x28
0x0000000000400583 <+29>: mov QWORD PTR [rbp-0x8],rax
0x0000000000400587 <+33>: xor eax,eax
0x0000000000400589 <+35>: mov DWORD PTR [rbp-0x18],0x7a69
0x0000000000400590 <+42>: mov BYTE PTR [rbp-0x12],0x41
0x0000000000400594 <+46>: nop
0x0000000000400595 <+47>: mov rax,QWORD PTR [rbp-0x8]
0x0000000000400599 <+51>: xor rax,QWORD PTR fs:0x28
0x00000000004005a2 <+60>: je 0x4005a9 <test_function+67>
0x00000000004005a4 <+62>: call 0x400440 <__stack_chk_fail@plt>
0x00000000004005a9 <+67>: leave
0x00000000004005aa <+68>: ret
End of assembler dump.
Viewing the test_function()
it is seen how the arguments are copied in reverse order (FILO) in the stack frame. At the beginning we see again, the function prologue, where RSP is copied into RBP to set up a new stack frame pointer. Here will be referenced the local variables of the function - flag and buffer. In sub rsp,0x30
instruction, memory is saved for these variables with subtraction from RSP. When each function ends, its stack frame is popped off and the execution is returned to the previous function. This is is how a FILO data structure behaves.
Analyzing the Segments
#include <stdio.h>
int global_var;
int global_initialized_var = 5;
void function() { // This is just a demo function.
int stack_var; // Notice this variable has the same name as the one in main().
printf("the function's stack_var is at address 0x%08x\n", &stack_var);
}
int main() {
int stack_var; // Same name as the variable in function()
static int static_initialized_var = 5;
static int static_var;
int *heap_var_ptr;
heap_var_ptr = (int *) malloc(4);
// These variables are in the data segment.
printf("global_initialized_var is at address 0x%08x\n", &global_initialized_var);
printf("static_initialized_var is at address 0x%08x\n\n", &static_initialized_var);
// These variables are in the bss segment.
printf("static_var is at address 0x%08x\n", &static_var);
printf("global_var is at address 0x%08x\n\n", &global_var);
// This variable is in the heap segment.
printf("heap_var is at address 0x%08x\n\n", heap_var_ptr);
// These variables are in the stack segment.
printf("stack_var is at address 0x%08x\n", &stack_var);
function();
}
The example above shows well, how variables become to be located in different memory segments. The output will yield:
global_initialized_var is at address 0x86a26010
static_initialized_var is at address 0x86a26014
static_var is at address 0x86a2601c
global_var is at address 0x86a26020
heap_var is at address 0x86d78260
stack_var is at address 0xd11957cc
the function's stack_var is at address 0xd11957a4
One thing to note are the four bytes in the heap. They are allocated using the malloc()
function and as in the example above, usually pointers are used to reference the heap in the memory. It is typecast into an integer since malloc()
returns a void pointer. Also note the 2 different stack variables. Each of them will be stored in different stack frames since they have different contexts in the stack segment.
Diving into the Heap
For better understanding how the memory in heap can be used, see the example below.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[]) {
char *char_ptr; // A char pointer
int *int_ptr; // An integer pointer
int mem_size;
if (argc < 2) // If there aren't command-line arguments,
mem_size = 50; // use 50 as the default value.
else
mem_size = atoi(argv[1]);
printf("\t[+] allocating %d bytes of memory on the heap for char_ptr\n", mem_size);
char_ptr = (char *) malloc(mem_size); // Allocating heap memory
if(char_ptr == NULL) { // Error checking, in case malloc() fails
fprintf(stderr, "Error: could not allocate heap memory.\n");
exit(-1);
}
strcpy(char_ptr, "This memory is located on the heap.");
printf("char_ptr (%p) --> '%s'\n", char_ptr, char_ptr);
printf("\t[+] allocating 12 bytes of memory on the heap for int_ptr\n");
int_ptr = (int *) malloc(12); // Allocated heap memory again
if(int_ptr == NULL) { // Error checking, in case malloc() fails
fprintf(stderr, "Error: could not allocate heap memory.\n");
exit(-1);
}
*int_ptr = 31337; // Put the value of 31337 where int_ptr is pointing.
printf("int_ptr (%p) --> %d\n", int_ptr, *int_ptr);
printf("\t[-] freeing char_ptr's heap memory...\n");
free(char_ptr); // Freeing heap memory
printf("\t[+] allocating another 15 bytes for char_ptr\n");
char_ptr = (char *) malloc(15); // Allocating more heap memory
if(char_ptr == NULL) { // Error checking, in case malloc() fails
fprintf(stderr, "Error: could not allocate heap memory.\n");
exit(-1);
}
strcpy(char_ptr, "new memory");
printf("char_ptr (%p) --> '%s'\n", char_ptr, char_ptr);
printf("\t[-] freeing int_ptr's heap memory...\n");
free(int_ptr); // Freeing heap memory
printf("\t[-] freeing char_ptr's heap memory...\n");
free(char_ptr); // Freeing the other block of heap memory
}
malloc()
is the allocation function, that is used to allocate memory, while free()
is used to free it. malloc()
will return a NULL pointer with a value of 0 if memory can't be allocated. free()
accepts a pointer as its only argument and by freeing the memory on the heap, allows it to be used later again. After compiling and running the code above, we will see:
[+] allocating 50 bytes of memory on the heap for char_ptr
char_ptr (0x559117b58670) --> 'This memory is located on the heap.'
[+] allocating 12 bytes of memory on the heap for int_ptr
int_ptr (0x559117b586b0) --> 31337
[-] freeing char_ptr's heap memory...
[+] allocating another 15 bytes for char_ptr
char_ptr (0x559117b586d0) --> 'new memory'
[-] freeing int_ptr's heap memory...
[-] freeing char_ptr's heap memory...
In the code above, the first memory allocation accepts a command-line argument with a default value of 50. The value can be modified at runtime by passing in argv[1]. Then it allocates and deallocates memory with printf()
checking for errors in case a NULL pointer is returned. By modifying the sizes in argv[1], we can see how different size in bytes impacts where the new memory will be allocated. For the default 50, the 15 bytes are allocated after the 12 of inp_ptr
, however, with larger sizes, can be seen how the 15 byte allocation will happen in the freed memory space - taking the example above, the new memory
would be allocated in 0x559117b58670
.
Low Level Versus High Level Data Management
In modern computers, the CPU is attached to the system memory through a bus. The CPU operates at much higher speeds than RAM and this is why RAM cannot be instantly available to the CPU. The reason for this is mainly the combined latency that the involved elements introduce. In other words, when the CPU requests to read from a certain memory address, the time for the command to arrive to memory chip, be processed and sent back takes way longer than a single CPU clock cycle. The result might be that the processor will spend clock cycles by waiting for the RAM. This is why instructions that operate on memory based operands are slower and should be avoided.The period of time when a single instruction reads the data, operates it and writes it back into the memory is unreasonable compared to the processors performance capabilities.
Processors avoid accessing RAM for every single instruction, while they use internal memory that can be accessed with minimal performance penalty. There are more components in the internal memory of a microprocessor such as registers that can be accessed easily, with almost no performance penalty.
RAM Confusion
There might be a confusion when referring to the virtual space of a process and physical RAM. While these two can be aligned together, in usual programs they differently represented. The segments divide the address space of the process. This is not to be confused with the physical pages of RAM. The RAM won't be represented into such segments, since it is shared by all processes in a system, where RAM pages interleave them arbitrarily. The memory segments in a program will exist in an OS that supports virtual memory and will be part of a process dividing the virtual address space, while it does not have direct access to RAM. RAM is a cache for virtual memory mechanism without being directly tied in its virtual space to the memory layout of the program.
When the program needs to have some arguments stored, they are usually placed in a register or on the stack. There are reasons why a variable must reside inside RAM and not in a register and in such cases it will be placed on the stack. However, as mentioned above, the operations that involve the RAM usage will be much slower, for which CPU components will be used for higher processing speeds. Physically, the stack is an area in RAM that is allocated for storing longer-term data. An operating system can manage multiple stacks within RAM, with each of them representing an active program or thread. Register values used in a function that store the machine state will be in the stack and later on loaded back into corresponding registers.
In certain cases, when the physical memory address space is fragmented, these segments might be mapped in specific locations of the RAM area. This might be convenient as an optimization measure, where the hardware would directly indicate which node owns a given physical address.
References
Hacking: The Art of Exploitation - Jon Erickson
Reversing Secrets of Reverse Engineering - Eldad Eilam
https://stackoverflow.com/questions/28278319/memory-segments-and-physical-ram
Section: Reverse Engineering
Back