Home > OS >  Why can't stdcall handle varying amounts of arguments?
Why can't stdcall handle varying amounts of arguments?

Time:07-21

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

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

My question is twofold:

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

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

CodePudding user response:

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.

CodePudding user response:

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.

CodePudding user response:

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.

  • Related