为什么va_arg()会对x86_64和arm产生不同的影响?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了为什么va_arg()会对x86_64和arm产生不同的影响?相关的知识,希望对你有一定的参考价值。

代码:

#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!
");

    uint32_t t1 = 1;  
    uint32_t t2 = 4;  
    uint32_t t3 = 4;  
    printf("result:%f
", average(3, t1, t2, t3));

    return 0;
}

当我在ubuntu(x86_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_arg。但是为什么我们能够在x86_64中获得正确的结果呢?

谢谢。

答案

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

调用者碰巧将32位arg零扩展到64位寄存器中。 (这不是必需的;程序中未定义的行为可能会让你遇到一个不同的调用者,它在arg传递寄存器中留下了很多垃圾。)

被调用者(average())正在寻找三个64位args,并查看调用者放置它们的相同寄存器,因此它恰好起作用。


在32位ARM上,long long不适合单个寄存器,因此寻找long long args的被调用者肯定在不同的地方寻找,而不是调用者放置uint32_t args的位置。

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

(请注意,这可能会导致调用者的本地人在堆栈上损坏,因为被调用者被允许破坏堆栈args。)


有关完整的详细信息,请使用编译器+编译选项查看代码的asm输出,以查看源中C未定义行为的确切行为。 objdump -d ./helloworld应该做的伎俩,或直接看编译器输出:How to remove "noise" from GCC/clang assembly output?

另一答案

在我的系统上(x86_64)

#include <stdio.h>


int main(void)
{
    printf("%zu
", sizeof(long long int));

    return 0;
}

这打印8,告诉我long long int是64位宽,我不知道手臂上的long long int的大小。

无论你的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!
");

    uint32_t t1 = 1;  
    uint32_t t2 = 4;  
    uint32_t t3 = 4;  
    printf("result:%f
", average(3, t1, t2, t3));

    return 0;
}

这是可移植的,应该在不同的体系结构中产生相同的结果。

以上是关于为什么va_arg()会对x86_64和arm产生不同的影响?的主要内容,如果未能解决你的问题,请参考以下文章

警告“将架构 arm64 映射到 x86_64”是啥意思?

iOS 指令集arm64、armv7s、armv7、i386、x86_64

ARM64 固有的 x86_64 点向量积

无法为架构 arm64 和 x86_64 构建 opencv ios 框架

iOS中的armv7,armv7s,arm64,i386,x86_64

X86-64和ARM64用户栈的结构 ---目录