[C language library function] variable parameter va_start,va_arg,va_end,va_list, stdarg.h library details

Posted by Krait on Fri, 10 Sep 2021 05:00:02 +0200

Detailed explanation of variable parameters

printf() is a function that we cannot avoid in programming. We studied the implementation principle of printf() and preliminarily understood that the printf() function uses putchar() to realize output in the library, but there is another problem that we didn't solve in the previous article, that is, how to implement the variable parameters in printf(), How to use variable parameters to complete our own output function?

Let's take another look at printf(), which is declared in the stdio.h file

int printf(const char *format, ...)

There are two kinds of parameters in printf(). One is the fixed parameter represented by const char *format, and the other is... Which represents the variable parameter

/* *****************************************************
* Name: my_vprintf
* fuction: Implement formatting string function
* Input: const char *fmt --  A pointer to a formatted string
         va_list ap -- Parameters of the pointer
* Output: None
* Return: None
* ****************************************************** */
static void my_vprintf(const char *fmt, va_list ap)
{
    char lead = ' ';
    int maxwidth = 0;

    for (; *fmt != '\0'; fmt++)
    {
        if (*fmt != '%')
        { //Search and judge in sequence. Push it out when% is encountered, otherwise continue cyclic output
            outc(*fmt);
            continue;
        }

        fmt++;
        if (*fmt == '0')
        { //'0' encountered indicates that the preamble is 0
            lead = '0';
            fmt++;
        }

        while (*fmt >= '0' && *fmt <= '9')
        { //The next number is the length. Calculate the specified length
            maxwidth *= 10;
            maxwidth += (*fmt - '0');
            fmt++;
        }

        switch (*fmt)
        { //Judgment format output
        case 'd':
            out_num(va_arg(ap, int), 10, lead, maxwidth);
            break;
        case 'o':
            out_num(va_arg(ap, unsigned int), 8, lead, maxwidth);
            break;
        case 'u':
            out_num(va_arg(ap, unsigned int), 10, lead, maxwidth);
            break;
        case 'x':
            out_num(va_arg(ap, unsigned int), 16, lead, maxwidth);
            break;
        case 'c':
            outc(va_arg(ap, int));
            break;
        case 's':
            outs(va_arg(ap, char *));
            break;

        default:
            outc(*fmt);
            break;
        }
    }
}
/* *****************************************************
* Name: printf
* fuction: None
* Input: const char *fmt --  A pointer to a formatted string
         ...  -- Variable number of arguments
* Output: None
* Return: None
* ****************************************************** */

void printf(const char *fmt, ...)
{
    va_list ap; /* Gets the parameter pointer of the input */
    va_start(ap, fmt); /* obtain */
    my_vprintf(fmt, ap);
    va_end(ap);
}

Variable parameters and stdarg.h Library

First, let's take a look at what's in the stdarg.h library

Library variable

The following are the variable types defined in the header file stdarg.h:

Serial numberVariable & Description
1va_list this is a list for va_start(),va_arg() and va_end() these three macros store the type of information.

Library macro

The following are the macros defined in the header file stdarg.h:

Serial numberMacro & Description
1void va_ The macro start (va_list ap, last_arg) initializes the ap variable, which is the same as va_arg and VA_ The end macro is used together. last_ Arg is the last known fixed parameter passed to the function, that is, the parameter before the ellipsis.
2**type va_arg(va_list ap, type) * * this macro retrieves the next parameter of type in the function parameter list.
3**void va_end(va_list ap) * * this macro allows the use of VA_ The function with variable parameters of the start macro returns. If VA is not called before returning from the function_ End, the result is undefined.

Note: all the above operations can only access the following variable parameters from beginning to end. They can be suspended, but they cannot be read in reverse

Library implementation

/* VC++ 6.0 */
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
#define va_ Start (AP, V) (AP = (va_list) & V + _intsizeof (V)) / / first optional parameter address
#define va_arg(ap, t) (*(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t))) / / the value of the next parameter
#define va_end(ap) (ap = (va_list)0) / / set the pointer to invalid

If you convert it to a function, yes

void va_start(va_list ap, xxx v) /* Where xxx is a variable of any type */
{
    ap = (va_list)&v + sizeof(v);
}
xxx va_arg(va_list ap, xxx t) /* Where xxx is a variable of any type */
{
    // (*(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))  
    ap += sizeof(t);
    return *((t *)(ap - sizeof(t)))
   	
}
void va_end(va_list ap) 
{
    ap = (va_list)0; //Force conversion
}

Storage and implementation of variable parameters

The following text is quoted from the blog Detailed explanation of variable parameter function

va can be used under C calling convention_ List series variable parameter macros implement variable parameter functions, where va means variable argument.

Typical usage is as follows:

#include <stdarg.h>

