从程序集调用 C 函数,传递参数并在 ARM 调用约定中获取返回值



我想调用一个C函数,比如:

int foo(int a, int b) {return 2;}

在程序集 (ARM) 代码内。我读到我需要提及

import foo

在我的汇编代码中,汇编程序在 C 文件中搜索foo。但是,我坚持从汇编中传递参数 a 和 b,并在汇编中再次检索一个整数(此处为 2)。有人可以用一个小例子来解释我如何做到这一点吗?

您已经编写了最小的示例。

int foo(int a, int b) {return 2;}

编译和反汇编

arm-none-eabi-gcc -O2 -c so.c -o so.o
arm-none-eabi-objdump -d so.o
so.o:     file format elf32-littlearm

Disassembly of section .text:
00000000 <foo>:
0:   e3a00002    mov r0, #2
4:   e12fff1e    bx  lr

任何与 a 和 b 有关的东西都是死代码,因此进行了优化。 虽然使用 C 学习 asm 很好/可以开始,但您真的想通过优化来做到这一点,这意味着您必须更加努力地制作实验代码。

int foo(int a, int b) {return 2;}
int bar ( void )
{
return(foo(5,4));
}

我们没有学到任何新东西。

Disassembly of section .text:
00000000 <foo>:
0:   e3a00002    mov r0, #2
4:   e12fff1e    bx  lr
00000008 <bar>:
8:   e3a00002    mov r0, #2
c:   e12fff1e    bx  lr

需要为呼叫执行此操作:

int foo(int a, int b);
int bar ( void )
{
return(foo(5,4));
}

现在我们看到

00000000 <bar>:
0:   e92d4010    push    {r4, lr}
4:   e3a01004    mov r1, #4
8:   e3a00005    mov r0, #5
c:   ebfffffe    bl  0 <foo>
10:   e8bd4010    pop {r4, lr}
14:   e12fff1e    bx  lr

(是的,这是为此编译器默认目标armv4t构建的,对于其他一些不知道我/我们如何知道的人来说应该是显而易见的)(也可以从这个例子中知道编译器有多新/旧(几年前有一个ABI更改在这里可见)(较新版本的gcc比旧版本更糟糕,因此较旧的版本仍然适合用于某些用例))

根据此编译器约定(现在,虽然此编译器确实对该编译器的某些版本使用某些文档的某些版本的 ARM 约定,但请始终记住这是编译器作者的选择,他们没有义务遵守任何人的书面标准,他们选择)

因此,我们看到第一个参数在 r0 中,第二个参数在 r1 中。 您可以使用更多操作数或更多类型的操作数来制作函数,以查看其中的细微差别。 寄存器中有多少以及它们何时开始使用堆栈。 例如,尝试一个 64 位变量,然后按该顺序尝试一个 32 作为操作数,然后反向尝试。

看看被叫方发生了什么。

int foo(int a, int b)
{
return((a<<1)+b+0x123);
}

我们看到 r0 和 r1 是前两个操作数,否则编译器将被严重破坏。

00000000 <foo>:
0:   e0810080    add r0, r1, r0, lsl #1
4:   e2800e12    add r0, r0, #288    ; 0x120
8:   e2800003    add r0, r0, #3
c:   e12fff1e    bx  lr

我们在调用者示例中没有明确看到的是 r0 是存储返回值的位置(至少对于此变量类型)。

ABI 文档并不容易阅读,但如果您首先"尝试一下",那么如果您希望参考文档,它应该对文档有所帮助。 归根结底,你有一个要使用的编译器,它有一个约定,可能是工具链的一部分,所以你必须遵守编译器约定而不是一些第三方文档(即使那个第三方是arm),你可能应该使用那个工具链的汇编器,这意味着你应该使用该汇编语言(许多不兼容的汇编语言, 该工具定义语言而不是目标)。

您可以看到自己弄清楚这一点是多么简单。

和。。。所以这很痛苦,但你可以看看编译器的程序集输出,至少有些人会让你。 使用 gcc,您可以使用 -save-temps 或 -S

