代码之家  ›  专栏  ›  技术社区  ›  lxgeek

为什么va\u arg()在x86\u 64和arm上产生不同的效果?

  •  0
  • lxgeek  · 技术社区  · 8 年前

    代码:

    #include <stdio.h>
    #include <stdarg.h>
    #include <stdlib.h>
    
    typedef unsigned int uint32_t;
    
    float average(int n_values, ... )
    {
        va_list var_arg; 
        int count;
        float sum = 0;
    
        va_start(var_arg, n_values);
    
        for (count = 0; count < n_values; count += 1) {
            sum += va_arg(var_arg, signed long long int);
        }   
    
        va_end(var_arg);
    
        return sum / n_values;
    }
    
    int main(int argc, char *argv[])
    {
        (void)argc;
        (void)argv;
    
        printf("hello world!\n");
    
        uint32_t t1 = 1;  
        uint32_t t2 = 4;  
        uint32_t t3 = 4;  
        printf("result:%f\n", average(3, t1, t2, t3));
    
        return 0;
    }
    

    当我在ubuntu(x86\u 64)中运行时,一切正常。

    lix@lix-VirtualBox:~/test/c$ ./a.out 
    hello world!
    result:3.000000
    lix@lix-VirtualBox:~/test/c$ uname -a
    Linux lix-VirtualBox 4.4.0-116-generic #140-Ubuntu SMP Mon Feb 12 21:23:04 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
    lix@lix-VirtualBox:~/test/c$ 
    

    但当我交叉编译并在openwrt(ARM 32位)中运行它时,它是错误的。

    [root@OneCloud_0723:/root/lx]#./helloworld 
    hello world!
    result:13952062464.000000
    [root@OneCloud_0723:/root/lx]#uname -a
    Linux OneCloud_0723 3.10.33 #1 SMP PREEMPT Thu Nov 2 19:55:17 CST 2017 armv7l GNU/Linux
    

    我知道不要用类型不正确的参数调用va\u arg。但为什么我们可以在x86\u 64中获得正确的结果,而不是在arm中?

    非常感谢。

    2 回复  |  直到 8 年前
        1
  •  7
  •   Peter Cordes    8 年前

    在x86-64 Linux上,每个32位参数在单独的64位寄存器中传递 (因为这正是x86-64 System V调用约定所要求的)。

    调用方碰巧有零将32位参数扩展到64位寄存器中。(这不是必需的;程序中未定义的行为可能会让另一个调用者在arg传递寄存器中留下高垃圾。)

    被叫方( average() )正在查找三个64位参数,并在调用者放置它们的相同寄存器中查找,因此它正好起作用。


    在32位ARM上, long long is不适合单个寄存器 ,因此被叫人正在寻找 长的,长的 args的位置肯定与调用方放置的位置不同 uint32_t args。

    被叫方看到的第一个64位参数可能是 ((long long)t1<<32) | t2 ,或者反过来。但由于被叫方正在寻找6x 32位的参数,因此它将查看调用者根本不打算作为参数的寄存器/内存。

    (请注意,这可能会导致调用方在堆栈上的局部变量损坏,因为被调用方被允许删除堆栈参数。)


    有关完整的详细信息,请使用编译器+编译选项查看代码的asm输出,以了解源代码中的C未定义行为到底导致了什么行为。 objdump -d ./helloworld 应该做到这一点,或者直接查看编译器输出: How to remove "noise" from GCC/clang assembly output? .

        2
  •  1
  •   Pablo    8 年前

    在我的系统上(x86\u 64)

    #include <stdio.h>
    
    
    int main(void)
    {
        printf("%zu\n", sizeof(long long int));
    
        return 0;
    }
    

    这印的是8,这告诉我 long long int 64位宽,我不知道 a的大小 双长整型 在手臂上。

    无论您的 va_arg 呼叫错误,您必须使用正确的类型,在 这个案子 uint32 ,因此您的函数具有未定义的行为,并且 正确的值。 average 应如下所示:

    float average(int n_values, ... )
    {
        va_list var_arg; 
        int count;
        float sum = 0;
    
        va_start(var_arg, n_values);
    
        for (count = 0; count < n_values; count += 1) {
            sum += va_arg(var_arg, uint32_t);
        }   
    
        va_end(var_arg);
    
        return sum / n_values;
    }
    

    也不要申报您的 uint32_t

    typedef unsigned int uint32_t;
    

    这是不可移植的,因为 int 不保证长度为4字节 所有体系结构。标准C库实际上在 stdint.h ,则应使用thos类型。

    因此,您的程序应该如下所示:

    #include <stdio.h>
    #include <stdarg.h>
    #include <stdlib.h>
    #include <stdint.h>
    
    float average(int n_values, ... )
    {
        va_list var_arg; 
        int count;
        float sum = 0;
    
        va_start(var_arg, n_values);
    
        for (count = 0; count < n_values; count += 1) {
            sum += va_arg(var_arg, uint32_t);
        }   
    
        va_end(var_arg);
    
        return sum / n_values;
    }
    
    int main(void)
    {
        printf("hello world!\n");
    
        uint32_t t1 = 1;  
        uint32_t t2 = 4;  
        uint32_t t3 = 4;  
        printf("result:%f\n", average(3, t1, t2, t3));
    
        return 0;
    }
    

    这是可移植的,在不同的 架构。