Reenviar una invocación de una función variadic en C


189

En C, ¿es posible reenviar la invocación de una función variable? Como en,

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

Reenviar la invocación de la manera anterior obviamente no es estrictamente necesario en este caso (ya que podría iniciar invocaciones de otras maneras, o usar vfprintf), pero la base de código en la que estoy trabajando requiere que el contenedor haga un trabajo real, y no No tiene (y no puede haber agregado) una función auxiliar similar a vfprintf.

[Actualización: parece haber cierta confusión basada en las respuestas que se han proporcionado hasta ahora. Para formular la pregunta de otra manera: en general, ¿puede ajustar alguna función variable arbitraria sin modificar la definición de esa función ?]


1
Pregunta increíble: afortunadamente, puedo agregar una sobrecarga de vFunc que requiere una va_list. Gracias por publicar.
Gishu

Respuestas:


157

Si no tiene una función análoga a la vfprintfque toma un va_listnúmero variable de argumentos en lugar de uno variable, no puede hacerlo . Verhttp://c-faq.com/varargs/handoff.html .

Ejemplo:

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

55
Dependiendo de cuán crucial sea el problema, la invocación por ensamblaje en línea aún ofrece esta posibilidad.
filip

60

No directamente, sin embargo, es común (y encontrará casi universalmente el caso en la biblioteca estándar) para que las funciones variadas vengan en pares con una varargsfunción alternativa de estilo. por ejemplo printf/vprintf

Las funciones v ... toman un parámetro va_list, cuya implementación a menudo se realiza con 'macro magic' específica del compilador, pero se garantiza que llamar a la función v ... style desde una función variadic como esta funcionará:

#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;
}

Esto debería darte el efecto que estás buscando.

Si está considerando escribir una función de biblioteca variadic, también debería considerar hacer que un compañero de estilo va_list esté disponible como parte de la biblioteca. Como puede ver en su pregunta, puede resultar útil para sus usuarios.


Estoy bastante seguro de que necesita llamar va_copyantes de pasar la myargsvariable a otra función. Consulte MSC39-C , donde dice que lo que está haciendo es un comportamiento indefinido.
aviggiano

2
@aviggiano: La regla es "No invoque va_arg()a un va_listque tenga un valor indeterminado", no lo hago porque nunca lo uso va_argen mi función de llamada. El valor de myargsafter the call (en este caso) a vprintfes indeterminado (suponiendo que nos haga va_arg). La norma dice que myargs"se pasará a la va_endmacro antes de cualquier referencia adicional a [it]"; que es exactamente lo que hago No necesito copiar esos argumentos porque no tengo intención de repetirlos en la función de llamada.
CB Bailey

1
Tienes razón. La página del manual de Linux lo confirma. Si apse pasa a una función que utiliza va_arg(ap,type), el valor de apno está definido después del retorno de esa función.
aviggiano

48

C99 soporta macros con argumentos variadic ; dependiendo de su compilador, puede declarar una macro que hace lo que quiere:

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

Sin embargo, en general, la mejor solución es usar la forma va_list de la función que está intentando ajustar, en caso de que exista.


12

Casi, utilizando las instalaciones disponibles en <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;
}

Tenga en cuenta que deberá usar la vprintfversión en lugar de la simple printf. No hay una manera de llamar directamente a una función variadic en esta situación sin usar va_list.


1
Gracias, pero de la pregunta: "la base de código en la que estoy trabajando [...] no tiene (y no puede haber agregado) una función auxiliar similar a vfprintf".
Patrick

11

Como en realidad no es posible reenviar esas llamadas de una manera agradable, trabajamos alrededor de esto configurando un nuevo marco de pila con una copia del marco de pila original. Sin embargo, esto es altamente inportable y hace todo tipo de suposiciones , por ejemplo, que el código utiliza punteros de trama y las convenciones de llamada 'estándar'.

Este archivo de encabezado permite ajustar funciones variadas para x86_64 e i386 (GCC). No funciona para argumentos de punto flotante, pero debería extenderse directamente para admitirlos.

