Home » Linux, OS Concepts and Networking » Operating System Concepts » Understanding C Program Memory Layout: Text, Data, Code, Heap, and Stack Segments Explained

Understanding C Program Memory Layout: Text, Data, Code, Heap, and Stack Segments Explained

A C program’s memory layout is divided into several key segments: text, data, code, heap, and stack. The text segment contains the compiled code and is typically read-only, ensuring that the program’s instructions cannot be modified during execution. The data segment is split into initialized and uninitialized sections; the initialized portion stores global and static variables with defined values, while the uninitialized portion (or .bss segment) holds variables set to zero by default. The heap segment is used for dynamic memory allocation, where memory is managed manually during runtime. The stack segment handles function calls, local variables, and control flow, growing and shrinking as functions execute. Understanding this layout is crucial for effective memory management, preventing errors such as memory leaks or stack overflows.

.bss segment

The .bss segment in a program’s memory layout is a portion of memory used to store uninitialized global and static variables. When a program is loaded into memory, the .bss segment is initialized to zero, which means any variables declared without an initial value in this segment will automatically be set to zero. The .bss segment helps in reducing the size of the executable file because it doesn’t need to store zero-initialized data; instead, it simply records the size of the segment, and the operating system handles the zeroing out during the program’s startup.

 $ vim uninitiailsed_global_variable.c 
#include <stdio.h>
 
int uninitilised_global_variable;
 
int main(int argc, char **argv) {
        return 0;
}
 $ gcc -o uninitiailsed_global_variable uninitiailsed_global_variable.c 
$ size uninitiailsed_global_variable
   text	   data	    bss	    dec	    hex	filename
   1017	    272	      8	   1297	    511	uninitiailsed_global_variable

.data segment

The .data segment contains any global or static variables which have a pre-defined value and can be modified. That is any variables that are not defined within a function (and thus can be accessed from anywhere) or are defined in a function but are defined as static so they retain their address across subsequent calls.

$ vim initiailsed_global_variable.c 
#include <stdio.h>
 
int initilised_global_variable = 78;
 
int main(int argc, char **argv) {
        printf("address of : initilised_global_variable = %p\n", &amp;initilised_global_variable);
        return 0;
}
$ size initiailsed_global_variable
   text	   data	    bss	    dec	    hex	filename
   1156	    280	      4	   1440	    5a0	initiailsed_global_variable

Now lets run this program to check, what is the address assigned to our initiailsed global integer variable as,

 $ address of : initilised_global_variable = 0x804a01c 

Now, lets check using nm tool what is the predefined address space our program got from the compiler as,

$ nm -s a.out 
0804a020 B __bss_start
0804a020 b completed.7200
0804a014 D __data_start
0804a014 W data_start
08048350 t deregister_tm_clones
080483c0 t __do_global_dtors_aux
08049f0c t __do_global_dtors_aux_fini_array_entry
0804a018 D __dso_handle
08049f14 d _DYNAMIC
0804a020 D _edata
0804a024 B _end
080484a4 T _fini
080484b8 R _fp_hw
080483e0 t frame_dummy
08049f08 t __frame_dummy_init_array_entry
080485e4 r __FRAME_END__
0804a000 d _GLOBAL_OFFSET_TABLE_
         w __gmon_start__
080484f0 r __GNU_EH_FRAME_HDR
080482ac T _init
08049f0c t __init_array_end
08049f08 t __init_array_start
0804a01c D initilised_global_variable
080484bc R _IO_stdin_used
         w _ITM_deregisterTMCloneTable
         w _ITM_registerTMCloneTable
08049f10 d __JCR_END__
08049f10 d __JCR_LIST__
         w _Jv_RegisterClasses
080484a0 T __libc_csu_fini
08048440 T __libc_csu_init
         U __libc_start_main@@GLIBC_2.0
0804840b T main
         U printf@@GLIBC_2.0
08048380 t register_tm_clones
08048310 T _start
0804a020 D __TMC_END__
08048340 T __x86.get_pc_thunk.bx

output of nm shows that we got the address for “int initilised_global_variable” i.e. 0x804a01c which is predefined as seen in “nm -s” in bold ( just before _IO_stdin_used in above nm output ) and after data segment address has started at “0804a014 D __data_start”

 $ vim local_initialised_and_uninitialised_variable.c 
#include <stdio.h>
 
int main(int argc, char **argv) {
        int local_initialised_variable = 78;
        int local_variable;
        return 0;
}
$ gcc -o local_initialised_and_uninitialised_variable local_initialised_and_uninitialised_variable.c 
$ size local_initialised_and_uninitialised_variable
   text	   data	    bss	    dec	    hex	filename
   1017	    272	      4	   1293	    50d	local_initialised_and_uninitialised_variable