int foo(int a, int b)
{
return 2;
}
.cpu arm7tdmi
.eabi_attribute 20, 1
.eabi_attribute 21, 1
.eabi_attribute 23, 3
.eabi_attribute 24, 1
.eabi_attribute 25, 1
.eabi_attribute 26, 1
.eabi_attribute 30, 2
.eabi_attribute 34, 0
.eabi_attribute 18, 4
.file   "so.c"
.text
.align  2
.global foo
.arch armv4t
.syntax unified
.arm
.fpu softvfp
.type   foo, %function
foo:
@ Function supports interworking.
@ args = 0, pretend = 0, frame = 0
@ frame_needed = 0, uses_anonymous_args = 0
@ link register save eliminated.
mov r0, #2
bx  lr
.size   foo, .-foo
.ident  "GCC: (15:9-2019-q4-0ubuntu1) 9.2.1 20191025 (release) [ARM/arm-9-branch revision 277599]"

这些几乎都不是你"需要"的。

最小值如下所示

.globl foo
foo:
mov r0,#2
bx lr

.global 或 .globl 是等价的,在某种程度上反映了你学习 GNU 汇编程序的年龄或方式/时间。

现在,如果您混合手臂和拇指指令,这将中断,这默认为手臂。

arm-none-eabi-as x.s -o x.o arm-none-eabi-objdump -d x.o

X.O:文件格式 ELF32-小臂

反汇编 .text 部分:

00000000 : 0: e3a00002 mov r0, #2 4: E12FFF1E BX LR

如果我们想要拇指,那么我们必须告诉它

.thumb
.globl foo
foo:
mov r0,#2
bx lr

我们得到拇指。

00000000 <foo>:
0:   2002        movs    r0, #2
2:   4770        bx  lr

使用 ARM 和 gnu 工具链,至少你可以混合 arm 和 thumb,链接器将负责过渡

int foo ( int, int );
int fun ( void )
{
return(foo(1,2));
}

我们不需要引导程序或其他东西来链接链接,因此我们可以看到它的一部分是如何工作的。

arm-none-eabi-ld so.o x.o -o so.elf
arm-none-eabi-ld: warning: cannot find entry symbol _start; defaulting to 0000000000008000
arm-none-eabi-objdump -d so.elf
so.elf:     file format elf32-littlearm

Disassembly of section .text:
00008000 <fun>:
8000:   e92d4010    push    {r4, lr}
8004:   e3a01002    mov r1, #2
8008:   e3a00001    mov r0, #1
800c:   eb000001    bl  8018 <foo>
8010:   e8bd4010    pop {r4, lr}
8014:   e12fff1e    bx  lr
00008018 <foo>:
8018:   2002        movs    r0, #2
801a:   4770        bx  lr

现在这被打破了,不仅仅是因为我们没有引导程序等,而且有一个 bl 到 foo,但 foo 是拇指,调用者是手臂。因此,对于 gnu 汇编程序,您可以使用这个快捷方式,我想我是从较旧的 gcc 那里学到的,但无论如何

.thumb
.thumb_func
.globl foo
foo:
mov r0,#2
bx lr

.thumb_func说你找到的下一个标签被认为是一个功能标签,而不仅仅是一个地址。

00008000 <fun>:
8000:   e92d4010    push    {r4, lr}
8004:   e3a01002    mov r1, #2
8008:   e3a00001    mov r0, #1
800c:   eb000003    bl  8020 <__foo_from_arm>
8010:   e8bd4010    pop {r4, lr}
8014:   e12fff1e    bx  lr
00008018 <foo>:
8018:   2002        movs    r0, #2
801a:   4770        bx  lr
801c:   0000        movs    r0, r0
...
00008020 <__foo_from_arm>:
8020:   e59fc000    ldr ip, [pc]    ; 8028 <__foo_from_arm+0x8>
8024:   e12fff1c    bx  ip
8028:   00008019    .word   0x00008019
802c:   00000000    .word   0x00000000

链接器增加了一个蹦床,我称之为蹦床,我想其他人称之为风向标。 无论哪种方式,只要我们编写正确的代码,工具链就会处理

。 请记住,特别是汇编程序的这种语法非常特定于汇编程序,其他汇编程序可能有其他语法来使其工作。 从 gcc 生成的代码中,我们看到了通用解决方案,它更类型化,但可能是一个更好的习惯。

.thumb
.type foo, %function
.global foo
foo:
mov r0,#2
bx lr

.type foo, %函数适用于 GNU 汇编程序中的 Arm 和 Thumb。 而且它不必放在 labe 之前(就像 .globl 或 .global 一样。 我们从使用这种汇编语言的工具链中得到相同的结果。

只是为了演示...

arm-none-eabi-as x.s -o x.o
arm-none-eabi-gcc -O2 -mthumb -c so.c -o so.o
arm-none-eabi-ld so.o x.o -o so.elf
arm-none-eabi-ld: warning: cannot find entry symbol _start; defaulting to 0000000000008000
arm-none-eabi-objdump -d so.elf
so.elf:     file format elf32-littlearm

Disassembly of section .text:
00008000 <fun>:
8000:   b510        push    {r4, lr}
8002:   2102        movs    r1, #2
8004:   2001        movs    r0, #1
8006:   f000 f807   bl  8018 <__foo_from_thumb>
800a:   bc10        pop {r4}
800c:   bc02        pop {r1}
800e:   4708        bx  r1
00008010 <foo>:
8010:   e3a00002    mov r0, #2
8014:   e12fff1e    bx  lr
00008018 <__foo_from_thumb>:
8018:   4778        bx  pc
801a:   e7fd        b.n 8018 <__foo_from_thumb>
801c:   eafffffb    b   8010 <foo>

你可以看到它双向工作拇指到手臂手臂到拇指,如果我们写 asm 写它为我们完成了其余的工作。

现在我个人讨厌统一的语法,这是手臂与CMSIS一起犯的主要错误之一。 但是,你想以此为生,你发现你非常讨厌大多数公司决策,更糟糕的是,必须与他们一起工作/运营。 通常,统一语法会生成错误的指令,并且必须摆弄语法才能使其工作,但是如果我必须获得特定的指令,那么我必须摆弄它以生成我想要的特定指令。 除了引导程序和其他一些例外,您通常不经常编写汇编语言,通常编译一些东西,然后获取编译器生成的代码并对其进行调整或替换。

在统一语法之前,我从 arm gnu 工具开始,所以我习惯了

.thumb
.globl hello
hello:
sub r0,#1
bne hello

而不是

.thumb
.globl hello
hello:
subs r0,#1
bne hello

并且可以在两种语法之间跳动(统一和不统一,是的,在一个工具中有两种汇编语言)。

以上所有内容都与 32 位 arm 有关,如果您对 64 位 arm 感兴趣,并且使用 gnu 工具,那么其中的一部分仍然适用,您只需要使用 aarch64 工具而不是来自 gnu 的 arm 工具。 ARM的aarch64是与aarch32完全不同的不兼容指令集。 但是像 .global 和 .type 这样的 gnu 语法...函数经常用于所有 GNU 支持的目标。 某些指令也有例外,但如果您采取相同的方法,让工具本身告诉您它们是如何工作的......通过使用它们...你可以弄清楚这一点。

so.elf:     file format elf64-littleaarch64

Disassembly of section .text:
0000000000400000 <fun>:
400000:   52800041    mov w1, #0x2                    // #2
400004:   52800020    mov w0, #0x1                    // #1
400008:   14000001    b   40000c <foo>
000000000040000c <foo>:
40000c:   52800040    mov w0, #0x2                    // #2
400010:   d65f03c0    ret

您需要做的是根据需要将参数放在正确的寄存器中(或堆栈上)。有关如何执行此操作的所有详细信息都是所谓的调用约定,并构成了应用程序二进制接口(ABI)的一个非常重要的部分。

有关 ARM (Armv7) 调用约定的详细信息,请访问:https://developer.arm.com/documentation/den0013/d/Application-Binary-Interfaces/Procedure-Call-Standard

最新更新