How Does the VM Split Work in Linux? – Free Linux Device Driver Course

Linux Kernel Virtual Address Space and the VM Split
Free Linux Kernel Development Course — EmbeddedPathashala
⏱ 20 min read
🎯 Beginner → Intermediate
🐧 Linux Kernel 6.x
🆓 Free Course

Welcome to EmbeddedPathashala’s free Linux kernel development course. In this lecture, we explore one of the most foundational concepts in Linux kernel programming — how Linux manages virtual memory by splitting the available address space between user-space processes and the kernel itself. This concept, called the VM split, directly affects how system calls work, how drivers access memory, and how embedded Linux boards like Raspberry Pi are configured at the kernel level.

What You Will Learn

  • 🔹 What a Virtual Address Space (VAS) is and why Linux uses it
  • 🔹 How the Linux kernel splits address space between user-space and kernel-space
  • 🔹 What the VM split is and the common ratios (3:1, 2:2, 1:3)
  • 🔹 The role of PAGE_OFFSET and the kernel segment
  • 🔹 How system calls trigger context switches within the same VAS
  • 🔹 How the VM split works on 64-bit Linux (modern kernels)
  • 🔹 How to view and configure VM split settings in a running Linux system
  • 🔹 Key kernel macros related to memory layout

Prerequisites

  • Basic understanding of C programming
  • Familiarity with Linux command line
  • Awareness of what a process is in Linux
  • Basic knowledge of memory addresses (hex notation)

No prior kernel programming experience needed — this free Linux kernel development course starts from the ground up.

What is a Virtual Address Space (VAS)?

Every process running on Linux gets its own Virtual Address Space (VAS). Think of it like a private map of memory addresses that only that process can see. Even though many processes run simultaneously on the same physical RAM, each believes it has exclusive use of a large chunk of memory addresses — thanks to the kernel’s memory management unit (MMU) doing translations behind the scenes.

On a 32-bit system, the VAS is 4 GB in size (addresses 0x00000000 to 0xFFFFFFFF). On a modern 64-bit system, the theoretical VAS is vastly larger — up to 128 TB or more per process in practice, depending on the architecture and kernel configuration.

The VAS of a typical Linux process contains several regions:

Process Virtual Address Space — Memory Regions
Stack (grows ↓)
local variables, function call frames
↓ grows downward
Memory-Mapped Region
shared libraries (.so), mmap() files, anonymous mappings
↑ grows upward
Heap
dynamic allocations — malloc(), new
BSS Segment
uninitialized global and static variables
Data Segment
initialized global and static variables
Text Segment [r-x]
compiled machine code — read-only, executable

Figure 1 — Typical Linux Process Virtual Address Space layout (low → high addresses)

The Problem: Kernel Code Lives in VAS Too

Here is the central challenge. When a user-space program calls something like write() to print to the terminal, the CPU must switch from user mode to kernel mode and execute the kernel’s implementation of the write system call. The kernel code must be accessible in memory at that moment.

One naive solution would be to give the kernel its own completely separate 4 GB address space. But that approach is very slow — every system call would require a full TLB flush (Translation Lookaside Buffer — the CPU’s address translation cache), which is extremely expensive in performance terms.

The elegant solution Linux uses: both user-space and kernel-space share the same virtual address space of each process. The VAS is simply divided — some portion belongs to the user program, and some portion is permanently reserved for the kernel. This division is called the VM split.

Understanding the Linux VM Split

The VM split (Virtual Memory split) divides a process’s virtual address space into two parts:

  • User segment — the lower portion, accessible only in unprivileged user mode
  • Kernel segment — the upper portion, accessible only when the CPU is in privileged kernel mode

The exact boundary between these two regions is defined by a kernel macro called PAGE_OFFSET. On a 32-bit ARM system running Linux, PAGE_OFFSET is typically 0x80000000, giving a 2 GB user : 2 GB kernel split. This is often written as 2:2.

32-bit Linux VM Split — User:Kernel 2:2 (ARM-32 typical)
0xFFFF FFFF = 4 GB
PAGE_OFFSET
0x8000 0000 = 2 GB
0x0000 0000
Kernel Segment
Kernel Virtual Address Space
kernel code, data, drivers,
per-CPU areas, vmalloc
2 GB (kernel-mode only)
User Segment
Process VAS
stack, heap, libs,
text, data segments
2 GB (user-mode accessible)
CPU in kernel mode
can access this
CPU in user mode
can access this

