
Probing dynamic memory layout
The dynamic memory layout is actually the runtime memory of a process, and it exists as long as the process is running. When you execute an executable object file, a program called loader takes care of the execution. It spawns a new process and it creates the initial memory layout which is supposed to be dynamic. To form this layout, the segments found in the static layout will be copied from the executable object file. More than that, two new segments will also be added to it. Only then can the process proceed and become running.
In short, we expect to have five segments in the memory layout of a running process. Three of these segments are directly copied from the static layout found in the executable object file. The two newly added segments are called Stack and Heap segments. These segments are dynamic, and they exist only when the process is running. This means that you cannot find any trace of them as part of the executable object file.
In this section, our ultimate goal is to probe the Stack and Heap segments and introduce tools and places in an operating system which can be used for this purpose. From time to time, we might refer to these segments as the process's dynamic memory layout, without considering the other three segments copied from the object file, but you should always remember that the dynamic memory of a process consists of all five segments together.
The Stack segment is the default memory region where we allocate variables from. It is a limited region in terms of size, and you cannot hold big objects in it. In contrast, the Heap segment is a bigger and adjustable region of memory which can be used to hold big objects and huge arrays. Working with the Heap segment requires its own API which we introduce as part of our discussion.
Remember, dynamic memory layout is different from Dynamic Memory Allocation. You should not mix these two concepts, since they are referring to two different things! As we progress, we'll learn more about different types of memory allocations, especially dynamic memory allocation.
The five segments found in the dynamic memory of a process are referring to parts of the main memory that are already allocated, dedicated, and private to a running process. These segments, excluding the Text segment, which is literally static and constant, are dynamic in a sense that their contents are always changing at runtime. That's due to the fact that these segments are constantly being modified by the algorithm that the process is executing.
Inspecting the dynamic memory layout of a process requires its own procedure. This implies that we need to have a running process before being able to probe its dynamic memory layout. This requires us to write examples which remain running for a fairly long time in order to keep their dynamic memory in place. Then, we can use our inspection tools to study their dynamic memory structure.
In the following section, we give an example on how to probe the structure of dynamic memory.
Memory mappings
Let's start with a simple example. Example 4.6 will be running for an indefinite amount of time. This way, we have a process that never dies, and in the meantime, we can probe its memory structure. And of course, we can kill it whenever we are done with the inspection. You can find the example in the following code box:
#include <unistd.h> // Needed for sleep function
int main(int argc, char** argv) {
// Infinite loop
while (1) {
sleep(1); // Sleep 1 second
};
return 0;
}
Code Box 4-6 [ExtremeC_examples_chapter4_6.c]: Example 4.6 used for probing dynamic memory layout
As you see, the code is just an infinite loop, which means that the process will run forever. So, we have enough time to inspect the process's memory. Let's first build it.
Note:
The unistd.h
header is available only on Unix-like operating systems; to be more precise, in POSIX-compliant operating systems. This means that on Microsoft Windows, which is not POSIX-compliant, you have to include the windows.h
header instead.
The following shell box shows how to compile the example in Linux:
$ gcc ExtremeC_examples_chapter4_6.c -o ex4_6.out
$
Shell Box 4-12: Compiling example 4.6 in Linux
Then, we run it as follows. In order to use the same prompt for issuing further commands while the process is running, we should start the process in the background:
$ ./ ex4_6.out &
[1] 402
$
Shell Box 4-13: Running example 4.6 in the background
The process is now running in the background. According to the output, the PID of the recently started process is 402, and we will use this PID to kill it in the future. The PID is different every time you run a program; therefore, you'll probably see a different PID on your computer. Note that whenever you run a process in the background, the shell prompt returns immediately, and you can issue further commands.
Note:
If you have the PID (Process ID) of a process, you can easily end it using the kill
command. For example, if the PID is 402, the following command will work in Unix-like operating systems: kill -9 402
.
The PID is the identifier we use to inspect the memory of a process. Usually, an operating system provides its own specific mechanism to query various properties of a process based on its PID. But here, we are only interested in the dynamic memory of a process and we'll use the available mechanism in Linux to find more about the dynamic memory structure of the above running process.
On a Linux machine, the information about a process can be found in files under the /proc
directory. It uses a special filesystem called procfs. This filesystem is not an ordinary filesystem meant for keeping actual files, but it is more of a hierarchical interface to query about various properties of an individual process or the system as a whole.
Note:
procfs is not limited to Linux. It is usually part of Unix-like operating systems, but not all Unix-like operating systems use it. For example, FreeBSD uses this filesystem, but macOS doesn't.
Now, we are going to use procfs to see the memory structure of the running process. The memory of a process consists of a number of memory mappings. Each memory mapping represents a dedicated region of memory which is mapped to a specific file or segment as part of the process. Shortly, you'll see that both Stack and Heap segments have their own memory mappings in each process.
One of the things that you can use procfs for is to observe the current memory mappings of the process. Next, we are going to show this.
We know that the process is running with PID 402. Using the ls
command, we can see the contents of the /proc/402
directory, shown as follows:
$ ls -l /proc/402
total of 0
dr-xr-xr-x 2 root root 0 Jul 15 22:28 attr
-rw-r--r-- 1 root root 0 Jul 15 22:28 autogroup
-r-------- 1 root root 0 Jul 15 22:28 auxv
-r--r--r-- 1 root root 0 Jul 15 22:28 cgroup
--w------- 1 root root 0 Jul 15 22:28 clear_refs
-r--r--r-- 1 root root 0 Jul 15 22:28 cmdline
-rw-r--r-- 1 root root 0 Jul 15 22:28 comm
-rw-r--r-- 1 root root 0 Jul 15 22:28 coredump_filter
-r--r--r-- 1 root root 0 Jul 15 22:28 cpuset
lrwxrwxrwx 1 root root 0 Jul 15 22:28 cwd -> /root/codes
-r-------- 1 root root 0 Jul 15 22:28 environ
lrwxrwxrwx 1 root root 0 Jul 15 22:28 exe -> /root/codes/a.out
dr-x------ 2 root root 0 Jul 15 22:28 fd
dr-x------ 2 root root 0 Jul 15 22:28 fdinfo
-rw-r--r-- 1 root root 0 Jul 15 22:28 gid_map
-r-------- 1 root root 0 Jul 15 22:28 io
-r--r--r-- 1 root root 0 Jul 15 22:28 limits
...
$
Shell Box 4-14: Listing the content of /proc/402
As you can see, there are many files and directories under the /proc/402
directory. Each of these files and directories corresponds to a specific property of the process. For querying the memory mappings of the process, we have to see the contents of the file maps
under the PID directory. We use the cat
command to dump the contents of the /proc/402/maps
file. It can be seen as follows:
$ cat /proc/402/maps
00400000-00401000 r-xp 00000000 08:01 790655 .../extreme_c/4.6/ex4_6.out
00600000-00601000 r--p 00000000 08:01 790655 .../extreme_c/4.6/ex4_6.out
00601000-00602000 rw-p 00001000 08:01 790655 .../extreme_c/4.6/ex4_6.out
7f4ee16cb000-7f4ee188a000 r-xp 00000000 08:01 787362 /lib/x86_64-linux-gnu/libc-2.23.so
7f4ee188a000-7f4ee1a8a000 ---p 001bf000 08:01 787362 /lib/x86_64-linux-gnu/libc-2.23.so
7f4ee1a8a000-7f4ee1a8e000 r--p 001bf000 08:01 787362 /lib/x86_64-linux-gnu/libc-2.23.so
7f4ee1a8e000-7f4ee1a90000 rw-p 001c3000 08:01 787362 /lib/x86_64-linux-gnu/libc-2.23.so
7f4ee1a90000-7f4ee1a94000 rw-p 00000000 00:00 0
7f4ee1a94000-7f4ee1aba000 r-xp 00000000 08:01 787342 /lib/x86_64-linux-gnu/ld-2.23.so
7f4ee1cab000-7f4ee1cae000 rw-p 00000000 00:00 0
7f4ee1cb7000-7f4ee1cb9000 rw-p 00000000 00:00 0
7f4ee1cb9000-7f4ee1cba000 r--p 00025000 08:01 787342 /lib/x86_64-linux-gnu/ld-2.23.so
7f4ee1cba000-7f4ee1cbb000 rw-p 00026000 08:01 787342 /lib/x86_64-linux-gnu/ld-2.23.so
7f4ee1cbb000-7f4ee1cbc000 rw-p 00000000 00:00 0
7ffe94296000-7ffe942b7000 rw-p 00000000 00:00 0 [stack]
7ffe943a0000-7ffe943a2000 r--p 00000000 00:00 0 [vvar]
7ffe943a2000-7ffe943a4000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
$
Shell Box 4-15: Dumping the content of /proc/402/maps
As you see in Shell Box 4-15, the result consists of a number of rows. Each row represents a memory mapping that indicates a range of memory addresses (a region) that are allocated and mapped to a specific file or segment in the dynamic memory layout of the process. Each mapping has a number of fields separated by one or more spaces. Next, you can find the descriptions of these fields from left to right:
- Address range: These are the start and end addresses of the mapped range. You can find a file path in front of them if the region is mapped to a file. This is a smart way to map the same loaded shared object file in various processes. We have talked about this as part of Chapter 3, Object Files.
- Permissions: This indicates whether the content can be executed (
x
), read (r
), or modified (w
). The region can also be shared (s
) by the other processes or be private (p
) only to the owning process. - Offset: If the region is mapped to a file, this is the offset from the beginning of the file. It is usually 0 if the region is not mapped to a file.
- Device: If the region is mapped to a file, this would be the device number (in the form of m:n), indicating a device that contains the mapped file. For example, this would be the device number of the hard disk that contains a shared object file.
- The inode: If the region is mapped to a file, that file should reside on a filesystem. Then, this field would be the inode number of the file in that filesystem. An inode is an abstract concept within filesystems such as ext4 which are mostly used in Unix-like operating systems. Each inode can represent both files and directories. Every inode has a number that is used to access its content.
- Pathname or description: If the region is mapped to a file, this would be the path to that file. Otherwise, it would be left empty, or it would describe the purpose of the region. For example,
[stack]
indicates that the region is actually the Stack segment.
The maps
file provides even more useful information regarding the dynamic memory layout of a process. We'll need a new example to properly demonstrate this.
Stack segment
First, let's talk more about the Stack segment. The Stack is a crucial part of the dynamic memory in every process, and it exists in almost all architectures. You have seen it in the memory mappings described as [stack]
.
Both Stack and Heap segments have dynamic contents which are constantly changing while the process is running. It is not easy to see the dynamic contents of these segments and most of the time you need a debugger such as gdb
to go through the memory bytes and read them while a process is running.
As pointed out before, the Stack segment is usually limited in size, and it is not a good place to store big objects. If the Stack segment is full, the process cannot make any further function calls since the function call mechanism relies heavily on the functionality of the Stack segment.
If the Stack segment of a process becomes full, the process gets terminated by the operating system. Stack overflow is a famous error that happens when the Stack segment becomes full. We discuss the function call mechanism in future paragraphs.
As explained before, the Stack segment is a default memory region that variables are allocated from. Suppose that you've declared a variable inside a function, as follows:
void func() {
// The memory required for the following variable is
// allocated from the stack segment.
int a;
...
}
Code Box 4-7: Declaring a local variable which has its memory allocated from the Stack segment
In the preceding function, while declaring the variable, we have not mentioned anything to let the compiler know which segment the variable should be allocated from. Because of this, the compiler uses the Stack segment by default. The Stack segment is the first place that allocations are made from.
As its name implies, it is a stack. If you declare a local variable, it becomes allocated on top of the Stack segment. When you're leaving the scope of the declared local variable, the compiler has to pop the local variables first in order to bring up the local variables declared in the outer scope.
Note:
Stack, in its abstract form, is a First In, Last Out (FILO) or Last In, First Out (LIFO) data structure. Regardless of the implementation details, each entry is stored (pushed) on top of the stack, and it will be buried by further entries. One entry cannot be popped out without removing the above entries first.
Variables are not the only entities that are stored in the Stack segment. Whenever you make a function call, a new entry called a stack frame is placed on top of the Stack segment. Otherwise, you cannot return to the calling function or return the result back to the caller.
Having a healthy stacking mechanism is vital to have a working program. Since the size of the Stack is limited, it is a good practice to declare small variables in it. Also, the Stack shouldn't be filled by too many stack frames as a result of making infinite recursive calls or too many function calls.
From a different perspective, the Stack segment is a region used by you, as a programmer, to keep your data and declare the local variables used in your algorithms, and by the operating system, as the program runner, to keep the data needed for its internal mechanisms to execute your program successfully.
In this sense, you should be careful when working with this segment because misusing it or corrupting its data can interrupt the running process or even make it crash. The Heap segment is the memory segment that is only managed by the programmer. We will cover the Heap segment in the next section.
It is not easy to see the contents of the Stack segment from outside if we are only using the tools we've introduced for probing the static memory layout. This part of memory contains private data and can be sensitive. It is also private to the process, and other processes cannot read or modify it.
So, for sailing through the Stack memory, one has to attach something to a process and see the Stack segment through the eyes of that process. This can be done using a debugger program. A debugger attaches to a process and allows a programmer to control the target process and investigate its memory content. We will use this technique and examine the Stack memory in the following chapter. For now, we leave the Stack segment to discuss more about the Heap segment. We will get back to the Stack in the next chapter.
Heap segment
The following example, example 4.7, shows how memory mappings can be used to find regions allocated for the Heap segment. It is quite similar to example 4.6, but it allocates a number of bytes from the Heap segment before entering the infinite loop.
Therefore, just like we did for example 4.6, we can go through the memory mappings of the running process and see which mapping refers to the Heap segment.
The following code box contains the code for example 4.7:
#include <unistd.h> // Needed for sleep function
#include <stdlib.h> // Needed for malloc function
#include <stdio.h> // Needed for printf
int main(int argc, char** argv) {
void* ptr = malloc(1024); // Allocate 1KB from heap
printf("Address: %p\n", ptr);
fflush(stdout); // To force the print
// Infinite loop
while (1) {
sleep(1); // Sleep 1 second
};
return 0;
}
Code Box 4-8 [ExtremeC_examples_chapter4_7.c]: Example 4.7 used for probing the Heap segment
In the preceding code, we used the malloc
function. It's the primary way to allocate extra memory from the Heap segment. It accepts the number of bytes that should be allocated, and it returns a generic pointer.
As a reminder, a generic pointer (or a void pointer) contains a memory address but it cannot be dereferenced and used directly. It should be cast to a specific pointer type before being used.
In example 4.7, we allocate 1024 bytes (or 1KB) before entering the loop. The program also prints the address of the pointer received from malloc
before starting the loop. Let's compile the example and run it as we did for example 4.7:
$ g++ ExtremeC_examples_chapter4_7.c -o ex4_7.out
$ ./ex4_7.out &
[1] 3451
Address: 0x19790010
$
Shell Box 4-16: Compiling and running example 4.7
Now, the process is running in the background, and it has obtained the PID 3451.
Let's see what memory regions have been mapped for this process by looking at its maps
file:
$ cat /proc/3451/maps
00400000-00401000 r-xp 00000000 00:2f 176521 .../extreme_c/4.7/ex4_7.out
00600000-00601000 r--p 00000000 00:2f 176521 .../extreme_c/4.7/ex4_7.out
00601000-00602000 rw-p 00001000 00:2f 176521 .../extreme_c/4.7/ex4_7.out
01979000-0199a000 rw-p 00000000 00:00 0 [heap]
7f7b32f12000-7f7b330d1000 r-xp 00000000 00:2f 30 /lib/x86_64-linux-gnu/libc-2.23.so
7f7b330d1000-7f7b332d1000 ---p 001bf000 00:2f 30 /lib/x86_64-linux-gnu/libc-2.23.so
7f7b332d1000-7f7b332d5000 r--p 001bf000 00:2f 30 /lib/x86_64-linux-gnu/libc-2.23.so
7f7b332d5000-7f7b332d7000 rw-p 001c3000 00:2f 30 /lib/x86_64-linux-gnu/libc-2.23.so
7f7b332d7000-7f7b332db000 rw-p 00000000 00:00 0
7f7b332db000-7f7b33301000 r-xp 00000000 00:2f 27 /lib/x86_64-linux-gnu/ld-2.23.so
7f7b334f2000-7f7b334f5000 rw-p 00000000 00:00 0
7f7b334fe000-7f7b33500000 rw-p 00000000 00:00 0
7f7b33500000-7f7b33501000 r--p 00025000 00:2f 27 /lib/x86_64-linux-gnu/ld-2.23.so
7f7b33501000-7f7b33502000 rw-p 00026000 00:2f 27 /lib/x86_64-linux-gnu/ld-2.23.so
7f7b33502000-7f7b33503000 rw-p 00000000 00:00 0
7ffdd63c2000-7ffdd63e3000 rw-p 00000000 00:00 0 [stack]
7ffdd63e7000-7ffdd63ea000 r--p 00000000 00:00 0 [vvar]
7ffdd63ea000-7ffdd63ec000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
$
Shell Box 4-17: Dumping the content of /proc/3451/maps
If you look at Shell Box 4-17 carefully, you will see a new mapping which is highlighted, and it is being described by [heap]
. This region has been added because of using the malloc
function. If you calculate the size of the region, it is 0x21000
bytes or 132 KB. This means that to allocate only 1 KB in the code, a region of the size 132 KB has been allocated.
This is usually done in order to prevent further memory allocations when using malloc
again in the future. That's simply because the memory allocation from the Heap segment is not cheap and it has both memory and time overheads.
If you go back to the code shown in Code Box 4-8, the address that the ptr
pointer is pointing to is also interesting. The Heap's memory mapping, shown in Shell Box 4-17, is allocated from the address 0x01979000
to 0x0199a000
, and the address stored in ptr
is 0x19790010
, which is obviously inside the Heap range, located at an offset of 16
bytes.
The Heap segment can grow to sizes far greater than 132 KB, even to tens of gigabytes, and usually it is used for permanent, global, and very big objects such as arrays and bit streams.
As pointed out before, allocation and deallocation within the heap segment require a program to call specific functions provided by the C standard. While you can have local variables on top of the Stack segment, and you can use them directly to interact with the memory, the Heap memory can be accessed only through pointers, and this is one of the reasons why knowing pointers and being able to work with them is crucial to every C programmer. Let's bring up example 4.8, which demonstrates how to use pointers to access the Heap space:
#include <stdio.h> // For printf function
#include <stdlib.h> // For malloc and free function
void fill(char* ptr) {
ptr[0] = 'H';
ptr[1] = 'e';
ptr[2] = 'l';
ptr[3] = 'l';
ptr[5] = 0;
}
int main(int argc, char** argv) {
void* gptr = malloc(10 * sizeof(char));
char* ptr = (char*)gptr;
fill(ptr);
printf("%s!\n", ptr);
free(ptr);
return 0;
}
Code Box 4-9 [ExtremeC_examples_chapter4_8.c]: Using pointers to interact with the Heap memory
The preceding program allocates 10 bytes from the Heap space using the malloc
function. The malloc
function receives the number of bytes that should be allocated and returns a generic pointer addressing the first byte of the allocated memory block.
For using the returned pointer, we have to cast it to a proper pointer type. Since we are going to use the allocated memory to store some characters, we choose to cast it to a char
pointer. The casting is done before calling the fill
function.
Note that the local pointer variables, gptr
and ptr
, are allocated from the Stack. These pointers need memory to store their values, and this memory comes from the Stack segment. But the address that they are pointing to is inside the Heap segment. This is the theme when working with Heap memories. You have local pointers which are allocated from the Stack segment, but they are actually pointing to a region allocated from the Heap segment. We show more of these in the following chapter.
Note that the ptr
pointer inside the fill
function is also allocated from the Stack but it is in a different scope, and it is different from the ptr
pointer declared in the main
function.
When it comes to Heap memory, the program, or actually the programmer, is responsible for memory allocation. The program is also responsible for deallocation of the memory when it is not needed. Having a piece of allocated Heap memory that is not reachable is considered a memory leak. By not being reachable, we mean that there is no pointer that can be used to address that region.
Memory leaks are fatal to programs because having an incremental memory leak will eventually use up the whole allowed memory space, and this can kill the process. That's why the program is calling the free
function before returning from the main
function. The call to the free
function will deallocate the acquired Heap memory block, and the program shouldn't use those Heap addresses anymore.
More on Stack and Heap segments will come in the next chapter.