.stack segment

The .stack segment in a program’s memory layout is a dynamic area used for managing function calls, local variables, and control flow. When a function is called, a “stack frame” is created on the stack, which stores the function’s local variables, return address, and sometimes function arguments. As functions call other functions, new stack frames are pushed onto the stack, and when a function returns, its stack frame is popped off. The stack grows and shrinks as functions are called and return, making it a Last-In-First-Out (LIFO) structure. The stack segment is crucial for proper execution flow, memory management, and preventing function call-related errors like stack overflow, which occurs when the stack grows beyond its allocated limit.

 $ vim variables_in_stack.c 
#include <execinfo.h>
#include <stdio.h>
#include <stdlib.h>
 
//check cat /proc/pid/maps to know address printed belogs to stack
 
int main(int argc, char **argv) {
        int local_initialised_variable = 78;
        int local_uninitialised_variable;
        printf("Address of : local_initialised_variable = %p\n", &local_initialised_variable);
        printf("Address of : local_uninitialised_variable = %p\n", &local_uninitialised_variable);
 
        //create a infinite loop so we can check address mapped in /proc
        while(1); //this is just to identify where we got the address mapped 
 
        return 0;
}
 $ gcc -o variables_in_stack variables_in_stack.c 
$ ./variables_in_stack 
Address of : local_initialised_variable = 0xbf961c34
Address of : local_uninitialised_variable = 0xbf961c38

Now, lets identify from proc map of a process we created just now to identify which area the addresses we printed belongs to,

know the process Id as program running above from another terminal as,

 $ ps -ax | grep variables_in_stack
 6971 pts/9    R+     1:37 ./variables_in_stack

We can see, we have got 6971 as process Id, now lets check the process map from proc file system as,

 $ cd /proc/6971/ 
 $ ls 
$ sudo cat maps 
08048000-08049000 r-xp 00000000 08:06 13894234   variables_in_stack
08049000-0804a000 r--p 00000000 08:06 13894234   variables_in_stack
0804a000-0804b000 rw-p 00001000 08:06 13894234   variables_in_stack
09859000-0987a000 rw-p 00000000 00:00 0          [heap]
b7539000-b76e9000 r-xp 00000000 08:06 18879084   /lib/i386-linux-gnu/libc-2.23.so
b76e9000-b76eb000 r--p 001af000 08:06 18879084   /lib/i386-linux-gnu/libc-2.23.so
b76eb000-b76ec000 rw-p 001b1000 08:06 18879084   /lib/i386-linux-gnu/libc-2.23.so
b76ec000-b76ef000 rw-p 00000000 00:00 0 
b771b000-b771d000 rw-p 00000000 00:00 0 
b771d000-b771f000 r--p 00000000 00:00 0          [vvar]
b771f000-b7720000 r-xp 00000000 00:00 0          [vdso]
b7720000-b7742000 r-xp 00000000 08:06 18874426   /lib/i386-linux-gnu/ld-2.23.so
b7742000-b7743000 rw-p 00000000 00:00 0 
b7743000-b7744000 r--p 00022000 08:06 18874426   /lib/i386-linux-gnu/ld-2.23.so
b7744000-b7745000 rw-p 00023000 08:06 18874426   /lib/i386-linux-gnu/ld-2.23.so
bf942000-bf963000 rw-p 00000000 00:00 0          [stack]

As we can see above, the address printed by our program are 0xbf961c34 and 0xbf961c38 and the stack memory allocated for this program ranges from bf942000 to bf963000 . Which shows that the local variables resides in the stack memory of a program.

Heap Memory allocation

The .heap segment in a program’s memory layout is a region used for dynamic memory allocation during a program’s runtime. Unlike the stack, which is managed automatically by the system for function calls, the heap is managed manually by the programmer through functions like malloc, calloc, and free in C, or new and delete in C++. The heap grows in size as memory is allocated, and it can shrink when memory is deallocated. Memory allocated on the heap remains available until explicitly freed, making it ideal for data that needs to persist beyond the scope of a single function. However, improper management of heap memory can lead to issues like memory leaks, where memory is allocated but never released, eventually exhausting available memory.

 $ vim memory_allocated_in_heap.c 
#include <execinfo.h>
#include <stdio.h>
#include <stdlib.h>
 
int main(int argc, char **argv) {
        int *memory = malloc(sizeof(int));
        printf("Address of : int memory allocated = %p\n", memory);
        while(1);
        return 0;
}

Subscribe our Rurban Life YouTube Channel.. "Rural Life, Urban LifeStyle"

Leave a Comment