How Linux Tracks Every Byte Your Process Owns – Free Linux Device Driver Course

FREE LINUX KERNEL PROGRAMMING COURSE

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.

Part 1
of 2
~18 min
read time
Beginner
friendly

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.

What the inode field means in /proc/PID/maps File-mapped vs anonymous segments at a glance What vsyscall, vvar, and vDSO really are User Virtual Addresses (UVA) explained Why MSB bits matter in 64-bit addresses

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.

Structure of One Line in /proc/PID/maps
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.

What an Inode Stores
🔒
Permissions
rwxr-xr-x
👥
Ownership
UID / GID
📋
File Size
in bytes
🕐
Timestamps
atime, mtime, ctime
💾
Disk Blocks
where 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.

✅ Quick Identification Trick

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.”

File-Mapped vs Anonymous Segments Side by Side
FILE-MAPPED SEGMENT
Inode field 524313 (non-zero)
Pathname field /bin/cat
Backing store File on disk
Examples Text segment, read-only data, shared libraries
ANONYMOUS SEGMENT
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.

Common Pathname Values in /proc/PID/maps
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.

Virtual Address Translation Flow
💻
Process
uses virtual address
0x55d83b65000
CPU + MMU
➔
📚
Page Tables
per-process lookup
translated to
➔
🧠
Physical RAM
actual memory cell

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.

💡 Remember This

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.

Why Normal System Calls Have Overhead
1️⃣
User Space
Process calls gettimeofday()
↓
2️⃣
CPU Mode Switch
Save registers, switch to ring 0
↓
3️⃣
Kernel Executes
Reads kernel clock, prepares result
↓
4️⃣
Switch Back
Restore registers, return to ring 3

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.

vsyscall vs vvar vs vDSO — Quick Comparison
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

🎯 Key Takeaways from Part 1

Inode = 0 means anonymous mapping Inode > 0 means file-backed mapping Pathname in [] is a kernel label All VAS addresses are virtual (UVAs) UVAs have top 16 bits = zero on x86-64 vsyscall is legacy, vDSO is modern vvar holds kernel time data vDSO is ASLR-compatible

Leave a Reply

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