代码之家  ›  专栏  ›  技术社区  ›  Jared Oberhaus

为什么在Windows x64上,此可变函数在第四个参数上失败?

  •  3
  • Jared Oberhaus  · 技术社区  · 17 年前

    下面是包含可变函数和对可变函数调用的代码。我希望它能适当地输出每个数字序列。它在编译为32位可执行文件时会执行,但在编译为64位可执行文件时不会执行。

    #include <stdarg.h>
    #include <stdio.h>
    
    #ifdef _WIN32
    #define SIZE_T_FMT "%Iu"
    #else
    #define SIZE_T_FMT "%zu"
    #endif
    
    
    static void dumpargs(size_t count, ...) {
    
        size_t i;
        va_list args;
    
        printf("dumpargs: argument count: " SIZE_T_FMT "\n", count);
    
        va_start(args, count);
    
        for (i = 0; i < count; i++) {
    
            size_t val = va_arg(args, size_t);
            printf("Value=" SIZE_T_FMT "\n", val);
        }
        va_end(args);
    }
    
    int main(int argc, char** argv) {
    
        (void)argc;
        (void)argv;
    
        dumpargs(1, 10);
        dumpargs(2, 10, 20);
        dumpargs(3, 10, 20, 30);
        dumpargs(4, 10, 20, 30, 40);
        dumpargs(5, 10, 20, 30, 40, 50);
    
        return 0;
    }
    

    dumpargs: argument count: 1
    Value=10
    dumpargs: argument count: 2
    Value=10
    Value=20
    dumpargs: argument count: 3
    Value=10
    Value=20
    Value=30
    dumpargs: argument count: 4
    Value=10
    Value=20
    Value=30
    Value=14757395255531667496
    dumpargs: argument count: 5
    Value=10
    Value=20
    Value=30
    Value=14757395255531667496
    Value=14757395255531667506
    

    编辑:

    size_t 这是因为在现实世界中,它是用于接受指针和长度列表的变量函数的。当然,长度参数应该是一个 尺寸 . 在某些情况下,调用者可能会传递已知长度的内容:

    void myfunc(size_t pairs, ...) {
        va_list args;
        va_start(args, count);
    
        for (i = 0; i < pairs; i++) {
            const void* ptr = va_arg(args, const void*);
            size_t len = va_arg(args, size_t);
            process(ptr, len);
        }
        va_end(args);
    }
    
    void user(void) {
        myfunc(2, ptr1, ptr1_len, ptr2, 4);
    }
    

    请注意 4 传入 myfunc sizeof 或者是 strlen 4. 变成 尺寸 在某处但问题是编译器没有捕捉到这一点(变量函数的常见危险)。

    在这里正确的做法是消除可变功能,并用提供类型安全性的更好机制取代它。然而,我想记录这个问题,并收集更详细的信息,确切地解释为什么这个问题存在于这个平台上,以及它的表现形式。

    5 回复  |  直到 17 年前
        1
  •  6
  •   Evan Teran    17 年前

    因此,基本上,如果函数是可变的,它必须符合某种调用约定(最重要的是,调用者必须清除arg,而不是callie,因为callie不知道会有多少arg)。

    它之所以在4号开始发生是因为 calling convention used on x86-64 . 据我所知,Visual C++和GCC都使用第一个参数的寄存器,然后使用堆栈。

    我猜即使是可变函数也是如此(这让我觉得很奇怪,因为它会使va_*宏更复杂)。

    在x86上,标准的C调用约定是始终使用堆栈。

        2
  •  4
  •   JaredPar    17 年前

    问题是您正在使用size\t来表示值的类型。这是不正确的,这些值实际上是Win64上的正常32位值。

    Size_t应仅用于根据平台的32位或64位大小更改大小的值(如指针)。将代码更改为使用int或_int32,这将解决您的问题。

        3
  •  2
  •   RBerteig Keith Adler    17 年前

    一个变量函数只有弱类型检验。特别是,函数签名并没有提供足够的信息让编译器知道函数假定的每个参数的类型。

    在本例中,大小\u t在Win32上为32位,在Win32上为64位。它必须像那样改变大小才能执行其定义的角色。因此,对于一个变量函数来说,要正确地提取类型为size\u t的参数,调用方必须确保编译器能够在调用模块的编译时判断出该参数是该类型的。

    10 是类型的常数 int . 没有定义的后缀字母将常量标记为size\u t类型。您可以将这一事实隐藏在特定于平台的宏中,但这并不比编写宏更清楚 (size_z)10 在呼叫站点。

    部分原因在于Win64中使用的实际调用约定,它似乎可以工作。从给出的示例中,我们可以看出函数的前四个整型参数是在寄存器中传递的,其余的则在堆栈中传递。这允许正确读取计数和前三个可变参数。

    然而,它只是 在其他平台上,任何事情都可能发生。

    在一些已知接口的情况下,可以警告类型不匹配。例如,gcc通常可以识别 printf() 与格式字符串不匹配,并发出警告。但在一般情况下,对于所有可变函数,这样做是非常困难的 坚固的 .

        4
  •  2
  •   Jared Oberhaus    17 年前

    原因是 size_t 在32位窗口上定义为32位值,在64位窗口上定义为64位值。当第四个参数传递到可变函数时,高位似乎未初始化。拉出的第四个和第五个值实际上是:

    Value=0xcccccccc00000028
    Value=0xcccccccc00000032
    

    我可以通过对所有参数进行简单转换来解决此问题,例如:

    dumpargs(5, (size_t)10, (size_t)20, (size_t)30, (size_t)40, (size_t)50);
    

    • 如何以类型安全的便携方式避免这种情况?
    • 在其他64位平台上是否会发生这种情况,使用64位值(忽略某些64位平台上的大小可能为32位)?
    • 我是否应该将值作为32位值取出,而不考虑目标平台,如果将64位值推入变量函数,是否会导致问题?
    • 标准对这种行为有何规定?

    编辑:

    The Standard ,但这是不可能的,而且要花钱 purchase and download . 因此,我认为引用它会侵犯版权。

    参考 comp.lang.c FAQ ,很明显,当编写一个函数时 variable number of arguments ,对于类型安全,您无能为力。这取决于打电话的人 make sure that each argument either perfectly matches or is explicitly cast

    对于那些理解C和C的人来说,这一点应该是显而易见的 printf (请注意,gcc有一个 check printf-style format strings

        5
  •  1
  •   HUAGHAGUAH    17 年前

    如果您是编写此函数的人,那么您的工作就是正确编写可变函数和/或正确记录函数的调用约定。

    您已经发现C对类型的处理既快又松(另请参见signedness和promotion),因此显式转换是最明显的解决方案。这在使用UL或ULL之类的东西显式定义整数常量时常见。

    最好的做法是大量记录、执行代码审查和/或编写单元测试,并记住这个bug。