转发C中可变参数函数的调用


189

在C语言中,是否可以转发可变函数的调用?如

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

在这种情况下,显然不必严格按照上述方式转发调用(因为您可以通过其他方式记录调用或使用vfprintf),但是我正在使用的代码库要求包装程序执行一些实际的工作,并且没有(也没有增加)类似于vfprintf的辅助函数。

[更新:根据到目前为止提供的答案,似乎有些困惑。为了那句问题的另一种方式:在一般情况下,你可以用一些任意可变参数函数,而无需修改函数的定义。]


1
很棒的问题-幸运的是,我可以添加一个带va_list的vFunc重载。感谢您的发布。
Gishu 2009年

Answers:



60

不直接,但是可变参数函数与varargs样式替代函数成对出现是很普遍的(您会在标准库中普遍看到这种情况)。例如printf/vprintf

v ...函数采用va_list参数,该参数的实现通常是通过编译器特定的“宏魔术”完成的,但可以确保从像这样的可变参数函数中调用v ... style函数将起作用:

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

这应该给您想要的效果。

如果您正在考虑编写可变参数的库函数,则还应该考虑使va_list样式的同伴作为库的一部分可用。从您的问题可以看出,它对您的用户非常有用。


我很确定您需要va_copy在将myargs变量传递给另一个函数之前进行调用。请参阅MSC39-C,其中指出您在做什么是未定义的行为。
aviggiano '16

2
@aviggiano:规则是“不要叫va_arg()va_list,我不这样做,因为我从来没有使用有一个不确定的值” va_arg在我的调用函数。该myargs调用(在这种情况下),之后vprintf不确定的(假设它确实我们va_arg)。该标准规定:myargs“应在va_end进一步引用[it]之前将其传递给宏”;这正是我的工作。我不需要复制该args,因为我无意在调用函数中对其进行迭代。
CB Bailey

1
你是对的。在Linux手册页确认。如果ap将传递给使用的函数,va_arg(ap,type)则该ap函数返回后,的值不确定。
aviggiano '16

47

C99支持具有可变参数的宏 ; 根据您的编译器,您也许可以声明一个可以执行所需操作的宏:

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

通常,最好的解决方案是使用要包装的函数的va_list形式(如果存在)。


12

几乎可以使用以下设施<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;
}

请注意,您将需要使用vprintf版本而不是普通版本printf。在这种情况下,没有一种无需使用即可直接调用可变参数函数的方法va_list


1
谢谢,但是来自一个问题:“我正在使用的代码库没有(也不能添加)类似于vfprintf的帮助器函数。”
帕特里克

11

由于实际上不可能以一种很好的方式转发此类调用,因此我们通过设置带有原始堆栈框架副本的新堆栈框架来解决此问题。但是,这是非常不可移植的,并且会做出各种假设,例如,代码使用帧指针和“标准”调用约定。

此头文件允许包装x86_64和i386(GCC)的可变参数函数。它不适用于浮点参数,但应直接扩展以支持这些参数。

#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

最后,您可以像这样包装调用:

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

使用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
谢谢,但是来自一个问题:“我正在使用的代码库没有(也不能添加)类似于vfprintf的帮助器函数。”
帕特里克

2

无法转发此类函数调用,因为您可以检索原始堆栈元素的唯一位置是my_print()。包装这样的调用的通常方法是具有两个函数,一个函数仅将参数转换为各种varargs结构,另一个函数实际上对这些结构进行操作。使用这种双功能模式,您可以(例如)包裹printf()在初始化结构my_printf()va_start(),然后将它们传递给vfprintf()


因此,如果您无法修改要包装的函数的实现,就无法转发调用?
帕特里克

对。最好的选择是推动添加vprintf样式的包装器。
约翰·米利金

1

是的,您可以执行此操作,但这有点难看,您必须知道最大数量的参数。此外,如果您所处的体系结构没有像x86那样在堆栈上传递参数(例如PowerPC),则必须知道是否使用了“特殊”类型(双精度,浮点型,altivec等),以及是否因此,请相应地处理它们。可能很快就会很痛苦,但是如果您使用的是x86,或者原始功能的边界明确且受限制,则可以正常使用。 它仍然是一个hack,用于调试目的。不要围绕此构建软件。无论如何,这是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;
}

由于某些原因,您不能将浮点数与va_arg一起使用,gcc表示它们会转换为double值,但程序会崩溃。仅此一点就表明该解决方案是黑客,并且没有通用解决方案。在我的示例中,我假设参数的最大数量为8,但是您可以增加该数量。包装函数也只使用整数,但是它与其他“普通”参数的工作方式相同,因为它们始终转换为整数。目标函数将知道它们的类型,但是您的中间包装器不需要。包装程序也不需要知道正确数量的参数,因为目标函数也可以知道。为了做有用的工作(除了仅记录呼叫),您可能必须同时知道两者。


0

本质上有三种选择。

一种是不传递它,而是使用目标函数的可变参数实现,而不传递椭圆。另一种是使用可变参数宏。第三种选择是我所缺少的所有东西。

我通常选择第一种方法,因为我觉得这很容易处理。选项二有一个缺点,因为调用可变参数宏有一些限制。

这是一些示例代码:

#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

最好的方法是

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

gcc提供了可以做到这一点的扩展:__builtin_apply和亲戚。请参见gcc手册中的构造函数调用

一个例子:

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

试一下Godbolt

文档中有一些注意事项,它可能在更复杂的情况下不起作用。而且您必须对参数的最大大小进行硬编码(这里我使用了1000)。但这可能是其他以C或汇编语言剖析堆栈的方法的合理选择。


0

不确定这是否有助于回答OP的问题,因为我不知道为什么在包装器函数中使用类似于vfprintf的辅助函数的限制适用。我认为这里的关键问题是转发变体参数列表而不解释它们是困难的。可能的是执行格式化(使用类似于vfprintf:vsnprintf的辅助函数),然后使用可变参数将格式化后的输出转发给包装函数(即不修改包装函数的定义)。所以,我们开始:

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

我在这里遇到了这种解决方案。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.