Memory Layout of a C Program
Advertisement
Memory Management is one of the most important topics for a Programmer, and so understanding the Memory Layout of a C Program and Memory Layout of a Process becomes essential.
For high-level languages such as Java, Python, C#, Memory is partially managed by the language itself as it has a Garbage Collector, which deallocates and frees the allocated memory while not in use. But there is no such garbage collector in C & C++, and so the programmer must manually release the allocated memory.
The C program is first compiled and translated to an executable object file. When the executable is run, it takes the main memory area, i.e. the RAM, and the CPU runs the executable instructions.
If you are not aware of the processes involved in compiling the C program from source to binary, read C Program Compilation Process.
The Typical Memory Layout of a C Program consists of the following segments:
- Command Line Arguments
- Stack
- Heap
- Uninitialized Data Segment (BSS)
- Initialized Data Segment
- Text/Code Segment
The above layout segments can be broadly classified into two:
- Static Memory Layout – Text/Code, Data Segments
- Dynamic Memory Layout – Stack & Heap
The C Program executable already contains some of the segments, and some are built dynamically at runtime.
First Let’s Discuss each segment of the Memory Layout in detail:
Static Memory Layout
The Static Memory layout consists of three segments, Text/Code segment, Initialized, and Uninitialized (bss) Data Segment. These three segments are already present in the final executable object file of the c program and are directly copied to the main memory layout.
We can use the size tool to take a look at the static memory layout of the c program executable object file.
Let’s take a look:
#include
int main()
{
return 0;
}
When the executable object file is analyzed with the size command, the static memory layout is displayed.
$ gcc .\cprogram.c -o cprogram.out
$ size cprogram.out
text data bss dec hex filename
1418 544 8 1970 7b2 cprogram.out
Text/Code Segment
Text or Code Segment includes the machine-level instructions for the final executable object file. This section is one of the key parts of the static memory structure as it includes the program’s central logic.
The text segment in the memory structure is below the heap and the data segment. This layout is chosen to shield the Text section from overwriting if the stack or heap overflows.
In the text section of the final executable object file, we only have read and execute permissions and no write permissions. This is done to prevent accidental modifications to the corresponding assembly code.
You can use the objdump command to dump various parts of the executable object file. In this section, the Text/Code Segment will be dumped using the objdump tool.
One point to remember here is that the objdump command will only run on Linux and not on any other platform.
$ objdump -S cprogram.out
cprogram.out: file format elf64-x86-64
Disassembly of section .init:
0000000000001000 <_init>:
1000: f3 0f 1e fa endbr64
1004: 48 83 ec 08 sub $0x8,%rsp
Disassembly of section .plt:
0000000000001020 <.plt>:
1020: ff 35 a2 2f 00 00 pushq 0x2fa2(%rip)
1026: f2 ff 25 a3 2f 00 00 bnd jmpq *0x2fa3(%rip)
102d: 0f 1f 00 nopl (%rax)
Disassembly of section .text:
0000000000001040 <_start>:
1040: f3 0f 1e fa endbr64
1044: 31 ed xor %ebp,%ebp
1046: 49 89 d1 mov %rdx,%r9
1049: 5e pop %rsi
0000000000001129
The output above is shortened, as we only need to see the main block. The main block in the above objdump output is the corresponding assembly code for the main function of the C Program.
Initialized Data Segment
All initialized global and static variables are stored in this section.
The data segment has read and write permissions. This allows the program to execute and change the value of the variable in the data segment at runtime.
Let’s change the previous C program and add some global variables.
#include
int a=10;
char ch='A';
int arr[5] = {1,2,3,4,5};
int main()
{
return 0;
}
Find the size of the cprogram.out
and compare it to the previous size.
$ size cprogram.out
text data bss dec hex filename
1418 580 8 1975 7b7 cprogram.out
Previously the size of the data segment was 544 bytes and after initializing global variables it increased to 580 bytes.
Uninitialized Data Segment (BSS)
The Uninitialized Data Section, also known as the “bss” segment, was named after an old assembly operator that stands for “block started by the symbol“.
The BSS Segment contains all the uninitialized global variables and static variables. This segment is placed above the data segment in the memory layout.
This segment also has both the read and write permissions.
#include
int a,b,c;
char ch;
int main()
{
return 0;
}
Find the size of the cprogram.out
and compare it to the previous size.
$ size cprogram.out
text data bss dec hex filename
1418 544 24 1986 7c2 cprogram.out
This time size of the bss segment increased from 8 bytes to 24 bytes, because we declared global variables but didn’t initialize it.
Dynamic Memory Layout
This is the runtime memory of the process and exists as long as the process is running.
Stack
Program execution can take place without a heap memory, but not without a stack segment. This illustrates the importance of stack memory for the execution of a program.
The stack is a region of memory in the process’s virtual address space where data is added or removed in the Last-in-First-out (LIFO) order.
A new stack-frame is added to the stack memory when a new function is invoked. The corresponding stack-frame is removed when the function returns.
One thing to note here is that every function has its own stack-frame, also known as Activation record.
The size of the stack is variable since it depends on the size of the local variables, parameters, and function calls. The Stack grows from a higher address to a lower address.
Every process has its own fixed/configurable stack memory. The stack memory is reclaimed by the OS when the process terminates.
Using the ulimit -s
command, we can see the max size of stack memory in the Linux system.
$ ulimit -s
8192
Use ulimit -a
command to list all the flags for the ulimit command.
$ ulimit -a
-t: cpu time (seconds) unlimited
-f: file size (blocks) unlimited
-d: data seg size (kbytes) unlimited
-s: stack size (kbytes) 8192
-c: core file size (blocks) 0
-m: resident set size (kbytes) unlimited
-u: processes 43585
-n: file descriptors 1024
-l: locked-in-memory size (kbytes) 65536
-v: address space (kbytes) unlimited
-x: file locks unlimited
-i: pending signals 43585
-q: bytes in POSIX msg queues 819200
-e: max nice 0
-r: max rt priority 0
-N 15: unlimited
To find the limits of a running process in Linux, use cat /proc//limits
command.
Create a C program with an infinite loop.
int main()
{
while(1){}
}
Run the executable object file in the background, it will give us the process id of the process. Use the process id to get the limits of the process.
Kill the background running process, or it will run indefinitely.
$ ./infi.out &
[1] 4853
$ cat /proc/4853/limits
Limit Soft Limit Hard Limit Units
Max cpu time unlimited unlimited seconds
Max file size unlimited unlimited bytes
Max data size unlimited unlimited bytes
Max stack size 8388608 unlimited bytes
Max core file size 0 unlimited bytes
Max resident set unlimited unlimited bytes
Max processes 43585 43585 processes
Max open files 1024 1048576 files
Max locked memory 67108864 67108864 bytes
Max address space unlimited unlimited bytes
Max file locks unlimited unlimited locks
Max pending signals 43585 43585 signals
Max msgqueue size 819200 819200 bytes
Max nice priority 0 0
Max realtime priority 0 0
Max realtime timeout unlimited unlimited us
$ kill 4853
[1] + 4853 terminated ./infi.out
Let’s find the Stack Size using C Program.
#include
#include
#include
#include
#include<sys/resource.h>
int main()
{
struct rlimit lim;
if(getrlimit(RLIMIT_STACK,&lim)==0)
{
printf("Soft Limit = %ld\n",lim.rlim_cur);
printf("Max Stack Size = %ld\n",lim.rlim_max);
}
else
printf("%s\n", strerror(errno));
return 0;
}
$ ./cprogram.out
Soft Limit = 8388608
Max Stack Size = -1
Let’s now see the stack memory layout and what the stack frame for a function contains.
Stack Memory Layout
A Stack frame contains four types of information:
- Parameters passed to the function (Reverse Order)
- The return address of the caller function.
- The base pointer of the caller function
- Local variables of the function
The size of the return address and base pointer is 4 bytes for 32-bit architecture and 8 bytes for 64-bit architecture.
#include
int sum(int a, int b)
{
return a + b;
}
float avg(int a, int b)
{
int s = sum(a, b);
return (float)s / 2;
}
int main()
{
int a = 10;
int b = 20;
printf("Average of %d, %d = %f\n", a, b, avg(a, b));
return 0;
}
Below is a comprehensive illustration of how the stack memory would look like as we execute the above C program.
The frame that is being executed is always the topmost frame of the stack. The pointer to the top-most frame in the stack is called the Frame Pointer or Base Pointer. The Base Pointer stores the starting address in callee’s stack frame where the caller’s base pointer value is copied.
The pointer to the top of the stack is called the Stack Pointer. Stack Pointer stores the address of the top of the stack memory.
The stack memory has automatic memory management for both allocation and de-allocation. The programmer has no control over the memory of the stack. When constructing a stack-frame, the local variable of the function is allocated and de-allocated when the stack-frame is about to pop up from the stack segment. This also defines the scope of a variable.
Stack Error Conditions
Let’s take a look at what errors we can face when dealing with the stack.
Stack Overflow
This is an error when a program has a long sequence of function calls, and the program stack expands past the full fixed size, resulting in a stack overflow.
What causes stack overflow condition:
- Recursive function calls
- Declaration of large arrays
Stack Memory has a limited size and thus it is not recommended to store large objects.
Stack Corruption
Stack corruption is a condition in which we corrupt the stack data by copying more data than the actual memory capacity.
Example:
#include
#include
int copy(char *argv)
{
char name[10];
strcpy(name, argv);
}
int main(int argc, char **argv)
{
copy(argv[1]);
printf("Exit\n");
return 0;
}
There is a copy function in the above code where a name array of 10 bytes of the char data type has been specified. And we’re copying data from the argument on the command line. If the user passes a string with a size larger than 10 bytes, the stack frame will overwrite another block and this will lead to stack corruption.
Heap
As we’ve seen, the stack has a limited size that doesn’t allow us to work with big data, and we don’t have control over it. This problem is solved by the Heap memory, a continuous part of virtual address space where the allocation and de-allocation of memory can be performed in real-time.
Unlike stack memory there is no such automatic memory management and the allocation and de-allocation of heap memory is the primary responsibility of the programmer.
To harness the heap memory, we need the Glibc API, which provides the functions to allocate and de-allocate the heap memory.
The malloc()
/calloc()
function is used to assign a memory block from the heap segment and the free()
function is used to restore the memory to the heap segment that was assigned by the malloc()
/calloc()
function.
Under the hood, the malloc()
and calloc()
functions use the brk()
and sbrk()
system calls to allocate and de-allocate the heap memory for a process.
These functions malloc, calloc, realloc, and free are defined in the header file, stdlib.h
.
One factor to keep in mind is that we can only use pointers to address a heap memory block.
Now let’s see an example of how the heap memory is allocated and de-allocated.
#include
#include
int func()
{
int a = 10;
int *aptr = &a;
int *ptr = (int *)malloc(sizeof(int));
*ptr = 20;
printf("Heap Memory Value = %d\n", *ptr);
printf("Pointing in Stack = %d\n", *aptr);
free(ptr);
}
int main()
{
func();
return 0;
}
The image above is a simple description of how a heap of memory is accessed using a malloc() function call. The picture indicates that the value of integer 20 is stored in the 4 Byte of heap area allocated by the malloc() function, but that is not really true. The value is actually stored in the physical memory, i.e. the RAM, the virtual address of the heap segment is converted to the physical address using the MMU (Memory Management Unit), and the value is written or accessed.
The heap memory block has no scope, so the programmer has to manually free the reserved space from the heap.
Hope you like it.
Learn more interesting stuff.
nice work. love the content , must have been a lot of hardwork . I appreciate your work.keep up the good work
Really good, i would non-cautiously say this is the best blog in the net regarding memory layout in c, i’ll make sure to point any one needed an understanding here,
Appreciable work…Thank you