The vDSO Virtual Dynamic Shared Object

The virtual Dynamic Shared Object (vDSO) is a small shared library that the Linux kernel automatically maps into the address space of every user-space process, so that a handful of system calls can be served entirely in userspace — as ordinary function calls reading shared memory — without trapping into the kernel at all (vdso(7)). It exists for pure performance: a real system call requires a privilege-level transition whose fixed cost (entry assembly, register save, speculation barriers, exit work) dwarfs the trivial work of, say, reading the current time. The vDSO is not a clever hack bolted onto the loader — it is a genuine, fully-formed ELF shared object that the kernel builds at compile time, keeps in its own memory, and hands to each process at execve(2), advertising its base address through the auxiliary-vector entry AT_SYSINFO_EHDR. This note covers what the object is and how the kernel produces and maps it; its companion vDSO Symbols and How libc Uses Them covers how userspace finds and calls it, and How the vDSO Reads Kernel Time Without Trapping covers the lock-free data read that makes the time functions correct.

All version-specific claims below are pinned to the Linux 6.12 LTS kernel tree (released 2024-11; LTS) unless noted, and to x86-64.

Mental Model

The right way to think about the vDSO is as a kernel-authored .so file that never touches the disk. When you build the kernel, the vDSO source (arch/x86/entry/vdso/) is compiled and linked into a tiny standalone ELF shared object, which is then embedded as a blob inside the kernel image. At process startup the kernel maps that blob, read-only and executable, into the new process — exactly as the dynamic linker would map libc.so.6, except the kernel does it and the file has no on-disk existence. Userspace sees it in /proc/self/maps as the pseudo-region [vdso]. Because it is a real ELF DSO with a symbol table and GNU symbol versioning, userspace can do ordinary symbol lookups on it; because its code only reads data the kernel publishes into an adjacent shared page (the [vvar] region), those lookups resolve to functions that run with no privilege transition.

flowchart LR
  subgraph KERN["Kernel image (built once)"]
    BLOB["vdso_image_64<br/>(embedded ELF blob<br/>+ vvar data page)"]
  end
  subgraph PROC["Process address space (per execve, ASLR-randomized)"]
    VDSO["[vdso] VMA<br/>(PT_LOAD: text, R+X)"]
    VVAR["[vvar] VMA<br/>(kernel-published data, R/O)"]
  end
  BLOB -->|"map_vdso():<br/>_install_special_mapping x2"| VDSO
  BLOB -->|"adjacent, just before [vdso]"| VVAR
  VVAR -.->|"vDSO code reads<br/>this page directly"| VDSO
  AUXV["auxv: AT_SYSINFO_EHDR = base of [vdso]"]
  VDSO -.->|"ARCH_DLINFO emits"| AUXV

How the kernel turns a build-time ELF blob into a per-process mapping. What it shows: one compiled vdso_image_64 blob backs two adjacent virtual-memory areas in every process — [vdso] (the executable code) and [vvar] (the read-only data the code consults) — placed at a randomized address, with the [vdso] base advertised to userspace via the AT_SYSINFO_EHDR auxiliary-vector entry. The insight to take: the vDSO is data plus code the kernel publishes; the process never asks for it and the function call never enters the kernel — the kernel pre-positioned everything the call needs in shared, read-only memory.

Why the vDSO Exists: the Cost of a Trap

A system call is a controlled trap across the user/kernel privilege boundary (see The Syscall Instruction and Trap Mechanism and Why System Calls Are Expensive). Even the cheapest syscall pays for the syscall instruction’s pipeline serialization, the swap to the kernel stack, saving the user register set into a [[The pt_regs Register Frame|pt_regs]] frame, the generic-entry bookkeeping (The Generic Syscall Entry and Exit Layer), and — since Meltdown/Spectre — page-table isolation switches and speculation barriers on the way in and out. For a syscall that does real work (open a file, send a packet) this overhead is amortized. But for a hot, side-effect-free, read-only call like gettimeofday(2) or clock_gettime(2) — which a latency-tracing or benchmarking program may invoke millions of times per second — the trap is essentially the entire cost.

The vDSO eliminates that cost for exactly the calls where it dominates. The man page states the motivation plainly: such calls “can dominate overall performance … due both to the frequency of the call as well as the context-switch overhead” (vdso(7)). The kernel arranges for the information these calls need (chiefly the current clock state) to live in a page the process can read directly, and provides userspace functions that read it — so the “syscall” becomes a normal function call plus a few memory reads. A historical LWN write-up quantifies the order of magnitude: a vDSO gettimeofday() is roughly an order of magnitude faster than the trapping equivalent on the hardware tested (LWN 2014).

Uncertain

Verify: the absolute speedup of vDSO clock_gettime vs. the trapping syscall. Reason: the 2014 LWN article reports figures that, as summarized, appear to be in microseconds; in practice on modern x86-64 a vDSO clock_gettime is on the order of tens of nanoseconds while a trapping syscall is hundreds of nanoseconds to a microsecond-plus — i.e. roughly one to two decimal orders of magnitude, not the specific µs values that summary implied. To resolve: microbenchmark clock_gettime(CLOCK_MONOTONIC, …) with and without LD_PRELOAD disabling the vDSO, or measure with vdso=0 boot. Treat any specific absolute number as approximate. uncertain

What the Object Actually Is: a Prelinked ELF DSO

The vDSO is a real ELF shared object, prelinked to a fixed virtual address with a single read-only PT_LOAD segment. Its layout is dictated by the linker script arch/x86/entry/vdso/vdso-layout.lds.S, which lays out the standard DSO machinery — .hash, .gnu.hash, .dynsym (dynamic symbol table), .dynstr (symbol strings), .gnu.version/.gnu.version_d (GNU symbol versioning), .dynamic (the PT_DYNAMIC table the loader walks), .rodata, .text, and an .eh_frame for unwinding — and explicitly forces a single PT_LOAD with read-execute flags:

PHDRS
{
	text		PT_LOAD		FLAGS(5) FILEHDR PHDRS; /* PF_R|PF_X */
	dynamic		PT_DYNAMIC	FLAGS(4);		/* PF_R */
	note		PT_NOTE		FLAGS(4);		/* PF_R */
	eh_frame_hdr	PT_GNU_EH_FRAME;
}

FLAGS(5) is PF_R | PF_X (4 | 1) — readable and executable, not writable. Because it is a normal DSO, the same parsing logic that works for any shared library works for the vDSO; the kernel’s own reference parser, tools/testing/selftests/vDSO/parse_vdso.c, opens with the comment “The vDSO is a regular ELF DSO that the kernel maps into user space when it starts a program. It works equally well in statically and dynamically linked binaries.” The general ELF structure it relies on — program headers, the dynamic table, the symbol/hash tables — is covered in ELF Format; the vDSO is just an ELF DSO that happens to be authored by the kernel rather than a userspace toolchain (contrast a normal program image, e.g. Go Executable Layout).

Crucially, the layout script also reserves the [vvar] data region just before the vDSO text. The comment is explicit: “User/kernel shared data is before the vDSO.” The vvar page (vvar_start = . - 4 * PAGE_SIZE) holds the struct vdso_data (timekeeping state) and, on 6.12, a struct vdso_rng_data for the random-number vDSO; the vDSO code reads these structures to answer the time/getrandom calls. This note treats [vvar] only as “the data the code reads”; the actual lock-free read of the timekeeping fields is the subject of How the vDSO Reads Kernel Time Without Trapping.

Symbols the x86-64 vDSO Exports

Which functions the vDSO offers is decided by the version script in arch/x86/entry/vdso/vdso.lds.S. At the v6.12 tag the exported set, inside a single LINUX_2.6 version node, is:

VERSION {
	LINUX_2.6 {
	global:
		clock_gettime;
		__vdso_clock_gettime;
		gettimeofday;
		__vdso_gettimeofday;
		getcpu;
		__vdso_getcpu;
		time;
		__vdso_time;
		clock_getres;
		__vdso_clock_getres;
#ifdef CONFIG_X86_SGX
		__vdso_sgx_enter_enclave;
#endif
		getrandom;
		__vdso_getrandom;
	local: *;
	};
}

