Variadic functions without `...`

On x86_64/Linux, compiled with gcc/clang -O3:

void void_unspec0(),void_unspec1(),void_unspec2(),void_unspec3(),void_void(void);

void call_void_void()
{
    void_void();
    void_void();
    void_void();
    void_void();
    void_void();
}

void call_void_unspec()
{
    void_unspec0();
    void_unspec0();
    void_unspec0();
    void_unspec0();
    void_unspec1(.0,.0,.0);
    void_unspec2(.0,.0,.0,.0,.0,.0,.0,.0);
    void_unspec3(.0,.0,.0,.0,.0,.0,.0,.0,.0,.0);
}

disassembles to:

0000000000000000 <call_void_void>:
   0:   48 83 ec 08             sub    $0x8,%rsp
   4:   e8 00 00 00 00          callq  9 <call_void_void+0x9>
   9:   e8 00 00 00 00          callq  e <call_void_void+0xe>
   e:   e8 00 00 00 00          callq  13 <call_void_void+0x13>
  13:   e8 00 00 00 00          callq  18 <call_void_void+0x18>
  18:   48 83 c4 08             add    $0x8,%rsp
  1c:   e9 00 00 00 00          jmpq   21 <call_void_void+0x21>
  21:   66 66 2e 0f 1f 84 00    data16 nopw %cs:0x0(%rax,%rax,1)
  28:   00 00 00 00 
  2c:   0f 1f 40 00             nopl   0x0(%rax)

0000000000000030 <call_void_unspec>:
  30:   48 83 ec 08             sub    $0x8,%rsp
  34:   31 c0                   xor    %eax,%eax
  36:   e8 00 00 00 00          callq  3b <call_void_unspec+0xb>
  3b:   31 c0                   xor    %eax,%eax
  3d:   e8 00 00 00 00          callq  42 <call_void_unspec+0x12>
  42:   31 c0                   xor    %eax,%eax
  44:   e8 00 00 00 00          callq  49 <call_void_unspec+0x19>
  49:   31 c0                   xor    %eax,%eax
  4b:   e8 00 00 00 00          callq  50 <call_void_unspec+0x20>
  50:   66 0f ef d2             pxor   %xmm2,%xmm2
  54:   b8 03 00 00 00          mov    $0x3,%eax
  59:   66 0f ef c9             pxor   %xmm1,%xmm1
  5d:   66 0f ef c0             pxor   %xmm0,%xmm0
  61:   e8 00 00 00 00          callq  66 <call_void_unspec+0x36>
  66:   66 0f ef ff             pxor   %xmm7,%xmm7
  6a:   b8 08 00 00 00          mov    $0x8,%eax
  6f:   66 0f ef f6             pxor   %xmm6,%xmm6
  73:   66 0f ef ed             pxor   %xmm5,%xmm5
  77:   66 0f ef e4             pxor   %xmm4,%xmm4
  7b:   66 0f ef db             pxor   %xmm3,%xmm3
  7f:   66 0f ef d2             pxor   %xmm2,%xmm2
  83:   66 0f ef c9             pxor   %xmm1,%xmm1
  87:   66 0f ef c0             pxor   %xmm0,%xmm0
  8b:   e8 00 00 00 00          callq  90 <call_void_unspec+0x60>
  90:   66 0f ef c0             pxor   %xmm0,%xmm0
  94:   6a 00                   pushq  $0x0
  96:   66 0f ef ff             pxor   %xmm7,%xmm7
  9a:   6a 00                   pushq  $0x0
  9c:   66 0f ef f6             pxor   %xmm6,%xmm6
  a0:   b8 08 00 00 00          mov    $0x8,%eax
  a5:   66 0f ef ed             pxor   %xmm5,%xmm5
  a9:   66 0f ef e4             pxor   %xmm4,%xmm4
  ad:   66 0f ef db             pxor   %xmm3,%xmm3
  b1:   66 0f ef d2             pxor   %xmm2,%xmm2
  b5:   66 0f ef c9             pxor   %xmm1,%xmm1
  b9:   e8 00 00 00 00          callq  be <call_void_unspec+0x8e>
  be:   48 83 c4 18             add    $0x18,%rsp
  c2:   c3                      retq   

In the second case (call_void_unspec()), the compiler is counting floating point arguments passed in registers, presumably because the SysVABI/AMD64 spec says it should.

When a function taking variable-arguments is called, %rax must be set to the total number of floating point parameters passed to the function in SSE registers

What is the reason for the rule in the ABI spec? Must unprototyped function calls abide by it given that functions defined with ... (ellipsis) are required to be prototyped (6.5.2.2p6) before a call? Can functions without ... be variadic too?

