Meneruskan permohonan fungsi variadik di C

189

Dalam C, apakah mungkin untuk meneruskan permohonan fungsi variadic? Seperti dalam,

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

Meneruskan permohonan dengan cara di atas jelas tidak sepenuhnya diperlukan dalam kasus ini (karena Anda dapat mencatat doa dengan cara lain, atau menggunakan vfprintf), tetapi basis kode yang saya kerjakan memerlukan pembungkus untuk melakukan beberapa pekerjaan aktual, dan tidak tidak memiliki (dan tidak bisa menambahkan) fungsi pembantu seperti vfprintf.

[Pembaruan: tampaknya ada beberapa kebingungan berdasarkan jawaban yang telah diberikan sejauh ini. Untuk mengungkapkan pertanyaan dengan cara lain: secara umum, dapatkah Anda membungkus beberapa fungsi variad yang sewenang-wenang tanpa mengubah definisi fungsi itu .]

Patrick
sumber
1
pertanyaan yang luar biasa - untungnya saya bisa menambahkan kelebihan vFunc yang membutuhkan va_list. Terima kasih sudah memposting.
Gishu

Jawaban:

157

Jika Anda tidak memiliki fungsi analog dengan vfprintfyang mengambil va_listalih - alih sejumlah variabel argumen, Anda tidak bisa melakukannya . Lihathttp://c-faq.com/varargs/handoff.html .

Contoh:

void myfun(const char *fmt, va_list argp) {
    vfprintf(stderr, fmt, argp);
}
Adam Rosenfield
sumber
5
Bergantung pada seberapa penting masalah ini, permohonan oleh inline assembly masih menawarkan kemungkinan ini.
filip
60

Tidak secara langsung, namun itu umum (dan Anda akan menemukan hampir secara universal kasus di perpustakaan standar) untuk fungsi variadic berpasangan dengan varargsfungsi alternatif gaya. misalnya printf/vprintf

Fungsi v ... mengambil parameter va_list, implementasi yang sering dilakukan dengan 'makro makro' kompiler khusus, tetapi Anda dijamin bahwa memanggil fungsi v ... style dari fungsi variadic seperti ini akan berfungsi:

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

Ini akan memberi Anda efek yang Anda cari.

Jika Anda mempertimbangkan untuk menulis fungsi pustaka variad, Anda juga harus mempertimbangkan membuat pendamping gaya va_list tersedia sebagai bagian dari pustaka. Seperti yang dapat Anda lihat dari pertanyaan Anda, itu bisa terbukti bermanfaat bagi pengguna Anda.

CB Bailey
sumber
Saya cukup yakin Anda perlu menelepon va_copysebelum meneruskan myargsvariabel ke fungsi lain. Silakan lihat MSC39-C , di mana ia menyatakan bahwa apa yang Anda lakukan adalah perilaku yang tidak terdefinisi.
aviggiano
2
@aviggiano: Aturannya adalah "Jangan panggil va_arg()pada va_listyang memiliki nilai tak tentu", saya tidak melakukan itu karena saya tidak pernah menggunakan va_argfungsi memanggil saya. The nilai dari myargssetelah panggilan (dalam hal ini) untuk vprintfyang tak tentu (dengan asumsi bahwa hal itu kami va_arg). Standar mengatakan bahwa myargs"harus diteruskan ke va_endmakro sebelum referensi lebih lanjut ke [itu]"; itulah yang saya lakukan. Saya tidak perlu menyalin argumen itu karena saya tidak punya niat untuk mengulanginya melalui mereka dalam fungsi panggilan.
CB Bailey
1
Kamu benar. The man Linux halaman menegaskan bahwa. Jika apdilewatkan ke fungsi yang menggunakan va_arg(ap,type)maka nilai aptidak ditentukan setelah kembalinya fungsi itu.
aviggiano
48

C99 mendukung makro dengan argumen variadik ; tergantung pada kompiler Anda, Anda mungkin dapat mendeklarasikan makro yang melakukan apa yang Anda inginkan:

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

Namun, secara umum, solusi terbaik adalah menggunakan bentuk va_list dari fungsi yang Anda coba bungkus, jika ada.

Commodore Jaeger
sumber
12

Hampir, menggunakan fasilitas yang tersedia di <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;
}

Perhatikan bahwa Anda perlu menggunakan vprintfversi daripada sekadar printf. Tidak ada cara untuk langsung memanggil fungsi variadic dalam situasi ini tanpa menggunakan va_list.

Greg Hewgill
sumber
1
Terima kasih, tetapi dari pertanyaan: "basis kode yang saya kerjakan [...] tidak memiliki (dan tidak bisa menambahkan) fungsi pembantu seperti vfprintf."
Patrick
11

Karena tidak mungkin untuk meneruskan panggilan semacam itu dengan cara yang baik, kami mengatasinya dengan menyiapkan bingkai tumpukan baru dengan salinan bingkai tumpukan asli. Namun ini sangat tidak dapat diport dan membuat semua jenis asumsi , misalnya bahwa kode menggunakan frame pointer dan konvensi pemanggilan 'standar'.

File header ini memungkinkan untuk membungkus fungsi variadic untuk x86_64 dan i386 (GCC). Itu tidak bekerja untuk argumen floating-point, tetapi harus lurus ke depan untuk memperpanjang untuk mendukung mereka.

