Andreas - 1 year ago 65
C Question

# Why does my variadic function work with both int and long long?

According to this answer numeric constants passed to variadic functions are always treated as

`int`
if they fit in one. This makes me wonder why the following code works with both,
`int`
and
`long long`
. Consider the following function call:

``````testfunc(4, 1000, 1001, 1002, 1003);
``````

`testfunc`
looks like this:

``````void testfunc(int n, ...)
{
int k;
va_list marker;

va_start(marker, n);
for(k = 0; k < n; k++) {
int x = va_arg(marker, int);
printf("%d\n", x);
}
va_end(marker);
}
``````

This works fine. It prints 1000, 1001, 1002, 1003. But to my surprise, the following code works as well:

``````void testfunc(int n, ...)
{
int k;
va_list marker;

va_start(marker, n);
for(k = 0; k < n; k++) {
long long x = va_arg(marker, long long);
printf("%lld\n", x);
}
va_end(marker);
}
``````

Why is that? Why does it work with
`long long`
too? I thought that numeric integer constants were passed as
`int`
if they fit in one? (cf. link above) So how can it be that it works with
`long long`
too?

Heck, it's even working when alternating between
`int`
and
`long long`
. This is confusing the heck out of me:

``````void testfunc(int n, ...)
{
int k;
va_list marker;

va_start(marker, n);
for(k = 0; k < n; k++) {

if(k & 1) {
long long x = va_arg(marker, long long);
printf("B: %lld\n", x);
} else {
int x = va_arg(marker, int);
printf("A: %d\n", x);
}
}
va_end(marker);
}
``````

How can this be? I thought all my parameters were passed as
`int`
... why can I arbitrarily switch back and forth between
`int`
and
`long long`
with no trouble at all? I'm really confused now...

Thanks for any light shed onto this!

That has nothing to do with C. It is just that the system you used (x86-64) passes the first few arguments in 64-bit registers, even for variadic arguments.

Essentially, on the architecture you used, the compiler produces code that uses a full 64-bit register for each argument, including variadic arguments. This is the ABI agreed upon the architecture, and has nothing to do with C per se; all programs, no matter how produced, are supposed to follow the ABI on the architecture it is supposed to run.

If you use Windows, x86-64 uses `rcx`, `rdx`, `r8`, and `r9` for the four first (integer or pointer) arguments, in that order, and stack for the rest. In Linux, BSD's, Mac OS X, and Solaris, x86-64 uses `rdi`, `rsi`, `rdx`, `rcx`, `r8`, and `r9` for the first six (integer or pointer) arguments, in that order, and stack for the rest.

You can verify this with a trivial example program:

``````extern void func(int n, ...);

void test_int(void)
{
func(0, 1, 2);
}

void test_long_long(void)
{
func(0, 1LL, 2LL);
}
``````

If you compile the above to x86-64 assembly (e.g. `gcc -Wall -O2 -march=x86-64 -mtune=generic -S`) in Linux, BSDs, Solaris, or Mac OS (X or later), you get approximately (AT&T syntax, source,target operand order)

``````test_int:
movl    \$2, %edx
movl    \$1, %esi
xorl    %edi, %edi
xorl    %eax, %eax
jmp     func

test_long_long:
movl    \$2, %edx
movl    \$1, %esi
xorl    %edi, %edi
xorl    %eax, %eax
jmp     func
``````

i.e. the functions are identical, and do not push the arguments to the stack. Note that `jmp func` is equivalent to `call func; ret`, just simpler.

However, if you compile for x86 (`-m32 -march=i686 -mtune=generic`), you get approximately

``````test_int:
subl    \$16, %esp
pushl   \$2
pushl   \$1
pushl   \$0
call    func
ret

test_long_long:
subl    \$24, %esp
pushl   \$0
pushl   \$2
pushl   \$0
pushl   \$1
pushl   \$0
call    func
which shows that the x86 calling conventions in Linux/BSDs/etc. involve passing the variadic arguments on stack, and that the `int` variant pushes 32-bit constants to the stack (`pushl \$x` pushes a 32-bit constant `x` to the stack), and the `long long` variant pushes 64-bit constants to the stack.