When C calls a function, the arguments have to go somewhere the function can
find them. On 64-bit Linux (the System V calling convention), the rule
is:
- The first six arguments go in six registers, in a fixed order:
rdi, rsi, rdx, rcx, r8, r9. (A register is a tiny named
storage slot inside the CPU.)
- Any further arguments are pushed on the stack, the region of memory
the function uses for its local variables.
For printf, the format string is the first argument, so it uses rdi.
That leaves five registers and then the stack for the values:
printf( fmt , v1 , v2 , v3 , v4 , v5 , v6 , v7 , v8 ... )
| | | | | | | | |
rdi rsi rdx rcx r8 r9 [stack slots, 8 bytes each]
marker number: %1$ %2$ %3$ %4$ %5$ %6$ %7$ %8$ ...
Here is the real compiled code for
printf("%d %d %d %d %d %d %d %d\n", 11, 22, 33, 44, 55, 66, 77, 88),
from objdump -d -M intel (each line: address, machine-code bytes,
instruction):
401142: 6a 58 push 0x58 ; 88 goes on the stack
401144: 6a 4d push 0x4d ; 77
401146: 6a 42 push 0x42 ; 66
401148: 41 b9 37 00 00 00 mov r9d,0x37 ; 55 -> r9
40114e: 41 b8 2c 00 00 00 mov r8d,0x2c ; 44 -> r8
401154: b9 21 00 00 00 mov ecx,0x21 ; 33 -> rcx
401159: ba 16 00 00 00 mov edx,0x16 ; 22 -> rdx
40115e: be 0b 00 00 00 mov esi,0xb ; 11 -> rsi
401163: 48 8d 05 9a 0e 00 00 lea rax,[rip+0xe9a] ; address of the format string
40116a: 48 89 c7 mov rdi,rax ; format string -> rdi
401172: e8 c9 fe ff ff call 401040 <printf@plt>
The ; notes are added by hand. The hex values are the numbers above:
0x16 is 22, 0x58 is 88. (r9d, ecx, edx, esi are the lower
32-bit halves of r9, rcx, rdx, rsi; writing one clears the rest.)
The first five values are in registers. The last three were pushed on the
stack in reverse order, so that 66 ends up on top, where printf looks
first.
What happens when the programmer forgets the values
With printf(text) there is only the format string. Registers rsi
through r9 still hold whatever the program was doing before the call. The
stack above printf's return address still holds the caller's local
variables. printf does not know that, so a %p takes the next slot as if
someone had passed it.
what printf reads for %1$p %2$p %3$p %4$p %5$p %6$p %7$p ...
---- leftovers in registers --- --- the stack ---
rsi rdx rcx r8 r9 [slot] [slot]
|
the caller's local variables are here
So the numbered markers %6$p, %7$p, and so on show you the stack, one
8-byte slot at a time, in order from lower to higher addresses. The slots
you see include the caller's local variables and, with luck, your own typed
line, because that was stored in a local variable too.
Positional markers: %N$p
%7$p means "print argument number N=7" directly, without printing the six
before it. Numbering starts at 1 for the first value after the format
string, so %1$p is rsi, and %6$p is the first stack slot. This is why
you can probe one slot at a time instead of typing a very long line.