The "Design Trick" Behind Pushing Parameters Onto the Stack from Right to Left
Table of Contents
- The “Design Trick” Behind Pushing Parameters Onto the Stack from Right to Left
- 1. The Core Contradiction: The “Fog” of Variadic Arguments
- 2. A Stack Layout Diagram for “Right to Left”
- 3. Thought Experiment: What If the Stack Were Pushed “Left to Right”?
- 4. What This Design “Gives” to Pwn
- From r9 to rdi, this kind of reverse order:
The “Design Trick” Behind Pushing Parameters Onto the Stack from Right to Left
This is a brilliantly clever little “trick” in low-level computer design. We are used to reading from left to right, but the CPU insists on pushing arguments onto the stack from right to left. This is entirely to solve a logical paradox: If I don’t know how many arguments you have, how am I supposed to find the first one?
1. The Core Contradiction: The “Fog” of Variadic Arguments
Imagine the definition of the printf function:
int printf(const char *format, ...);Here, ... means the number of arguments is uncertain. When you call printf("%d %f %s", a, b, c), at the very moment the function begins running, it actually does not know whether you passed 3 arguments or 300.
The only thing it definitely knows is the first argument — the format string.
2. A Stack Layout Diagram for “Right to Left”
Suppose we pass 3 optional arguments through the stack (for simplicity, ignoring register argument passing for now):
printf(format, a, b, c)
If they are pushed in right-to-left order (c -> b -> a -> format), and the stack grows downward (from high addresses to low addresses):
| Memory Address | Stored Content | Note |
|---|---|---|
| High address | c | Last argument |
| b | ||
| a | ||
| Low address (RSP) | format | First argument (known) |
Why is this “clever”?
With this layout, no matter how many arguments you pass afterward, format is always located at [RSP] (or at a fixed position above the return address)
After printf enters, the first thing it can do is precisely capture format. Then it starts parsing the string: when it sees a %d, it knows, “Oh, move 8 bytes toward the higher address and I’ll find an integer”; when it sees a %s, it moves again toward the higher address.
3. Thought Experiment: What If the Stack Were Pushed “Left to Right”?
If the order were format -> a -> b -> c:
formatwould be pushed first and end up at a high address.cwould be pushed last and end up at the current stack top,RSP. Disaster strikes: Whenprintfbegins executing, it looks atRSP. At that point,RSPpoints toc. Butprintfhas no idea whether the current position iscorborformat, because it does not know how much stuff you pushed in! It cannot use a fixed offset to find the most criticalformatstring.
[!TIP]
Conclusion: Pushing the stack “from right to left” ensures that the first argument stays fixed near the top of the stack, giving the function the ability to parse the remaining arguments.
4. What This Design “Gives” to Pwn
For students studying binary security, this design leads directly to a very famous exploitation technique: the Format String Vulnerability.
Since printf parses the % symbols inside format and then blindly goes to the stack (or registers) to find data,
- if you input an
%p %p %p %pasformat, - then
printfwill obediently print out data from the stack that it was never supposed to show you in the first place (such as return addresses, canary values, or even function pointers). This happens becauseprintftrusts too much in the “indexing guide” provided byformat, and the right-to-left stack-pushing rule guarantees that it can always find this “guide.”
From r9 to rdi, this kind of reverse order:
The short answer is: for registers themselves, there is indeed no hard physical requirement on the order of mov. As long as, at the exact moment the call instruction executes, those 6 registers contain the correct values, the CPU is satisfied.
However, there really are a few “hidden rules” and bits of “engineering aesthetics” here that you may not know:
1. Why did I write it in reverse just now? (Mental inertia)
When writing assembly or reading compiler output, you often see the order from r9 to rdi written in reverse, mainly for two reasons:
- A sense of continuity with the stack: In the 32-bit era, arguments had to be pushed via
push, and the stack itself was pushed from right to left. Many veteran programmers or compilers subconsciously preserve this rhythm of “handle the last one first, handle the first one last” when writing 64-bit assembly. - Protecting RDI/RSI: In complex functions,
RDIandRSI(the first and second arguments) are usually the most important ones (for example, they may be the starting address of a string). Placing theirmovlast helps prevent accidentally clobbering already computedRDIwhile preparing other arguments (such as calculating the complex logic for the 5th argument).
2. The Real Hidden Rule: The “Hidden Boss” of Variadic Arguments (the AL Register)
When you call a variadic function like printf, besides those 6 registers, there is one extremely important hidden rule:
You must set the AL register (the low 8 bits of RAX).
-
Rule: For variadic functions, the
ALregister must store how many vector registers (XMM registers) were used to pass floating-point values. -
In practice: If you’re only printing ordinary integers or strings (with no floating-point values involved), you must execute
xor eax, eax(that is, makeAL = 0).
If you forget this step: the program may not necessarily crash, but in some strict system environments, printf may exhibit undefined behavior because it cannot determine the number of floating-point arguments. This is one of the easiest traps to fall into when manually constructing a ROP chain in Pwn challenges.
3. A Required Course for Pwn Players: Ordering Inside a ROP Chain
Although order does not matter in an ordinary mov instruction, in the ROP (Return-Oriented Programming) chains you care about most, order is the lifeline.
When you pass arguments through a Gadget like pop rdi; ret, your arguments are stored on the stack. Since the stack is read according to address growth direction, the physical order of your ROP chain must match exactly:
- The argument corresponding to the first
pop rdi. - The argument corresponding to the second
pop rsi.
At this point, you can’t just “write things however you want” — you must arrange your Payload according to the order of the Gadgets you found.
4. The “Dependency Trap” Between Registers
Sometimes the order must be fixed because there are dependencies between registers.
Look at this example:
Code segment
; 错误示例:如果你想让 p1 = p2 + 1mov edi, esi ; 此时 esi 还没初始化!mov esi, 200In this case, you must first mov esi, 200, and then mov edi, esi. When generating code, the compiler performs Data Flow Analysis to ensure that the order of register preparation does not create the awkward situation of “using it before it has been computed.”
Summary: Your Checklist of “Hidden Rules”
-
Freedom: For ordinary
movargument passing, the order is up to you; writing it in forward order is completely fine. -
AL finisher move: When calling functions like
printf, rememberxor eax, eax(setALto 0). -
ROP chain: When constructing an attack payload on the stack, the order is determined by the layout of Gadgets in memory and cannot be misplaced.
-
16-byte alignment: This is the biggest hidden rule. Before
call, the value ofRSPmust be divisible by 16, otherwise floating-point operations involved (even insideprintf) may trigger anSIGSEGVsegmentation fault.
Checkpoint:
With this whole “register seating chart” and its hidden rules, is there anything that still feels not intuitive enough? If this is OK, we can try manually writing a complete assembly Demo that includes AL setup and argument preparation to verify it! Or would you like to talk about how that 16-byte alignment ends up causing so much pain?