Memory Layout of a C Program Read it later

4.5/5 - (6 votes)

As a programmer, understanding memory layout is a fundamental concept that one must grasp to develop efficient and reliable programs, so understanding the Memory Layout of a C Program and the Memory Layout of a Process becomes essential.

High-level languages like Java, Python, and C# partially manage memory themselves through their Garbage Collector, which actively deallocates and frees allocated memory when not in use. But there is no such garbage collector in C & C++, so the programmer must manually release the allocated memory.

The C program is first compiled and translated to an executable object file. When you run the executable, the CPU executes the instructions, while taking up the main memory area – RAM.

If you don’t know how to compile a C program from source to binary, I recommend reading about the C Program Compilation Process.

What is Memory Layout in C Program?

Memory layout is the process of allocating and organizing data in the memory of a computer system. As a computer loads a program into memory for execution, it divides the memory into several regions, with each region reserved for a specific purpose, such as accessing data and instructions.

The Typical Memory Layout of a C Program consists of the 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 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 of C Program

The Static Memory layout consists of three segments, Text/Code segment, Initialized, and Uninitialized (bss) Data Segment.

The compiler generates the final executable object file for a C program with the three segments of the Static Memory Layout – the Text/Code segment, the Initialized Data segment, and the Uninitialized Data (BSS) segment.

These segments are then directly copied to the main memory layout when the program is loaded into memory. This process ensures that the program can execute efficiently and use memory effectively.

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

The program stores its executable code in the Text/Code segment, which is also known as “read-only” memory (ROM) since the code cannot be modified during program execution.

The processor executes the machine instructions that are contained in this segment, and it is typically located at the lowest address of the memory space.

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

When we write a C program, the compiler allocates space in the Initialized Data segment to store the initial values of global and static variables that we initialize with a value.

As a result, this segment is where we can find the values of these variables when the compiler loads our program into memory.

By understanding how this process works, we can write more efficient programs that use memory effectively and avoid memory-related bugs.

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 the size of the data segment was 544 bytes and after initializing global variables it increased to 580 bytes.

Uninitialized Data Segment (BSS)

When a C program does not initialize global and static variables with a value, the compiler allocates space in the Uninitialized Data Segment, also known as the “bss” segment. This segment is named after an old assembly operator that stands for “block started by the symbol”.

By default, the initial value of these variables is set to zero. The Uninitialized Data Segment is placed above the Data Segment in the memory layout, making it easy for the program to access these variables.

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 of C Program

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.

In simple terms, the stack is a memory region within a process’s virtual address space. It follows the Last-in-First-out (LIFO) order to add or remove data.

When a program invokes a new function, it adds a new stack-frame to the stack memory, and it removes the corresponding stack-frame 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. When a process terminates, the OS actively reclaims the stack memory. This means that the memory used by the stack is not held indefinitely and is released by the OS. This is important because it ensures that the memory is available for use by other processes.

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 <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/resource.h>
int main()
{
    struct rlimit lim;
    if(getrlimit(RLIMIT_STACK,&lim)==0)
    {
   	 printf("Soft Limit = %ldn",lim.rlim_cur);
         printf("Max Stack Size = %ldn",lim.rlim_max);
    }
    else
        printf("%sn", 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 C Program

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 = %fn", 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 topmost frame of the stack always executes, and it stores the pointer to the base pointer, known as the Frame Pointer or Base Pointer. The Base Pointer copies the caller’s base pointer value and stores it in the starting address of the callee’s stack frame.

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 function allocates and deallocates its local variable when it 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

Since the Stack has a limited memory size, it is advisable to avoid storing 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.

To solve this problem, Heap memory provides a continuous part of virtual address space where programs can perform memory allocation and deallocation 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.

Memory Layout C Heap Program Example

To fully grasp the concept of memory allocation and deallocation in the heap, let’s explore an example. This will help you gain a better understanding of how the process works in practice.

#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 = %dn", *ptr);
    printf("Pointing in Stack = %dn", *aptr);
    free(ptr);
}
int main()
{
    func();
    return 0;
}
Heap Memory Layout
Heap Layout

The picture in the above shows a simple description of accessing a heap of memory using a malloc() function call. It is important to understand that although the malloc() function allocates 4 bytes of heap area, it does not directly store the value of integer 20.

In reality, the program stores the value in physical memory, i.e., the RAM. The MMU (Memory Management Unit) converts the virtual address of the heap segment to the physical address, and the program writes or accesses the value from there.

The heap memory block has no scope, so the programmer has to manually free the reserved space from the heap.

Wrapping Up

Memory layout is an important concept to understand in C programming. By understanding how variables and data structures are stored in memory, we can write more efficient and bug-free programs. In this blog, we explored the different segments of memory, the stack and heap, and how variables are stored in memory. We also looked at code examples and discussed important functions and parameters.

Reference:

Was This Article Helpful?

3 Comments

  1. 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,

Leave a Reply

Your email address will not be published. Required fields are marked *