Read this carefully, because it corrects two widely-repeated claims:

  • The often-cited “four functions” list is incomplete. Many references — including the vdso(7) man page and older articles — list only __vdso_clock_gettime, __vdso_gettimeofday, __vdso_getcpu, and __vdso_time for x86-64. The kernel source is ground truth, and at 6.12 it also exports __vdso_clock_getres (the resolution of a clock) and, newest, __vdso_getrandom. The __vdso_getrandom entry is recent: the userspace getrandom() vDSO acceleration landed in Linux 6.11 (work by Jason A. Donenfeld; the implementation files vgetrandom.c/vgetrandom-chacha.S carry 2022–2024 copyright). I verified the release by diffing the version script across tags — getrandom/__vdso_getrandom are absent from vdso.lds.S at the v6.10 tag and present from v6.11 onward — so the symbol is present in the 6.12 LTS tree but absent from kernels older than 6.11 and from the vdso(7) man-page snapshot. __vdso_sgx_enter_enclave is conditional on CONFIG_X86_SGX (Software Guard Extensions) and is a transition primitive, not a time call.

  • Each function is exported twice. There is both a __vdso_-prefixed name (e.g. __vdso_clock_gettime) and a bare alias (clock_gettime). The implementation source vclock_gettime.c shows the bare names are weak aliases of the __vdso_ symbols:

int __vdso_gettimeofday(struct __kernel_old_timeval *tv, struct timezone *tz)
{
	return __cvdso_gettimeofday(tv, tz);
}
 
int gettimeofday(struct __kernel_old_timeval *, struct timezone *)
	__attribute__((weak, alias("__vdso_gettimeofday")));

The convention (per the man page) is that the prefixed __vdso_* (or __kernel_* on some architectures) names are the canonical ones userspace should resolve; the unprefixed weak aliases exist for convenience. The actual implementations are thin: __vdso_gettimeofday just calls the generic __cvdso_gettimeofday from lib/vdso/gettimeofday.c (shared across architectures), and __vdso_getcpu (in vgetcpu.c) is a one-liner that calls vdso_read_cpunode(cpu, node) to read the CPU/NUMA-node pair the kernel caches per-CPU.

Uncertain

Verify: that the runtime presence of each exported symbol matches the link-time list. Reason: the version script lists what is built into the DSO, but at runtime some functions only resolve to a fast path when the active clocksource supports it (e.g. a non-TSC clocksource forces clock_gettime to fall back to a real syscall internally), and __vdso_getrandom requires both kernel and a recent glibc to be wired up. The symbol is present; the acceleration is conditional. To resolve: inspect a running system’s vDSO with objdump and test each clock id. uncertain

How the Kernel Maps It: map_vdso and Two Special Mappings

At execve(2), after the ELF loader has laid out the program and its interpreter, the architecture’s arch_setup_additional_pages() is called. On x86-64 (in arch/x86/entry/vdso/vma.c) this is a one-liner guarded by the vdso64_enabled knob:

int arch_setup_additional_pages(struct linux_binprm *bprm, int uses_interp)
{
	if (!vdso64_enabled)
		return 0;
	return map_vdso(&vdso_image_64, 0);
}

map_vdso(image, addr) does the real work. With addr == 0 it asks the memory manager for a free virtual address via get_unmapped_area() — and this is where Address Space Layout Randomization (ASLR) enters: the vDSO base is randomized per execve, so you must never hard-code its address. The man page is emphatic: “You must not assume the vDSO is mapped at any particular location … The base address will usually be randomized at run time every time a new process image is created.” It then installs two special mappings:

	addr = get_unmapped_area(NULL, addr,
				 image->size - image->sym_vvar_start, 0, 0);
	...
	text_start = addr - image->sym_vvar_start;
 
	/* MAYWRITE to allow gdb to COW and set breakpoints */
	vma = _install_special_mapping(mm, text_start, image->size,
				       VM_READ|VM_EXEC|
				       VM_MAYREAD|VM_MAYWRITE|VM_MAYEXEC,
				       &vdso_mapping);
	...
	vma = _install_special_mapping(mm, addr, -image->sym_vvar_start,
				       VM_READ|VM_MAYREAD|VM_IO|VM_DONTDUMP|
				       VM_PFNMAP,
				       &vvar_mapping);
	...
	current->mm->context.vdso = (void __user *)text_start;
	current->mm->context.vdso_image = image;

