How to replace values in va_list?

2019-06-17 03:01发布

问题:

I want to do some exercise about va_list. This is my code.

int myscanf( char* fmt, ... ) {
  va_list ap;
  va_start ( ap, fmt );
  vfscanf ( stdin, fmt, ap );
  va_end ( ap );
}

int main() {
  int a, b;
  myscanf( "%d %d", &a, &b );
}

As I shown above, I have written a scanf() and it is work.

Now I want to redirect the value of arguments in myscanf().

For example, I can redirect fmt to the space which is allocated in myscanf()

int myscanf( char* fmt, ... ) {
  char newFmt[10] = "%d %d";
  va_list ap;
  va_start ( ap, fmt );
  vfscanf ( stdin, newFmt, ap );
  va_end ( ap );
}

However, I feel confused when I try to change the value of others arguments.

I can fetch these variable argument by va_arg(), but I can't modify them because va_arg() is a macro.

int myscanf( char* fmt, ... ) {
  va_list ap;
  va_start ( ap, fmt );

  int* arg1 = (int)va_arg(ap, int*); // get the value of &a in main()
  int newA; // I want to read a value by scanf() and store it to &newA
  // ??? = &newA // <- how to do?

  vfscanf ( stdin, fmt, ap );
  va_end ( ap );
}

Any suggestion?

-----------edit-----------

Thanks for replies,

But something should be clarified.

The "value" in this case is "address". Therefore, my purpose is changing the target address so that the vfscanf() will read and write the value to the another address space.

For example,

int gA, gB, gC, gD;

int myscanf( char* fmt, ... ) {
  va_list ap;
  va_start ( ap, fmt );

  // do something for making the following vfscanf() to write value into gC and gD directly
  vfscanf ( stdin, fmt, ap );

  // don't assign *gA to *gC and *gB to *gD after performing vfscanf()
  va_end ( ap );
}

int main() {
  myscanf( "%d %d", &gA, &gB );
}

As I change fmt to newFmt, we want to change the value (in this case is address) in va_list directly.

And the parsing problem is solved because that I can allocate a space dynamically while I parse a "%..." from format string. These addresses of spaces will replace inputs repeatedly if the question above is solved.

回答1:

Variadic Functions

The arguments to scanf will always be pointers, not values as in your example. The correct way of getting an argument of scanf would be int *arg1 = va_arg(ap, int*); - and you don't need to cast.

If you want to manipulate the way scanf behaves, you have to know first how variadic functions work (you can get it by reading the manual of any of the va_* family of functions). The variable ap in most architectures is a pointer to the function's stack frame. It points to the next variable after fmt in this case.


Your example

In the case of scanf in your example, it will point to a list of pointers (because all arguments to scanf must be pointers). So you should put that into your pointers like this:

int *a = va_arg(ap, int*);

/* Then you can modify it like this: */
*a = 666;

There are some problems with this.

When you finish manipulating the arguments, you must pass fmt and ap to vfscanf, which will then parse fmt and expect n elements (the amount of elements in the format string). The problem is that ap now will only give us n - x elements (x being the number of elements you "poped" in your own function). A little example:

myscanf("%d %d", &a, &b);
/* n = 2 */

...

int *a = va_arg(ap, int *);
/* x = 1 */

...
vfscanf(stdin, fmt, ap);
/* n = 2 cause fmt is still the same, however
 * x = 1, so the number of elements "popable" from the stack is only
 * n - x = 2 - 1 = 1.
 */

In this simple example you can already see the problem. vfscanf will call va_arg for each element it finds in the format string, which is n, but only n - x are popable. This means undefined behavior - vfscanf will be writing somewhere it shouldn't and most probably will crash your program.


Hack Around

To overcome that, I propose a little work with va_copy. The signature of va_copy is:

void va_copy(va_list dest, va_list src);

And something to know about it (from the manual):

Each invocation of va_copy() must be matched by a corresponding invocation of va_end() in the same function. Some systems that do not supply va_copy() have __va_copy instead, since that was the name used in the draft proposal.

The solution:

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

