ส่งต่อการเรียกใช้ฟังก์ชัน Variadic ใน C


189

ใน C เป็นไปได้ไหมที่จะส่งต่อการเรียกใช้ฟังก์ชัน Variadic ในขณะที่

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

การส่งต่อการเรียกใช้ในลักษณะข้างต้นไม่จำเป็นอย่างชัดเจนในกรณีนี้ (เนื่องจากคุณสามารถเข้าสู่ระบบการร้องขอในรูปแบบอื่นหรือใช้ vfprintf) แต่รหัสฐานที่ฉันกำลังทำงานอยู่นั้นต้องใช้กระดาษห่อเพื่อทำงานจริงและไม่จำเป็น ไม่มี (และไม่สามารถเพิ่มได้) ฟังก์ชันตัวช่วยคล้ายกับ vfprintf

[อัปเดต: ดูเหมือนจะมีความสับสนตามคำตอบที่ให้มาจนถึงตอนนี้ ในการตั้งคำถามด้วยวิธีอื่น: โดยทั่วไปคุณสามารถรวมฟังก์ชัน Variadic ตามใจชอบได้โดยไม่ต้องแก้ไขนิยามของฟังก์ชันนั้น]


1
คำถามที่น่ากลัว - โชคดีที่ฉันสามารถเพิ่มการโอเวอร์โหลด vFunc ที่ใช้ va_list ขอบคุณสำหรับการโพสต์
Gishu

คำตอบ:


157

หากคุณไม่ได้มีความคล้ายคลึงกับฟังก์ชั่นvfprintfที่ใช้va_listแทนจำนวนตัวแปรของการขัดแย้งที่คุณไม่สามารถทำมันได้ ดูhttp://c-faq.com/varargs/handoff.html

ตัวอย่าง:

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

5
ขึ้นอยู่กับความสำคัญของปัญหาการร้องขอโดยชุดประกอบแบบอินไลน์ยังคงมีความเป็นไปได้นี้
ฆา

60

ไม่ใช่โดยตรง แต่เป็นเรื่องปกติ (และคุณจะพบเกือบทุกกรณีในไลบรารีมาตรฐาน) เพื่อให้ฟังก์ชัน Variadic มาคู่กับvarargsฟังก์ชันทางเลือกสไตล์ เช่นprintf/vprintf

ฟังก์ชั่น v ... ใช้พารามิเตอร์ va_list การดำเนินการซึ่งมักจะทำกับ 'เมจิกมาโคร' ของคอมไพเลอร์ แต่คุณรับประกันได้ว่าการเรียกใช้ฟังก์ชัน v ... สไตล์จากฟังก์ชัน Variadic เช่นนี้จะทำงาน:

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

สิ่งนี้จะให้ผลที่คุณต้องการ

หากคุณกำลังพิจารณาที่จะเขียนฟังก์ชั่นไลบรารี Variadic คุณควรพิจารณาที่จะทำให้สหายสไตล์ va_list พร้อมใช้งานเป็นส่วนหนึ่งของไลบรารี อย่างที่คุณเห็นจากคำถามของคุณมันสามารถพิสูจน์ได้ว่ามีประโยชน์สำหรับผู้ใช้ของคุณ


ฉันค่อนข้างแน่ใจว่าคุณต้องโทรva_copyก่อนส่งmyargsตัวแปรไปยังฟังก์ชันอื่น โปรดดูMSC39-Cซึ่งระบุว่าสิ่งที่คุณกำลังทำคือพฤติกรรมที่ไม่ได้กำหนด
aviggiano

2
@aviggiano: กฎคือ "อย่าโทรหาva_arg()สิ่งva_listที่มีค่าไม่แน่นอน" ฉันไม่ทำเพราะฉันไม่เคยใช้va_argฟังก์ชั่นการโทรของฉัน ค่าของmyargsหลังจากที่โทร (ในกรณีนี้) จะvprintfเป็นไม่แน่นอน (สมมติว่ามันไม่ให้เราva_arg) มาตรฐานบอกว่าmyargs"จะถูกส่งผ่านไปยังva_endมาโครก่อนที่จะมีการอ้างอิงเพิ่มเติมไปยัง [มัน]"; ซึ่งเป็นสิ่งที่ฉันทำ ฉันไม่จำเป็นต้องคัดลอกเนื้อหานั้นเพราะฉันไม่ต้องการวนซ้ำในฟังก์ชั่นการโทร
CB Bailey