#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

Al final puedes envolver llamadas como esta:

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

Use 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
Gracias, pero de la pregunta: "la base de código en la que estoy trabajando [...] no tiene (y no puede haber agregado) una función auxiliar similar a vfprintf".
Patrick

2

No hay forma de reenviar tales llamadas a funciones porque está en la única ubicación donde puede recuperar elementos de pila sin procesar my_print(). La forma habitual de encapsular llamadas como esas es tener dos funciones, una que simplemente convierte los argumentos en varias varargsestructuras, y otra que realmente opera sobre esas estructuras. Usando un modelo de doble función, puede (por ejemplo) ajustar printf()inicializando las estructuras my_printf()con va_start(), y luego pasarlas a vfprintf().


Entonces, ¿no hay forma de reenviar la invocación si no puede modificar la implementación de la función que desea ajustar?
Patrick

Correcto. Su mejor opción es presionar para que se agregue un contenedor estilo vprintf.
John Millikin

1

Sí, puedes hacerlo, pero es algo feo y tienes que saber la cantidad máxima de argumentos. Además, si está en una arquitectura donde los argumentos no se pasan en la pila como el x86 (por ejemplo, PowerPC), tendrá que saber si se usan tipos "especiales" (doble, flotantes, altivec, etc.) y si entonces, trate con ellos en consecuencia. Puede ser doloroso rápidamente, pero si está en x86 o si la función original tiene un perímetro bien definido y limitado, puede funcionar. Todavía será un hack , úsalo para propósitos de depuración. No construyas tu software alrededor de eso. De todos modos, aquí hay un ejemplo de trabajo en 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;
}

Por alguna razón, no puede usar flotantes con va_arg, gcc dice que se convierten a doble pero el programa falla. Eso por sí solo demuestra que esta solución es un truco y que no hay una solución general. En mi ejemplo supuse que el número máximo de argumentos era 8, pero puede aumentar ese número. La función envuelta también solo usaba enteros, pero funciona de la misma manera con otros parámetros 'normales' ya que siempre se convierten a enteros. La función de destino conocerá sus tipos, pero su envoltorio intermediario no necesita saberlo. El contenedor tampoco necesita saber el número correcto de argumentos ya que la función de destino también lo sabrá. Sin embargo, para hacer un trabajo útil (excepto solo registrar la llamada), probablemente tenga que saber ambos.


1

gcc ofrece una extensión que puede hacer esto: __builtin_applyy parientes. Consulte Construcción de llamadas de función en el manual de gcc.

Un ejemplo:

#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;
}

Pruébalo en Godbolt

Hay algunas precauciones en la documentación que podrían no funcionar en situaciones más complicadas. Y tienes que codificar un tamaño máximo para los argumentos (aquí usé 1000). Pero podría ser una alternativa razonable a los otros enfoques que implican diseccionar la pila en lenguaje C o ensamblador.


Servicial. ¡Gracias!
rsmoorthy

0

Hay esencialmente tres opciones.

Una es no transmitirla, sino utilizar la implementación variada de la función de destino y no transmitir las elipses. El otro es usar una macro variadic. La tercera opción es todo lo que me falta.

Por lo general, voy con la opción uno, ya que siento que esto es realmente fácil de manejar. La opción dos tiene un inconveniente porque hay algunas limitaciones para llamar a macros variables.

Aquí hay un código de ejemplo:

#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 mejor manera de hacer esto es

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

No estoy seguro de si esto ayuda a responder la pregunta de OP, ya que no sé por qué se aplica la restricción para usar una función auxiliar similar a vfprintf en la función de contenedor. Creo que el problema clave aquí es que reenviar la lista de argumentos variables sin interpretarlos es difícil. Lo que es posible es realizar el formateo (usando una función auxiliar similar a vfprintf: vsnprintf) y reenviar la salida formateada a la función ajustada con argumentos variables (es decir, sin modificar la definición de la función ajustada). Así que, aquí vamos:

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;
}

Encontré esta solución aquí .

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.