How Linux Tracks Every Byte Your Process Owns
A practical deep-dive into the Virtual Address Space: reading memory maps, understanding inode references, and decoding the special kernel segments every process carries.
What You Will Learn
Every process running on Linux lives inside its own private virtual bubble called the Virtual Address Space (VAS). The kernel keeps a precise map of every region inside that bubble — what it contains, who owns it, and what permissions apply. In this tutorial you will learn exactly how to read that map and understand every field in it.
Quick Recap: The /proc/PID/maps Format
Before diving into the inode and pathname fields, let us quickly revisit the full structure of a single line in /proc/PID/maps. This file gives you the complete memory layout of any running process.
| Field # | Example Value | Meaning |
|---|---|---|
| 1 | 55d83b65000-55d83b68000 | Start – End virtual address range |
| 2 | r-xp | Permissions (read/write/execute/private) |
| 3 | 00000000 | File offset where mapping begins |
| 4 | 08:01 | Device (major:minor) of the file |
| 5 ▶ | 524313 | Inode number of the file (focus of this section) |
| 6 ▶ | /bin/cat | Pathname of the mapped file (focus of this section) |
Fields 5 and 6 (highlighted in green) are what this tutorial section focuses on.
Field 5: The Inode Number — The VFS Identity Card
When the kernel maps a file into memory, it needs to track which exact file is being mapped. It does this using the inode number.
An inode is a data structure inside the Virtual FileSystem (VFS) — Linux’s unified file system layer that works the same way regardless of whether you are using ext4, XFS, Btrfs, or any other filesystem. Every file on your system gets its own inode, and that inode stores all the important metadata about the file: its size, ownership, permissions, timestamps, and the list of disk blocks where the data lives.
⚠ Notice what is NOT in the inode: the filename. Filenames live in directory entries, not in inodes.
The inode number in /proc/PID/maps is only present when the mapping comes from an actual file on disk. For memory regions that have no file backing them — things like the stack, the heap, or anonymous mmap() allocations — this field is simply 0.
Glance at the inode field. Non-zero = file-backed mapping. Zero = anonymous mapping (stack, heap, or pure memory allocation). You do not need to read the pathname at all to make this determination.
Let us verify this with the ls -i command, which prints the inode number of any file:
$ ls -i /bin/cat
524313 /bin/cat
The inode number printed by ls -i matches what appears in the memory map. This is the kernel’s way of saying: “this virtual address range is backed by the data from that exact file on disk.”
| Inode field | 524313 (non-zero) |
| Pathname field | /bin/cat |
| Backing store | File on disk |
| Examples | Text segment, read-only data, shared libraries |
| Inode field | 0 |
| Pathname field | (empty) |
| Backing store | None / swap space |
| Examples | Stack, heap, mmap(MAP_ANONYMOUS) |
Field 6: The Pathname — Which File Is Mapped?
The seventh and final field in the maps output tells you the full path of the file whose contents are loaded into that memory region. For a process like cat, the text segment (where machine code lives) will show /bin/cat as its pathname.
When the inode field is non-zero, the pathname is always a real filesystem path. When the inode is zero, this field is empty — or in some special cases it will contain a label in square brackets like [stack], [heap], or [vdso]. These square-bracket labels are inserted by the kernel to help you understand what a particular anonymous region is actually used for.
| Pathname Value | What It Represents | File-backed? |
|---|---|---|
| /bin/cat | Executable code and data sections of the binary | Yes |
| /lib/x86_64-linux-gnu/libc.so.6 | Shared C library mapped into the process | Yes |
| (empty) | Pure anonymous mapping — heap growth, mmap allocations | No |
| [stack] | The main thread’s user-space stack | No |
| [heap] | The process heap (managed by malloc/free) | No |
| [vdso] | Virtual Dynamic Shared Object (kernel helper) | No |
| [vvar] | Read-only kernel variables for fast time access | No |
| [vsyscall] | Legacy fast syscall page (x86-64 only) | No |
All Addresses Are Virtual — Why That Matters
Every address you see in /proc/PID/maps is a virtual address, not a physical RAM address. This is a fundamental point worth taking time to understand.
Your CPU does not let programs access physical RAM directly. Instead, each process runs inside its own isolated virtual address space. The kernel, along with the CPU’s Memory Management Unit (MMU), translates every virtual address into a physical one using a set of data structures called page tables. Each process has its own private set of page tables.
0x55d83b65000
Because each process has its own page tables, two different processes can both have a segment starting at the same virtual address (say 0x400000) while pointing to completely different physical memory cells. Their virtual worlds never collide.
The addresses you see are called User Virtual Addresses (UVAs) because they belong to the user-space half of the address space. On a 64-bit x86 system, you will notice that the top 16 bits (the most significant bits) of every UVA are always zero. This is by design — it is how the hardware separates user-space virtual addresses from kernel-space virtual addresses.
On x86-64 Linux, if the top 16 bits of an address are all zero, it is a user-space (UVA) address. If the top 16 bits are all one (i.e., 0xFFFF...), it is a kernel-space virtual address. You can use this rule to instantly tell which side of the address space you are looking at.
# User Virtual Address — top 16 bits are zero
0x0000_55d8_3b65_000 ← this is a UVA (user space)
# Kernel Virtual Address — top 16 bits are all 1s
0xffff_8800_0000_0000 ← this is a KVA (kernel space)
The Special Segments: vsyscall, vvar, and vDSO
When you look at the memory map of any process, you will always see three unusual entries near the very top: [vsyscall], [vvar], and [vdso]. These are not your code and they are not shared libraries you linked against. They are kernel-injected mappings that every process automatically gets.
To understand why they exist, you first need to understand a small problem: system calls are slow.
For syscalls like gettimeofday() that run thousands of times per second, this switching overhead adds up significantly.
The solution is to avoid the mode switch entirely for a small set of syscalls that only need to read kernel data, never write it. The three mappings implement this optimization in two generations:
🔉 [vsyscall] — The First Generation (Legacy)
The [vsyscall] page is a very old optimization introduced in early 64-bit Linux kernels. A small piece of kernel code is mapped at a fixed virtual address so that user-space programs can call it directly without triggering a full system call. On x86-64, this page handles gettimeofday(), time(), and getcpu().
The vsyscall approach has a drawback: because it maps kernel code at a fixed, predictable address in every process, it weakens address space layout randomisation (ASLR). Modern kernels keep it only for backward compatibility.
📄 [vvar] — The Data Side of vDSO
[vvar] is a read-only page that holds kernel variables which change frequently — such as the current time value. Instead of switching to kernel mode to read the time, user-space vDSO code reads it directly from this page. The kernel updates these variables, and user space reads them without any mode switch. Think of [vvar] as a notice board that the kernel writes to and every process reads from.
🌟 [vdso] — The Modern Replacement
The Virtual Dynamic Shared Object (vDSO) is the current, recommended way to implement fast user-space system calls. Unlike the vsyscall page at a fixed address, the vDSO is mapped at a randomised address by the kernel’s ELF loader at process startup. This means it is fully compatible with ASLR. Programs using the standard C library automatically benefit from vDSO without any special coding — the C library selects it transparently.
| Property | [vsyscall] | [vvar] | [vdso] |
|---|---|---|---|
| Purpose | Fast legacy syscalls | Kernel data (read-only) | Modern fast syscalls |
| Address randomised? | No (fixed) | Yes | Yes |
| Contains code? | Yes | No (data only) | Yes |
| ASLR compatible? | No | Yes | Yes |
| Status | Legacy / kept for compat | Current | Current & preferred |