Two vm_special_mappings are created: the [vdso] mapping (VM_READ | VM_EXEC, the code) and, immediately preceding it, the [vvar] mapping (VM_READ | VM_IO | VM_DONTDUMP | VM_PFNMAP, the kernel-published data). A “special mapping” is a VMA backed not by a file or anonymous memory but by a kernel-supplied fault handler: when the process first touches a page, vdso_fault() returns the corresponding page of the embedded image->data, and vvar_fault() inserts the physical frame of the live kernel data (timekeeping state, and for paravirtualized/Hyper-V guests, the pvclock/hvclock pages). The VM_MAYWRITE on [vdso] is not so the program can patch the vDSO — the comment says it exists “to allow gdb to COW and set breakpoints,” i.e. a debugger can copy-on-write a private page to plant an int3. Finally the kernel records the chosen text base in current->mm->context.vdso, which is the value it will advertise to userspace.

There is also map_vdso_once(), which scans all VMAs and returns -EEXIST if a vDSO/vvar mapping is already present — this backs the arch_prctl(ARCH_MAP_VDSO_*) interface used by tools like CRIU (Checkpoint/Restore In Userspace) to re-map the vDSO at a controlled address after restore.

The Handshake: AT_SYSINFO_EHDR in the Auxiliary Vector

A process cannot find the vDSO by scanning memory — ASLR forbids that. Instead, the kernel tells it where the vDSO is, through the auxiliary vector (auxv): a list of (type, value) pairs the kernel pushes onto the new process’s stack at execve, just past argv/envp. Among these pairs is AT_SYSINFO_EHDR, whose value is the base address of the vDSO’s ELF header — i.e. exactly the context.vdso recorded by map_vdso. The man page defines it as “The address of a page containing the virtual Dynamic Shared Object (vDSO)” (getauxval(3)).

The auxv entry is emitted by the architecture’s ARCH_DLINFO macro, expanded inside the ELF loader’s auxv-construction. On x86-64 (arch/x86/include/asm/elf.h, v6.12):

#define ARCH_DLINFO							\
do {									\
	if (vdso64_enabled)						\
		NEW_AUX_ENT(AT_SYSINFO_EHDR,				\
			    (unsigned long __force)current->mm->context.vdso); \
	NEW_AUX_ENT(AT_MINSIGSTKSZ, get_sigframe_size());		\
} while (0)

Note what x86-64 emits and what it does not: it emits AT_SYSINFO_EHDR (the vDSO base) but not AT_SYSINFO. AT_SYSINFO — “the entry point to the system call function in the vDSO” — is an IA-32 (32-bit/compat) concept, set in ARCH_DLINFO_IA32 so 32-bit code can call __kernel_vsyscall to issue a syscall via the kernel’s preferred instruction. On native 64-bit, userspace issues the syscall instruction directly, so there is no need for a AT_SYSINFO entry-point pointer; the only thing the kernel advertises is the address of the whole DSO via AT_SYSINFO_EHDR. Confusing the two is a common error. How userspace then reads this entry (getauxval(AT_SYSINFO_EHDR)) and parses the DSO is the subject of vDSO Symbols and How libc Uses Them.

Inspecting the vDSO on a Live System

Because the vDSO is a real, mapped region, you can see and extract it:

# 1. See the two regions in any process's memory map.
$ cat /proc/self/maps | grep -E 'vdso|vvar'
7ffc1b3f8000-7ffc1b3fc000 r--p 00000000 00:00 0          [vvar]
7ffc1b3fe000-7ffc1b400000 r-xp 00000000 00:00 0          [vdso]
 
# 2. Dump the live vDSO out of a process and disassemble it as an ELF .so.
$ dd if=/proc/self/mem bs=1 skip=$((0x7ffc1b3fe000)) count=8192 2>/dev/null > vdso.so
$ objdump -T vdso.so      # -T = dynamic symbol table
0000...  w   DF .text  ...  LINUX_2.6   clock_gettime
0000...  g   DF .text  ...  LINUX_2.6   __vdso_clock_gettime
...

The [vvar] region is read-only (r--p) and the [vdso] region is read-execute (r-xp), matching the VMA flags from map_vdso. The dynamic symbol table shows both the weak unversioned aliases (w) and the canonical __vdso_* globals (g), all carrying the LINUX_2.6 version. The addresses change on every run because of ASLR.

Failure Modes and Common Misunderstandings

  • “The vDSO is at a fixed address.” It is not — its base is randomized by ASLR on every execve, which has been the default for many years. Hard-coding 0xffffffffff600000 or 0xffffffffff5ff000 is the vsyscall behavior — the obsolete, fixed-address predecessor described in The vsyscall Legacy Mechanism. Those fixed addresses appear in pre-2014 documentation (including the LWN article’s description of the vvar page) and are stale for the vDSO. Always read AT_SYSINFO_EHDR.
  • “Calling a vDSO function always avoids a syscall.” Not guaranteed. The vDSO time functions only take the no-trap fast path when the active clocksource is vDSO-capable (e.g. TSC, the Time Stamp Counter); with an incompatible clocksource the vDSO routine internally issues a real clock_gettime syscall. The symbol is always there; the acceleration is conditional on the timekeeping configuration. See How the vDSO Reads Kernel Time Without Trapping.
  • Disabling it. Booting with vdso=0 (handled by vdso_setup/__setup("vdso=", …) in vma.c) clears vdso64_enabled, so arch_setup_additional_pages maps nothing and AT_SYSINFO_EHDR is not emitted. Userspace then transparently falls back to real syscalls (glibc checks the function pointer for NULL — see vDSO Symbols and How libc Uses Them). This is occasionally used to debug timing anomalies or to force every clock read through the kernel.
  • Static binaries. The vDSO works for statically-linked programs too — there is no dynamic linker involved, but the kernel still maps the vDSO and still emits AT_SYSINFO_EHDR, and a static binary can use getauxval() plus parse_vdso.c to find and call it. The misconception that “static binaries don’t get the vDSO” is wrong.
  • The vvar/vDSO split confuses pmap/core dumps. [vvar] is VM_DONTDUMP, so it is excluded from core dumps (dumping live kernel data would be meaningless and a leak); [vdso] is included so a debugger can symbolize backtraces through it.

Alternatives and When the vDSO Does Not Apply

The vDSO only helps calls that are (a) extremely frequent and (b) safely answerable from read-only published data. It is therefore the right tool for clock reads, CPU-id queries, and (recently) userspace random-number generation — and the wrong tool for anything with side effects (you cannot write() from the vDSO, because there is real kernel work to do). For everything else, the standard trap path applies: see The Syscall Instruction and Trap Mechanism. Two related mechanisms sit nearby:

  • vsyscall (The vsyscall Legacy Mechanism) — the obsolete predecessor: a single page mapped at a fixed address with a fixed layout. Its fixed address made it both a usability convenience (no auxv parsing) and a security liability (a reliable target for return-oriented programming), which is precisely why it was replaced by the randomized, ELF-structured vDSO. Modern kernels keep vsyscall only in an emulated, xonly form for ancient binaries.
  • io_uring (io_uring as a Syscall Batching Mechanism) — attacks the same “trap is expensive” problem from the opposite direction: instead of removing the trap for one hot call, it amortizes the trap across thousands of batched I/O operations via shared-memory rings. The vDSO removes the trap; io_uring batches it.

Production Notes

  • CRIU and checkpoint/restore. Because the vDSO base is randomized and embedded in saved register/stack state, restoring a checkpointed process requires re-establishing a compatible vDSO mapping; the arch_prctl map-vDSO interface backed by map_vdso_once() exists largely for this. A vDSO whose layout changed between kernel versions can break naive restore.
  • Containers and time namespaces. When a process is in a non-root time namespace (CONFIG_TIME_NS), the vvar layout differs: the kernel faults in a timens variant of the data page so that clock_gettime(CLOCK_MONOTONIC, …) reflects the namespace’s offset, still without a trap. vdso_join_timens() in vma.c clears and re-faults the relevant VMAs when a task changes namespace. This is how container runtimes can virtualize the clock cheaply.
  • Seccomp and the vDSO. Because a vDSO time read never traps, it is invisible to seccomp filters and to strace — there is no sys_enter/sys_exit tracepoint to catch. A sandbox that allow-lists syscalls does not need to (and cannot) gate vDSO clock reads; conversely, a tracer trying to observe every time read will miss them unless it forces vdso=0. This is a frequent source of “why doesn’t strace show my clock_gettime?” confusion.

See Also