1
คุณพูดถูก คนลินุกซ์ยืนยันหน้านั้น หากapถูกส่งไปยังฟังก์ชันที่ใช้va_arg(ap,type)แล้วค่าapจะไม่ถูกกำหนดหลังจากส่งคืนของฟังก์ชันนั้น
aviggiano

48

C99 รองรับมาโครที่มีอากิวเมนต์ variadic ; ขึ้นอยู่กับคอมไพเลอร์ของคุณคุณอาจประกาศแมโครที่ทำสิ่งที่คุณต้องการ:

#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รุ่นมากกว่าธรรมดาไม่มีทางที่จะเรียกฟังก์ชัน variadic va_listในสถานการณ์เช่นนี้ได้โดยตรงโดยไม่ต้องใช้


1
ขอบคุณ แต่จากคำถาม: "codebase ที่ฉันใช้อยู่ [... ] ไม่มี (และไม่สามารถเพิ่มได้) ฟังก์ชันตัวช่วยคล้ายกับ vfprintf"
Patrick

11

เนื่องจากเป็นไปไม่ได้ที่จะโอนสายดังกล่าวไปในทางที่ดีเราจึงทำการแก้ไขโดยการตั้งค่าสแต็กเฟรมใหม่พร้อมสำเนาของสแต็กเฟรมดั้งเดิม อย่างไรก็ตามเรื่องนี้ไม่สามารถอธิบายได้สูงและทำให้สมมติฐานทุกประเภทเช่นรหัสใช้พอยน์เตอร์เฟรมและการประชุมแบบ 'มาตรฐาน'

ไฟล์ส่วนหัวนี้อนุญาตให้มีการตัดฟังก์ชั่น Variadic สำหรับ x86_64 และ i386 (GCC) มันไม่สามารถใช้งานได้กับการโต้แย้งแบบ floating-point แต่ควรตรงไปข้างหน้าเพื่อขยายสำหรับการสนับสนุนเหล่านั้น

#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
ขอบคุณ แต่จากคำถาม: "codebase ที่ฉันใช้อยู่ [... ] ไม่มี (และไม่สามารถเพิ่มได้) ฟังก์ชันตัวช่วยคล้ายกับ vfprintf"
Patrick

2

ไม่มีวิธีที่จะส่งต่อการเรียกใช้ฟังก์ชันดังกล่าวได้เนื่องจากมีเพียงตำแหน่งเดียวที่คุณสามารถดึงองค์ประกอบสแต็กแบบดิบmy_print()ได้ วิธีปกติในการตัดการเรียกเช่นนั้นก็คือมีสองฟังก์ชั่นหนึ่งที่เพิ่งแปลงอาร์กิวเมนต์เป็นโครงสร้างต่าง ๆvarargsและอีกอันที่ทำงานกับโครงสร้างเหล่านั้น เช่นการใช้รูปแบบที่สองครั้งที่ฟังก์ชั่นที่คุณสามารถ (ตัวอย่าง) ห่อprintf()โดยการเริ่มต้น structs ในmy_printf()ด้วยแล้วผ่านไปยังva_start()vfprintf()


ดังนั้นจึงไม่มีวิธีที่จะส่งต่อการเรียกร้องถ้าคุณไม่สามารถปรับใช้งานของฟังก์ชั่นที่คุณต้องการห่อ?
แพทริค

ขวา. ทางออกที่ดีที่สุดของคุณคือการเพิ่มเสื้อคลุมสไตล์ vprintf ที่จะเพิ่ม
John Millikin

1