int VarArgFunc(int dwFixedArg, ...){ //Take the address of the fixed parameter as the starting point to determine the memory starting address of each variable parameter in turn

    va_list pArgs = NULL;  //Define VA_ The pointer pArgs of list type is used to store the parameter address
    va_start(pArgs, dwFixedArg); //Initialize the pArgs pointer to point to the first variable parameter. The second parameter of the macro is the previous parameter of the variable parameter list, that is, the last fixed parameter
    int dwVarArg = va_arg(pArgs, int); //This macro returns the current argument value in the argument list and causes pArgs to point to the next argument in the list. The second parameter of the macro is the current argument type to return
    //If the function has multiple variable parameters, VA is called in turn_ Arg macro gets each variable parameter
    va_end(pArgs);  //Set the pointer pArgs to invalid to end the acquisition of variable parameters
    /* Code Block using variable arguments */

}
//The function can be declared as extern int VarArgFunc(int dwFixedArg,...) in the header file;, VarArgFunc(FixedArg, VarArg) is used when calling;

The variable parameter macro obtains the address of each variable parameter in turn from the fixed parameter closest to the first variable parameter according to the stack growth direction and parameter stacking characteristics.

The definition and implementation of variable parameter macros vary from operating system, hardware platform and compiler (but the principle is similar).

System V Unix defines VA in the varargs.h header file_ The start macro is va_start(va_list arg_ptr), while ANSI C defines VA in the stdarg.h header file_ The start macro is va_start(va_list arg_ptr, prev_param).

The two macros are not compatible. ANSI C is usually used for program migration.

Gcc compiler uses built-in macros to indirectly implement variable parameter macros, such as #define_ start(v,l) __ builtin_ va_ start(v,l). Because the gcc compiler needs to consider cross platform processing, and its implementation varies from platform to platform. For example, under x86-64 or PowerPC processors, not all parameters are passed through the stack, and the implementation of variable parameter macros is more complex than x86 processors.

In the x86 platform VC6.0 compiler, the variable parameter macro in the stdarg.h header file is defined as follows:

typedef char * va_list;
#define _INTSIZEOF(n)    ( (sizeof(n)+sizeof(int)-1) & ~(sizeof(int)-1) )
#define va_start(ap,v)     ( ap = (va_list)&v + _INTSIZEOF(v) )
#define va_arg(ap, type)   ( *(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)) )
#define va_end(ap)       ( ap = (va_list)0 )

The meaning of each macro is as follows:

  1. _ The intsizeof macro takes into account that some systems require memory address alignment. From the macro name, it should be aligned according to sizeof(int), i.e. stack granularity, that is, the address of parameters in memory is a multiple of sizeof(int)=4. For example, if 1 ≤ sizeof(n) ≤ 4, then_ INTSIZEOF(n)=4; If 5 ≤ sizeof(n) ≤ 8, then_ INTSIZEOF(n)=8. For ease of understanding, simplify the macro to

#define _INTSIZEOF(n) ((sizeof(n) + x) & ~(x))x = sizeof(int) - 1 = 3 = 0b'0000 0000 0000 0011~x = 0b'1111 1111 1111 1100

The sum of a number and (~ x) is a multiple of sizeof(int), i.e_ INTSIZEOF(n) round n to a multiple of sizeof(int).

  1. va_ The start macro obtains the memory address of a fixed parameter in the stack before the first variable parameter according to (va_list) & v, plus_ INTSIZEOF(v), that is, after the memory size occupied by v, make ap point to the next parameter after the fixed parameter (the address of the first variable parameter).

    Fixed parameter address for va_start macro, so it cannot be declared as a register variable (invalid address) or as an array type (difficult to determine the length).

  2. va_ The Arg macro gets the variable parameter value of type. First ap+=_INTSIZEOF(type), that is, ap skips the current variable parameter and points to the address of the next variable parameter; Then ap-_INTSIZEOF(type) gets the memory address of the current variable parameter, and returns the current variable parameter value after type conversion.

    va_ The equivalent implementation of Arg macro is as follows: move the pointer to the next variable parameter and return the left shifted value [- 1] (array subscript indicates offset), that is, the current variable parameter value #define va_arg(ap,type) ((type *)((ap) += _INTSIZEOF(type)))

  3. va_ The end macro causes the ap to no longer point to a valid memory address. Some implementations of this macro are defined as ((void*)0), and no code will be generated for it during compilation. It makes no difference whether it is called or not. But in some implementations_ The end macro is used to complete some necessary cleaning work before the function returns, such as VA_ The start macro may modify the stack in some way, so that the return operation cannot be completed, Va_ The end macro can restore the relevant modifications; Another example is VA_ The start macro may dynamically allocate memory to the parameter list to facilitate traversal_ list,va_ The end macro frees up previously dynamically allocated memory. Therefore, from using VA_ You must call VA once before exiting in the function of the start macro_ End macro. Variable parameters can be traversed multiple times within a function, but each time they must be in VA_ The start macro starts because the ap pointer no longer points to the first argument after traversal.

The following figure shows the distribution of variable parameters in the stack based on variable parameter macros:

Variable parameter macros cannot intelligently identify the number and type of variable parameters, so they need to judge the number and type of variable parameters when implementing variable parameter functions.

The former can explicitly provide variable parameter items or set traversal end conditions (such as - 1, '\ 0' or carriage return).

The latter can explicitly provide enumeration values of variable parameter types, or contain sufficient type information in fixed parameters (for example, the printf function can determine each variable parameter type by analyzing the format string), and even the calling function and the called function can agree on the type organization of variable parameters.

I've seen it here. If it's helpful, give me a praise 👍👍👍 Go again!
Give me a favor. There is no Bug in the code~

Topics: C