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_OFFSETand 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:
local variables, function call frames
shared libraries (.so), mmap() files, anonymous mappings
dynamic allocations — malloc(), new
uninitialized global and static variables
initialized global and static variables
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.
0x8000 0000 = 2 GB
can access this
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.
No TLB flush needed — kernel segment is always mapped in every process’s VAS.
Figure 3 — System call executes within the same process VAS — no TLB flush required
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.
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:
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) |
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
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
/* 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;
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.
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_OFFSETvalue 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 thankmalloc()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)
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.
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.
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.
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.
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.
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.
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.
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.
🚀 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