ใช่คุณสามารถทำได้ แต่มันค่อนข้างน่าเกลียดและคุณต้องรู้จำนวนอาร์กิวเมนต์สูงสุด นอกจากนี้หากคุณอยู่ในสถาปัตยกรรมที่อาร์กิวเมนต์ไม่ถูกส่งผ่านบนสแต็กเช่น x86 (เช่น PowerPC) คุณจะต้องทราบว่ามีการใช้ประเภท "พิเศษ" (double, float, altivec เป็นต้น) หรือไม่ ดังนั้นจัดการกับพวกเขาตาม อาจเจ็บปวดได้อย่างรวดเร็ว แต่ถ้าคุณใช้ x86 หรือถ้าฟังก์ชั่นดั้งเดิมมีขอบเขตชัดเจนและ จำกัด ก็สามารถใช้งานได้ มันจะยังคงเป็นแฮ็คใช้สำหรับการดีบัก อย่าสร้างซอฟต์แวร์ให้กับคุณ ต่อไปนี้เป็นตัวอย่างการทำงานใน 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 บอกว่ามันถูกแปลงเป็นสองเท่า แต่โปรแกรมขัดข้อง สิ่งนี้แสดงให้เห็นว่าโซลูชั่นนี้เป็นแฮ็กและไม่มีวิธีแก้ปัญหาทั่วไป ในตัวอย่างของฉันฉันคิดว่าจำนวนการโต้แย้งสูงสุดคือ 8 แต่คุณสามารถเพิ่มจำนวนนั้นได้ ฟังก์ชั่นที่ห่อหุ้มยังใช้เลขจำนวนเต็มเท่านั้น แต่มันทำงานในลักษณะเดียวกันกับพารามิเตอร์ 'ปกติ' อื่น ๆ เนื่องจากมันส่งไปยังจำนวนเต็มเสมอ ฟังก์ชั่นเป้าหมายจะรู้ประเภทของมัน แต่ผู้ห่อหุ้มตัวกลางของคุณไม่จำเป็นต้องใช้ กระดาษห่อยังไม่จำเป็นต้องรู้จำนวนอาร์กิวเมนต์ที่เหมาะสมเนื่องจากฟังก์ชั่นเป้าหมายจะรู้ได้ด้วย หากต้องการทำงานที่มีประโยชน์ (ยกเว้นเพียงบันทึกการโทร) คุณอาจต้องรู้ทั้งคู่


1

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

มีข้อควรระวังในเอกสารประกอบที่อาจไม่ทำงานในสถานการณ์ที่ซับซ้อนมากขึ้น และคุณต้องฮาร์ดโค้ดขนาดสูงสุดสำหรับอาร์กิวเมนต์ (ที่นี่ฉันใช้ 1,000) แต่อาจเป็นทางเลือกที่สมเหตุสมผลกับวิธีการอื่น ๆ ที่เกี่ยวข้องกับการผ่าซ้อนในภาษา C หรือภาษาแอสเซมบลี


เป็นประโยชน์ ขอบคุณ!
rsmoorthy

0

มีสามตัวเลือกเป็นหลัก

หนึ่งคือการไม่ผ่านมัน แต่ใช้การใช้งาน Variadic ของฟังก์ชั่นเป้าหมายของคุณและไม่ส่งผ่านจุดไข่ปลา อีกอันหนึ่งคือการใช้มาโครแบบ variadic ตัวเลือกที่สามคือทุกสิ่งที่ฉันขาดหายไป

ฉันมักจะไปกับตัวเลือกที่หนึ่งเนื่องจากฉันรู้สึกว่ามันง่ายต่อการจัดการ ตัวเลือกที่สองมีข้อเสียเปรียบเนื่องจากมีข้อ จำกัด บางประการเกี่ยวกับการเรียกมาโครแบบแปรปรวน

นี่คือตัวอย่างรหัส:

#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

ไม่แน่ใจว่าสิ่งนี้ช่วยตอบคำถามของ OP หรือไม่เพราะฉันไม่รู้ว่าทำไมข้อ จำกัด ในการใช้ฟังก์ชันตัวช่วยจึงคล้ายกับ vfprintf ในฟังก์ชันตัวตัดคำ ฉันคิดว่าปัญหาสำคัญที่นี่คือการส่งต่อรายการอาร์กิวเมนต์ Variadic โดยไม่ต้องตีความพวกเขาเป็นเรื่องยาก สิ่งที่เป็นไปได้คือดำเนินการจัดรูปแบบ (โดยใช้ฟังก์ชั่นตัวช่วยคล้ายกับ 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;
}

ผมมาในการแก้ปัญหานี้ที่นี่

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.