2 answers

  • answered 2018-11-08 01:06 Jonathan Leffler

    What the C standard says

    Note that variadic functions can only be called when a prototype is present. If you try to call printf() without a prototype present, you get UB (undefined behaviour).

    C11 §6.5.2.2 Function calls ¶6 says:

    ¶6 If the expression that denotes the called function has a type that does not include a prototype, the integer promotions are performed on each argument, and arguments that have type float are promoted to double. These are called the default argument promotions. If the number of arguments does not equal the number of parameters, the behavior is undefined. If the function is defined with a type that includes a prototype, and either the prototype ends with an ellipsis (, ...) or the types of the arguments after promotion are not compatible with the types of the parameters, the behavior is undefined. If the function is defined with a type that does not include a prototype, and the types of the arguments after promotion are not compatible with those of the parameters after promotion, the behavior is undefined, except for the following cases:

    • one promoted type is a signed integer type, the other promoted type is the corresponding unsigned integer type, and the value is representable in both types;
    • both types are pointers to qualified or unqualified versions of a character type or void.

    Applied to the original code in the question

    The original code in the question was similar to this — consecutive identical function calls have been reduced to a single call.

    void void_unspec(), void_void(void);
    
    void call_void_void()
    {
        void_void();
    }
    
    void call_void_unspec()
    {
        void_unspec();
        void_unspec(.0,.0,.0);
        void_unspec(.0,.0,.0,.0,.0,.0,.0,.0);
        void_unspec(.0,.0,.0,.0,.0,.0,.0,.0,.0,.0);
    }
    

    This code invokes UB because the number of arguments to the function calls to void_unspec() do not all match number of arguments it is defined to take (regardless of what the definition is; it cannot simultaneously take 0, 3, 8 and 10 arguments). This is not a constraint violation, so no diagnostic is required. The compiler generally does whatever it thinks is best for backwards compatibility, and usually doesn't cause outright crashes, but any problems that arise of the programmer's making for violating the rules of the standard.

    And because the standard says the behaviour is undefined, there's no specific reason that the compiler must set %rax (of course, the C standard doesn't know anything about %rax), but simple consistency suggests that it should.

    Applied to the revised code in the question

    The code in the question was revised like this (repeated consecutive calls omitted again):

    void void_unspec0(), void_unspec1(), void_unspec2(), void_unspec3(), void_void(void);
    
    void call_void_void()
    {
        void_void();
    }
    
    void call_void_unspec()
    {
        void_unspec0();
        void_unspec1(.0,.0,.0);
        void_unspec2(.0,.0,.0,.0,.0,.0,.0,.0);
        void_unspec3(.0,.0,.0,.0,.0,.0,.0,.0,.0,.0);
    }
    

    The code no longer inevitably invokes undefined behaviour. However, where the void_unspec0() etc functions are defined, they should look like something equivalent to:

    void void_unspec0(void) { … }
    void void_unspec1(double a, double b, double c) { … }
    void void_unspec2(double a, double b, double c, double d, double e, double f, double g, double h) { … }
    void void_unspec3(double a, double b, double c, double d, double e, double f, double g, double h, double i, double j) { … }
    

    One equivalent notation would be:

    void void_unspec2(a, b, c, d, e, f, g, h)
        double a, b, c, d, e, f, g, h;
    {
        …
    }
    

    This is using a K&R pre-standard non-prototype definition.

    If the function definitions don't match these, then §6.5.2.2¶6 says the result of the calls is undefined behaviour. That saves the standard having to legislate what happens in all sorts of questionable circumstances. As before, the compiler is at liberty to pass the number of floating point values in %rax; that makes sense. But there is very little that can be done in the way of arguing about what will happen — either the calls match the definition and everything is OK, or they don't and there are unspecified (and unspecifiable) potential problems.

    Note in passing that neither call_void_void() nor call_void_unspec() is defined with a prototype. They are both functions that take zero arguments, but there is no prototype visible that enforces that, so code in the same file could call call_void_void(1, "abc") without the compiler complaining. (In this respect, as in many others, C++ is a different language, with different rules.)

  • answered 2018-11-08 01:16 John Bollinger

    Can functions without ... be variadic too?

    Paragraph 6.5.2.2/6 of the standard is perhaps the most relevant:

    If the expression that denotes the called function has a type that does not include a prototype, the integer promotions are performed on each argument, and arguments that have type float are promoted to double. These are called the default argument promotions. If the number of arguments does not equal the number of parameters, the behavior is undefined.

    (Emphasis added.) This is the case when the declared type of the function does not contain a parameter list (as distinguished from having a parameter list consisting of just void). The caller is still responsible for passing the correct number of parameters.

    If the function is defined with a type that includes a prototype, and either the prototype ends with an ellipsis (, ...) or the types of the arguments after promotion are not compatible with the types of the parameters, the behavior is undefined.

    This is distinguishing the properties of the function definition from the type of the subexpression of the function call that denotes the function. Note that it explicitly says that the behavior of calling a variadic function via a function expression whose type does not include a prototype is undefined. It also requires type matching between the promoted arguments and the parameters.

    If the function is defined with a type that does not include a prototype, and the types of the arguments after promotion are not compatible with those of the parameters after promotion, the behavior is undefined, except for the following cases:

    • one promoted type is a signed integer type, the other promoted type is the corresponding unsigned integer type, and the value is representable in both types;
    • both types are pointers to qualified or unqualified versions of a character type or void.

    This is the case of a K&R-style function definition. It, too, requires number and type matching between arguments and parameters, so such functions are non-variadic.

    Therefore,

    What is the reason for the rule in the ABI spec? Must unprototyped function calls abide by it given that functions defined with ... (ellipsis) are required to be prototyped?

    I suppose the reason for the rule is to convey which FP registers need to be saved or preserved by the function implementation. Since a call to a variadic function via a function expression whose type does not include a prototype has UB, a C implementation has no particular need to follow that ABI provision.