首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >在没有原型的文件中调用的函数在ARM和x86-64上产生不同的结果。

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

Stack Overflow用户
提问于 2017-11-07 22:07:38
回答 3查看 164关注 0票数 1

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

C.主要:

代码语言:javascript
复制
#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:

代码语言:javascript
复制
void lib_fn(unsigned slot, int pon_off);

lib.c:

代码语言:javascript
复制
#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;
}

汇编:

代码语言:javascript
复制
gcc -O2 -Wall -Wextra main.c lib.c

在手臂上奔跑:

代码语言:javascript
复制
$ ./a.out
sizeof unsigned long long: 8
sizeof int: 4
slot: 0
pon_off: 0

在x86-64上运行:

代码语言:javascript
复制
$ ./a.out
sizeof unsigned long long: 8
sizeof int: 4
slot: 0
pon_off: 1

如您所见,pon_off在ARM上为0,而在x86-64上为1。我猜这与参数大小有关,因为lib_fn()需要两个it,它们在一起有8个字节长,一个long long是8个字节长。

  1. 为什么pon_off在ARM和x86-64上的打印方式不同?
  2. 这和电话会议有关吗?
EN

回答 3

Stack Overflow用户

回答已采纳

发布于 2017-11-08 01:21:42

这和电话会议有关吗?

是的,它与呼叫约定/ ABI有关。

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

在ARM上,寄存器宽度(堆栈上的"arg槽“最小宽度)为32位,64位整数args采用两个寄存器。

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

具有与寄存器大小相等的最小"arg槽“宽度几乎是通用的,无论arg是在寄存器中传递还是在堆栈上传递。不过,这不一定是int的宽度:args可以在一个寄存器中传递。

对于原型,根据原型中的类型,将更宽/更窄的类型转换为被调用方所期望的类型。所以很明显一切都正常。

在没有原型的情况下,调用中表达式的类型决定了arg是如何传递的。unsigned long long slot 在ARM的调用约定中接受第一个2 arg传递寄存器, lib_fn 希望在其中查找其2整型args.

(声称所有东西都在没有原型的情况下转换为int的答案是错误的。没有一个原型等同于int lib_fn(...);,但是printf仍然可以与doubleint64_t一起工作。请注意,当传递到变量函数时,float被隐式转换为double,就像较窄的整数类型被上转换为int一样,这就是为什么%fdouble格式的原因,而float没有格式,这与您传递指针的扫描格式不同。这正是C的设计方式,没有理由这样做。但是无论如何,C要求更广泛的类型能够像对各种函数一样被传递,并且所有的调用约定都包含这一点。)

顺便说一句,其他的破坏是可能的,:有些实现对各种函数(因此是非原型的)使用了与普通函数不同的调用约定。

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

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

有关读取编译器asm输出的介绍,请参见如何消除GCC/clang组件输出中的“噪音”?,特别是马特·戈德波特的CppCon2017 talk “我的编译器最近为我做了什么?”打开编译器的盖子“

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

代码语言:javascript
复制
#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);
}

(**-m32**),参见戈德波特编译器资源管理器上的source+asm,ARM,x86-64,x86-32,gcc 6.3**。(我实际上复制了foo并将其重命名为lib_fn,这样它就不会在调用者的一个版本中有原型,而不是为每个体系结构设置两个单独的编译器窗口。在更复杂的情况下,这会很方便,因为您可以区分编译器窗格)。

对于x86-64,输出与没有原型的输出基本相同。如果没有,调用方必须将al (使整个拉克斯为零)为零,以指示此变量函数调用在XMM寄存器中没有传递任何FP args。(在Windows调用约定中,您不会有这种情况,因为Windows约定是针对各种函数以及在牺牲正常函数的情况下实现它们的简单性而优化的。)

ARM的

代码语言:javascript
复制
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期待slot在R0,pon_off在R1。

打破x86-64

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

代码语言:javascript
复制
lib_fn_noproto((unsigned __int128)slot, pon_off);

汇编成:

代码语言:javascript
复制
    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占据了前两个时隙。

票数 4
EN

Stack Overflow用户

发布于 2017-11-07 22:45:36

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

请比较在手臂x64-86上生成的程序集

  1. 在ARM上,没有符号的long存储在2个寄存器中,int存储在1个寄存器中,在x86上都存储在64位寄存器中。
  2. 在ARM上,函数在获取参数时,读取每个参数的单个寄存器,使第一个变量的高低部分被拆分为2个参数。最后通过的第二个论点被省略了。在x64-86上,它仍然从这两个64位寄存器中获取值。

附带注意:在x64-86上,寄存器只传递很少的起始函数参数,如果有更多的参数,下一个参数将存储在堆栈中。

票数 1
EN

Stack Overflow用户

发布于 2017-11-07 22:11:27

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

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

注意,修饰符%d只能与int参数一起使用,对于未签名的一个使用%u

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

票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/47168239

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档