#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

Pada akhirnya Anda dapat menjawab panggilan seperti ini:

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
sumber
3

Gunakan 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
sumber
1
Terima kasih, tetapi dari pertanyaan: "basis kode yang saya kerjakan [...] tidak memiliki (dan tidak bisa menambahkan) fungsi pembantu seperti vfprintf."
Patrick
2

Tidak ada cara untuk meneruskan panggilan fungsi tersebut karena satu-satunya lokasi di mana Anda dapat mengambil elemen tumpukan mentah my_print(). Cara biasa untuk membungkus panggilan seperti itu adalah memiliki dua fungsi, yang hanya mengubah argumen menjadi berbagai varargsstruct, dan yang lain yang benar-benar beroperasi pada struct tersebut. Menggunakan model fungsi ganda, Anda dapat (misalnya) membungkus printf()dengan menginisialisasi struct my_printf()dengan va_start(), dan kemudian meneruskannya vfprintf().

John Millikin
sumber
Jadi tidak ada cara untuk meneruskan doa jika Anda tidak dapat memodifikasi implementasi fungsi yang ingin Anda bungkus?
Patrick
Baik. Taruhan terbaik Anda adalah mendorong pembungkus gaya vprintf untuk ditambahkan.
John Millikin
1

Ya, Anda bisa melakukannya, tetapi itu agak jelek dan Anda harus tahu jumlah maksimal argumen. Lebih jauh lagi jika Anda menggunakan arsitektur di mana argumen tidak diteruskan pada stack seperti x86 (misalnya, PowerPC), Anda harus tahu apakah tipe "spesial" (double, float, altivec, dll.) Digunakan dan jika jadi, hadapi mereka sesuai. Ini bisa terasa menyakitkan dengan cepat tetapi jika Anda menggunakan x86 atau jika fungsi asli memiliki batas yang didefinisikan dengan baik dan terbatas, itu bisa bekerja. Itu masih akan menjadi hack , gunakan untuk tujuan debugging. Jangan membangun perangkat lunak Anda di sekitar itu. Bagaimanapun, berikut ini contoh yang berfungsi pada 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;
}

Untuk beberapa alasan, Anda tidak dapat menggunakan float dengan va_arg, gcc mengatakan mereka dikonversi menjadi dua kali lipat tetapi program macet. Itu saja menunjukkan bahwa solusi ini adalah peretasan dan tidak ada solusi umum. Dalam contoh saya, saya berasumsi bahwa jumlah maksimum argumen adalah 8, tetapi Anda dapat meningkatkan jumlah itu. Fungsi yang dibungkus juga hanya menggunakan bilangan bulat tetapi berfungsi dengan cara yang sama dengan parameter 'normal' lainnya karena selalu dilemparkan ke bilangan bulat. Fungsi target akan tahu tipenya tetapi pembungkus perantara Anda tidak perlu. Wrapper juga tidak perlu tahu jumlah argumen yang tepat karena fungsi target juga akan mengetahuinya. Untuk melakukan pekerjaan yang bermanfaat (kecuali hanya mencatat panggilan), Anda mungkin harus mengetahui keduanya.

pengguna10146
sumber
1

gcc menawarkan ekstensi yang dapat melakukan ini: __builtin_applydan kerabat. Lihat Membuat Panggilan Fungsi di manual gcc.

Sebuah contoh:

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

Cobalah di godbolt

Ada beberapa peringatan dalam dokumentasi yang mungkin tidak berfungsi dalam situasi yang lebih rumit. Dan Anda harus menuliskan kode ukuran maksimum untuk argumen (di sini saya menggunakan 1000). Tapi itu bisa menjadi alternatif yang masuk akal untuk pendekatan lain yang melibatkan membedah tumpukan dalam bahasa C atau bahasa assembly.

Nate Eldredge
sumber
Bermanfaat. Terima kasih!
rsmoorthy
0

Pada dasarnya ada tiga opsi.

Pertama adalah untuk tidak meneruskannya tetapi untuk menggunakan implementasi variadik dari fungsi target Anda dan tidak meneruskan elips. Yang lain adalah menggunakan makro variadic. Opsi ketiga adalah semua hal yang saya lewatkan.

Saya biasanya memilih opsi satu karena saya merasa ini sangat mudah ditangani. Opsi dua memiliki kelemahan karena ada beberapa batasan untuk memanggil macro variadic.

Berikut ini beberapa contoh kode:

#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
sumber
0

Cara terbaik untuk melakukan ini adalah

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
sumber
0

Tidak yakin apakah ini membantu menjawab pertanyaan OP karena saya tidak tahu mengapa pembatasan untuk menggunakan fungsi pembantu seperti vfprintf dalam fungsi wrapper berlaku. Saya pikir masalah utama di sini adalah meneruskan daftar argumen variadik tanpa menafsirkannya sulit. Apa yang mungkin, adalah untuk melakukan pemformatan (menggunakan fungsi pembantu seperti vfprintf: vsnprintf) dan meneruskan output yang diformat ke fungsi yang dibungkus dengan argumen variadic (yaitu tidak mengubah definisi fungsi yang dibungkus). Jadi, ini dia:

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

Saya menemukan solusi ini di sini .

Bert Regelink
sumber