Transférer une invocation d'une fonction variadique en C


193

En C, est-il possible de transmettre l'invocation d'une fonction variadique? Un péché,

int my_printf(char *fmt, ...) {
    fprintf(stderr, "Calling printf with fmt %s", fmt);
    return SOMEHOW_INVOKE_LIBC_PRINTF;
}

Transférer l'invocation de la manière ci-dessus n'est évidemment pas strictement nécessaire dans ce cas (puisque vous pouvez enregistrer les invocations d'une autre manière, ou utiliser vfprintf), mais la base de code sur laquelle je travaille nécessite que le wrapper effectue un travail réel, et ne le fait pas Je n'ai pas (et je n'ai pas pu ajouter) une fonction d'assistance semblable à vfprintf.

[Mise à jour: il semble y avoir une certaine confusion sur la base des réponses fournies jusqu'à présent. Pour formuler la question d'une autre manière: en général, pouvez-vous envelopper une fonction variadique arbitraire sans modifier la définition de cette fonction .]


1
question géniale - heureusement, je peux ajouter une surcharge vFunc qui prend une va_list. Merci d'avoir posté.
Gishu

Réponses:


158

Si vous n'avez pas de fonction analogue à vfprintfcelle qui prend un va_listau lieu d'un nombre variable d'arguments, vous ne pouvez pas le faire . Voirhttp://c-faq.com/varargs/handoff.html .

Exemple:

void myfun(const char *fmt, va_list argp) {
    vfprintf(stderr, fmt, argp);
}

6
Selon l'importance du problème, l'invocation par l'assemblage en ligne offre toujours cette possibilité.
filip le

Au moins un argument doit être passé, myfun("")n'est pas pris en charge - existe-t-il une solution pour y remédier? (MSVC imprime: trop peu d'arguments pour l'appel)
mvorisek

@mvorisek: Non, il n'y a pas de solution bien définie à cela, car les fonctions variadiques doivent prendre au moins un paramètre nommé avant le .... Votre solution de contournement peut sembler fonctionner avec votre compilateur particulier, mais il s'agit toujours d'un comportement non défini selon la norme du langage.
Adam Rosenfield le

62

Pas directement, mais il est courant (et vous trouverez presque universellement le cas dans la bibliothèque standard) que les fonctions variadiques viennent par paires avec une varargsfonction alternative de style. par exemple printf/vprintf

Les fonctions v ... prennent un paramètre va_list, dont l'implémentation est souvent faite avec une `` macro magic '' spécifique au compilateur, mais vous êtes assuré que l'appel de la fonction v ... style à partir d'une fonction variadique comme celle-ci fonctionnera:

#include <stdarg.h>

int m_printf(char *fmt, ...)
{
    int ret;

    /* Declare a va_list type variable */
    va_list myargs;

    /* Initialise the va_list variable with the ... after fmt */

    va_start(myargs, fmt);

    /* Forward the '...' to vprintf */
    ret = vprintf(fmt, myargs);

    /* Clean up the va_list */
    va_end(myargs);

    return ret;
}

Cela devrait vous donner l'effet que vous recherchez.

Si vous envisagez d'écrire une fonction de bibliothèque variadique, vous devriez également envisager de rendre disponible un compagnon de style va_list dans le cadre de la bibliothèque. Comme vous pouvez le voir sur votre question, cela peut s'avérer utile pour vos utilisateurs.


Je suis presque sûr que vous devez appeler va_copyavant de passer la myargsvariable à une autre fonction. Veuillez consulter MSC39-C , où il indique que ce que vous faites est un comportement non défini.
aviggiano

3
@aviggiano: La règle est "Ne pas appeler va_arg()sur un va_listqui a une valeur indéterminée", je ne le fais pas car je ne l'utilise jamais va_argdans ma fonction d'appel. La valeur de myargsaprès l'appel (dans ce cas) à vprintfest indéterminée (en supposant que c'est le cas va_arg). La norme dit que myargs"doit être passé à la va_endmacro avant toute référence ultérieure à [elle]"; c'est exactement ce que je fais. Je n'ai pas besoin de copier ces arguments car je n'ai pas l'intention de les parcourir dans la fonction appelante.
CB Bailey

1
Tu as raison. La page de manuel Linux le confirme. Si apest passé à une fonction qui utilise va_arg(ap,type)alors la valeur de apn'est pas définie après le retour de cette fonction.
aviggiano

48

C99 prend en charge les macros avec des arguments variadiques ; en fonction de votre compilateur, vous pourrez peut-être déclarer une macro qui fait ce que vous voulez:

#define my_printf(format, ...) \
    do { \
        fprintf(stderr, "Calling printf with fmt %s\n", format); \
        some_other_variadac_function(format, ##__VA_ARGS__); \
    } while(0)

En général, cependant, la meilleure solution est d'utiliser la forme va_list de la fonction que vous essayez d'encapsuler, s'il en existe une.


12

Presque, en utilisant les installations disponibles dans <stdarg.h>:

#include <stdarg.h>
int my_printf(char *format, ...)
{
   va_list args;
   va_start(args, format);
   int r = vprintf(format, args);
   va_end(args);
   return r;
}

Notez que vous devrez utiliser la vprintfversion plutôt que la version standard printf. Il n'y a pas moyen d'appeler directement une fonction variadique dans cette situation sans utiliser va_list.


1
Merci, mais à partir de la question: "la base de code sur laquelle je travaille [...] n'a pas (et ne peut pas avoir ajouté) une fonction d'assistance semblable à vfprintf."
Patrick le

11

Comme il n'est pas vraiment possible de transférer de tels appels d'une manière agréable, nous avons contourné ce problème en configurant un nouveau cadre de pile avec une copie du cadre de pile d'origine. Cependant, ceci est hautement non portable et fait toutes sortes d'hypothèses , par exemple que le code utilise des pointeurs de trame et les conventions d'appel «standard».

Ce fichier d'en-tête permet d'encapsuler des fonctions variadiques pour x86_64 et i386 (GCC). Cela ne fonctionne pas pour les arguments à virgule flottante, mais devrait être simple à étendre pour les prendre en charge.

#ifndef _VA_ARGS_WRAPPER_H
#define _VA_ARGS_WRAPPER_H
#include <limits.h>
#include <stdint.h>
#include <alloca.h>
#include <inttypes.h>
#include <string.h>

/* This macros allow wrapping variadic functions.
 * Currently we don't care about floating point arguments and
 * we assume that the standard calling conventions are used.
 *
 * The wrapper function has to start with VA_WRAP_PROLOGUE()
 * and the original function can be called by
 * VA_WRAP_CALL(function, ret), whereas the return value will
 * be stored in ret.  The caller has to provide ret
 * even if the original function was returning void.
 */

#define __VA_WRAP_CALL_FUNC __attribute__ ((noinline))

#define VA_WRAP_CALL_COMMON()                                        \
    uintptr_t va_wrap_this_bp,va_wrap_old_bp;                        \
    va_wrap_this_bp  = va_wrap_get_bp();                             \
    va_wrap_old_bp   = *(uintptr_t *) va_wrap_this_bp;               \
    va_wrap_this_bp += 2 * sizeof(uintptr_t);                        \
    size_t volatile va_wrap_size = va_wrap_old_bp - va_wrap_this_bp; \
    uintptr_t *va_wrap_stack = alloca(va_wrap_size);                 \
    memcpy((void *) va_wrap_stack,                                   \
        (void *)(va_wrap_this_bp), va_wrap_size);


#if ( __WORDSIZE == 64 )

/* System V AMD64 AB calling convention */

static inline uintptr_t __attribute__((always_inline)) 
va_wrap_get_bp()
{
    uintptr_t ret;
    asm volatile ("mov %%rbp, %0":"=r"(ret));
    return ret;
}


#define VA_WRAP_PROLOGUE()           \
    uintptr_t va_wrap_ret;           \
    uintptr_t va_wrap_saved_args[7]; \
    asm volatile  (                  \
    "mov %%rsi,     (%%rax)\n\t"     \
    "mov %%rdi,  0x8(%%rax)\n\t"     \
    "mov %%rdx, 0x10(%%rax)\n\t"     \
    "mov %%rcx, 0x18(%%rax)\n\t"     \
    "mov %%r8,  0x20(%%rax)\n\t"     \
    "mov %%r9,  0x28(%%rax)\n\t"     \
    :                                \
    :"a"(va_wrap_saved_args)         \
    );

#define VA_WRAP_CALL(func, ret)            \
    VA_WRAP_CALL_COMMON();                 \
    va_wrap_saved_args[6] = (uintptr_t)va_wrap_stack;  \
    asm volatile (                         \
    "mov      (%%rax), %%rsi \n\t"         \
    "mov   0x8(%%rax), %%rdi \n\t"         \
    "mov  0x10(%%rax), %%rdx \n\t"         \
    "mov  0x18(%%rax), %%rcx \n\t"         \
    "mov  0x20(%%rax),  %%r8 \n\t"         \
    "mov  0x28(%%rax),  %%r9 \n\t"         \
    "mov           $0, %%rax \n\t"         \
    "call             *%%rbx \n\t"         \
    : "=a" (va_wrap_ret)                   \
    : "b" (func), "a" (va_wrap_saved_args) \
    :  "%rcx", "%rdx",                     \
      "%rsi", "%rdi", "%r8", "%r9",        \
      "%r10", "%r11", "%r12", "%r14",      \
      "%r15"                               \
    );                                     \
    ret = (typeof(ret)) va_wrap_ret;

#else

/* x86 stdcall */

static inline uintptr_t __attribute__((always_inline))
va_wrap_get_bp()
{
    uintptr_t ret;
    asm volatile ("mov %%ebp, %0":"=a"(ret));
    return ret;
}

#define VA_WRAP_PROLOGUE() \
    uintptr_t va_wrap_ret;

#define VA_WRAP_CALL(func, ret)        \
    VA_WRAP_CALL_COMMON();             \
    asm volatile (                     \
    "mov    %2, %%esp \n\t"            \
    "call  *%1        \n\t"            \
    : "=a"(va_wrap_ret)                \
    : "r" (func),                      \
      "r"(va_wrap_stack)               \
    : "%ebx", "%ecx", "%edx"   \
    );                                 \
    ret = (typeof(ret))va_wrap_ret;
#endif

#endif

À la fin, vous pouvez encapsuler les appels comme ceci:

int __VA_WRAP_CALL_FUNC wrap_printf(char *str, ...)
{
    VA_WRAP_PROLOGUE();
    int ret;
    VA_WRAP_CALL(printf, ret);
    printf("printf returned with %d \n", ret);
    return ret;
}

3

Utilisez vfprintf:

int my_printf(char *fmt, ...) {
    va_list va;
    int ret;

    va_start(va, fmt);
    ret = vfprintf(stderr, fmt, va);
    va_end(va);
    return ret;
}

1
Merci, mais à partir de la question: "la base de code sur laquelle je travaille [...] n'a pas (et ne peut pas avoir ajouté) une fonction d'assistance semblable à vfprintf."
Patrick le

2

Il n'y a aucun moyen de transférer de tels appels de fonction car le seul emplacement où vous pouvez récupérer des éléments de pile bruts se trouve my_print(). La manière habituelle d'encapsuler des appels comme ça est d'avoir deux fonctions, une qui convertit simplement les arguments dans les différentes varargsstructures, et une autre qui opère réellement sur ces structures. En utilisant un tel modèle à double fonction, vous pouvez (par exemple) encapsuler printf()en initialisant les structures dans my_printf()with va_start(), puis les transmettre à vfprintf().


Il n'y a donc aucun moyen de transférer l'appel si vous ne pouvez pas modifier l'implémentation de la fonction que vous souhaitez encapsuler?
Patrick le

Droite. Votre meilleur pari est de faire pression pour qu'un wrapper de style vprintf soit ajouté.
John Millikin le

2

gcc propose une extension qui peut faire cela: __builtin_applyet parents. Voir Construire des appels de fonction dans le manuel de gcc.

Un exemple:

#include <stdio.h>

int my_printf(const char *fmt, ...) {
    void *args = __builtin_apply_args();
    printf("Hello there! Format string is %s\n", fmt);
    void *ret = __builtin_apply((void (*)())printf, args, 1000);
    __builtin_return(ret);
}

int main(void) {
    my_printf("%d %f %s\n", -37, 3.1415, "spam");
    return 0;
}

Essayez-le sur godbolt

Il y a quelques avertissements dans la documentation que cela pourrait ne pas fonctionner dans des situations plus compliquées. Et vous devez coder en dur une taille maximale pour les arguments (ici j'ai utilisé 1000). Mais cela pourrait être une alternative raisonnable aux autres approches qui impliquent de disséquer la pile en C ou en langage d'assemblage.


1

Oui, vous pouvez le faire, mais c'est un peu moche et vous devez connaître le nombre maximal d'arguments. De plus si vous êtes sur une architecture où les arguments ne sont pas passés sur la pile comme le x86 (par exemple, PowerPC), vous devrez savoir si des types "spéciaux" (double, floats, altivec etc.) sont utilisés et si alors, traitez-les en conséquence. Cela peut être pénible rapidement mais si vous êtes sur x86 ou si la fonction d'origine a un périmètre bien défini et limité, cela peut fonctionner. Ce sera toujours un hack , utilisez-le à des fins de débogage. Ne construisez pas votre logiciel autour de cela. Quoi qu'il en soit, voici un exemple de travail sur x86:

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

int old_variadic_function(int n, ...)
{
  va_list args;
  int i = 0;

  va_start(args, n);

  if(i++<n) printf("arg %d is 0x%x\n", i, va_arg(args, int));
  if(i++<n) printf("arg %d is %g\n",   i, va_arg(args, double));
  if(i++<n) printf("arg %d is %g\n",   i, va_arg(args, double));

  va_end(args);

  return n;
}

int old_variadic_function_wrapper(int n, ...)
{
  va_list args;
  int a1;
  int a2;
  int a3;
  int a4;
  int a5;
  int a6;
  int a7;
  int a8;

  /* Do some work, possibly with another va_list to access arguments */

  /* Work done */

  va_start(args, n);

  a1 = va_arg(args, int);
  a2 = va_arg(args, int);
  a3 = va_arg(args, int);
  a4 = va_arg(args, int);
  a5 = va_arg(args, int);
  a6 = va_arg(args, int);
  a7 = va_arg(args, int);

  va_end(args);

  return old_variadic_function(n, a1, a2, a3, a4, a5, a6, a7, a8);
}

int main(void)
{
  printf("Call 1: 1, 0x123\n");
  old_variadic_function(1, 0x123);
  printf("Call 2: 2, 0x456, 1.234\n");
  old_variadic_function(2, 0x456, 1.234);
  printf("Call 3: 3, 0x456, 4.456, 7.789\n");
  old_variadic_function(3, 0x456, 4.456, 7.789);
  printf("Wrapped call 1: 1, 0x123\n");
  old_variadic_function_wrapper(1, 0x123);
  printf("Wrapped call 2: 2, 0x456, 1.234\n");
  old_variadic_function_wrapper(2, 0x456, 1.234);
  printf("Wrapped call 3: 3, 0x456, 4.456, 7.789\n");
  old_variadic_function_wrapper(3, 0x456, 4.456, 7.789);

  return 0;
}

Pour une raison quelconque, vous ne pouvez pas utiliser de flottants avec va_arg, gcc dit qu'ils sont convertis en double mais le programme plante. Cela seul démontre que cette solution est un hack et qu'il n'y a pas de solution générale. Dans mon exemple, j'ai supposé que le nombre maximum d'arguments était de 8, mais vous pouvez augmenter ce nombre. La fonction encapsulée n'utilisait également que des entiers, mais elle fonctionne de la même manière avec d'autres paramètres «normaux» puisqu'ils sont toujours convertis en entiers. La fonction cible connaîtra leurs types mais votre wrapper intermédiaire n'en a pas besoin. L'encapsuleur n'a pas non plus besoin de connaître le bon nombre d'arguments puisque la fonction cible le saura également. Pour faire un travail utile (sauf simplement enregistrer l'appel), vous devrez probablement connaître les deux.


0

Il existe essentiellement trois options.

La première consiste à ne pas la transmettre mais à utiliser l'implémentation variadique de votre fonction cible et à ne pas transmettre les ellipses. L'autre consiste à utiliser une macro variadique. La troisième option est tout ce qui me manque.

Je choisis généralement l'option 1 car j'ai l'impression que c'est vraiment facile à gérer. La deuxième option présente un inconvénient car il existe certaines limitations à l'appel des macros variadiques.

Voici un exemple de code:

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

#define Option_VariadicMacro(f, ...)\
    printf("printing using format: %s", f);\
    printf(f, __VA_ARGS__)

int Option_ResolveVariadicAndPassOn(const char * f, ... )
{
    int r;
    va_list args;

    printf("printing using format: %s", f);
    va_start(args, f);
    r = vprintf(f, args);
    va_end(args);
    return r;
}

void main()
{
    const char * f = "%s %s %s\n";
    const char * a = "One";
    const char * b = "Two";
    const char * c = "Three";
    printf("---- Normal Print ----\n");
    printf(f, a, b, c);
    printf("\n");
    printf("---- Option_VariadicMacro ----\n");
    Option_VariadicMacro(f, a, b, c);
    printf("\n");
    printf("---- Option_ResolveVariadicAndPassOn ----\n");
    Option_ResolveVariadicAndPassOn(f, a, b, c);
    printf("\n");
}

0

La meilleure façon de procéder est

static BOOL(__cdecl *OriginalVarArgsFunction)(BYTE variable1, char* format, ...)(0x12345678); //TODO: change address lolz

BOOL __cdecl HookedVarArgsFunction(BYTE variable1, char* format, ...)
{
    BOOL res;

    va_list vl;
    va_start(vl, format);

    // Get variable arguments count from disasm. -2 because of existing 'format', 'variable1'
    uint32_t argCount = *((uint8_t*)_ReturnAddress() + 2) / sizeof(void*) - 2;
    printf("arg count = %d\n", argCount);

    // ((int( __cdecl* )(const char*, ...))&oldCode)(fmt, ...);
    __asm
    {
        mov eax, argCount
        test eax, eax
        je noLoop
        mov edx, vl
        loop1 :
        push dword ptr[edx + eax * 4 - 4]
        sub eax, 1
        jnz loop1
        noLoop :
        push format
        push variable1
        //lea eax, [oldCode] // oldCode - original function pointer
        mov eax, OriginalVarArgsFunction
        call eax
        mov res, eax
        mov eax, argCount
        lea eax, [eax * 4 + 8] //+8 because 2 parameters (format and variable1)
        add esp, eax
    }
    return res;
}

0

Je ne sais pas si cela aide à répondre à la question de OP car je ne sais pas pourquoi la restriction d'utilisation d'une fonction d'assistance semblable à vfprintf dans la fonction wrapper s'applique. Je pense que le problème clé ici est qu'il est difficile de transmettre la liste d'arguments variadiques sans les interpréter. Ce qui est possible, c'est d'effectuer le formatage (en utilisant une fonction d'assistance semblable à vfprintf: vsnprintf) et de transmettre la sortie formatée à la fonction encapsulée avec des arguments variadiques (c'est-à-dire ne pas modifier la définition de la fonction encapsulée). Alors, on y va:

int my_printf(char *fmt, ...)
{
    int ret;

    if (fmt == NULL) {
        /* Invalid format pointer */
        ret = -1;
    } else {
        va_list args;
        int len;

        va_start(args, fmt);

        /* Get length of format including arguments */
        len = vsnprintf(NULL, 0, fmt, args);

        if (len < 0) {
            /* vsnprintf failed */
            ret = -1;
        } else {
            /* Declare a character buffer and write the formatted string */
            char formatted[len + 1];
            vsnprintf(formatted, sizeof(formatted), fmt, args);

            /* Call the wrapped function using the formatted output and return */
            fprintf(stderr, "Calling printf with fmt %s", fmt);
            ret = printf(formatted);
        }

        va_end(args);
    }

    return ret;
}

Je suis tombé sur cette solution ici .


Vous en avez besoin ret = printf("%s", formatted);, sinon si la chaîne formatée contient un, %il commence à le remplacer par les déchets trouvés sur la pile.
egmont le

De plus, vous devez "redémarrer" args(c'est-à-dire va_end(args); va_start(args, fmt);) entre les deux vsnprintf()appels.
egmont le
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.