Every dynamic process on Linux maps a shared library that has no file on disk. It appears in /proc/<pid>/maps under the name [vdso] — square brackets, which is the kernel’s way of saying there is no path here. It has an address, given to the process in the auxv as AT_SYSINFO_EHDR. And it has, if you read it, a perfectly ordinary ELF header.
Here is that header, read out of the running process’s own memory:
$ cd courses/img/assets/samples && ./build_samples.sh 2>&1 | sed -n '/I4/,/I5/p'
size, e_ident magic, e_type, e_machine, e_phentsize, e_phnum, e_phoff:
vdso_size=8192
magic=7f454c46
e_type=3 e_machine=62 e_phentsize=56 e_phnum=6 e_phoff=64
AT_SYSINFO_EHDR=136793659318272
EHDREQ_VDSO=1
7f 45 4c 46 is \x7fELF. e_type = 3 is ET_DYN. e_machine = 62 is EM_X86_64. Six program headers, 56 bytes each, at offset 64. That is a well-formed 64-bit shared object, and there is no file anywhere that contains it.
Why should a course about the kernel’s handoff care? Because it is the cleanest available proof of what the previous five courses described. Every one of them taught you to read an ELF file that a compiler produced. This is an ELF file the kernel produced, at run time, for every process it creates — and the same header fields, in the same order, at the same offsets, mean exactly what they meant in readelf output.
Why a kernel builds a library at all. The motivation is a performance argument, and it is worth stating because it explains the shape of the solution:
So the vDSO is not a weird exception. It is the ordinary machinery of the chain, with the linker step removed. It is ET_DYN, so the kernel maps it at a base and reports that base in the auxv — the same AT_BASE mechanism as the dynamic linker, and the reason AT_SYSINFO_EHDR exists as a separate entry at all.
What makes it a teaching object is that you can read it. There is no readelf that will open it, because there is no file. But the header is in your address space, so you can open /proc/self/mem, seek to the address /proc/self/maps gave you, and read sixty-four bytes. That is the whole technique, and it is the same technique the artifact uses for every other check in this course.
The header, exactly:
$ python3 crosscheck.py 2>&1 | sed -n '/G4/,$p' | head -9 G4 7 passed 0 failed
Each of those seven is a real check, run on every invocation:
7f454c46. Not “looks like ELF” — the four bytes, compared exactly.e_type is 3.ET_DYN, so it is a shared object and the kernel must give it a base. This is not a label; it is the reason for the AT_SYSINFO_EHDR entry.e_machine is 62.EM_X86_64, so it was built for this CPU and cannot be run on another architecture — which is true of it and not true of the executable, whose architecture comes from its own header. Two ELF images in one process, built for the same machine, and the kernel produced one of them.e_phentsize is 56 and e_phoff is 64. The standard 64-bit layout, identical to every other file in this course. The kernel emits the same structure a linker would.EHDREQ_VDSO = 1 — and this one is a story, because getting it wrong was instructive.The last item deserves its own explanation, because the mistake is a good one to have made. The natural way to check “does AT_SYSINFO_EHDR point at the vDSO?” is to run the auxv dumper in one process and the /proc/maps reader in another, then compare. That cannot work, because they are two different processes and ASLR puts the vDSO somewhere different in each. The first version of this script did exactly that and printed two unrelated addresses, which looked like a bug in the kernel.
The fix is to compare within one process: have the measuring program read its own auxv and its own map, and print the two side by side. It does, and they are equal — EHDREQ_VDSO=1. Any time you verify something a randomiser perturbs, both observations must come from the same process, or you are comparing two samples of a distribution and calling it a match. That is a general rule about experiments, and this course hit it hard enough to be worth stating as a rule rather than a footnote.
$ python3 stackwalk.py 2>&1 | grep vdso
[ok ] AT_SYSINFO_EHDR == the base of the [vdso] mapping
-- 0x7f92255a0000 vs 0x7f92255a0000
And the square brackets, which are the detail that makes the whole thing click: [vdso], [stack], [heap]. The kernel uses brackets for a region it made itself and has no file for. Every one of those is the kernel writing into an address space, and the vDSO is the one where it also wrote a file’s worth of structure into it.
The complete program, and it is short. Everything here has appeared in this course already; that is the point.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(void) {
unsigned long lo = 0, hi = 0;
FILE *f = fopen("/proc/self/maps", "r");
char line[512];
while (fgets(line, sizeof line, f)) {
unsigned long a, b; char p[8], d[64];
if (sscanf(line, "%lx-%lx %7s %*s %*s %*s %63s",
&a, &b, p, d) >= 4 && strstr(d, "vdso")) {
lo = a; hi = b;
}
}
fclose(f);
/* the kernel's own view of its own memory */
FILE *m = fopen("/proc/self/mem", "rb");
unsigned char h[64]; /* exactly one Elf64_Ehdr */
fseek(m, lo, SEEK_SET);
if (fread(h, 1, 64, m) != 64) return 1;
fclose(m);
printf("magic %02x%02x%02x%02x\n", h[0],h[1],h[2],h[3]);
printf("e_type %d e_machine %d\n",
h[16] | h[17]<<8, h[18] | h[19]<<8);
return 0;
}
Three things to notice. It is 64 bytes and no more — an Elf64_Ehdr is a fixed-size structure, which is the only reason this is possible. Every field is a hand-decoded little-endian byte pair, because there is no readelf and there is no file, so the struct layout has to be written out. And the sscanf has a %*s in it for the file offset — which is skipped, not parsed, because this region has no file and therefore no offset worth having.
That last detail is the real signature of the vDSO. Compare it to the same maps line for an ordinary library:
$ ./maps | grep -E 'vdso|ld-linux' | head -3 71e4c0d45000-71e4c0d47000 r-xp 00000000 [vdso] 7881a3b73000-7881a3b74000 r--p 00000000 103:02 8678400 /usr/lib/.../ld-linux-x86-64.so.2 7881a3b74000-7881a3b81000 r-xp 00001000 103:02 8678400 /usr/lib/.../ld-linux-x86-64.so.2
Look at the vDSO line: no device, no inode, no path. The rwxp permissions and the address range are there, because those are the kernel’s own bookkeeping. Everything that would identify a backing file is blank, because there is none. That single line is the entire concept: a mapped, executable, ELF-shaped thing with nothing behind it.
$ cd courses/img/assets/samples $ ./build_samples.sh 2>&1 | sed -n '/I4/,/I5/p' $ python3 crosscheck.py 2>&1 | sed -n '/G4/,/G5/p' $ ./maps | grep vdso
Then go past what was measured, in a controlled way:
1. Read the vDSO's PROGRAM HEADERS, not just its
header. Six of them at offset 64. Print
p_type, p_flags, p_offset, p_vaddr, p_filesz,
p_memsz for each. (There will be two PT_LOADs
and a PT_GNU_RELRO among others, and the PT_LOAD
vaddrs will be RELATIVE TO ZERO because
e_type is ET_DYN. This is the same program
header table format from
/courses/elf/lessons/program-header-table,
in a file nobody compiled.)
2. Use AT_SYSINFO_EHDR as the base and add each
PT_LOAD's p_vaddr. Where do they land
relative to the 8192 bytes you measured? Do
they fit? (They must. That is checkable
arithmetic and it is a real check that the
header you read belongs to the mapping you
read it from.)
3. Look for the SYMBOL TABLE. The vDSO's
section headers are all zero on most kernels
(it is built for the loader, which wants
program headers, not sections). Confirm that,
and then explain what that implies: the
vDSO cannot be read with readelf-style
tooling even in principle, because it has no
section header table to walk.
4. Compare the vDSO across two processes, properly.
Run ./maps | grep vdso twice. The ADDRESSES
differ (ASLR). The SIZE does not. Then run
it twice in ONE process and confirm the
address is stable. (The stability is the
point: the vDSO is mapped once, at process
creation, and never moves.)
5. strace -e trace=clock_gettime a program
that calls it, and one that does not, and
count the syscalls. (Zero for a program
using the vDSO path. This is the entire
reason the vDSO exists, and you can watch
it not happen.)
Exercise 1 is the one that ties this course to the whole chain, because it makes the vDSO into a program header table you have to parse. Same structure, same field order, same 56-byte record — in a file that no toolchain produced. And the p_vaddr values are relative to zero precisely because e_type is ET_DYN, which is the rule the relocations course established for shared objects and the linker-script course found written into the default script.
Exercise 3 has the most interesting answer. The vDSO has no section header table, so the family of tools that navigate ELF by section simply cannot read it. That is not a defect — sections exist for linkers, and the vDSO is never linked, only mapped. It is a clean demonstration that the two halves of ELF serve different consumers, and that a format can be completely valid while being unreadable by the tools you already know. The program header table is the kernel’s half; the section header table is the toolchain’s; the vDSO is a file that only has the first one.
The most direct connection in the course is to the dynamic linking course’s dlopen concept, and it is a genuine contrast rather than a repetition. That concept covered loading a library at run time from a path, and the loader’s search rules, and the mode bits. Every one of those concerns evaporates for the vDSO, because the kernel is the loader and there is no path to search. No search path, no DT_NEEDED, no scope insertion, no symbol resolution — just an address in the auxv. It is the dlopen mechanism with the entire search problem designed out, which is a useful thing to have seen precisely because the search problem is invisible in the normal case.
The connection to Where the Thread Blocks Come From is about who allocates. That concept measured the loader allocating TLS storage a linker could not. The vDSO is the same idea taken further: the kernel allocating an entire ELF image that no linker, compiler, or human wrote. Both exist because a per-process value cannot be baked into a file. The difference is that the TLS block is data the linker described and the kernel supplied, while the vDSO is structure the kernel both described and supplied.
And the connection to the beginning of the chain is the one that makes the course’s title true. The object-file course began with “an ELF file is a header, a program header table, a section header table, and sections.” The vDSO is the case that proves which of those the kernel cares about: header, program headers, sections, and no section header table at all. The kernel built it, so the kernel needed only the parts the kernel reads. Everything the previous five courses taught you about the section header table, the string table, and the symbol table is real and important and absent here — and its absence is the cleanest available statement of what the two halves of the format are for.
One connection forward, which is the next concept. The vDSO is mapped at a chosen address like everything else, and it is not the only thing. Where the Kernel Put Everything puts the whole address space on the page, with the floor it will not go below and the reason a hint is not a request.