Why can't stdcall handle varying amounts of arguments?
Asked Answered
B

4

5

My understanding is that for the cdecl calling convention, the caller is responsible for cleaning the stack and therefore can pass any number of arguments.

On the other hand, stdcall callees clean the stack and therefore cannot receive varying amounts of arguments.

My question is twofold:

  1. Couldn't stdcall functions also get a parameter about how many variables there are and do the same?

  2. How do cdecl functions know how many arguments they've received?

Byer answered 20/7, 2022 at 12:47 Comment(4)
The callee doesn't know how many variadic arguments have been provided, it has to "figure it out" based on other argumentsCampanulaceous
1. Could work 2. They don't know, but will assume they received the proper amount. As such, if there is a mismatch the function may not work correctly but at least the stack is not messed up because the caller is responsible for removing the arguments. Also, excess arguments are safely ignored.Schober
@UnholySheep, what do you mean by figure it out? Like for printf depending on the number of '%' found?Byer
Yes, that's how printf works internally - it will pick the next variadic argument whenever it encounters a specifier for printing a value. Which is also why it's undefined behavior to provide too few argumentsCampanulaceous
F
3

Couldn't stdcall functions also get a parameter of how many variables are there and do the same?

If the caller has to pass a separate arg with the number of bytes to be popped, that's more work than just doing add esp, 16 or whatever after the call (cdecl style caller-pops). It would totally defeat the purpose of stdcall, which is to save a few bytes of space at each call site, especially for naive code-gen that wouldn't defer popping args across a couple calls, or reuse the space allocated by a push with mov stores. (There are often multiple call-sites for each function, so the extra 2 bytes for ret imm16 vs. ret is amortized over that.)

Even worse, the callee can't use a variable number efficiently on x86 / x86-64. ret imm16 only works with an immediate (constant embedded in the machine code), so to pop a variable number of bytes above the return address, a function would have to copy the return address high up in the stack and do a plain ret from there. (Or defeat branch return-address branch prediction by popping the return address into a register.)

See also:


How do cdecl functions know how many arguments they've received?

They don't.

C is designed around the assumption that variadic functions don't know how many args they received, so functions need something like a format string or sentinel to know how many to iterate. For example, the POSIX execl(3) (wrapper for the execve(2) system call) takes a NULL-terminated list of char* args.

Thus calling conventions in general don't waste code-size and cycles on providing a count as a side-channel; whatever info the function needs will be part of the real C-level args.

Fun fact: printf("%d", 1, 2, 3) is well-defined behaviour in C, and is required to safely ignore args beyond the ones referenced by the format string.

So using stdcall and calculating based on the format-string can't work. You're right, if you wanted to make a callee-pops convention that worked for variadic functions, you would need to pass a size somewhere, e.g. in a register. But like I said earlier, the caller knows the right number, so it would be vastly easier to let the caller manage the stack, instead of making the callee dig up this extra arg later. That's why no real-world calling conventions work this way, AFAIK.

Fervidor answered 20/7, 2022 at 16:33 Comment(2)
Thank you for the detailed answer and the additional reading links. I didn't quite understand what a sentinel is, if you could clear that up that'd be greatByer
@TanoCranem: en.wikipedia.org/wiki/Sentinel_valueFervidor
G
4

Couldn't stdcall functions also get a parameter of how many variables are there and do the same?

Yes, sure. You could invent any calling convention. But then that wouldn't be stdcall anymore.

How do cdecl functions know how many arguments they've received?

They don't. They assume to find the required number of arguments in the locations specified by the calling convention. If they are missing, then that's a bug which the code cannot observe. The following code compiles:

printf("%s");

even though it is missing an argument. The result is undefined. For printf-style functions compilers generally issue warnings (if they can) due to knowledge of the functions' internals, but that's not a solution that can be generically applied.

If a caller provides the wrong number or types of arguments, then the behavior is undefined.

Gey answered 20/7, 2022 at 13:32 Comment(5)
could you give an example of an object with a non trivial destructor? And is that something that could happen in C as well?Byer
Does cdecl specify where to provide a number of arguments? I couldn't find anything about thatByer
std::string is an object with non-trivial destructor. C doesn't have language-level destructors so that is not an issue. cdecl doesn't specify the number of arguments, neither passed nor expected. That's why the printf code doesn't report failure at runtime. It just simply causes undefined behavior.Gey
I removed the part about calling destructors. That's actually not part of the protocol of the stdcall or cdecl calling conventions.Gey
@IInspectable: An even better argument is that printf("%s", "", 1, 2, 3); is well-defined and required to ignore args past the one referenced by the format string. (Deleted my earlier comments and turned them into an answer.)Fervidor
E
3

