ARMGCC-C语言函数内嵌汇编

364 阅读7分钟

前言

最近在写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

参考文献:

blog.csdn.net/qq_26093511…

blog.csdn.net/yypony/arti…

blog.csdn.net/luteresa/ar…