Memory Layout of a C Program

Memory Layout of a C Program
Share this blog with others!
  • 81
    Shares

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 following segments:

  1. Command Line Arguments
  2. Stack
  3. Heap
  4. Uninitialized Data Segment (BSS)
  5. Initialized Data Segment
  6. Text/Code Segment
Memory Layout of C Program
Memory Layout

The above layout segments can be broadly classified into two:

  1. Static Memory Layout – Text/Code, Data Segments
  2. Dynamic Memory Layout – Stack & Heap

The C Program executable already contain some of the segments, and some are built dynamically at runtime.

First Let’s Discuss about each segments 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<stdio.h>
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 <main>:
	1129:    f3 0f 1e fa     		 endbr64
	112d:    55              		 push   %rbp
	112e:    48 89 e5        		 mov	%rsp,%rbp
	1131:    b8 00 00 00 00  		 mov	$0x0,%eax
	1136:    5d              		 pop	%rbp
	1137:    c3              		 retq   
	1138:    0f 1f 84 00 00 00 00     nopl   0x0(%rax,%rax,1)
	113f:    00

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 the 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<stdio.h>
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 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 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<stdio.h>
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.

Memory Management in C
Memory Management in C

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/<pid>/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<stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h> 
#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:

  1. Parameters passed to the function (Reverse Order)
  2. The return address of the caller function.
  3. The base pointer of the caller function
  4. 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 <stdio.h>
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.

Stack Layout
Stack Layout

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.

Pointer to the top of 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:

  1. Recursive function calls
  2. 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 <stdio.h>
#include <string.h>
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 how the heap memory is allocated and de-allocated.

#include <stdio.h>
#include <stdlib.h>
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;
}
Heap Memory Layout
Heap Layout

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 stuffs.

Memory Layout of a C Program

Memory Layout of a C Program

Share this blog with others!81SharesMemory Management is one of the most important topics for a Programmer, and so understanding the Memory Layout of a C…
Read More

Share this blog with others!
  • 81
    Shares
5 1 vote
Article Rating
Subscribe
Notify of
guest
1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
shivam kumar
shivam kumar
10 days ago

nice work. love the content , must have been a lot of hardwork . I appreciate your work.keep up the good work

1
0
Would love your thoughts, please comment.x
()
x