Figure 2 — 32-bit Linux VM split: 2 GB user-space + 2 GB kernel-space within a single 4 GB VAS

The three common 32-bit VM split options are:

Split Ratio (User:Kernel) User Space Kernel Space PAGE_OFFSET Common Use Case
3:1 3 GB 1 GB 0xC0000000 x86 Linux desktop (classic default)
2:2 2 GB 2 GB 0x80000000 ARM-32 embedded systems, Raspberry Pi
1:3 1 GB 3 GB 0x40000000 Kernel-heavy embedded systems with large drivers

How System Calls Work with the VM Split

Now that you understand the VM split, let’s see how system calls actually work within this framework. This is critical knowledge for any Linux kernel developer or device driver author.

System Call Flow — User Mode to Kernel Mode Transition
① User-space program calls write()
Executes in user segment (lower VAS). CPU is in unprivileged user mode.
↓ CPU traps into kernel (software interrupt / syscall instruction)
② Context switch — within the SAME VAS
CPU registers updated: stack pointer → kernel stack, privilege level → ring 0.
No TLB flush needed — kernel segment is always mapped in every process’s VAS.
↓ Kernel executes sys_write() in kernel segment (upper VAS)
③ Kernel runs in process context
The kernel code runs on behalf of the user process. It has access to the kernel segment and the user segment of the calling process. It can sleep, schedule, and block.
↓ Kernel returns from system call
④ Back to user-space
CPU restores user registers, stack pointer → user stack, drops to unprivileged user mode. Program continues after the write() call.

Figure 3 — System call executes within the same process VAS — no TLB flush required

💡 Key Insight for Kernel Developers: Because both user and kernel VAS exist within the same per-process address space, the kernel can directly access user-space memory (with appropriate functions like copy_from_user() and copy_to_user()). This is why device drivers can read data from a user buffer passed via an ioctl() call.

The PAGE_OFFSET Macro — Where the Kernel Segment Begins

In the Linux kernel source, the starting address of the kernel segment is represented by the macro PAGE_OFFSET. Every virtual address that is >= PAGE_OFFSET belongs to the kernel segment. Every address below it belongs to the user segment.

You can verify the PAGE_OFFSET on a running 32-bit Linux system using the kernel’s /proc/config.gz:

# First, load the configs module to access /proc/config.gz
sudo modprobe configs

# Decompress and search for VM split and PAGE_OFFSET settings
zcat /proc/config.gz | grep -E "VMSPLIT|PAGE_OFFSET"

On a Raspberry Pi (ARM-32, Broadcom BCM2835/BCM2837) running a recent Linux kernel, you will typically see:

# CONFIG_VMSPLIT_3G is not set
# CONFIG_VMSPLIT_3G_OPT is not set
CONFIG_VMSPLIT_2G=y
# CONFIG_VMSPLIT_1G is not set
CONFIG_PAGE_OFFSET=0x80000000

The CONFIG_VMSPLIT_2G=y entry confirms the 2:2 split. The CONFIG_PAGE_OFFSET=0x80000000 tells us the kernel segment starts at 2 GB — the midpoint of the 4 GB VAS.

📌 Note: On 32-bit x86 systems, the classic Linux default has always been CONFIG_VMSPLIT_3G=y (3 GB user, 1 GB kernel). This is a build-time kernel configuration choice — not a runtime setting. Changing it requires recompiling the kernel.

VM Split on 64-bit Linux — Modern Kernels (5.x, 6.x)

On 64-bit Linux — which is the standard today for both servers and modern embedded systems running Linux — the VM split concept still applies, but the numbers are dramatically different. The theoretical 64-bit address space is 264 bytes (16 exabytes), but architectures use only a portion of those bits.

On x86-64 (AMD64), Linux currently uses 48 bits of virtual address space (with 5-level paging, up to 57 bits). The virtual address space is split into two canonical halves:

