14

Consider this C (not C++!) code:

int g();

int f() {
    return g();
}

Clang (with any optimization level above zero) compiles this to:

f:
        xor     eax, eax
        jmp     g@PLT

I am trying to understand the reason for zeroing eax before jumping to g.

Various answers on Stack Overflow, especially this one, have led me to conclude that the number of parameters expected by g is not specified (to specify it as 0, we should have written int g(void);), and therefore that the compiler must assume that g can be variadic. But variadic functions require this register to contain the number of variables passed in vector registers, which is why clang needs to zero it (no vector registers are used here).

This all makes sense, but then I'm confused about why gcc doesn't zero the register:

"f":
        jmp     "g"

If zeroing this register is necessary per the ABI, then can we use this behavior of gcc to cause a miscompilation? Alternatively, if it's not necessary, then isn't clang wasting an instruction?

4
  • @fuz why did you remove the itanium-abi tag? This is indeed not about Itanium, but the Itanium ABI is also used by x86-64, isn’t it? Commented yesterday
  • 1
    The Itanium C++ ABI (for mapping C++ things to C-ABI things, like that non-POD types aren't passed in registers) is indeed used across platforms. But this is purely C code so it's not applicable. The relevant ABI here is the AMD64 System V psABI. (The x86-64 "processor supplement" for the generic System V ABI.) Where is the x86-64 System V ABI documented? Commented yesterday
  • 1
    In c++ a function int g(); is a prototype (a function with no parameters) but in c that’s an incomplete function definition where parameters have been unspecified (you are required to use int g(void); to indicate a parameterless function.) This makes a difference in the concept, which allows you to use a unspecified parameter list function with a variable parameter list of expressions. IMHO this is maintained in the standard to allow legacy code to be compiled, because it allows feeding to pass arbitrary expressions whose lists are not checked. Commented yesterday
  • 1
    @LuisColorado The latest C standard has finally done away with this. Commented 23 hours ago

3 Answers 3

21

C23 removed the possibility of unprototyped function declarations

GCC15+ defaults to -std=gnu23 where int g(); means int g(void);
With -std=gnu17 or earlier, it will xor eax,eax.

Clang 11+ through current trunk defaults to -std=gnu17 where int g(); isn't a prototype and leaves the arguments unspecified. The current nightly built still defaults to that C version, but with -std=gnu23 it emits the same asm as GCC.

You can check by including int ver = __STDC_VERSION__; (at global scope) in your program. "ver": .long 202311 vs. 201710.

Godbolt with GCC and Clang each with -std=gnu17 and -std=gnu23, showing that's what makes the difference.


You're using GCC16 (nightly build); we can tell since it puts symbol names in quotes (e.g. jmp "g") in Intel syntax so it won't face-plant on a C function whose name is also a register name, which would compile to jmp rax which would assemble like AT&T jmp *%rax. In previous GCC versions, Intel-syntax was very much a second-class citizen, nice for human reading but not fully robust for production use.

Your GCC version defaults to -std=gnu23.


@Joshua is correct that this would be a valid optimization even in ISO C17 for an unprototyped function when the call-site passes no args. It's UB if the passed args don't match the callee's declaration, and variadic functions require at least one fixed arg. So a call with zero args can only be to a function that never accepts any args, and thus isn't variadic and doesn't need AL=0.

(Or is it actually not UB if the callee doesn't touch those args on the path of execution actually taken? If that's the case, this argument doesn't fully hold.)

But that only applies if the callee is also written in C, which is not necessarily true. GCC and Clang aim to be useful for systems programming where the callee could be written in assembly language, or compiler-generated from some other language.

If you properly prototype your functions, like writing int g(void); when that's what you mean, there's no need for the compiler to aggressively optimize.


Fun fact: in practice with variadic callees generated by modern compilers, AL=0 or non-zero is all they check for. And non-zero will simply result in dumping all 8 XMM regs to a stack array, even if the value is > 8 in violation of the ABI.

But it will actually break in variadic callees generated by older compilers which used AL for a computed jump into a sequence of movaps instructions. Why does printf still work with RAX lower than the number of FP args in XMM registers? compares GCC4.5 (computed jump) vs. more recent GCC (test/jz conditional branch all-or-nothing).

With variadic callees generated by more modern compilers with the all-or-nothing conditional branch, leaving non-zero garbage in AL just costs performance if there weren't actually any variadic FP args. Violating the ABI won't cause a correctness problem. But binary libraries compiled by older or different compilers, or hand-written in asm, are supposed to inter-operate with modern compiled code that still follows the same ABI.

Sign up to request clarification or add additional context in comments.

4 Comments

Image
Wow, good catch! You're right that I was using latest trunk on both compilers, on Godbolt.
Image
… and where is the (explicit) prototype of main()? You are not required (neither in c nor in c++ languages) to provide a prototype for main. In any case I used to realize that the register eax was used to hold the return value of the function (which main should initialize to zero, making the main function specially “special”). I assumed from this point on that the c runtime was initializing the return value to zero before calling main. But this can be also a conformant implementation (this only makes all functions that don’t explicitly return inside the body to implicitly return 0)
Image
main isn't allowed to be variadic; the only two allowed signatures are int main(void) and int main(int, char**). (And many implementations also pass a third arg, char **envp). So CRT / libc code definitely doesn't have to set EAX before calling main. (And for calls that do zero AL, it isn't a return value; EAX is call-clobbered. See stackoverflow.com/questions/6212665/… for what this part of the x86-64 SysV ABI's calling convention is about.)
Image
… I understand the overhead imposed but I’m not the implementor or of clang. I cannot speak about the author’s motivation to zero eax
6

EDIT: Answer invalidated by OP's incorrect assumption. OP says g() is a variadic function, but he compiled with a new enough C that that just isn't true.

Technically, gcc is wrong, but in practice it will never matter. The only way to get this problem to happen where you haven't invoked undefined behavior is to construct g like follows:

void g(int lastfixed, ...)
{
   if (_some condition involving a global variable_)
   {
        va_list ap;
        va_start(ap, lastfixed);
   } else {
      /* never look at lastfixed */
   }
}

Somebody's going to come back and say this is still undefined behavior because lastfixed isn't passed in all cases. But no, that's not how that works. The function open() is defined as follows: int open(const char *, int, ...) where the third argument is either not present or another int. It is never passed in vector registers for that reason. The actual implementation is in C (or was two decades ago anyway) and looks like this:

int open(const char *path, int flags, int mode)
{
   int rtn = __syscall(__NR_open, path, flags, mode);
   if (rtn < 0) errno = -rtn, rtn = -1;
   return rtn;
}

This works even when mode is a trap value. (Yes, the Itanium has a trap value integer.)

This is convinced to compile by carefully not including the header file that declares open, so you don't get an incompatibility error. (A technique that is not valid in C++ but is in C.) GCC's hand is forced, this construct is defined.

5 Comments

Image
Technically, gcc is wrong That might depend on the version of C being compiled. Depending on the C version, int g() either means "g takes an unspecified number of arguments, all of which will undergo default argument promotion" or "g takes no arguments".
Image
I could invert my answer for a different C version, but OP is under the impression that the version of C he is using results in unspecified number of arguments.
Image
The OP is wrong about that. GCC15+ defaults to -std=gnu23, and we can see from the Intel-syntax asm output using double-quotes around symbol names that they're using GCC16-nightly, presumably on Godbolt given the lack of other directives etc. I posted an answer. GCC -std=gnu17 matches Clang. Clang -std=gnu23 matches GCC.
Image
It is ALWAYS meant as "g takes an unspecified number of arguments, all of which will undergo default argument promotion". Or am i wrong?
Image
C23 6.7.7.4p13: "For a function declarator without a parameter type list: the effect is as if it were declared with a parameter type list consisting of the keyword void. A function declarator provides a prototype for the function."
-4

You can observe differences in code generation in a compiler that is not derived from another. In c there’s a specification that enforces the language to return 0 from main if you don’t explicitly return from it. Well. This will be done for any function (this includes main) if you expressly make eax zero before calling it. Gcc enforces this when making a call to main but never else, but clang could be doing otherwise.

I’m not saying that this is the cause, but it could be. Initializing eax to zero before calling a function is not a requirement of c but can ensure a default return value if you don’t make explicitly a return. I’m not sure if your assertion is true as ax is, indeed the place for the return value, and not for any parameter. IMHO all parameters passed to a variadic function (or a function with unspecified parameter list) should be passed in the stack. But that’s also not a requirement. I don’t know the ABI enough to be able to discuss how the parameters of a variadic or unspecified parameter list function should be passed. The only thing I can say is that normally the use of EAX i yo hold the return value, not parameters.

Remember that you don’t need to include a explicit prototype of main. So in case this is done only for unspecified parameter list functions the reason stated above can hold for your case.

1 Comment

Image
RAX/EAX is call-clobbered; the implementation of g in this example will freely use RAX so the final EAX value is unrelated to its initial value. In the x86-64 System V ABI, AL is an extra parameter to variadic functions. It only happens to be in the same register as (part of) the return value. See stackoverflow.com/questions/6212665/…

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.