Passing the number of arguments in a callee cleans the stack convention would be possible but the additional overhead of the extra parameter outweighs its usefulness. It wastes stack space with the extra parameter and complicates the callees stack handling.

The reason stdcall was invented is because it makes the code smaller. One adjustment in the callee vs adjusting every place it is called (on x86 or on another architecture when there are more parameters than you can pass in registers). The x86 even has a retn # instruction where # is the number of bytes to adjust. Windows NT switched from cdecl to stdcall early in its development and it supposedly reduced the size and improved speed (I believe Larry Osterman blogged about this (mini answer here)).

cdecl functions do not know how many parameters there are. You are allowed (on the ABI level) to pass more arguments than the function will actually use. A printf style function will use the format parameter as a "guide" to access the parameters one by one. When this is done the callee also has to be informed of the type of each parameter (so it knows the size which in turn, in an implementation defined manner, allows it to walk the list of parameters. On Windows x86 the parameters are on the stack, all you need is the parameter size to calculate their offset as you walk the stack). The va_list and its macros in stdarg.h provides the helping glue for C functions to access these parameters.

Ergotism answered 20/7, 2022 at 14:12 Comment(0)
F
3

Couldn't stdcall functions also get a parameter of how many variables are there and do the same?

If the caller has to pass a separate arg with the number of bytes to be popped, that's more work than just doing add esp, 16 or whatever after the call (cdecl style caller-pops). It would totally defeat the purpose of stdcall, which is to save a few bytes of space at each call site, especially for naive code-gen that wouldn't defer popping args across a couple calls, or reuse the space allocated by a push with mov stores. (There are often multiple call-sites for each function, so the extra 2 bytes for ret imm16 vs. ret is amortized over that.)

Even worse, the callee can't use a variable number efficiently on x86 / x86-64. ret imm16 only works with an immediate (constant embedded in the machine code), so to pop a variable number of bytes above the return address, a function would have to copy the return address high up in the stack and do a plain ret from there. (Or defeat branch return-address branch prediction by popping the return address into a register.)

See also:


How do cdecl functions know how many arguments they've received?

They don't.

C is designed around the assumption that variadic functions don't know how many args they received, so functions need something like a format string or sentinel to know how many to iterate. For example, the POSIX execl(3) (wrapper for the execve(2) system call) takes a NULL-terminated list of char* args.

Thus calling conventions in general don't waste code-size and cycles on providing a count as a side-channel; whatever info the function needs will be part of the real C-level args.

Fun fact: printf("%d", 1, 2, 3) is well-defined behaviour in C, and is required to safely ignore args beyond the ones referenced by the format string.

So using stdcall and calculating based on the format-string can't work. You're right, if you wanted to make a callee-pops convention that worked for variadic functions, you would need to pass a size somewhere, e.g. in a register. But like I said earlier, the caller knows the right number, so it would be vastly easier to let the caller manage the stack, instead of making the callee dig up this extra arg later. That's why no real-world calling conventions work this way, AFAIK.

Fervidor answered 20/7, 2022 at 16:33 Comment(2)
Thank you for the detailed answer and the additional reading links. I didn't quite understand what a sentinel is, if you could clear that up that'd be greatByer
@TanoCranem: en.wikipedia.org/wiki/Sentinel_valueFervidor
B
0

My summary, based on @IInspectable's answer.

stdcall functions could also get a parameter of how many variables there are, but then that wouldn't be stdcall anymore.

cdecl don't know how many arguments to read. It is assumed that the function will be able to derive the number of arguments based on a pre-determined amount of arguments, like a format string for printf.

If a caller provides the less arguments than could be derived, or of an unexpected type, then the behavior is undefined. (Thanks for the correction @Peter Cordes)

Byer answered 21/7, 2022 at 9:38 Comment(2)
If a caller provides the wrong number or types of arguments, then the behavior is undefined. - Not quite correct. The caller can safely provide too many args, beyond what the format string wants, or past a sentinel arg. The callee simply never knows about them. (And without a hypothetical calling convention that includes a count, couldn't detect that even at an asm level. x86-64 System V includes a count of up to 8 FP args passed in registers for variadic functions, but that's the only case I'm aware of.)Fervidor
Also, that last wrong sentence is copy/pasted (without attribution) from the answer you accepted. And part of the first sentence. The other parts seem to be paraphrases of other answers, putting the ideas into your own words. Those parts are correct, but you should at least mention the authors of other answers when you use their phrasing.Fervidor

© 2022 - 2024 — McMap. All rights reserved.