Prześlij wywołanie funkcji wariadycznej w C

189

Czy w C można przekazać wywołanie funkcji wariadycznej? Jak w,

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

Przekazywanie wywołania w powyższy sposób oczywiście nie jest absolutnie konieczne w tym przypadku (ponieważ można rejestrować wywołania w inny sposób lub użyć vfprintf), ale podstawa kodu, nad którą pracuję, wymaga od opakowania, aby wykonała jakąś rzeczywistą pracę, i nie wykonuje nie ma (i nie mógł dodać) funkcji pomocniczej podobnej do vfprintf.

[Aktualizacja: wydaje się, że istnieje pewne zamieszanie w oparciu o odpowiedzi, które zostały dostarczone do tej pory. Aby sformułować pytanie w inny sposób: ogólnie, czy można owinąć dowolną funkcję wariadyczną bez zmiany jej definicji .]

Patrick
źródło
1
niesamowite pytanie - na szczęście mogę dodać przeciążenie vFunc, które zajmuje va_list. Dzięki za wysłanie.
Gishu,

Odpowiedzi:

157

Jeśli nie masz funkcji analogicznej do vfprintftej, która pobiera va_listzmienną liczbę argumentów, nie możesz tego zrobić . Widziećhttp://c-faq.com/varargs/handoff.html .

Przykład:

void myfun(const char *fmt, va_list argp) {
    vfprintf(stderr, fmt, argp);
}
Adam Rosenfield
źródło
5
W zależności od tego, jak istotna jest kwestia, wywołanie przez zestaw wbudowany nadal oferuje tę możliwość.
Filip
60

Nie bezpośrednio, jednak powszechne (i prawie tak samo jest w standardowej bibliotece), że funkcje variadic występują w parach z varargsalternatywną funkcją stylu. np. printf/vprintf

Funkcje v ... pobierają parametr va_list, którego implementacja często odbywa się za pomocą specyficznej dla kompilatora „magii makro”, ale masz gwarancję, że wywołanie funkcji stylu v ... z funkcji variadic, takiej jak ta, zadziała:

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

To powinno dać efekt, którego szukasz.

Jeśli zastanawiasz się nad napisaniem funkcji biblioteki variadic, powinieneś również rozważyć udostępnienie elementu towarzyszącego w stylu va_list jako części biblioteki. Jak widać z pytania, może okazać się przydatne dla użytkowników.

CB Bailey
źródło
Jestem pewien, że musisz zadzwonić va_copyprzed przekazaniem myargszmiennej do innej funkcji. Proszę zobaczyć MSC39-C , gdzie stwierdza, że ​​to, co robisz, jest niezdefiniowanym zachowaniem.
aviggiano
2
@aviggiano: Reguła brzmi: „Nie wywołuj va_arg()elementu va_listo nieokreślonej wartości”, nie robię tego, ponieważ nigdy nie używam va_argfunkcji wywoływania. Wartość od myargspo wywołaniu (w tym przypadku) do vprintfjest nieokreślona (przy założeniu, że robi się z nami va_arg). Norma mówi, że myargs„przekazuje się va_endmakro przed dalszym odniesieniem do [it]”; co dokładnie robię. Nie muszę kopiować tych argumentów, ponieważ nie mam zamiaru iterować ich w funkcji wywołującej.
CB Bailey,
1
Masz rację. Linux man strona potwierdza, że. Jeśli apzostanie przekazana do funkcji, która używa, va_arg(ap,type)wartość appo jej zwróceniu jest niezdefiniowana.
aviggiano
48

C99 obsługuje makra z argumentami variadic ; w zależności od kompilatora możesz zadeklarować makro, które robi to, co chcesz:

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

Ogólnie jednak najlepszym rozwiązaniem jest użycie postaci va_list funkcji, którą próbujesz zawinąć, jeśli taka istnieje.

Komandor Jaeger
źródło
12

Prawie korzystając z udogodnień dostępnych w <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;
}

Pamiętaj, że będziesz musiał użyć vprintfwersji zamiast zwykłego printf. W tej sytuacji nie ma sposobu bezpośredniego wywołania funkcji variadic bez użycia va_list.

Greg Hewgill
źródło
1
Dzięki, ale z pytania: „baza kodu, nad którą pracuję [...] nie ma (i nie mogła dodać) funkcji pomocniczej podobnej do vfprintf.”
Patrick,
11

Ponieważ tak naprawdę nie jest możliwe przekazywanie takich wywołań w przyjemny sposób, obejrzeliśmy to, ustawiając nową ramkę stosu z kopią oryginalnej ramki stosu. Jest to jednak wysoce nieprzenośne i przyjmuje wszelkie założenia , np. Że kod używa wskaźników ramek i „standardowych” konwencji wywoływania.

Ten plik nagłówka pozwala na zawijanie różnych funkcji dla x86_64 i i386 (GCC). Nie działa w przypadku argumentów zmiennoprzecinkowych, ale powinien być prosty do rozszerzenia o obsługę tych argumentów.

#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

