在没有原型的文件中调用的函数在ARM和x86-64上产生不同的结果

时间:2017-11-07 22:07:39

标签: c arm x86-64

我们有3个文件:main.clib.hlib.c

main.c中:

#include <stdio.h>
#include <stdlib.h>

/* #include "lib.h" */

int main(void)
{
    printf("sizeof unsigned long long: %zu\n", sizeof(unsigned long long));
    printf("sizeof int: %zu\n", sizeof(int));
    unsigned long long slot = 0;
    int pon_off = 1;
    lib_fn(slot, pon_off);
    return EXIT_SUCCESS;
}

lib.h:

void lib_fn(unsigned slot, int pon_off);

lib.c:

#include <stdio.h>
#include <stdlib.h>

void lib_fn(unsigned slot, int pon_off)
{
    printf("slot: %d\n", slot);
    printf("pon_off: %d\n", pon_off);
    return;
}

编译:

gcc -O2 -Wall -Wextra main.c lib.c

在ARM上运行:

$ ./a.out
sizeof unsigned long long: 8
sizeof int: 4
slot: 0
pon_off: 0

在x86-64上运行:

$ ./a.out
sizeof unsigned long long: 8
sizeof int: 4
slot: 0
pon_off: 1

如您所见,pon_off在ARM上为0,在x86-64上为1。我猜它有 与参数大小相关的事件lib_fn()需要两个整数 长度为8个字节,单个long long长度为8个字节。

  1. 为什么pon_off在ARM和x86-64上的打印方式不同?

  2. 是否与调用约定有关?

3 个答案:

答案 0 :(得分:4)

  

它与调用约定有关吗?

是的,它有所有与调用约定/ ABI。

在x86-64上,&#34;自然&#34;函数参数的宽度是64位,而较窄的整数args仍然使用整个&#34; slot&#34;。 (首先6 integer/pointer args and first 8 FP args in registers (SysV)或前4个args(Windows),然后堆叠)。

在ARM上,寄存器宽度(&#34; arg slot&#34;堆栈上的最小宽度)是32位,64位整数args需要两个寄存器。

在32位x86(gcc -m32)上,您会看到与32位ARM相同的行为。在AArch64上,您会看到与x86-64相同的行为,因为它们的调用约定都是&#34; normal&#34;并且不要将单独的窄齿包装成单个寄存器。 (x86-64 System V确实将结构成员打包到最多2个寄存器中,而不是每个成员使用单独的寄存器!)

有一个最小的&#34; arg插槽&#34;等于寄存器大小的宽度几乎是通用的,无论args是在寄存器中还是在堆栈中传递。但这并不一定是int的宽度:AVR (8-bit RISC microcontroller) has 16-bit int which takes two registers, but char / uint8_t args can be passed in a single register.

根据原型中的类型,使用原型,更宽/更窄的类型将转换为被调用者期望的类型。显然一切都有效。

没有原型,调用中表达式的类型决定了arg的传递方式。 unsigned long long slot在ARM的调用约定中获取前2个arg传递寄存器,其中lib_fn期望找到其2个整数args。

(声称在没有原型的情况下所有内容都转换为int的答案是错误的。没有原型等同于int lib_fn(...);,但printf仍适用于double和{{ 1}}。请注意,int64_t在传递给可变函数时会隐式转换为float,就像将较窄的整数类型上转换为double一样,这就是为什么int } %f的格式,double没有格式,与扫描传递指针的扫描方式不同。这就是C的设计方式;没有理由但是无论如何,C要求更广泛的类型能够按原样传递给可变参数函数,并且所有调用约定都可以容纳它。)

BTW,其他破坏是可能的:某些实现对可变参数(因而是非原型)使用不同于正常函数的调用约定。

例如,在Windows上,您可以将一些编译器设置为默认为the _stdcall calling convention,其中被调用者从堆栈中弹出args。 (即在弹出返回地址后用floatret 8。但很明显这个调用约定不适用于可变函数,所以默认不适用于它们,并且他们会使用esp+=8或者调用者负责清理堆栈args的东西,因为只有调用者知道他们传递了多少个args。希望在这种模式下,编译器至少会警告隐式声明的函数是否错误,因为错误会导致崩溃(堆栈指向调用后的错误位置)。

让我们来看看这个案例的asm

有关阅读编译器asm输出的介绍,请参阅How to remove "noise" from GCC/clang assembly output?,特别是Matt Godbolt的CppCon2017讲话“What Has My Compiler Done for Me Lately? Unbolting the Compiler's Lid”

为了使asm尽可能简单,我删除了打印并将代码放在一个返回void的函数中。 (这允许tail-call optimization跳转到函数, it 返回给调用者。)编译器输出中的唯一指令是arg设置并跳转到lib_fn。

_cdecl

See the source+asm on the Godbolt compiler explorer,适用于ARM,x86-64和x86-32(#ifdef USE_PROTO void lib_fn(unsigned slot, int pon_off); #endif void foo(void) { unsigned long long slot = 0; int pon_off = 1; lib_fn(slot, pon_off); } ),含gcc 6.3 。 (我实际上复制了-m32并重命名了foo,因此在一个版本的调用者中没有原型,而不是每个架构都有2个独立的编译器窗口。在更复杂的情况下,这将是方便的因为你可以在编译器窗格之间进行区分。)

对于x86-64,输出与原型基本相同。如果没有,调用者必须将lib_fnusing xor eax,eax to zero the whole RAX)归零,以指示此可变函数调用未在XMM寄存器中传递FP args。 (在Windows调用约定中,你不会有这种情况,因为Windows约定针对可变函数进行了优化,并且以正常函数为代价实现它们的简单性。)

对于ARM:

al

lib_fn期待R0中的foo: @ no prototype mov r2, #1 @ pon_off mov r0, #0 @ slot low half mov r1, #0 @ slot high half b lib_fn_noproto bar: @ with proto, u long long is converted to unsigned according to C rules, like the callee expects mov r1, #1 mov r0, #0 b lib_fn 和R1中的slot

打破x86-64

如果您使用pon_off,则在x86-64上遇到同样的问题。

unsigned __int128

汇编为:

lib_fn_noproto((unsigned __int128)slot, pon_off);

以与32位ARM相同的方式打破的调用约定,其中64位arg占用前2个插槽。

答案 1 :(得分:1)

这是因为x64-86和ARM如何将参数传递给函数(正如Peter Cordes在他的评论中提到的那样)。

请比较ARMx64-86上生成的汇编:

  1. On ARM unsigned long long存储在2个寄存器中,int in 1存储在x86上,两者都存储在64位寄存器中。
  2. 在ARM上,函数在获取参数时,为每个参数读取单个寄存器,使第一个变量的高低部分分成2个参数。最后传递的第二个参数是省略的。在x64-86上,它仍然从这两个64位寄存器中获取值。
  3. 旁注:在x64-86上,只有少数起始函数参数由寄存器传递,如果有更多,则下一个参数存储在堆栈中。

答案 2 :(得分:0)

如果没有函数原型并且使用了隐式声明,编译器会假定所有参数都是int类型。

看起来int在arm和x64-86 architecutre上是不同的。

请注意,修饰符%d只能与int参数一起使用,对于无符号一次使用%u

这就是为什么有警告你。