64-bit Linux Virtual Address Space Layout (x86-64, kernel 6.x)
0xFFFF FFFF FFFF FFFF
0xFFFF 8000 0000 0000
← Non-canonical hole
0x0000 7FFF FFFF FFFF
0x0000 0000 0000 0000
Kernel Virtual Address Space
~128 TB (or more with 5-level paging)
kernel text, modules, vmalloc,
direct physical memory map, percpu
Non-Canonical Address Hole
(hardware forbidden zone — causes #GP fault if accessed)
User Virtual Address Space
~128 TB per process
stack, heap, mmap, text, data

Figure 4 — 64-bit Linux VAS layout (x86-64, 4-level paging, kernel 6.x)

Key differences between 32-bit and 64-bit VM layout:

Aspect 32-bit Linux 64-bit Linux (modern)
Total VAS 4 GB 128 TB (4-level) / 64 PB (5-level)
User space 1–3 GB (configurable) ~128 TB
Kernel space 1–3 GB (configurable) ~128 TB
PAGE_OFFSET 0x80000000 (2:2 typical) 0xFFFF888000000000 (x86-64)
VM split configurable? Yes — kernel build time No — both halves are huge
KASLR Limited support Fully supported (since kernel 3.14)
⚠️ Important for Embedded Linux Developers: Many embedded SoCs (like older ARM Cortex-A8/A9-based boards) still use 32-bit Linux kernels for various reasons — smaller memory footprint, legacy driver support, and simpler boot requirements. So understanding the 32-bit VM split is still very relevant in the embedded world in 2024–2025.

KASLR — Kernel Address Space Layout Randomization

KASLR (Kernel Address Space Layout Randomization) is a security feature that randomizes where the kernel is loaded within the kernel segment at boot time. This makes it much harder for attackers to exploit kernel vulnerabilities by guessing kernel symbol addresses.

You can check if KASLR is active on your system:

# Check if KASLR is enabled in kernel config
grep CONFIG_RANDOMIZE_BASE /boot/config-$(uname -r)

# Expected output if enabled:
# CONFIG_RANDOMIZE_BASE=y

# You can also check via kernel command line
cat /proc/cmdline
# If you see nokaslr, it has been disabled
📌 Kernel Developer Note: When debugging kernel panics or using tools like gdb with a running kernel, KASLR can make symbol resolution tricky. You can temporarily disable it at boot by adding nokaslr to the kernel command line in your bootloader (GRUB/U-Boot).

On 64-bit Linux kernels (5.x and 6.x), KASLR randomizes:

  • The base load address of the kernel image
  • The location of kernel modules
  • The vmalloc/ioremap region base
  • The physical memory direct mapping base

Key Kernel Macros for Memory Layout

When writing Linux kernel modules or device drivers, you will encounter several macros that describe the kernel’s memory layout. Here are the most important ones:

#include <linux/mm.h>
#include <asm/page.h>

/* PAGE_OFFSET: start of kernel segment in VAS */
/* On 32-bit ARM: typically 0x80000000          */
/* On 64-bit x86: typically 0xFFFF888000000000 */
printk(KERN_INFO "PAGE_OFFSET = 0x%lx\n", PAGE_OFFSET);

/* TASK_SIZE: the maximum address for user space */
/* On 32-bit: equals PAGE_OFFSET (e.g. 2GB or 3GB) */
/* On 64-bit x86: 0x00007FFFFFFFFFFF               */
printk(KERN_INFO "TASK_SIZE   = 0x%lx\n", TASK_SIZE);

/* High memory boundary (32-bit only) */
#ifdef CONFIG_HIGHMEM
printk(KERN_INFO "high_memory = 0x%lx\n", (unsigned long)high_memory);
#endif
Macro / Symbol Meaning Typical Value (ARM-32)
PAGE_OFFSET Start of kernel segment in VAS 0x80000000
TASK_SIZE Max user-space address 0x80000000 (= PAGE_OFFSET)
PHYS_OFFSET Start of physical RAM on the board Board-dependent (e.g. 0x80000000)
__pa(vaddr) Convert kernel virtual → physical address vaddr - PAGE_OFFSET + PHYS_OFFSET
__va(paddr) Convert physical → kernel virtual address paddr - PHYS_OFFSET + PAGE_OFFSET

Common Mistakes and Troubleshooting

❌ Mistake 1: Dereferencing a user-space pointer directly in kernel code
/* WRONG — never do this in a driver */
char *user_ptr = (char *)arg;  /* from ioctl */
char c = *user_ptr;            /* can cause oops or security issue */

/* CORRECT — always use copy_from_user() */
char buf[64];
if (copy_from_user(buf, (char __user *)arg, sizeof(buf)))
    return -EFAULT;
❌ Mistake 2: Confusing physical and virtual addresses

In kernel code, pointers are always virtual addresses. You cannot dereference a physical address directly. Always use __va(), ioremap(), or phys_to_virt() to get a usable virtual address before reading/writing.

❌ Mistake 3: Assuming kernel VAS is the same across all processes

The kernel segment layout is shared, but the kernel’s page tables are present in every process’s page table tree. When the kernel executes in process context, it uses the current process’s page tables — but only accesses the kernel segment portion.

Hands-On: Exploring Memory Layout on Your Linux System

You can observe the VM split and kernel segment layout directly on any Linux system. Here are practical commands that work on both desktop and embedded boards:

# 1. View the virtual memory map of any running process
cat /proc/self/maps

# 2. Check the kernel's own memory map (shows kernel segments)
sudo cat /proc/iomem

# 3. View kernel symbols and their virtual addresses
sudo cat /proc/kallsyms | head -20
# Note: addresses will be 0x0... if KASLR hides them for security

# 4. On 32-bit systems — check the configured VM split
grep -E "VMSPLIT|PAGE_OFFSET" /boot/config-$(uname -r)

# 5. Check system architecture
uname -m
# x86_64 for 64-bit, armv7l for 32-bit ARM

# 6. Write a simple kernel module to print PAGE_OFFSET
# (see the code example below)

A minimal kernel module to print key memory layout values:

/* vas_info.c — simple kernel module to print VAS layout info */
#include <linux/init.h>
#include <linux/module.h>
#include <linux/mm.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Print kernel VAS layout macros");

static int __init vas_info_init(void)
{
    pr_info("=== Kernel VAS Layout Info ===\n");
    pr_info("PAGE_OFFSET : 0x%lx\n", PAGE_OFFSET);
    pr_info("TASK_SIZE   : 0x%lx\n", TASK_SIZE);
    pr_info("VMALLOC_START: 0x%lx\n", VMALLOC_START);
    pr_info("VMALLOC_END  : 0x%lx\n", VMALLOC_END);
    return 0;
}

static void __exit vas_info_exit(void)
{
    pr_info("vas_info module unloaded\n");
}

module_init(vas_info_init);
module_exit(vas_info_exit);
# Build and insert the module
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
sudo insmod vas_info.ko

# View the output
dmesg | tail -10

# Remove the module
sudo rmmod vas_info

Best Practices for Kernel Memory Layout Awareness

  • ✅ Always use copy_from_user() / copy_to_user() when exchanging data between user-space and kernel-space in drivers.
  • ✅ Never hardcode memory addresses in kernel code — use the provided macros (PAGE_OFFSET, TASK_SIZE, etc.).
  • ✅ Use IS_ERR() and error pointers — kernel functions often encode errors as high virtual addresses (in the kernel segment range).
  • ✅ Keep KASLR enabled on production systems — never disable it in deployed kernels without a strong reason.
  • ✅ On embedded systems, always verify the VM split in your kernel config before writing device drivers — the PAGE_OFFSET value directly affects driver calculations involving physical-to-virtual address conversions.
  • ✅ Use kernel’s vmalloc() for large allocations — it uses the vmalloc region within the kernel segment and handles fragmentation better than kmalloc() for large buffers.

Summary and Key Takeaways

  • 🔹 Every Linux process has a Virtual Address Space (VAS) — a private map of virtual memory addresses.
  • 🔹 Linux shares this VAS between the user segment (lower addresses) and the kernel segment (upper addresses) — this is the VM split.
  • 🔹 The boundary is defined by PAGE_OFFSET, which is a build-time kernel configuration on 32-bit systems.
  • 🔹 Common 32-bit splits: 3:1 (x86 classic), 2:2 (ARM-32 embedded, e.g. Raspberry Pi).
  • 🔹 On modern 64-bit Linux, both halves are ~128 TB — the split is not a limitation anymore.
  • 🔹 System calls perform a privilege-level context switch within the same VAS — efficient because no TLB flush is needed.
  • 🔹 KASLR randomizes kernel layout within the kernel segment to enhance security.
  • 🔹 Key macros: PAGE_OFFSET, TASK_SIZE, __pa(), __va(), VMALLOC_START.

Frequently Asked Questions (FAQ)

Q1: Why does the kernel need to share the process VAS? Why not use a completely separate address space?

If the kernel used a completely separate address space, every system call would require a full TLB flush — clearing the CPU’s address translation cache. On modern hardware with millions of TLB entries, this would make system calls orders of magnitude slower. Sharing the VAS avoids this cost entirely.

Q2: Can a user-space process read kernel memory?

No. Even though the kernel segment is mapped in every process’s page tables, the page table entries for the kernel segment are marked as supervisor-only (kernel-mode only). If a user-space program tries to access an address above PAGE_OFFSET, the MMU generates a protection fault (segfault). This is enforced by hardware.

Q3: What is the difference between process context and interrupt context in kernel mode?

In process context, the kernel runs on behalf of a specific user-space process (like during a system call). It can sleep and be scheduled. In interrupt context, the kernel handles a hardware interrupt — it is not associated with any specific process, cannot sleep, and must complete quickly. Memory layout is the same, but the behavior rules differ significantly.

Q4: Does the VM split affect how much RAM a 32-bit Linux system can use?

Yes. With a 3:1 split, the kernel only has 1 GB of virtual address space to work with for mapping physical RAM. If the machine has more than 1 GB of RAM, the kernel must use techniques like highmem (high memory) — temporarily mapping physical pages into its small kernel window as needed. The 2:2 split gives the kernel more breathing room. This is one reason 32-bit Linux on machines with 4+ GB of RAM is problematic — which is why 64-bit Linux is standard today.

Q5: Is PAGE_OFFSET the same on all ARM boards running Linux?

Not necessarily — it depends on the kernel configuration. However, for ARM Cortex-A boards running standard Linux (like Raspberry Pi, BeagleBone), the 2:2 split with PAGE_OFFSET=0x80000000 is the most common. Some specialized embedded kernels may use different splits.

Q6: How does Kernel Page Table Isolation (KPTI) relate to the VM split?

KPTI (introduced in Linux 4.15 as a mitigation for Meltdown) takes the VM split concept further — it actually maintains two separate sets of page tables for each process: a minimal one used while in user mode (containing almost no kernel mappings) and a full one used in kernel mode. This prevents Meltdown-style speculative execution attacks but does add a small performance cost due to TLB flushes on transitions.

Q7: How do I check the VM split on my embedded board?

You can use zcat /proc/config.gz | grep -E "VMSPLIT|PAGE_OFFSET" if the configs module is available. Alternatively, check your kernel’s .config file or the /boot/config-$(uname -r) file. The value of CONFIG_PAGE_OFFSET directly tells you the kernel segment start address.

Q8: Does the VM split affect how I write Linux device drivers?

Directly yes — especially on 32-bit systems. When your driver maps device registers with ioremap(), the returned virtual address will be in the kernel segment (above PAGE_OFFSET). When copying data between your driver and user-space, you must always use copy_from_user()/copy_to_user() — direct pointer dereference across the user/kernel boundary is both dangerous and often incorrect. In this free Linux device drivers course, we will use this knowledge extensively in every driver we write.

Conclusion

Understanding the Linux kernel’s virtual address space and the VM split is not just academic — it is the bedrock on which all kernel programming rests. Whether you are writing a character device driver, debugging a kernel oops, understanding how system calls work, or preparing for a Linux kernel developer interview at companies like Texas Instruments, Qualcomm, or STMicroelectronics, this knowledge is non-negotiable.

In this free Linux kernel development course on EmbeddedPathashala, we have learned how the 4 GB (or larger on 64-bit) virtual address space is split between user-space and kernel-space, why this design is efficient for system calls, what PAGE_OFFSET means, and how these concepts apply to both legacy 32-bit embedded systems and modern 64-bit Linux platforms.

In the next lecture, we will go deeper into how the kernel allocates and manages memory within its own kernel segment — covering kmalloc(), vmalloc(), slab allocators, and the buddy system.

🔑 Keywords Covered in This Lecture
Virtual Address Space VM Split PAGE_OFFSET Kernel Segment User Segment System Call Process Context KASLR KPTI copy_from_user copy_to_user ioremap Linux Kernel 6.x ARM Embedded Linux Free Kernel Course

🚀 Continue Learning — Free Linux Kernel Development Course

EmbeddedPathashala offers completely free, in-depth courses on Linux kernel programming, Linux device drivers, and embedded systems.

📚 View Full Course Index 🔔 Join Free Newsletter

Leave a Reply

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