1. Overview

In Linux, we can analyze the memory layout of a process by utilizing system-provided commands and virtual files. This analysis can help us better understand how the kernel loads a program in memory and where its variables are allocated. Further, having this knowledge can be useful for troubleshooting purposes.

In this tutorial, we’ll explore two methods for getting the value of a memory address in Linux. The first method uses the [/proc/[pid]/maps*](/linux/proc-id-maps) and [*/proc/[pid]/mem](https://man7.org/linux/man-pages/man5/proc.5.html) virtual files in the /proc pseudo-filesystem, while the second method uses the GNU Debugger**.

2. Sample Program

First, we’ll create a simple C program that we’ll execute to analyze its memory utilization:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char* argv[]) {
    char *str=malloc(sizeof(char));
    strcpy(str, "Hello World!!!");
    getchar();
}

Here, we dynamically allocated a char array using the malloc function. Next, we copied the string Hello World!!! using the strcpy() function. Finally, we blocked the program’s execution with the getchar() function, so that we have time to examine its memory.

Another key point is that the character array is allocated in the process’s heap space due to its dynamic size. As a result, our task involves searching the heap for the Hello World!!! string.

Next, let’s save the above code in a file named testmem.c file, compile it and execute it:

$ gcc testmem.c -o testmem
$ ./testmem

We used the -o option to give a custom name to the executable that we produce. Next, let’s open another terminal session to find the process ID of our executable:

$ ps aux | grep testmem
24487 ubuntu ./testmem
24586 ubuntu grep --color=auto testmem

As we can see, our process has a process identifier (PID) of 24487.

3. The /proc/[pid]/maps File

The file /proc/[pid]/maps contains a list of the virtual memory regions used by a process with the identifier pid. To further illustrate this example, our process is assigned an identifier value of 24487. Therefore, we can print the corresponding maps file to find the memory addresses used:

$ cat /proc/24487/maps
558f541fb000-558f541fc000 r--p 00000000 08:01 570000 /home/user/article22/testmem
...
558f54a6e000-558f54a8f000 rw-p 00000000 00:00 0 [heap]
7f203cdce000-7f203cdf0000 r--p 00000000 08:01 4694 /usr/lib/x86_64-linux-gnu/libc-2.31.so
...
7f203cfbc000-7f203cfc2000 rw-p 00000000 00:00 0
7f203cfc9000-7f203cfca000 r--p 00000000 08:01 4690 /usr/lib/x86_64-linux-gnu/ld-2.31.so
...
7f203cff8000-7f203cff9000 rw-p 00000000 00:00 0
7ffc5843b000-7ffc5845c000 rw-p 00000000 00:00 0 [stack]
7ffc585b2000-7ffc585b5000 r--p 00000000 00:00 0 [vvar]
7ffc585b5000-7ffc585b6000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]

In the presented output, the first column is a range of memory addresses, while the last column indicates how the respective memory address range is used. Specifically, the heap keyword in this column means that the given row corresponds to the heap memory of our program.

Therefore, the example above shows memory addresses occupied by the heap span from 558f54a6e000 to 558f54a8f000. So, we can infer that the Hello World!!! value is located somewhere within this address range.

4. The /proc/[pid]/mem File

The /proc/[pid]/mem file contains the virtual memory pages of a process. The file supports the open(), read(), and lseek() calls. Our objective is to identify the heap addresses that we’ve discovered in the maps file of the previous section and print their contents to the standard output. Therefore, we’ll use the starting address of the heap as an offset to read the file.

We can employ the dd command with its skip option to copy a specific portion of the file. Before proceeding, let’s convert the hexadecimal address value that we got from the maps file into decimal format:

