简体   繁体   中英

How are variadic parameters represented on the stack when they are forwarded multiple times?

void fun2(char *format, ...){
   va_list arg_list;
   va_start(arg_list, format);
   vprintf(format, arg_list);
   va_end(arg_list);
}

void fun1(char *format, ...){
   fun2(format);
}

int main(){
   fun1("test: %d", 100);
}

Output:

test: 100

https://onlinegdb.com/OfdDeSJg_

Does the above example have something wrong or not recommended?

I guess that when the fun2(format); call is made, only the pointer to the first parameter ( format ) is passed, is that correct?

When vprintf in fun2 accesses the integer 100, where is this integer? in the stack reserved for fun1 , in the stack reserved for fun2 , in the stack reserved for vprintf , or somewhere else?

If as I imagine only the pointer to the first parameter is passed to fun2 , does this mean that when the functions and macros called by fun2 access the integer 100 they are accessing the stack reserved for fun1 ?

They aren't forwarded. This code doesn't work, unless you are lucky.

If you are lucky, it's probably because the optimizer saw that format stays in the same argument position (register or the stack), so fun1 doesn't actually have any "real" code in it (it doesn't move any arguments around), so it changed fun1 to a jump instruction that directly jumps to fun2 (aka tail-call optimization). Then since fun1 didn't mess with any of the arguments and didn't create a stack frame, they're still all in the right positions for calling a void (char*, ...) function, where fun1 picks them up.

ie the assembly code is probably something like this:

// Pretend calling convention: format is in ecx, and varargs on the stack

fun2:
    // actual code would go here
    ret

fun1:
    // jmp instruction doesn't affect the stack, nor ecx
    // so fun2 receives exactly the same inputs as if main
    // had directly called fun2
    jmp fun2

main:
    mov ecx, "test: %d"
    push 100
    call fun1
    ret

You can't rely on it. If you want to reliably forward varargs you either change your function to use a va_list parameter (like vprintf) or you write your own assembly code.

Yes, the example is doing something wrong. As a result, you are running into undefined behavior. As user253751's answer says, undefined behavior might happen to produce behavior that you want, but you cannot rely on it. user253751's answer gives a guess to why it might happen to produce the particular result you see in your particular case. But the same code might crash or produce wildly nonsensical results in another run or another environment.

Basically, when you called fun2(format) , you passed 0 varargs. So you cannot safely read even a single vararg out of fun2 's va_list (the variable arg_list ). But fun2 passes its va_list to vprintf , which, based on the format string, which contains one %d placeholder, will try to read one int from the va_list . Reading more varargs than were passed causes undefined behavior.

In the section for va_arg in the C99 standard, it says:

If there is no actual next argument, or if type is not compatible with the type of the actual next argument (as promoted according to the default argument promotions), the behavior is undefined, except for the following cases:

  • one type is a signed integer type, the other type is the corresponding unsigned integer type, and the value is representable in both types;
  • one type is pointer to void and the other is a pointer to a character type.

In the section for fprintf (which vprintf and printf refer to) in the C99 standard, it says:

If there are insufficient arguments for the format, the behavior is undefined.

Functions accepting varargs in C have no way of knowing how many or what types of varargs were passed. They depend on some kind of convention to determine when to stop reading and to know what types to read, and the caller has to adhere to the convention. For example, a function accepting a variable number of non-null pointers might need to be "terminated" with a null pointer at the end. The function depends on the caller to correctly pass a null pointer as the last argument, or the function will have undefined behavior. In another example, the printf family of functions uses the number and types of placeholders in the format string to determine the number and types of varargs to read, and depends on the caller to correctly match the number and types of arguments to the placeholders in the format string.

So here, your fun2 has no way of knowing the number or types of varargs that were passed. But since fun2 passes its va_list to vprintf , its varargs must follow the same convention as printf , ie the number and types of varargs must match the number and types of placeholders in the format string. However, in fun1 , it is passing a format string containing one %d placeholder, but did not pass one int in the varargs. Therefore it is violating the convention and causing undefined behavior.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM