前言
最近在写ARMV8架构下的测试代码,对于CORTEX-A76核的测试代码避免不了C语言函数内嵌汇编的编写,此文就对C语言函数内嵌汇编的规则进行说明,并配有实例。
运行环境:IDE:ARM-DS5,编译器:ARM C Compiler6,target:ARMV8-AARCH64
ATPCS规则
在汇编程序调用C语言函数、C语言函数调用汇编程序和C语言函数内嵌汇编中,都会涉及到子程序的调用、子程序的返回和参数传递等问题。在ARM体系结构中,使用ATPCS规则来约束这些参数的传递,规则内容如下:
-
子程序间通过寄存器r0-r3来传递参数。这时,寄存器r0-r3可记作a1-a4。被调用的子程序在返回前无须恢复寄存器r0-r3的内容。如果参数个数多于4个,将剩余的字数据通过数据栈来传递。
-
在子程序中,使用寄存器r4-r11来保存局部变量。这时,寄存器r4-r11可以记作v1-v8。如果在子程序中使用了寄存器v1-v8中某些寄存器,则子程序进入时必须保存这些寄存器的值,在返回前必须恢复这些寄存器的值。在Thumb程序中,通常只能使用寄存器r4-r7来保存局部变量。另外r9、r10和r11还有一个特殊的作用,分别记为:静态基址寄存器sb、数据栈限制指针sl和帧指针fp。
-
寄存器r12用作子程序间调用时临时保存栈指针,函数返回时使用该寄存器进行出栈,记作IP;在子程序间的链接代码中常有这种使用规则,被调用函数在返回之前不必恢复 r12。
-
寄存器r13用作堆栈指针,记作sp。在子程序中寄存器R13不能用作其他用途。寄存器sp在进入子程序时的值和退出子程序的值必须相等。
-
寄存器r14称为链接寄存器,记作lr,它用于保存子程序的返回地址。如果在子程序中保存了返回地址,寄存器r14则可以用作其他用途,但在程序返回时要恢复。
-
寄存器r15为程序计数器,记作pc,它不能用作其他用途。在中断程序中,所有的寄存器都必须保护,编译器会自动保护R4~R11。
-
ATPCS中的各寄存器在ARM编译器和汇编器中都是预定义的,也即它们在编译工具集中已经指定,不能改变。
函数调用
C函数调用汇编函数
下面的例子以读取CPUID为例来说明函数之间的调用关系:
首先新建一个get_id.s文件,写入以下代码:
.section .text
.globl get_id
get_id:
//此处的MPIDR_EL1为只读寄存器,其中位域[8:10]存放CPUID数值
MRS X0, MPIDR_EL1
UBFX W0, W0 ,#8, #3
RET
.end
然后新建main.c,写入以下代码
#include <stdio.h>
/*汇编函数声明*/
int get_id();
void main()
{
//此处需要利用get_id读取CPUID数值
int val = 0;
val = get_id();
}
最后运行,就会正确读取到当前运行core的CPUID。
此处需要注意:
-
在get_id.s中的ret命令是汇编函数的返回命令,ret默认的参数是lr,即程序跳转到LR寄存器存放的地址中继续执行,ret也可以显示的跳转,如ret x25,则跳转到x25寄存器所在的地址去执行。在main函数中执行val = get_id()语句反汇编是:
BL get_id() //BL指令是把当前地址存放到LR寄存器中,保存现场。
所以使用ret指令是返回到 val = get_id()的下一句语句去执行。
-
在main.c必须进行函数声明,利用ATPCS规则来定义输入参数和返回值。
C语言函数调用含有内嵌汇编的函数
假如现在我需要把获取CPUID的汇编函数封装成C语言函数应该怎么办呢?
首先新建一个get_id.c文件,在里面进行内嵌汇编编写,一般方法是:
int get_id()
{
asm("MRS X0, MPIDR_EL1");
asm("UBFX W0, W0 ,#8, #3");
}
上述代码利用ATPCS规则来看是把需要读取的返回值传递到了x0里面,然后直接在main.c里面就可以直接调用这个函数了。其实这个思路是错误的,在代码调式中val的值并不等于预想中的x0的值,通过反汇编代码可以看出:
get_id
SUB sp,sp,#0x10
MRS X0, MPIDR_EL1
UBFX W0, W0 ,#8, #3
LDR w0,[sp,#0xC]
ADD sp,sp,#0x10
RET
在返回之前,执行了一句LDR w0,[sp,#0xC],这是因为我们在get_id()函数里面没有写return语句导致处理器会在栈区拉一个值当作返回值,这个值会把原先的x0值覆盖,导致返回失败。
这种内嵌汇编的方式对于不需要的传参和返回值的函数可以使用,比如:
void test(void)
{
asm("mov x1,#0x123");
}
对于有参的内嵌汇编需要使用以下规则,内嵌汇编语法如下:
_asm_(汇编语句模板: 输出部分: 输入部分: 破坏描述部分)
共四个部分:汇编语句模板,输出部分,输入部分,破坏描述部分,各部分使用“:”格开,汇编语句模板必不可少,其他三部分可选,如果使用了后面的部分,而前面部分为空,也需要用“:”格开,相应部分内容为空。例如:
__asm__ __volatile__("cli": : :"memory")
1、汇编语句模板
汇编语句模板由汇编语句序列组成,语句之间使用“;”、“\n”或“\n\t”分开。指令中的操作数可以使用占位符引用C语言变量,操作数占位符最多10个,名称如下:%0,%1,…,%9。指令中使用占位符表示的操作数,总被视为 long型(4个字节) ,但对其施加的操作根据指令可以是字或者字节,当把操作数当作字或者字节使用时,默认为低字或者低字节。对字节操作可以显式的指明是低字节还是次字节。 方法是在%和序号之间插入一个字母,“b”代表低字节,“h”代表高字节,例如:%h1 。
2、输出部分
输出部分描述输出操作数,不同的操作数描述符之间用逗号格开,每个操作数描述符由限定字符串和C 语言变量组成。每个输出操作数的限定字符串必须包含“=”表示他是一个输出操作数。
例:
__asm__ __volatile__("pushfl ; popl %0 ; cli":"=g" (x) )
描述符字符串表示对该变量的限制条件,这样GCC 就可以根据这些条件决定如何分配寄存器,如何产生必要的代码处理指令操作数与C表达式或C变量之间的联系。
3、输入部分
输入部分描述输入操作数,不同的操作数描述符之间使用逗号格开,每个操作数描述符由限定字符串和C语言表达式或者C语言变量组成。
__asm__ __volatile__("pushfl ; popl %0 ; cli"
:"=g" (x)
:"r"(a),"r"(b)
:"memory");
4、限制字符
限制字符有很多种,有些是与特定体系结构相关,它们的作用是指示编译器如何处理其后的C语言变量与指令操作数之间的关系。
5、破坏描述部分
破坏描述符用于通知编译器我们使用了哪些寄存器或内存,由逗号格开的字符串组成,每个字符串描述一种情况,一般是寄存器名;除寄存器外还有“memory”。例如:“%eax”,“%ebx”,“memory”等。
根据以上规则描述,我们可以很轻松写出get_id的内嵌汇编:
int get_id()
{
int val;
__asm__ __volatile__(
"MRS X0, MPIDR_EL1;
UBFX %0, W0 ,#8, #3;"
:"=r" (val)
:"memory");
}
汇编调用C语言函数
这一个过程就相对简单,直接在汇编代码调用C语言函数即可:
。。。。。。。。。。。。。
。。。。。。。。。。。。
b main
参考文献:
https://blog.csdn.net/qq_26093511/article/details/79546321
https://blog.csdn.net/yypony/article/details/17633323
https://blog.csdn.net/luteresa/article/details/119327138?utm_source=app&app_version=4.20.0&code=app_1562916241&uLinkId=usr1mkqgl919blen