int myscanf(char *fmt, ...)
{
    va_list ap, hack;

    /* start our reference point as usual */
    va_start(ap, fmt);

    /* make a copy of it */
    va_copy(hack, ap);

    /* get the addresses for the variables we wanna hack */
    int *a = va_arg(hack, int*);
    int *b = va_arg(hack, int*);

    /* pass vfscanf the _original_ ap reference */
    vfscanf(stdin, fmt, ap);

    va_end(ap);
    va_end(hack);

    /* hack the elements */
    *a = 666;
    *b = 999;
}

int main(void)
{
    int a, b;
    printf("Type two values: ");
    myscanf("%d %d", &a, &b);

    printf("Values: %d %d\n", a, b);
    return 0;
}


Conclusion and Warnings

There are a couple of things you should note. First, if you put the hacking of the elements before calling vfscanf, the values you set will be lost, because vfscanf will overwrite those locations.

Next, you should also note that this is a very specific use case. I knew beforehand that I was going to pass two integers as arguments, so I designed myscanf with that in mind. But this means you need a parsing pass to find out which arguments are of which type - if you don't do it, you'll enter undefined behavior again. Writing that kind of parser is very straight-forward and shouldn't be a problem.


After your edit

After what you said in your clarification edit, I can only propose a little wrapper function around vfscanf(), because you can't directly manipulate va_list variables. You can't write directly to the stack (in theory, you can't, but if you did some inline-assembly you could, but that's gonna be an ugly hack and very non-portable).

The reason it's gonna be extremely ugly and non-portable is that the inline assembly will have to take into account how the architecture treats argument passing. Writing inline-assembly by itself is already very ugly... Check out this for the official GCC manual on it's inline assembly.

Back to your problem:

  • Stack Overflow: How do I fill a va_list

That answer explains a whole lot, so I won't say it here again. The final conclusion of the answer is **no, you don't do it". What you _can do however, is a wrapper. Like this:

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

int a, b, c, d;

void ms_wrapper(char *newfmt, ...)
{
    va_list ap;
    va_start(ap, newfmt);

    vfscanf(stdin, newfmt, ap);

    va_end(ap);
}

int myscanf(char *fmt, ...)
{
    /* manipulate fmt.... */
    char *newfmt = "%d %d";

    /* come up with a way of building the argument list */

    /* call the wrapper */
    ms_wrapper(newfmt, &c, &d);
}

int main(void)
{
    a = 111;
    b = 222;
    c = 000;
    d = 000;

    printf("Values a b: %d %d\n", a, b);
    printf("Values c d: %d %d\n\n", c, c);

    printf("Type two values: ");
    myscanf("%d %d", &a, &b);

    printf("\nValues a b: %d %d\n", a, b);
    printf("Values c d: %d %d\n", c, d);

    return 0;
}

Beware that you can only build argument lists for variadic functions in your compile-time. You can't have a dynamically changing list of parameters. In other words, you'll have to hard-code each case you'd ever wanna handle. If the user enters something different, your program will behave very oddly and most probably crash.



回答2:

It is not possible directly but you can do as below.

int myscanf( char* fmt, ... ) {
  va_list ap;
  va_start ( ap, fmt );
  int newA;
  scanf("%d",&new);
  vfscanf ( stdin, fmt, ap );
  va_end ( ap );
}

I think this will do same as you want.



回答3:

On a given platform you may use some tricky hack:

  1. va_list is basically a pointer to some data (typically char *),
  2. va_arg is basically pointer arithmetic and cast

So, you can allocate an array of two pointers to int, set the values and call vfscanf with it. Something like:

int *hack[2];
hack[0] = &gC;
hack[1] = &gD;
vscanf(stdin, fmt, (va_list)hack);

BEWARE this is highly non portable, very tricky and error prone. There is a lot of problem with such, even if it basically works on many platforms.



回答4:

The only way is to pass updated arguments directly, since va_list can not be modified. In your case you should parse format string to have an idea about actual content of va_list and then pass compatible set of arguments to fscanf() (not vfscanf()) directly.