Five courses into this chain you can read a relocation record, a linker script, a DT_NEEDED entry, and the loader’s own symbol bindings. Every one of those facts lives in a file. Now ask the question none of them could answer: when the kernel runs that file, how does the program find out where it landed?
The program header table is at some address. The entry point is at some address. The dynamic linker is at some address. The page size is 4096. The hardware capabilities are a bitmask. None of that is in the C language, none of it is in main’s signature, and no function in the C library will hand it to you. It is passed anyway, in a third array, and the reason you have never seen it is that the array has no name.
$ cd courses/img/assets/samples && ./build_samples.sh 2>&1 | sed -n '/I1/,/I2/p' argc = 1 argv[0] = ./stack envp[0] = SHELL=/bin/bash AUX 33 0x00007b98c83c7000 AUX 51 0x0000000000000d30 AUX 16 0x00000000178bfbff AUX 6 0x0000000000001000 AUX 17 0x0000000000000064 AUX 3 0x0000577a01179040 AUX 4 0x0000000000000038 AUX 5 0x000000000000000e AUX 7 0x00007b98c83c9000 AUX 8 000000000000000000 AUX 9 0x0000577a0117a0a0 AUX 11 0x00000000000003e8
Twenty-two entries, and not one of them is reachable from C. That is the whole point of this concept, and everything else in the course is a consequence of it.
What the kernel is doing, in one picture. This is not a diagram from a textbook; it is the shape of the bytes the previous five courses already taught you to read.
So the auxiliary vector is not a convenience and not a legacy. It is the only channel between the kernel and the program at process start, and it exists because the ELF file cannot contain the answer. The file says e_entry = 0x1130, which is an offset. The program needs an address. Something has to add the load base, and the kernel is the only thing that knows what it chose.
The same argument explains the other entries. AT_PHDR: the program header table’s address, which again is an offset in the file. AT_BASE: where the dynamic linker was put — a fact about a completely different file that your program never mentions. AT_PAGESZ: not in the file, because the file is portable and the page size is not.
And this is exactly why the entries are not in the file. The ELF course established the header fields and their job of describing the file. The auxv describes the same file as loaded, and those are different objects. A PIE executable has e_entry = 0x1130 in the file and is entered at 0x55d4883a7130 in memory. The gap is the kernel’s decision, and the auxv is where it is reported.
Twenty-two entries on this kernel, x86-64, no setuid. Here they are with names, and every one is reproduced by python3 stackwalk.py:
$ python3 stackwalk.py 2>&1 | sed -n '/THE IMAGE/,/KERNEL DESC/p'
argc 1
argv[0] /home/.../samples/imgdump
envp 64 entries, first: SHELL=/bin/bash
auxv 22 entries, 912 bytes of stack
AT_SYSINFO_EHDR 33 0x000072982688e000
AT_MINSIGSTKSZ 51 0x0000000000000d30
AT_HWCAP 16 0x00000000178bfbff
AT_PAGESZ 6 0x0000000000001000
AT_CLKTCK 17 0x0000000000000064
AT_PHDR 3 0x0000575f08dcf040
AT_PHENT 4 0x0000000000000038
AT_PHNUM 5 0x000000000000000e
AT_BASE 7 0x0000729826890000
AT_FLAGS 8 0x0000000000000000
AT_ENTRY 9 0x0000575f08dd0130
AT_UID 11 0x00000000000003e8
AT_EUID 12 0x00000000000003e8
AT_GID 13 0x00000000000003e8
AT_EGID 14 0x00000000000003e8
AT_SECURE 23 0x0000000000000000
AT_RANDOM 25 0x00007ffcba0335e9
AT_HWCAP2 26 0x0000000000000002
AT_EXECFN 31 0x00007ffcba034faa -> '.../imgdump'
AT_PLATFORM 15 0x00007ffcba0335f9 -> 'x86_64'
AT_RSEQ_FEATURE_SIZE 27 0x0000000000000021
AT_RSEQ_ALIGN 28 0x0000000000000040
Three things in that dump are worth stopping on, because each is a small trap.
It is not sorted. The kernel appends entries as various subsystems notice they need to say something, so the order is 33, 51, 16, 6, 17, 3, 4, 5, 7, 8, 9, 11…. Any code that reads the auxv must search for the tag it wants, exactly as it must search a hash table for a symbol. Assuming position is a bug, and the “missing” AT_PHDR that results is the kind of crash that looks like memory corruption.
Some values are addresses and some are numbers, and the tag does not tell you which.AT_PAGESZ is tag 6 and its value is 0x1000. That is a size, not an address. AT_PHNUM is tag 5 and its value is 0xe — fourteen, not an address. AT_PHENT is 0x38, which is 56, the size of a 64-bit program header. Dereferencing AT_PAGESZ gives you whatever is mapped at address 4096, which on this machine is nothing, and on a machine with a different floor might be something else entirely. The next concept takes this apart properly because it is the most common auxv bug.
Three entries are pointers to strings that live on the stack itself.AT_PLATFORM points at "x86_64" and AT_EXECFN at the pathname the kernel actually ran. The next concept shows where those strings sit in the byte layout, and the artifact asserts that AT_EXECFN matches /proc/<pid>/cmdline read at the same instant.
And two numbers deserve a note because they look like mistakes. AT_CLKTCK is 100, which is not a frequency — it is sysconf(_SC_CLK_TCK), the kernel’s fixed unit for the times() accounting, and it has been 100 on Linux for its entire history. AT_MINSIGSTKSZ is 3376, and that one has moved: it was 2048 for most of Linux’s life and rose for the unwinder work. A program that hardcoded 2048 and allocated a signal stack that size is relying on a number the kernel has already changed once.
There is no API, so you read the array yourself. The whole of it, in C, once you have the stack pointer — which is its own problem, and the subject of the next concept:
Ten lines, and the shape is fixed by the kernel. Three details carry the whole thing:
a[0] != 0, not a[0] != AT_NULL — those are the same thing, but the first is what the termination rule actually is: a zero tag ends the array, and the value in the next word is also zero.type=4076743392 and a crash three lines later. This is the single most common auxv bug and it produces output that looks like corruption rather than like a mistake.Now the check that makes it trustworthy. The kernel claims the program headers are at AT_PHDR and the entry point is at AT_ENTRY. The file says where they are relatively. If the kernel is telling the truth about a file it loaded at one base, the two absolute addresses must differ by exactly what the file says the two offsets differ by:
$ python3 stackwalk.py 2>&1 | grep 'AT_ENTRY - AT_PHDR' [ok ] AT_ENTRY - AT_PHDR == e_entry - e_phoff -- 0x10f0 == 0x10f0
Both are 0x10f0, and neither is derivable from the other. The left side came from the kernel walking the stack; the right side came from this script reading 24 bytes of an ELF header. Two independent sources, one number. That is the check worth building, and it is the spine of the whole course.
$ cd courses/img/assets/samples $ ./build_samples.sh 2>&1 | sed -n '/I1/,/I2/p' $ python3 crosscheck.py 2>&1 | tail -14 $ python3 stackwalk.py -v 2>&1 | grep '16 bytes'
Then push on the claims rather than repeating them:
1. Run stackwalk.py twenty times and diff the
auxv dumps. Which entries are CONSTANT
across runs, and which change? You should
find AT_RANDOM, AT_EXECFN, AT_PLATFORM
and the four credential entries constant,
and every address moving. Then explain the
credential entries: why would AT_UID be the
same every time while AT_ENTRY moves?
2. Delete the "step by two words" and change
the loop to a += 1. Run it. Write down the
first nonsense tag you see and where it came
from. (It is an ADDRESS, read as a tag.
This is the bug, and seeing it once is worth
more than being told about it.)
3. Add a new entry the kernel does not set,
say tag 999, to the table in stackwalk.py.
It will never appear. Now add tag 23 if you
removed it, run under "setpriv --reuid=0
--regid=0 --clear-groups" and see what
AT_SECURE does. (That is the entry that
tells a program the environment is
untrustworthy, and it is the reason
AT_SECURE exists at all.)
4. Compare the entry count across a static
binary (-static) and a dynamic one. The
auxv is a KERNEL structure, not a linker
one, so it cannot vanish -- but check
whether AT_BASE does.
Exercise 1 has the best answer in it. AT_UID is constant while AT_ENTRY is not, and the reason is that the first is a fact about who you are and the second is a fact about where the kernel put you. Those are different kinds of fact, and the auxv carries both without distinguishing them — which is precisely why the pointer-versus-size problem in the next concept exists, and why a program that treats the auxv as an array of addresses is wrong often enough to matter.
Exercise 4 is the one that ties this to the whole chain. The static linking course established that -static removes the dynamic loader from a program. It does not remove the kernel’s handoff, because the kernel does not know or care what the program will link against. The auxv is built before the loader exists. That is a good thing to be able to state precisely, because “static binaries skip the loading step” is true in the userspace sense and misleading in the kernel sense.
This concept is the floor the previous five courses stood on without mentioning. The object-file course established that e_entry is a number in a header. The relocations course established that a PIE gets loaded at a randomized base and therefore cannot have absolute addresses in its data. Both are consequences of the fact this concept measures: the base is chosen by the kernel, after the file is complete, and the only way it is reported is through this array.
The connection to the dynamic linking course is the sharpest one. That course described the loader’s global scope — the ordered list every undefined symbol is answered from — and never said where that scope comes from. It comes from here. The kernel decides which files to map, maps them, and then hands the loader two addresses: AT_BASE for itself and AT_PHDR for the program. The loader’s first job is to read the program headers at the address in the auxv, not at an address it computed. The scope is then built from the DT_NEEDED entries it finds there.
That makes AT_PHDR load-bearing for a program the reader has already met. Rebuilding the Scope Yourself asks the reader to parse DT_NEEDED out of a running process — and a real implementation of that exercise gets its program header address from the auxv, because there is nowhere else to get it. The artifact in this course is the same exercise, one layer down, and it is the reason AT_PHDR is checked against /proc/<pid>/maps rather than merely printed.
And the connection forward is inside this module. The auxv is on the stack, and the stack has a layout that the previous concept glossed as “an array”. Reading the Initial Stack by Hand takes the three arrays apart byte by byte, and it starts from a segfault — because the layout has an off-by-one in it that catches everyone, including the person who wrote this course’s first version.
One connection outward, because it explains a flag. AT_SECURE is 0 on every run in this course, and the PIE concept measured randomisation as a hardening measure. The two are the same mechanism seen from two sides: the kernel randomises the base and tells the program whether the environment is trustworthy, so a program that is told “not secure” can choose to stop trusting its own environment strings. A statically linked, position-dependent program gets a fixed address at noAT_BASE and no AT_PHDR surprise; that is the trade the two courses are making, and seeing both entries in one array is what makes it concrete.