Na koniec możesz zawijać takie połączenia:

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;
}
coltox
źródło
3

Użyj 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;
}
Greg Rogers
źródło
1
Dzięki, ale z pytania: „baza kodu, nad którą pracuję [...] nie ma (i nie mogła dodać) funkcji pomocniczej podobnej do vfprintf.”
Patrick,
2

Nie ma możliwości przekazania takich wywołań funkcji, ponieważ znajduje się jedyne miejsce, w którym można pobrać surowe elementy stosu my_print(). Zwykłym sposobem na zawijanie takich wywołań jest posiadanie dwóch funkcji, jednej, która po prostu konwertuje argumenty na różne varargsstruktury, a drugiej, która faktycznie działa na tych strukturach. Korzystając z takiego modelu dwufunkcyjnego, możesz (na przykład) zawinąć printf(), inicjując struktury za my_printf()pomocą va_start(), a następnie przekazać je do vfprintf().

John Millikin
źródło
Więc nie ma możliwości przekazania wywołania, jeśli nie możesz zmodyfikować implementacji funkcji, którą chcesz owinąć?
Patrick,
Dobrze. Najlepiej jest nalegać na dodanie opakowania w stylu vprintf.
John Millikin,
1

Tak, możesz to zrobić, ale jest to trochę brzydkie i musisz znać maksymalną liczbę argumentów. Ponadto, jeśli masz architekturę, w której argumenty nie są przekazywane na stosie, takie jak x86 (na przykład PowerPC), będziesz musiał wiedzieć, czy są używane typy „specjalne” (podwójne, zmiennoprzecinkowe, altivec itp.) I czy więc radzcie sobie z nimi odpowiednio. Może to być szybko bolesne, ale jeśli używasz x86 lub jeśli oryginalna funkcja ma dobrze zdefiniowany i ograniczony obwód, może działać. To nadal będzie hack , użyj go do celów debugowania. Nie buduj wokół tego oprogramowania. Tak czy inaczej, oto działający przykład na 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;
}

Z jakiegoś powodu nie można używać liczb zmiennoprzecinkowych z va_arg, gcc mówi, że są one konwertowane na podwójne, ale program ulega awarii. Samo to pokazuje, że to rozwiązanie jest włamaniem i że nie ma ogólnego rozwiązania. W moim przykładzie założyłem, że maksymalna liczba argumentów wynosiła 8, ale można zwiększyć tę liczbę. Funkcja zawinięcia również używała tylko liczb całkowitych, ale działa tak samo z innymi „normalnymi” parametrami, ponieważ zawsze rzutują na liczby całkowite. Funkcja docelowa zna ich typy, ale opakowanie pośrednie nie musi tego robić. Opakowanie również nie musi znać odpowiedniej liczby argumentów, ponieważ funkcja docelowa również to zna. Aby wykonać przydatną pracę (oprócz rejestrowania połączenia), prawdopodobnie będziesz musiał znać oba.

użytkownik10146
źródło
1

gcc oferuje rozszerzenie, które może to zrobić: __builtin_applyi krewnych. Zobacz: Konstruowanie wywołań funkcji w podręczniku gcc.

Przykład:

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

Wypróbuj na Godbolt

Dokumentacja zawiera pewne ostrzeżenia, że ​​może nie działać w bardziej skomplikowanych sytuacjach. I musisz ustalić na stałe maksymalny rozmiar argumentów (tutaj użyłem 1000). Ale może to być rozsądna alternatywa dla innych podejść, które polegają na analizie stosu w języku C lub asemblerze.

Nate Eldredge
źródło
Pomocny. Dzięki!
rsmoorthy
0

Istnieją zasadniczo trzy opcje.

Jednym z nich jest nie przekazywanie go, ale stosowanie implementacji variadic funkcji docelowej i nie przekazywanie elips. Drugim jest użycie makra variadic. Trzecia opcja to wszystko, czego mi brakuje.

Zwykle wybieram opcję pierwszą, ponieważ wydaje mi się, że jest to naprawdę łatwe w obsłudze. Opcja druga ma wadę, ponieważ istnieją pewne ograniczenia w wywoływaniu makr variadic.

Oto przykładowy kod:

#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");
}
Johannes
źródło
0

Najlepszym sposobem na to jest

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;
}
SSpoke
źródło
0

Nie jestem pewien, czy to pomaga odpowiedzieć na pytanie OP, ponieważ nie wiem, dlaczego obowiązuje ograniczenie używania funkcji pomocniczej podobnej do vfprintf w funkcji opakowania. Myślę, że kluczowym problemem jest to, że przekazywanie listy argumentów variadic bez ich interpretacji jest trudne. Możliwe jest przeprowadzenie formatowania (za pomocą funkcji pomocniczej podobnej do vfprintf: vsnprintf) i przesłanie sformatowanego wyjścia do zawiniętej funkcji z argumentami variadic (tj. Bez modyfikacji definicji zawiniętej funkcji). Więc zaczynamy:

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

Natrafiłem tutaj na to rozwiązanie .

Bert Regelink
źródło