<p>I'm reading Appel's book <a href="https://www.cs.princeton.edu/%7Eappel/modern/c/index.html" rel="noreferrer">Modern Compiler Implementation in C</a>. I just read the following statement in chapter 6, and I wonder if it is accurate. It says that on modern machines, the calling conventions are to pass parameters in registers.</p>
<blockquote>
<p>[in older systems], function arguments were passed on the stack.  But this causes needless memory traffic. Studies of actual programs have shown that very few functions have more than four arguments, and almost none have more than six. Therefore, parameter-passing conventions for modern machines specify that the first k arguments (for k = 4 or k = 6, typically) of a function are passed in registers rp,...,rp+k−1, and the rest of the arguments are passed in memory.</p>
</blockquote>
<p>But I thought C and C++ still used the &quot;C calling convention&quot;, where arguments were passed on the stack, before the <code>call</code> instruction put the return address on the stack and jumped to the function implementation. Does it depend on the machine architecture? Maybe I think that because I've only been looking at X86 so far? Or maybe the compilers can choose other calling conventions for private functions that aren't exposed in shared libraries. Is this statement from the book accurate?</p>

<calling-conventions>
Are modern compilers passing parameters in registers instead of on the stack?
CC BY-SA 4.0
2023-07-27T16:03:15.460Z
A calling convention is only really necessary when code from one compilation calls a function defined in a separate compilation. When the same compilation job includes both the caller and the callee, the compiler can do whatever behaviour-preserving transformations it wants, including inlining the function or even eliminating the call entirely if it determines that the call is unnecessary.
kaya3
CC BY-SA 4.0
2023-07-27T16:07:30.623Z
The move from 32- to 64-bit x86 eliminated most of the calling conventions, so cdecl and fastcall are the same thing now. Of course, this never applied to other architectures in the first place.
Bbrk24
CC BY-SA 4.0
2023-07-28T02:28:17.453Z
No version of the C or C++ language specification defines calling conventions, and nothing in either language depends on there even *being* a stack at all.  Both languages bend over backwards to avoid provisions that could stand in the way of efficient implementation on diverse architectures, including esoteric ones.  That implementations should pass arguments in registers when that is feasible and efficient is perfectly in line with C philosophy, and allowed -- but not *required*.
John Bollinger
CC BY-SA 4.0
2023-07-28T06:59:42.987Z
Define modern. Go is generally considered a modern language, but its 1.0 through 1.16 compilers were passing all arguments on the stack, only [1.17](https://go.dev/doc/go1.17) introduced register passing...
Matthieu M.
CC BY-SA 4.0
2023-07-28T16:59:35.383Z
@MatthieuM.: The publication date of the book is 1998, when it was a reasonable assumption that everyone writing a compiler would care enough about performance to not intentionally pick an inefficient calling convention like Go did, except when required for compatibility with crusty old ABI standards, like i386 System V on Linux, and cdecl or stdcall conventions on x86 Windows.  Using a custom calling convention that's worse than the standard one for the target platform is not normal, but most rules have exceptions.
Peter Cordes
CC BY-SA 4.0
2023-07-28T19:23:43.267Z
Here's a quick counter example of a C function compiling to ARM with register parameters https://godbolt.org/z/xhvdx5v58
GandhiGandhi
CC BY-SA 4.0
2023-07-29T06:07:01.577Z
If you're wondering about x86-64 (which didn't exist even on paper until about 2000, publicly at least), there are plenty of SO Q&As about the fact that x86-64 ditched the bad legacy calling conventions of 32-bit x86, which you could find with a search like `site:stackoverflow.com x86 args in registers`.  e.g.  [Why does the x86-64 System V calling convention pass args in registers instead of just the stack?](https://stackoverflow.com/q/51976465) / [What are the calling conventions for UNIX & Linux system calls (and user-space functions) on i386 and x86-64](https://stackoverflow.com/q/2535989)
Peter Cordes
CC BY-SA 4.0
2023-07-29T06:08:36.013Z
See also [Why not store function parameters in XMM vector registers?](https://stackoverflow.com/q/33707228) and [What are callee and caller saved registers?](https://stackoverflow.com/a/56178078)    re: general design considerations for calling conventions.  Especially the way GCC devs (Jan Hubicka specifically) evaluated different designs for the x86-64 System V (see my answer on  [Why does Windows64 use a different calling convention from all other OSes on x86-64?](https://stackoverflow.com/a/35619528))
Peter Cordes
CC BY-SA 4.0
2023-07-29T06:12:31.420Z
@GandhiGandhi: You forgot to enable optimization so the asm is *way* more cluttered than is needs to be.  https://godbolt.org/z/eMx6W9ETY uses `-O3` (and `__attribute__((noinline,noipa))` to prevent optimizing away stuff in the caller) to get minimal asm for both caller and callee.  See also [How to remove "noise" from GCC/clang assembly output?](https://stackoverflow.com/q/38552116)
Peter Cordes
CC BY-SA 4.0
2023-07-30T03:26:29.210Z
Raymond Chen's _Old New Thing_ blog has been going through the various machine architectures that Windows supports. There's usually a chapter on parameter passing. For example, here's the start of his aerie on ARM: https://devblogs.microsoft.com/oldnewthing/20220726-00/?p=106898 and on parameter passing on ARM: https://devblogs.microsoft.com/oldnewthing/20220823-00/?p=107041. If you hunt around, ypu can find a similar series for many different machines
Flydog57
<p>In general the calling convention used by default for C/C++ code mostly depends on the CPU architecture and the operating system more than the specific compiler. Unix-like systems in particular use C function calls as the interface between application and platform. So alternative compilers generally had to follow the same calling convention as the system's primary compiler.</p>
<p>You could say &quot;why not use the system calling convention when calling system libraries and use a different calling convention for your own functions&quot;, but there are two problems with that.</p>
<ul>
<li>It's not obvious to the C compiler what is a function in a system library verses what is a function in your own program. That determination is not made until link time.</li>
<li>Some functions in the platform libraries take function pointers to callback functions. The compiler again doesn't know whether a function will be used as such a callback when compiling it.</li>
</ul>
<p>Early C compilers, building code for what we would now call CISC CPUs passed all parameters on the stack.</p>
<p>However when RISC CPUs came along, compilers for those CPUs tended to pass parameters in registers. Given your book was written in 1998 I suspect these RISC CPUs are what the author meant when he talked about &quot;modern computers&quot;.</p>
<p>Most of the old-school CISC architectures have died off or at least been relegated to embedded uses over the years. x86 being the notable exception.</p>
<p>When x86-64 came along, new calling conventions were devised. These new conventions followed the model used by RISC systems and passed parameters in registers.</p>
<p>This has essentially left 32-bit x86 code as the last bastion of old-school C calling conventions.</p>

CC BY-SA 4.0
2023-07-28T21:10:31.400Z
2023-07-28T21:10:31.400Z
Peter Green
<blockquote>
<p>on modern machines, the calling conventions are to pass parameters in registers</p>
</blockquote>
<p>It has nothing to do with modern machines.</p>
<p>In 1970s <em>B</em> (before <em>C</em>) on the Honeywell GCOS-3 system for example, the first two arguments to each function were passed in the <em>A</em> and <em>Q</em> 36-bit registers.</p>
<p>The called function could either save them on the stack or use them directly.
Many simple functions could execute very quickly, because they did everything in registers and unless they called other functions didn't even need to adjust the stack pointer.</p>

CC BY-SA 4.0
2023-07-29T00:52:29.890Z
2023-07-29T00:52:29.890Z
Ray Butterworth
CC BY-SA 4.0
2023-07-30T03:36:38.617Z
Way back in the dark ages when Lisp was first conceived (around 1960), two Lisp fundamentals were CAR and CDR (contents of the address register and contents of the data register) https://en.m.wikipedia.org/wiki/CAR_and_CDR#Etymology. Passing things around in registers has been around forever.
Flydog57
<p>Compilers going for the most speed will inline functions aggressively, which involves bypassing the calling convention entirely.  Also, when a function is strictly local, or when the compiler has access to the full program, it can safely ignore the official calling convention, and instead hardcode the function to take its arguments however is most effective. For the most part, older hardware didn’t have enough registers to use them for function arguments all the time, and newer hardware does.</p>
<p>Variadic functions in C, such as <code>printf(format, ...)</code>, historically used the stack, on the DEC PDP-8, and the way they’re specified to work essentially assumes that the arguments will be pushed onto the stack in reverse order and cleaned up by the caller (although there are other possible ways to do it).  Classic Fortran essentially assumes that function arguments are passed by reference, when they’re not global variables. A few older languages, such as COBOL, have a statement like <code>PERFORM</code> that doesn’t work the same way as function calls in Algol. Lisp machines are a particularly interesting example of an instruction set designed to accelerate the data structures of Lisp, which still uses the terms <code>car</code> and <code>cdr</code> after the format of the registers that were once used to pass arguments on a long-obsolete mainframe. But most new languages (and extensions to old ones) support the same kind of function calls that have become mainstream.</p>
<p>System APIs also define a particular convention for system calls and callbacks.  On the x86, these almost all passed arguments on the stack, largely because the machine had so few general-purpose registers that any non-tail call would need to spill its local variables onto the stack anyway.  (However, some compilers offered an optional <code>fastcall</code> calling convention that passed arguments in registers.)  When AMD created 64-bit long mode, they added more registers, and all 64-bit x86 systems pass function arguments in registers.  This included the x32 calling convention on Linux, which takes advantage of the extra registers in 32-bit mode, but by then the original was too established to change.</p>
<p>Most other general-purpose ISAs have at least 32 named registers (because they derive from 32-bit classic RISC designs, which all have 32-bit fixed-width instruction words that can be fetched in one bus cycle. which leave enough room for three register fields of five bits each), and therefore passed function arguments in registers from the beginning. SPARC was notable for having windows of input and output registers that shifted upon each function call.</p>

CC BY-SA 4.0
2023-07-28T21:23:52.523Z
2023-07-29T06:47:52.323Z
2023-07-29T06:47:52.323Z
Davislor
CC BY-SA 4.0
2023-07-29T03:55:15.437Z
*largely because the machine had so few general-purpose registers that any non-tail call would need to spill its local variables onto the stack anyway.* - It's not clear to me what point you're making here.  Classic x86-32 calling conventions have three call-clobbered ("volatile") registers, EAX, ECX, and EDX.  They're clobbered whether you pass args in them or not, and the other 4 registers (not including the stack pointer) can be used to hold locals across calls.  Passing one (thiscall), two (32-bit fastcall), or three (GCC regparm=3) args in those regs doesn't change that.
Peter Cordes
CC BY-SA 4.0
2023-07-29T03:57:49.620Z
It does leave a callee with no scratch registers in a function with 3 args.  The call-clobbered registers have "precious" values, and the call-preserved registers need to be saved before overwriting.  But even in cases where a leaf function does need to reserve some stack space and spill something, that's more or less break-even with the caller pushing the arg and the callee reloading it.  And that doesn't always happen.
Peter Cordes
CC BY-SA 4.0
2023-07-29T03:59:06.737Z
My guess has always been that classic calling conventions were designed more for simplicity than performance, especially for simplicity of variadic functions so all args could form one contiguous array without requiring shadow space (like Win x64) for the callee to dump reg args into.  (The return address gets in the way).
Peter Cordes
CC BY-SA 4.0
2023-07-29T04:02:45.107Z
*although there are other possible ways to do it [variadic functions]* - Note that ISO C says it's ok to call `printf("%d\n", 1, 2, 3);`.  A variadic function must be safely callable with more args than it can find out about (from earlier args such as the format string).  If you wanted to make a callee-pops convention for variadic functions, you'd need to separately pass a size to pop.  (And that's very inconvenient on x86; ISA support for callee-pops is limited to an immediate operand for `ret im16`, otherwise you have to copy the return address or something for runtime-variable callee pops.)
Peter Cordes
CC BY-SA 4.0
2023-07-29T04:09:33.987Z
See [C calling convention: who cleans the stack in variadic functions vs normal functions?](https://stackoverflow.com/q/64641240) / [Stack cleanup in stdcall (callee-pops) for variable arguments](https://stackoverflow.com/q/65863892) for discussion of some of the difficulty of callee-pops for variadic on x86.  But yes, some systems have used totally different ways to pass args, like dynamically allocating space for an "activation record" (I think IBM did that, maybe on their mainframes, instead of having a stack at all.)
Peter Cordes
CC BY-SA 4.0
2023-07-29T04:32:13.407Z
@PeterCordes Great feedback, as usual. I recall that, in addition, EBP was additionally the frame pointer unless you were compiling without those, and EBX typically needed to hold a shared-library or data structure base address. The ISA is non-orthogonal, so there were potentially some instructions you could only use if your scratch registers were esi, edi, eax, ebx, ebx, ecx or edx. And register-allocation algorithms were not as sophisticated as they are now. As you say, though, it was possible to define a calling convention that used only registers.
Davislor
CC BY-SA 4.0
2023-07-29T04:34:57.273Z
@PeterCordes As for variadic functions, you could in theory pass a pointer to a heap-allocated argument block or something, although I can’t think of anyone who did it that way. I wanted to leave room for someone to say, “On the Obsolete Systems Obscuratron/76, ...."
Davislor
CC BY-SA 4.0
2023-07-29T05:19:44.110Z
Yes, in position-independent code for 32-bit, traditionally compilers used EBX to hold a pointer to the GOT (global offset table); older GCC wouldn't even let an `asm` statement declare a clobber on it.  (Newer compilers are more flexible, and can choose a convenient register for a GOT pointer.  It was never part of the ABI at least on Linux, so each function had to read EIP separately and save/restore EBX.)  Efficient code will of course not waste instructions and a register on using EBP as a frame pointer unless required (e.g. alloca / VLA, or lack of DWARF stack-unwind metadata)
Peter Cordes
CC BY-SA 4.0
2023-07-29T05:22:22.330Z
Implicit uses for registers: that's why EAX, ECX, and EDX were chosen as the 3 call-clobbered.  REP-string stuff is rarer (especially in modern x86 where it's less efficient.)  EBX is pretty much only cmpxchg8b and cpuid, hence choosing it as the GOT pointer for 32-bit PIC.  See [Why are RBP and RSP called general-purpose registers?](https://stackoverflow.com/a/51347294) for some implicit uses for every legacy GPR.
Peter Cordes
CC BY-SA 4.0
2023-07-29T05:58:11.837Z
@PeterCordes I’d remembered BX having some addressing modes that the other GPRs don’t, but looking it up, that is not the case.
Davislor
CC BY-SA 4.0
2023-07-29T06:05:02.950Z
You're remembering 16-bit limitations.  BX is one of the 4 regs that can be used (in limited ways) in addressing modes with 16-bit address-size.  386 introduced a new encoding with an optional SIB byte, allowing encoding almost any combination of base and index for 32-bit address-size.  (ESP can't be an index).  [16-bit Assembly: cannot deref some registers](https://stackoverflow.com/q/52740507) / [Why don't x86 16-bit addressing modes have a scale factor, while the 32-bit version has it?](https://stackoverflow.com/q/55657904)
Peter Cordes
CC BY-SA 4.0
2023-07-29T06:43:50.453Z
@PeterCordes Ah, yes. And the 32-bit x86 conventions (except x32)  just minor variations on the 16-bit ones.
Davislor
CC BY-SA 4.0
2023-07-29T07:06:25.713Z
I've heard that 16-bit x86 calling conventions were less standardized, and register args were not rare.  But maybe that discussion included hand-written asm, not compilers.  I couldn't find the comment (perhaps from \@prl, but it was someone with lots of x86 experience, including back to those 16-bit DOS days).   (Also, [x32](https://en.wikipedia.org/wiki/X32_ABI) is a flavour of x86-64 System V with 32-bit pointers (ILP32), totally unrelated to 32 or 16-bit code.  I wouldn't call it a 32-bit convention at all, since `uint64_t` can be passed in one register.)
Peter Cordes
CC BY-SA 4.0
2023-07-29T07:07:30.983Z
I did find another good Q&A with discussion of why EBX is a good choice for a GOT pointer and to be call-preserved: [x86 Assembly - Why is \[e\]bx preserved in calling conventions?](https://stackoverflow.com/q/22214208) - also mentions important uses of other regs.
Peter Cordes
CC BY-SA 4.0
2023-07-29T07:17:41.787Z
My recollection of 16-bit x86 calling conventions is that the most common were Pascal (arguments pushed in order, callee cleans up the stack) and C (designed around variadic functions, as we've discussed). I’ve also seen a Fortran convention (all arguments passed by reference). Each of these could have `near` or `far` object and code pointers, whose combinations were the small, compact, large and huge data models. Most 16-bit Windows code was written in C and used the C calling convention for internal calls, but the Windows API used `far pascal`.
Davislor
CC BY-SA 4.0
2023-07-29T07:26:44.787Z
@PeterCordes There were also, as I recall, some less-widely-used variations like the `fastcall` I mentioned and the `huge` convention (in real-mode only). BIOS and DOS services were accessed through interrupts, which as you know was a different instruction from `call`, and always passed their arguments in registers. Perhaps I should mention those. And if you were writing assembly by hand, or declaring a function with no internal linkage, you didn’t have to follow any formal calling convention at all; no external code would need to call it.
Davislor
CC BY-SA 4.0
2023-07-29T07:32:41.397Z
Yeah, I found [x86 16 bit assembly function argument registers best practices](https://stackoverflow.com/q/74308368) while looking for discussion of 16-bit calling conventions.  Sep's answer suggests using a register-args convention similar to what the DOS `int 21h` ABI uses.  And yeah, a compiler could inline DOS `int 21h` calls into your code; most registers (except AX) are call-preserved with those.  Otherwise they could wrap them like Unix libc does, so you have a plain function following the standard calling convention that internally uses `int 80h` or `syscall` or whatever.
Peter Cordes
CC BY-SA 4.0
2023-07-29T08:18:15.303Z
Minor self-correction: the four memory models on 8088/8086 were small, compact, medium and large, where compact had `near` function pointers and `far` data pointers, `medium` was the reverse, small used only `near` pointers and large used only `far` pointers. Some compilers also had `tiny` and `huge`, which followed special rules. I wrote "huge" where I meant "medium."
Davislor
CC BY-SA 4.0
2023-07-29T09:46:20.217Z
The "tiny" memory model is `.com` executables, where everything is `near` and furthermore CS/DS/ES/SS are all equal to each other.  (I'm fairly well acquainted with 8086 segmentation and some about near/far pointers and memory models thanks to endless SO questions from people who think "learning assembly" means turning back to the dark ages when assembly was commonly used, in the 16-bit x86 era.  I was just lucky enough not to have to suffer through that era as a user or developer of x86 software (I had Atari ST then Linux), so it's an interesting study in computer architecture and adapting C)
Peter Cordes
CC BY-SA 4.0
2023-07-29T19:52:24.437Z
*x32... which takes advantage of the extra registers in 32-bit mode* - No, the x32 ABI has the CPU in 64-bit long mode.  That's the only way extra registers are available.  It often uses `67h` address-size prefixes to override the default address-size from 64 to 32-bit, but it's still an ILP32 ABI for 64-bit **mode**.  Again, it's totally unrelated to the i386 System V ABI; it's documented in a section of the AMD64 System V ABI, because it's a minor modification of it.
Peter Cordes
<p>In early C, it was required that code which called functions that would today have prototypes:</p>
<pre><code>int test1(int,int,int);
int test2(int, ...);
</code></pre>
<p>process the calls <code>test1(1,2,3);</code> and <code>test2(1,2,3);</code> identically, save only for the address of the target function.  A convention that passes all arguments on the stack works better for the second style of function than one which tries to put some on the stack and some in registers, and putting arguments on the stack when calling the second style of function meant it was necessary to put arguments on the stack when calling all functions.</p>

CC BY-SA 4.0
2023-07-28T17:53:33.010Z
2023-07-28T17:53:33.010Z
supercat
CC BY-SA 4.0
2023-07-28T21:18:55.927Z
Passing parameters in general purpose registers is not too bad. A variadic function can just push them to the stack in the function prologue. What does ramp up the complexity is using more than one type of register for parameter passing.
Peter Green
CC BY-SA 4.0
2023-07-29T02:13:19.093Z
@PeterGreen: Having the function prologue stack registers works nicely on the ARM, which can stack them ahead of the return address.  On most other platforms, it would be necessary to either shuffle the return address around the parameters or skip over the return address when retrieving the arguments.
supercat
CC BY-SA 4.0
2023-07-29T02:31:39.523Z
Using a link register for return address doesn't seem exclusive to arm. You are right it would be annoying on architectures where the call instruction pushes the return address to the stack though.
Peter Green
CC BY-SA 4.0
2023-07-29T03:41:10.970Z
x86-64 System V aggressively uses registers for parameter passing, but still satisfies this requirement that an unprototyped call can safely be made to a function that's declared normally (assuming it uses types compatible with the default argument promotions), even for variadic functions.  Yes, @PeterGreen, code-gen for variadic functions is inconvenient, having to keep track of whether you've used up the 6 GPRs or the 8 XMM registers, depending on whether the arg type for `va_arg` is int or FP (or `__m128i`).
Peter Cordes
CC BY-SA 4.0
2023-07-29T03:41:55.007Z
(cont.) ... since you can't conveniently make one contiguous array of args due to the return address being in the way.  (It would be UB for `va_arg` with an integer parameter to read an FP arg, so the calling convention assumes that doesn't happen).  Unless callers can see a non-variadic prototype, they set AL = # of XMM regs used for FP args.
Peter Cordes
CC BY-SA 4.0
2023-07-29T03:44:07.337Z
@PeterGreen: Windows x64 makes an interesting tradeoff to make variadic functions efficient, only allowing 4 *total* args to be passed in registers (unlike x86-64 which allows up to 6 integer *and* up to 8 FP).  For variadic functions, FP args need to be duplicated to the corresponding integer reg, so callees can simply dump the four integer arg-passing regs to the **shadow space** (32B above the return address, which the caller reserved for the callee), forming an array contiguous with the stack args.  https://godbolt.org/z/eh47enYdn
Peter Cordes
CC BY-SA 4.0
2023-07-30T17:40:02.637Z
@PeterCordes: If the C calling conventions hadn't been designed in a manner that "just so happened" to support variadic calling convention, I wonder how hard it would have been to say that an argument which starts with an open bracket would accept a comma-separated list of items that would be promoted and placed into a structure, whose address would then be passed?  So a printf-style output would be `printf("%d/%d", [value1, value2]);`, with the function itself always accepting a format pointer and an argument pointer?
supercat
CC BY-SA 4.0
2023-07-30T17:46:23.507Z
@PeterCordes: As an alternative, on many platforms, it might have been practical to have it pass a double-indirect function pointer which, if passed a copy of itself and a pointer to an "argumnet descriptor" struct, would fetch the next argument into that structure.  The machine code for the function call could be a "call arg_prep" instruction, followed in the machine code with information about the arguments (their types, their frame-relative or global address, level of indirection, etc.)  For programs that pass a lot of variadic arguments, the information about arguments could be smaller...
supercat
CC BY-SA 4.0
2023-07-30T17:47:13.617Z
....than code to load and push them, and such an approach could provide type safety and type coercion in ways the present design cannot.
supercat
<p>It's true that some of the most common <a href="https://en.wikipedia.org/wiki/X86_calling_conventions" rel="noreferrer">calling conventions for  <strong>32-bit</strong> x86 code</a> require all parameters to be passed on the stack.  One of these conventions is called &quot;cdecl&quot;, but that's really just a name and isn't inherently tied to the C or C++ language in any way.</p>
<p>But x86-32 is not a modern architecture these days, and is the exception here rather than the rule.  Compilers for most other architectures mostly use calling conventions where some parameters are passed in registers.  This includes x86-64, where the two most popular calling conventions are Microsoft x64, which uses four registers, and System V, which uses six (see the Wikipedia article linked above, further down).  For ARM, the Arm company publishes recommended calling conventions (the Procedure Call Standard); the <a href="https://github.com/ARM-software/abi-aa/blob/844a79fd4c77252a11342709e3b27b2c9f590cf1/aapcs32/aapcs32.rst" rel="noreferrer">32-bit version</a>  has four parameters passed in registers, and the <a href="https://github.com/ARM-software/abi-aa/blob/844a79fd4c77252a11342709e3b27b2c9f590cf1/aapcs64/aapcs64.rst" rel="noreferrer">64-bit version</a> uses eight.</p>

CC BY-SA 4.0
2023-07-28T06:22:22.490Z
2023-07-28T06:22:22.490Z
Nate Eldredge
<h1>There are many calling conventions, and most pass at least some arguments in the register</h1>
<p>The comment in your book is basically accurate. There is no fully &quot;standard&quot; C calling convention, and each combination of language, machine architecture and compiler can choose the ones that it prefers. Functions with multiple calling conventions can exist inside of the same binary as well <a href="https://learn.microsoft.com/en-us/cpp/cpp/argument-passing-and-naming-conventions?view=msvc-170" rel="noreferrer">as shown by the MSVC x86 calling convention docs</a>.</p>
<p>Almost all calling conventions try to pass integer and pointer arguments in normal registers and floating point arguments in floating pointer registers for the performance benefits mentioned, as reading data from the stack can be less efficient. You might be thinking of the <a href="https://en.wikipedia.org/wiki/X86_calling_conventions#cdecl" rel="noreferrer"><code>cdecl</code> calling convention</a>, an X86 calling convention that is one of the few that does require all arguments to be passed on the stack.</p>
<p>You are correct regarding private functions -- if a function can only be interacted with from inside code totally under the control of one compiler, you can create and use custom calling conventions. A trivial example of this are <a href="https://gcc.gnu.org/onlinedocs/gccint/Leaf-Functions.html" rel="noreferrer">trivial functions that don't call other functions or allocate stack space called leaf functions</a>; because they don't require all of the same machinery that normal function calls might, they might be compiled to not have the standard calling convention in use.</p>
<p>Some other programming languages utilize a custom calling optimized for their own internal use but still utilize the conventional architecture/platform ABI for exporting functions that can be called from C code (and thus most languages that can create bindings to C code, as it is commonly used as a common FFI interface). Since most operating interfaces and system calls are exposed as C functions, these compiler normally have to be able to correctly compile calls to the appropriate platform/architecture convention in use to access them as well.</p>

CC BY-SA 4.0
2023-07-27T16:46:12.537Z
2023-07-27T16:46:12.537Z
JennyJams
CC BY-SA 4.0
2023-07-28T06:34:05.333Z
Agreed, and some language implementations have pragmata specifying e.g. which registers may be used for integer parameters, which registers must be preserved and so on.
Mark Morgan Lloyd
# Are modern compilers passing parameters in registers instead of on the stack?
Posted by: username_0 on 2023-07-27
I'm reading Appel's book [Modern Compiler Implementation in C](https://www.cs.princeton.edu/%7Eappel/modern/c/index.html). I just read the following statement in chapter 6, and I wonder if it is accurate. It says that on modern machines, the calling conventions are to pass parameters in registers.

> [in older systems], function arguments were passed on the stack. But this causes needless memory traffic. Studies of actual programs have shown that very few functions have more than four arguments, and almost none have more than six. Therefore, parameter-passing conventions for modern machines specify that the first k arguments (for k = 4 or k = 6, typically) of a function are passed in registers rp,...,rp+k−1, and the rest of the arguments are passed in memory.

But I thought C and C++ still used the "C calling convention", where arguments were passed on the stack, before the `call` instruction put the return address on the stack and jumped to the function implementation. Does it depend on the machine architecture? Maybe I think that because I've only been looking at X86 so far? Or maybe the compilers can choose other calling conventions for private functions that aren't exposed in shared libraries. Is this statement from the book accurate?
**Tags:** calling-conventions
> **Comments:**
> **username_1**: A calling convention is only really necessary when code from one compilation calls a function defined in a separate compilation. When the same compilation job includes both the caller and the callee, the compiler can do whatever behaviour-preserving transformations it wants, including inlining the function or even eliminating the call entirely if it determines that the call is unnecessary.  
> **username_2**: The move from 32- to 64-bit x86 eliminated most of the calling conventions, so cdecl and fastcall are the same thing now. Of course, this never applied to other architectures in the first place.  
> **username_3**: No version of the C or C++ language specification defines calling conventions, and nothing in either language depends on there even *being* a stack at all.  Both languages bend over backwards to avoid provisions that could stand in the way of efficient implementation on diverse architectures, including esoteric ones.  That implementations should pass arguments in registers when that is feasible and efficient is perfectly in line with C philosophy, and allowed -- but not *required*.  
> **username_4**: Define modern. Go is generally considered a modern language, but its 1.0 through 1.16 compilers were passing all arguments on the stack, only [1.17](https://go.dev/doc/go1.17) introduced register passing...  
> **username_5**: @MatthieuM.: The publication date of the book is 1998, when it was a reasonable assumption that everyone writing a compiler would care enough about performance to not intentionally pick an inefficient calling convention like Go did, except when required for compatibility with crusty old ABI standards, like i386 System V on Linux, and cdecl or stdcall conventions on x86 Windows.  Using a custom calling convention that's worse than the standard one for the target platform is not normal, but most rules have exceptions.  
> **username_6**: Here's a quick counter example of a C function compiling to ARM with register parameters https://godbolt.org/z/xhvdx5v58  
> **username_5**: If you're wondering about x86-64 (which didn't exist even on paper until about 2000, publicly at least), there are plenty of SO Q&As about the fact that x86-64 ditched the bad legacy calling conventions of 32-bit x86, which you could find with a search like `site:stackoverflow.com x86 args in registers`.  e.g.  [Why does the x86-64 System V calling convention pass args in registers instead of just the stack?](https://stackoverflow.com/q/51976465) / [What are the calling conventions for UNIX & Linux system calls (and user-space functions) on i386 and x86-64](https://stackoverflow.com/q/2535989)  
> **username_5**: See also [Why not store function parameters in XMM vector registers?](https://stackoverflow.com/q/33707228) and [What are callee and caller saved registers?](https://stackoverflow.com/a/56178078)    re: general design considerations for calling conventions.  Especially the way GCC devs (Jan Hubicka specifically) evaluated different designs for the x86-64 System V (see my answer on  [Why does Windows64 use a different calling convention from all other OSes on x86-64?](https://stackoverflow.com/a/35619528))  
> **username_5**: @GandhiGandhi: You forgot to enable optimization so the asm is *way* more cluttered than is needs to be.  https://godbolt.org/z/eMx6W9ETY uses `-O3` (and `__attribute__((noinline,noipa))` to prevent optimizing away stuff in the caller) to get minimal asm for both caller and callee.  See also [How to remove "noise" from GCC/clang assembly output?](https://stackoverflow.com/q/38552116)  
> **username_7**: Raymond Chen's _Old New Thing_ blog has been going through the various machine architectures that Windows supports. There's usually a chapter on parameter passing. For example, here's the start of his aerie on ARM: https://devblogs.microsoft.com/oldnewthing/20220726-00/?p=106898 and on parameter passing on ARM: https://devblogs.microsoft.com/oldnewthing/20220823-00/?p=107041. If you hunt around, ypu can find a similar series for many different machines  
---
## 6 answers
### Answer by username_13 on 2023-07-27. Score: 25
# There are many calling conventions, and most pass at least some arguments in the register

The comment in your book is basically accurate. There is no fully "standard" C calling convention, and each combination of language, machine architecture and compiler can choose the ones that it prefers. Functions with multiple calling conventions can exist inside of the same binary as well [as shown by the MSVC x86 calling convention docs](https://learn.microsoft.com/en-us/cpp/cpp/argument-passing-and-naming-conventions?view=msvc-170).

Almost all calling conventions try to pass integer and pointer arguments in normal registers and floating point arguments in floating pointer registers for the performance benefits mentioned, as reading data from the stack can be less efficient. You might be thinking of the [`cdecl` calling convention](https://en.wikipedia.org/wiki/X86_calling_conventions#cdecl), an X86 calling convention that is one of the few that does require all arguments to be passed on the stack.

You are correct regarding private functions -- if a function can only be interacted with from inside code totally under the control of one compiler, you can create and use custom calling conventions. A trivial example of this are [trivial functions that don't call other functions or allocate stack space called leaf functions](https://gcc.gnu.org/onlinedocs/gccint/Leaf-Functions.html); because they don't require all of the same machinery that normal function calls might, they might be compiled to not have the standard calling convention in use.

Some other programming languages utilize a custom calling optimized for their own internal use but still utilize the conventional architecture/platform ABI for exporting functions that can be called from C code (and thus most languages that can create bindings to C code, as it is commonly used as a common FFI interface). Since most operating interfaces and system calls are exposed as C functions, these compiler normally have to be able to correctly compile calls to the appropriate platform/architecture convention in use to access them as well.
> **Comments:**
> **username_14** (2023-07-28): Agreed, and some language implementations have pragmata specifying e.g. which registers may be used for integer parameters, which registers must be preserved and so on.  
---
### Answer by username_12 on 2023-07-28. Score: 10
It's true that some of the most common [calling conventions for **32-bit** x86 code](https://en.wikipedia.org/wiki/X86_calling_conventions) require all parameters to be passed on the stack. One of these conventions is called "cdecl", but that's really just a name and isn't inherently tied to the C or C++ language in any way.

But x86-32 is not a modern architecture these days, and is the exception here rather than the rule. Compilers for most other architectures mostly use calling conventions where some parameters are passed in registers. This includes x86-64, where the two most popular calling conventions are Microsoft x64, which uses four registers, and System V, which uses six (see the Wikipedia article linked above, further down). For ARM, the Arm company publishes recommended calling conventions (the Procedure Call Standard); the [32-bit version](https://github.com/ARM-software/abi-aa/blob/844a79fd4c77252a11342709e3b27b2c9f590cf1/aapcs32/aapcs32.rst) has four parameters passed in registers, and the [64-bit version](https://github.com/ARM-software/abi-aa/blob/844a79fd4c77252a11342709e3b27b2c9f590cf1/aapcs64/aapcs64.rst) uses eight.
---
### Answer by username_11 on 2023-07-28. Score: 7
In early C, it was required that code which called functions that would today have prototypes:
int test1(int,int,int);
int test2(int, ...);

process the calls test1(1,2,3); and test2(1,2,3); identically, save only for the address of the target function.  A convention that passes all arguments on the stack works better for the second style of function than one which tries to put some on the stack and some in registers, and putting arguments on the stack when calling the second style of function meant it was necessary to put arguments on the stack when calling all functions.
> **Comments:**
> **username_8** (2023-07-28): Passing parameters in general purpose registers is not too bad. A variadic function can just push them to the stack in the function prologue. What does ramp up the complexity is using more than one type of register for parameter passing.  
> **username_11** (2023-07-29): @PeterGreen: Having the function prologue stack registers works nicely on the ARM, which can stack them ahead of the return address.  On most other platforms, it would be necessary to either shuffle the return address around the parameters or skip over the return address when retrieving the arguments.  
> **username_8** (2023-07-29): Using a link register for return address doesn't seem exclusive to arm. You are right it would be annoying on architectures where the call instruction pushes the return address to the stack though.  
> **username_5** (2023-07-29): x86-64 System V aggressively uses registers for parameter passing, but still satisfies this requirement that an unprototyped call can safely be made to a function that's declared normally (assuming it uses types compatible with the default argument promotions), even for variadic functions.  Yes, @PeterGreen, code-gen for variadic functions is inconvenient, having to keep track of whether you've used up the 6 GPRs or the 8 XMM registers, depending on whether the arg type for `va_arg` is int or FP (or `__m128i`).  
> **username_5** (2023-07-29): (cont.) ... since you can't conveniently make one contiguous array of args due to the return address being in the way.  (It would be UB for `va_arg` with an integer parameter to read an FP arg, so the calling convention assumes that doesn't happen).  Unless callers can see a non-variadic prototype, they set AL = # of XMM regs used for FP args.  
> **username_5** (2023-07-29): @PeterGreen: Windows x64 makes an interesting tradeoff to make variadic functions efficient, only allowing 4 *total* args to be passed in registers (unlike x86-64 which allows up to 6 integer *and* up to 8 FP).  For variadic functions, FP args need to be duplicated to the corresponding integer reg, so callees can simply dump the four integer arg-passing regs to the **shadow space** (32B above the return address, which the caller reserved for the callee), forming an array contiguous with the stack args.  https://godbolt.org/z/eh47enYdn  
> **username_11** (2023-07-30): @PeterCordes: If the C calling conventions hadn't been designed in a manner that "just so happened" to support variadic calling convention, I wonder how hard it would have been to say that an argument which starts with an open bracket would accept a comma-separated list of items that would be promoted and placed into a structure, whose address would then be passed?  So a printf-style output would be `printf("%d/%d", [value1, value2]);`, with the function itself always accepting a format pointer and an argument pointer?  
> **username_11** (2023-07-30): @PeterCordes: As an alternative, on many platforms, it might have been practical to have it pass a double-indirect function pointer which, if passed a copy of itself and a pointer to an "argumnet descriptor" struct, would fetch the next argument into that structure.  The machine code for the function call could be a "call arg_prep" instruction, followed in the machine code with information about the arguments (their types, their frame-relative or global address, level of indirection, etc.)  For programs that pass a lot of variadic arguments, the information about arguments could be smaller...  
> **username_11** (2023-07-30): ....than code to load and push them, and such an approach could provide type safety and type coercion in ways the present design cannot.  
---
### Answer by username_8 on 2023-07-28. Score: 6
In general the calling convention used by default for C/C++ code mostly depends on the CPU architecture and the operating system more than the specific compiler. Unix-like systems in particular use C function calls as the interface between application and platform. So alternative compilers generally had to follow the same calling convention as the system's primary compiler.

You could say "why not use the system calling convention when calling system libraries and use a different calling convention for your own functions", but there are two problems with that.

* It's not obvious to the C compiler what is a function in a system library verses what is a function in your own program. That determination is not made until link time.
* Some functions in the platform libraries take function pointers to callback functions. The compiler again doesn't know whether a function will be used as such a callback when compiling it.

Early C compilers, building code for what we would now call CISC CPUs passed all parameters on the stack.

However when RISC CPUs came along, compilers for those CPUs tended to pass parameters in registers. Given your book was written in 1998 I suspect these RISC CPUs are what the author meant when he talked about "modern computers".

Most of the old-school CISC architectures have died off or at least been relegated to embedded uses over the years. x86 being the notable exception.

When x86-64 came along, new calling conventions were devised. These new conventions followed the model used by RISC systems and passed parameters in registers.

This has essentially left 32-bit x86 code as the last bastion of old-school C calling conventions.
---
### Answer by username_9 on 2023-07-29. Score: 4
> on modern machines, the calling conventions are to pass parameters in registers

It has nothing to do with modern machines.

In 1970s *B* (before *C*) on the Honeywell GCOS-3 system for example, the first two arguments to each function were passed in the *A* and *Q* 36-bit registers.

The called function could either save them on the stack or use them directly.
Many simple functions could execute very quickly, because they did everything in registers and unless they called other functions didn't even need to adjust the stack pointer.
> **Comments:**
> **username_7** (2023-07-30): Way back in the dark ages when Lisp was first conceived (around 1960), two Lisp fundamentals were CAR and CDR (contents of the address register and contents of the data register) https://en.m.wikipedia.org/wiki/CAR_and_CDR#Etymology. Passing things around in registers has been around forever.  
---
### Answer by username_10 on 2023-07-28. Score: 4
Compilers going for the most speed will inline functions aggressively, which involves bypassing the calling convention entirely. Also, when a function is strictly local, or when the compiler has access to the full program, it can safely ignore the official calling convention, and instead hardcode the function to take its arguments however is most effective. For the most part, older hardware didn’t have enough registers to use them for function arguments all the time, and newer hardware does.

Variadic functions in C, such as `printf(format, ...)`, historically used the stack, on the DEC PDP-8, and the way they’re specified to work essentially assumes that the arguments will be pushed onto the stack in reverse order and cleaned up by the caller (although there are other possible ways to do it). Classic Fortran essentially assumes that function arguments are passed by reference, when they’re not global variables. A few older languages, such as COBOL, have a statement like `PERFORM` that doesn’t work the same way as function calls in Algol. Lisp machines are a particularly interesting example of an instruction set designed to accelerate the data structures of Lisp, which still uses the terms `car` and `cdr` after the format of the registers that were once used to pass arguments on a long-obsolete mainframe. But most new languages (and extensions to old ones) support the same kind of function calls that have become mainstream.

System APIs also define a particular convention for system calls and callbacks. On the x86, these almost all passed arguments on the stack, largely because the machine had so few general-purpose registers that any non-tail call would need to spill its local variables onto the stack anyway. (However, some compilers offered an optional `fastcall` calling convention that passed arguments in registers.) When AMD created 64-bit long mode, they added more registers, and all 64-bit x86 systems pass function arguments in registers. This included the x32 calling convention on Linux, which takes advantage of the extra registers in 32-bit mode, but by then the original was too established to change.

Most other general-purpose ISAs have at least 32 named registers (because they derive from 32-bit classic RISC designs, which all have 32-bit fixed-width instruction words that can be fetched in one bus cycle. which leave enough room for three register fields of five bits each), and therefore passed function arguments in registers from the beginning. SPARC was notable for having windows of input and output registers that shifted upon each function call.
> **Comments:**
> **username_5** (2023-07-29): *largely because the machine had so few general-purpose registers that any non-tail call would need to spill its local variables onto the stack anyway.* - It's not clear to me what point you're making here.  Classic x86-32 calling conventions have three call-clobbered ("volatile") registers, EAX, ECX, and EDX.  They're clobbered whether you pass args in them or not, and the other 4 registers (not including the stack pointer) can be used to hold locals across calls.  Passing one (thiscall), two (32-bit fastcall), or three (GCC regparm=3) args in those regs doesn't change that.  
> **username_5** (2023-07-29): It does leave a callee with no scratch registers in a function with 3 args.  The call-clobbered registers have "precious" values, and the call-preserved registers need to be saved before overwriting.  But even in cases where a leaf function does need to reserve some stack space and spill something, that's more or less break-even with the caller pushing the arg and the callee reloading it.  And that doesn't always happen.  
> **username_5** (2023-07-29): My guess has always been that classic calling conventions were designed more for simplicity than performance, especially for simplicity of variadic functions so all args could form one contiguous array without requiring shadow space (like Win x64) for the callee to dump reg args into.  (The return address gets in the way).  
> **username_5** (2023-07-29): *although there are other possible ways to do it [variadic functions]* - Note that ISO C says it's ok to call `printf("%d\n", 1, 2, 3);`.  A variadic function must be safely callable with more args than it can find out about (from earlier args such as the format string).  If you wanted to make a callee-pops convention for variadic functions, you'd need to separately pass a size to pop.  (And that's very inconvenient on x86; ISA support for callee-pops is limited to an immediate operand for `ret im16`, otherwise you have to copy the return address or something for runtime-variable callee pops.)  
> **username_5** (2023-07-29): See [C calling convention: who cleans the stack in variadic functions vs normal functions?](https://stackoverflow.com/q/64641240) / [Stack cleanup in stdcall (callee-pops) for variable arguments](https://stackoverflow.com/q/65863892) for discussion of some of the difficulty of callee-pops for variadic on x86.  But yes, some systems have used totally different ways to pass args, like dynamically allocating space for an "activation record" (I think IBM did that, maybe on their mainframes, instead of having a stack at all.)  
> **username_10** (2023-07-29): @PeterCordes Great feedback, as usual. I recall that, in addition, EBP was additionally the frame pointer unless you were compiling without those, and EBX typically needed to hold a shared-library or data structure base address. The ISA is non-orthogonal, so there were potentially some instructions you could only use if your scratch registers were esi, edi, eax, ebx, ebx, ecx or edx. And register-allocation algorithms were not as sophisticated as they are now. As you say, though, it was possible to define a calling convention that used only registers.  
> **username_10** (2023-07-29): @PeterCordes As for variadic functions, you could in theory pass a pointer to a heap-allocated argument block or something, although I can’t think of anyone who did it that way. I wanted to leave room for someone to say, “On the Obsolete Systems Obscuratron/76, ...."  
> **username_5** (2023-07-29): Yes, in position-independent code for 32-bit, traditionally compilers used EBX to hold a pointer to the GOT (global offset table); older GCC wouldn't even let an `asm` statement declare a clobber on it.  (Newer compilers are more flexible, and can choose a convenient register for a GOT pointer.  It was never part of the ABI at least on Linux, so each function had to read EIP separately and save/restore EBX.)  Efficient code will of course not waste instructions and a register on using EBP as a frame pointer unless required (e.g. alloca / VLA, or lack of DWARF stack-unwind metadata)  
> **username_5** (2023-07-29): Implicit uses for registers: that's why EAX, ECX, and EDX were chosen as the 3 call-clobbered.  REP-string stuff is rarer (especially in modern x86 where it's less efficient.)  EBX is pretty much only cmpxchg8b and cpuid, hence choosing it as the GOT pointer for 32-bit PIC.  See [Why are RBP and RSP called general-purpose registers?](https://stackoverflow.com/a/51347294) for some implicit uses for every legacy GPR.  
> **username_10** (2023-07-29): @PeterCordes I’d remembered BX having some addressing modes that the other GPRs don’t, but looking it up, that is not the case.  
> **username_5** (2023-07-29): You're remembering 16-bit limitations.  BX is one of the 4 regs that can be used (in limited ways) in addressing modes with 16-bit address-size.  386 introduced a new encoding with an optional SIB byte, allowing encoding almost any combination of base and index for 32-bit address-size.  (ESP can't be an index).  [16-bit Assembly: cannot deref some registers](https://stackoverflow.com/q/52740507) / [Why don't x86 16-bit addressing modes have a scale factor, while the 32-bit version has it?](https://stackoverflow.com/q/55657904)  
> **username_10** (2023-07-29): @PeterCordes Ah, yes. And the 32-bit x86 conventions (except x32)  just minor variations on the 16-bit ones.  
> **username_5** (2023-07-29): I've heard that 16-bit x86 calling conventions were less standardized, and register args were not rare.  But maybe that discussion included hand-written asm, not compilers.  I couldn't find the comment (perhaps from \@prl, but it was someone with lots of x86 experience, including back to those 16-bit DOS days).   (Also, [x32](https://en.wikipedia.org/wiki/X32_ABI) is a flavour of x86-64 System V with 32-bit pointers (ILP32), totally unrelated to 32 or 16-bit code.  I wouldn't call it a 32-bit convention at all, since `uint64_t` can be passed in one register.)  
> **username_5** (2023-07-29): I did find another good Q&A with discussion of why EBX is a good choice for a GOT pointer and to be call-preserved: [x86 Assembly - Why is \[e\]bx preserved in calling conventions?](https://stackoverflow.com/q/22214208) - also mentions important uses of other regs.  
> **username_10** (2023-07-29): My recollection of 16-bit x86 calling conventions is that the most common were Pascal (arguments pushed in order, callee cleans up the stack) and C (designed around variadic functions, as we've discussed). I’ve also seen a Fortran convention (all arguments passed by reference). Each of these could have `near` or `far` object and code pointers, whose combinations were the small, compact, large and huge data models. Most 16-bit Windows code was written in C and used the C calling convention for internal calls, but the Windows API used `far pascal`.  
> **username_10** (2023-07-29): @PeterCordes There were also, as I recall, some less-widely-used variations like the `fastcall` I mentioned and the `huge` convention (in real-mode only). BIOS and DOS services were accessed through interrupts, which as you know was a different instruction from `call`, and always passed their arguments in registers. Perhaps I should mention those. And if you were writing assembly by hand, or declaring a function with no internal linkage, you didn’t have to follow any formal calling convention at all; no external code would need to call it.  
> **username_5** (2023-07-29): Yeah, I found [x86 16 bit assembly function argument registers best practices](https://stackoverflow.com/q/74308368) while looking for discussion of 16-bit calling conventions.  Sep's answer suggests using a register-args convention similar to what the DOS `int 21h` ABI uses.  And yeah, a compiler could inline DOS `int 21h` calls into your code; most registers (except AX) are call-preserved with those.  Otherwise they could wrap them like Unix libc does, so you have a plain function following the standard calling convention that internally uses `int 80h` or `syscall` or whatever.  
> **username_10** (2023-07-29): Minor self-correction: the four memory models on 8088/8086 were small, compact, medium and large, where compact had `near` function pointers and `far` data pointers, `medium` was the reverse, small used only `near` pointers and large used only `far` pointers. Some compilers also had `tiny` and `huge`, which followed special rules. I wrote "huge" where I meant "medium."  
> **username_5** (2023-07-29): The "tiny" memory model is `.com` executables, where everything is `near` and furthermore CS/DS/ES/SS are all equal to each other.  (I'm fairly well acquainted with 8086 segmentation and some about near/far pointers and memory models thanks to endless SO questions from people who think "learning assembly" means turning back to the dark ages when assembly was commonly used, in the 16-bit x86 era.  I was just lucky enough not to have to suffer through that era as a user or developer of x86 software (I had Atari ST then Linux), so it's an interesting study in computer architecture and adapting C)  
> **username_5** (2023-07-29): *x32... which takes advantage of the extra registers in 32-bit mode* - No, the x32 ABI has the CPU in 64-bit long mode.  That's the only way extra registers are available.  It often uses `67h` address-size prefixes to override the default address-size from 64 to 32-bit, but it's still an ILP32 ABI for 64-bit **mode**.  Again, it's totally unrelated to the i386 System V ABI; it's documented in a section of the AMD64 System V ABI, because it's a minor modification of it.  
---
