我们有3个文件:main.c
,lib.h
和lib.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个字节。
为什么pon_off
在ARM和x86-64上的打印方式不同?
是否与调用约定有关?
答案 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。 (即在弹出返回地址后用float
做ret 8
。但很明显这个调用约定不适用于可变函数,所以默认不适用于它们,并且他们会使用esp+=8
或者调用者负责清理堆栈args的东西,因为只有调用者知道他们传递了多少个args。希望在这种模式下,编译器至少会警告隐式声明的函数是否错误,因为错误会导致崩溃(堆栈指向调用后的错误位置)。
有关阅读编译器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_fn
(using 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
。
如果您使用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在他的评论中提到的那样)。
旁注:在x64-86上,只有少数起始函数参数由寄存器传递,如果有更多,则下一个参数存储在堆栈中。
答案 2 :(得分:0)
如果没有函数原型并且使用了隐式声明,编译器会假定所有参数都是int
类型。
看起来int在arm和x64-86 architecutre上是不同的。
请注意,修饰符%d
只能与int参数一起使用,对于无符号一次使用%u
这就是为什么有警告你。