4

I have a concern about variadic functions under Linux amd64 (x86_64).

My example build and work fine on linux i386 (ia32), but when built for linux amd64, GCC produces such errors:

stdarg.c: In function ‘vtest’:
stdarg.c:21:5: attention : passing argument 2 of ‘vptest’ from incompatible pointer type [enabled by default]
stdarg.c:5:1: note: expected ‘struct __va_list_tag (*)[1]’ but argument is of type ‘struct __va_list_tag **’

Here the example:

#include <stdio.h>
#include <stdarg.h>

static int
vptest(int count, va_list *a)
{
  printf("%8s:   a = %p\n", __func__, a);
  printf("%8s:   %d: %d\n", __func__, count, va_arg(*a, int));
  return 0;
}

static int
vtest(int count, va_list ap)
{
  printf("%8s: &ap = %p\n", __func__, &ap);

  /* passing a pointer to ap allows ap to be used again in the calling function */
  for(; count > 1; count --) {
    vptest(count, &ap);
  }
  if (count) {
    printf("%8s:   %d: %d\n", __func__, count, va_arg(ap, int));
  }
  return 0;
}

static
int test(int count, ...)
{
  va_list ap;

  va_start(ap, count);
  printf("%8s: &ap = %p\n", __func__, &ap);

  /* after passing ap to subfunction, this function must not use ap again without calling va_start */
  vtest(count, ap);

  va_end(ap);

  return 0;
}

int
main(void)
{
  test(4,
       1, 2, 3, 4);

  return 0;
}

According to a C11 draft (ISO/IEC 9899:2011)

The object ap may be passed as an argument to another function; if that function invokes the va_arg macro with parameter ap, the value of ap in the calling function is indeterminate and shall be passed to the va_end macro prior to any further reference to ap.

But latter add

It is permitted to create a pointer to a va_list and pass that pointer to another function, in which case the original function may make further use of the original list after the other function returns.

It's not clear to me if the AMD 64 ABI is wrong here regarded to the standard.

Changing the function vtest() to use a pointer at first call fixes the problem, but it feels wrong to have something not working in inner functions actually works in outer function.

@@ -12,16 +12,16 @@
 }

 static int
-vtest(int count, va_list ap)
+vtest(int count, va_list *a)
 {
-  printf("%8s: &ap = %p\n", __func__, &ap);
+  printf("%8s:   a = %p\n", __func__, a);

   /* passing a pointer to ap allows ap to be used again in the calling function */
   for(; count > 1; count --) {
-    vptest(count, &ap);
+    vptest(count, a);
   }
   if (count) {
-    printf("%8s:   %d: %d\n", __func__, count, va_arg(ap, int));
+    printf("%8s:   %d: %d\n", __func__, count, va_arg(*a, int));
   }

   return 0;
@@ -37,7 +37,7 @@
   printf("%8s: &ap = %p\n", __func__, &ap);

   /* after passing ap to subfunction, this function must not use ap again without calling va_start */
-  vtest(count, ap);
+  vtest(count, &ap);

   va_end(ap);

If someone could find somewhere if AMD64 ABI behavor is matching the standard. Additional points for people who provide me others ABI with the (same) problem on stdarg usage.

Regards

Yann Droneaud
  • 5,277
  • 1
  • 23
  • 39
  • 1
    possible duplicate of [Is GCC mishandling a pointer to a va_list passed to a function?](http://stackoverflow.com/questions/8047362/is-gcc-mishandling-a-pointer-to-a-va-list-passed-to-a-function) – Michael Burr Mar 20 '12 at 18:41
  • @MichaelBurr yes it's a duplicate of it. In fact http://stackoverflow.com/a/8048892/611560 is the answer to my question. – Yann Droneaud Mar 21 '12 at 09:03

1 Answers1

3

The behavior is perfectly conformant, because despite the argument to vtest being written as va_list ap, ap does not have type va_list but rather whatever pointer type va_list decays into. This is conformant because va_list is allowed by the standard to be an array type. The solution to this problem is to use va_copy to copy ap into a local va_list:

va_list ap2;
va_copy(ap2, ap);
// ...
vptest(count, &ap2);
// ...
va_end(ap2);

Since the definition and type of ap2 are under your control, &ap2 has the correct type to pass to vptest.

R.. GitHub STOP HELPING ICE
  • 208,859
  • 35
  • 376
  • 711
  • It's hard to believe that `ap` doesn't have type `va_list` when it's an argument. – Yann Droneaud Mar 20 '12 at 20:50
  • Link from @michael-burr is interesting in this regard http://stackoverflow.com/a/8047513/611560 – Yann Droneaud Mar 20 '12 at 20:56
  • using `va_copy()` this way *possibly* relies on unspecified behaviour (ie whether its implemented as macro or function)... – Christoph Mar 20 '12 at 21:02
  • 1
    @ydroneaud: The same thing happens if you `typedef char foo[1];` and use `foo` as an argument type. It also happens with `jmp_buf`, but you expect that since `jmp_buf` is defined as an array type. – R.. GitHub STOP HELPING ICE Mar 20 '12 at 22:59
  • @R..: `va_copy` expects the second argument to be of type `va_list`, which is not the case if `ap` is a parameter because of type adjustments; `va_copy` is affected the same way as the original code; as specified, `stdarg.h` doesn't deal gracefully with `va_list` having array type; I suspect that using `va_copy` will work in practice (the obvious way to implement `va_copy` will work with both real and adjusted type), but to be fully portable, you need a configure-time probe and conditionally compile code on `HAVE_VA_LIST_AS_ARRAY`; – Christoph Mar 20 '12 at 23:42
  • I suspect that there were no common implementations where `va_list` had array type when this part of the standard was formulated; the general practice to use length-1 arrays to automatically pass-by-pointer was obviously known at that time (`jmp_buf` comes to mind), but not in this context -- while the specification explicitly supports allocated memory (which is even mentioned in the C99 rationale and the reason why omitting `va_end` might leak memory), it's defective in case of array types... – Christoph Mar 20 '12 at 23:51
  • If `va_list` has any state beyond being a single stack pointer, it's necessary to either pass around a struct by value (likely very expensive) or define `va_list` as an array type. Surely the committee considered the possibility of it being an array type (this is why it's undefined to access the `va_list` in the caller after a callee has used it; in normal implementations it will be either in the original state or the final state depending on whether it's an array type). – R.. GitHub STOP HELPING ICE Mar 20 '12 at 23:57
  • Moreover, there is no type mismatch when passing `ap` to a function, regardless of whether `va_list` is an array type, because the array type it decays to is exactly the array type the function expects. The problematic case is passing `&ap` to a function that expects `va_list *`. This is not the situation with `va_copy`, which takes `va_list`, not `va_list *`. – R.. GitHub STOP HELPING ICE Mar 20 '12 at 23:59
  • @R..: the case which was considered by the committee (documented in the rationale) was that `va_list` points to allocated memory, which has the same implications regarding statefulness as using an array type, but additionally requires `va_end` to free the memory; one could argue that the fact that `va_copy` should accept both real and adjusted type is implied, but I *suspect* it just wasn't considered; as a simple `#define va_copy(DEST, SRC) (*(DEST) = *(SRC))` works with expressions of both array- and pointer-type, it's no problem in practice, but the specification is dubious... – Christoph Mar 21 '12 at 00:10
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/9116/discussion-between-christoph-and-r) – Christoph Mar 21 '12 at 00:11