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

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

main.c中:

 #include  #include  /* #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  #include  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()有两个整数, lib_fn() 8个字节,一个long long 8个字节。

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

  2. 它与调用约定有关吗?

它与调用约定有关吗?

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

在x86-64上,函数参数的“自然”宽度是64位,而较窄的整数args仍然使用整个“槽”。 ( 寄存器中的前6个整数/指针args和前8个FP args(SysV)或前4个args(Windows),然后堆栈)。

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

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

具有等于​​寄存器大小的最小“arg slot”宽度几乎是通用的,无论args是在寄存器中还是在堆栈中传递。 这不一定是int的宽度,但是: AVR(8位RISC微控制器)有16位int ,它有两个寄存器,但char / uint8_t args可以在一个寄存器中传递。


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

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

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


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

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


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

有关读取编译器asm输出的介绍,请参阅如何从GCC / clang组件输出中删除“noise”? ,特别是Matt Godbolt的CppCon2017演讲“我的编译器最近为我做了什么? 解开编译器的盖子“ 。

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

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

请参阅Godbolt编译器资源管理器中的source + asm ,适用于ARM,x86-64和x86-32( -m32 )以及gcc 6.3 。 (我实际上复制了foo lib_fn命名了lib_fn因此在一个版本的调用者中没有原型,而不是为每个架构设置2个单独的编译器窗口。在更复杂的情况下,这将是方便的,因为你可以在编译器窗格之间进行区分) 。

对于x86-64,输出基本上与原型相同/不同。 如果没有,调用者必须将al ( 使用xor eax,eax使整个RAX归零 )来指示此可变函数调用在XMM寄存器中没有传递FP args。 (在Windows调用约定中,您不会这样做,因为Windows约定针对可变参数函数进行了优化,并且以牺牲正常函数为代价来实现它们的简单性。)

对于ARM:

 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 

lib_fn期望R0中的slot和R1中的pon_off


打破x86-64

如果你使用unsigned __int128你在x86-64上会遇到同样的问题。

 lib_fn_noproto((unsigned __int128)slot, pon_off); 

编译为:

  mov edx, 1 # pon_off = EDX = 1 xor edi, edi # low half of slot = RDI = 0 xor esi, esi # high half of slot = RSI = 0 xor eax, eax # number of xmm register args = 0 jmp lib_fn_noproto 

它打破了调用约定的方式与32位ARM 完全相同 ,64位arg采用前2个插槽。

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

请比较ARM和x64-86上生成的程序集:

  1. 在ARM上,unsigned long long存储在2个寄存器中,int在1中,在x86上存储在64位寄存器中。
  2. 在ARM上,函数在获取参数时,为每个参数读取单个寄存器,使第一个变量的高低部分分成2个参数。 最后传递的第二个参数是省略的。 在x64-86上,它仍然从这两个64位寄存器获取值。

旁注:在x64-86上,只有少数启动函数参数由寄存器传递,如果有更多,则下一个参数存储在堆栈中。

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

看起来int在arm和x64-86 architecutre上有所不同。

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

这就是为什么有警告给你。