$ echo $((16#558f54a6e000))
94074088906752

Here, we use the arithmetic expansion double parentheses construct to perform an arithmetic evaluation. The number before the # character indicates the arithmetic base. Now, we assign this decimal value to the skip option of the dd command:

$ sudo dd bs=1 skip="94074088906752" count=1024 if="/proc/24487/mem" status=none | hexdump -C
00000000  00 00 00 00 00 00 00 00  91 02 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000290  00 00 00 00 00 00 00 00  21 00 00 00 00 00 00 00  |........!.......|
000002a0  48 65 6c 6c 6f 20 57 6f  72 6c 64 21 21 21 00 00  |Hello World!!!..|
000002b0  00 00 00 00 00 00 00 00  51 0d 02 00 00 00 00 00  |........Q.......|
000002c0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000400

As we can see, we used the dd command to extract 1024 bytes of data, while skipping content until reaching the heap’s memory starting address. In addition, we set the block size to 1 byte with the bs option, set the input file with the option if, and disabled any status information printings with the status=none option.

The command’s output is then piped to the hexdump command that displays the contents of the heap on the standard output. The -C option stands for canonical hex with ASCII display.

As a result, we see that the value Hello World!!! assigned to the str character array is printed. Notably, this value is stored after 2A0 bytes within the range of the heap address, specifically at the virtual memory address 558f54a6e2a0.

5. Using GDB

The second method that we’ll look at for getting the value of a memory address uses the GDB debugger. To begin, we have to attach the debugger to the running process and subsequently execute the relevant commands. For our purposes, we’ll use the running program that we’ve previously created.

5.1. Getting the Value of a Known Memory Address

In this example, we’ll use the memory address that we found in the previous section while describing the /proc/[pid]/maps and /proc/[pid]/mem files. We can use the x command to examine the value of the memory address:

$ sudo gdb -pid=24487
...
(gdb) x/14c 0x558f54a6e2a0
0x558f54a6e2a0: 72 'H'  101 'e' 108 'l' 108 'l' 111 'o' 32 ' '  87 'W'  111 'o'
0x558f54a6e2a8: 114 'r' 108 'l' 100 'd' 33 '!'  33 '!'  33 '!'

Indeed, after using -pid to attach the debugger to the running process, we see that the string value Hello World!!! resides at 0x558f54a6e2a0.

A key point is that we use two parameters in the x command. The first parameter is the number of bytes to print after the defined address, and the second is the format of the values. In this case, 14c means that we intend to print 14 bytes formatted as characters (c).

5.2. Getting the Mapped Address Space

To display the memory address space of a process with GDB, similar to how we printed the /proc/[pid]/maps file, we can use the info proc map GDB command:

(gdb) info proc map
process 24487
Mapped address spaces:
          Start Addr           End Addr       Size     Offset objfile
      0x558f541fb000     0x558f541fc000     0x1000        0x0 /home/ubuntu/article22/testmem
      ...
      0x558f54a6e000     0x558f54a8f000    0x21000        0x0 [heap]
      0x7f203cdce000     0x7f203cdf0000    0x22000        0x0 /usr/lib/x86_64-linux-gnu/libc-2.31.so
      ...
      0x7f203cfbc000     0x7f203cfc2000     0x6000        0x0
      0x7f203cfc9000     0x7f203cfca000     0x1000        0x0 /usr/lib/x86_64-linux-gnu/ld-2.31.so
     ...
      0x7f203cff8000     0x7f203cff9000     0x1000        0x0
      0x7ffc5843b000     0x7ffc5845c000    0x21000        0x0 [stack]
      0x7ffc585b2000     0x7ffc585b5000     0x3000        0x0 [vvar]
      0x7ffc585b5000     0x7ffc585b6000     0x1000        0x0 [vdso]
  0xffffffffff600000 0xffffffffff601000     0x1000        0x0 [vsyscall]

Indeed, we can see that the output is the same as the /proc/[pid]/maps file.

5.3. Searching for a Value

Another feature of GDB is that we can search a range of memory addresses to find values based on a search pattern. We can do this kind of search with the GDB find command.

There are two ways to define the search space:

  • provide a start and an end memory address
  • specify a start address together with the length of bytes to search

Moreover, the search pattern itself can incorporate a type which can be a string value:

(gdb) find 0x558f54a6e000, +1024,  {char[14]}"Hello World!!!"
0x558f54a6e2a0
1 pattern found.

Here, we can see that GDB found the Hello World!!! string and printed the memory address that contains it. In particular, we searched a memory region of 1024 bytes, starting from the 558f54a6e000 address upwards (+), for our 14-character string. This space covers a part of the heap space used by the process.

6. Conclusion

In this article, we explored two approaches for getting the value of a memory address. In the first approach, we used the /proc/[pid]/maps and /proc/[pid]/mem files, while in the second we used GDB. To facilitate our testing, we developed a small C program that allocated a dynamic character array in the heap.