跳到主要内容

6. 在C或C++代码中使用内嵌汇编

6.1 编写内嵌汇编代码​

编译器支持在 C 或 C++ 源代码中编写汇编代码,例如访问 C 或 C++ 无法提供的目标处理器特性。

__asm关键字可以使用内嵌汇编语法将汇编代码集成到函数中。例如:

#include <stdio.h>

// 高级嵌汇编
int add(int i, int j)
{
int res = 0;
__asm("ADD %0, %1, %2"
: "=r"(res) // 输出操作数
: "r"(i), "r"(j)); // 输入操作数
return res;
}

// 普通嵌汇编
__attribute__((noinline)) int add1(int i, int j)
{
__asm("ADD r0, r0, r1");
return i + j;
}

int main(void)
{
int a = 1;
int b = 2;
int c = 0;
c = add(a, b);
printf("Result of %d + %d = %d\n", a, b, c);
}

使用内嵌汇编而非单独编写 .s 文件,可以实现嵌汇编指令完成与C代码中的变量等快速进行交互。

6.1.1 内嵌汇编语句的结构​

在C语言的函数中嵌入一段汇编语言程序段是编译器提供的“asm”功能,形式看起来陌生,它不是标准C所定义的形式,而是编译器对C语言的扩充,支持传递变量,比单纯的汇编语言代码要复杂,涉及到怎么分配和使用寄存器,以及C代码中的变量应该存放在哪个寄存器中。

嵌汇编的一般形式为:

__asm__ __volatile__("<asm routine>":output:input:modify);

其中__asm__表示汇编代码的开始,__volatile__是可选项,作用为避免asm指令被删除、移动或组合。<asm routine>为汇编指令部分,支持数字前加前缀“%”,如%0,%1,%2等表示使用寄存器的样板操作数。指令中有几个操作数,就说明有几个变量需要与寄存器结合,由编译器根据后面输出部分和输入部分的约束条件进行相应的处理。

输出部分:用于规定输出变量如何与寄存器结合的约束,可以有多个约束,互相以逗号分开。每个约束以“=”开头,接着用一个字母表示操作数类型,然后是变量的结合约束,如 "=r"(__dummy),r表示目标操作数可以使用任一个通用寄存器,__dummy为定义的C变量。如果为 "m"(__dummy) 表示目标操作数是存放在内存单元__dummy中。

输入部分:与输出部分相似,但没有“=”。如果输入与输出部分约束使用同一个寄存器,将对应的操作数编号(如"0","1","2"等)放在约束条件中。存在多个输入使用逗号间隔。

修改部分:通常以"memory"作为约束条件,表示操作完成后内存中的内容已有改变。如果原来某个寄存器的内容来自内存,那么现在内存中这个单元的内容已经改变。当输入"r0","r1"时,即当前语句分配寄存器不使用R0和R1,针对语句中使用固定寄存器时有作用。

注意:指令部分为必选项,其他为可选项,当输入部分存在,而输出部分不存在时,分号:要保留,当仅修改部分“memory”存在时,3个分号都要保留。因为宏定义的关系,__asm__可以只写asm,__volatile__只写volatile。

主要约束字母及含义如下:

表6.1 主要约束字母及含义

字母含义可用于
m内存单元输入/输出
r任一通用寄存器输入/输出
g“任意”,由编译器决定使用通用寄存器还是内存单元输入/输出
I整数常量表达式,取值范围 0~31仅输入
l、h整数常量表达式,无取值范围限制仅输入

约束 I/l/h 要求操作数为整数常量表达式,不能是变量;且只能用于输入部分,写成输出约束(如 "=I")会被拒绝。

6.1.1.1 普通字符串形式​

第一种,编译结果所有汇编指令为一行,每条指令需用分号分隔。该形式通过连接符告知功能代码在1行。

asm("\
asm_instruct1;\
asm_instruct2;\
asm_instruct3;\
");

第二种,如果需要占用不同的行,参见下面示例:

asm(
"asm_instruct1"
"\n\t"
"asm_instruct2"
"\n\t"
"asm_instruct3"
);

第三种编译结果每条汇编指令为一行,\n\t用于每一指令的分隔,也可为如下格式:

asm(
"asm_instruct1\n\t"
"asm_instruct2\n\t"
"asm_instruct3\n\t"
);

6.1.1.2 嵌汇编注意事项​

  1. 用户在使用R6-R13等前,必须对其进行入栈处理,结束时再出栈,以保护其中数据不丢失(确认工具默认已保护的可忽略),使用时用户可参考使用 PUSH list/POP list 指令。POP list不能使LR(即R13)出栈,应先使用 POP list 将LR之外的寄存器出栈,再 POP LR(若LR被入栈)。

  2. KF32编译器中在O2等级优化下,R0-R5中可能存有中间数据,为防止数据丢失,建议将嵌汇编内容放置在一个新的子函数中,因此子函数内R0-R5可自由使用,R6-R13等根据需要使用前需保护,否则R0-R5也可能需要保护,否则会存在运行错误。

  3. 嵌汇编中可通过C语言的变量名、函数名直接获取变量地址、函数地址。汇编文件访问C文件中的全局变量、函数时,需要先在汇编文件中使用 .extern 声明外部变量,而后可直接使用变量名、函数名作为其地址。

  4. 汇编文件中未初始化的全局变量定义为 .common a,1,其中数值1表示一个字节长度。C文件可通过extern声明汇编文件的变量为外部变量后使用。

  5. 汇编文件中函数需使用 .export 声明,如:.export function,C文件可通过extern声明外部变量后使用。

  6. C文件中的宏定义与汇编中EQU 无法通用,需分别定义。

  7. 若在嵌汇编中使用芯片特殊功能寄存器,则需引用汇编头文件,嵌汇编语句可在函数外使用,示例如:asm(".include \KF32XXXX.inc"); // 汇编包含头文件

    包含时需要增加选项传递头文件路径,否则需要全路径include。工具选项 -Wa,-I"XXXXX/include" 负责C项目将编译选项传递头文件路径给汇编器。在使用ChipON IDE的开发环境时,默认已经添加了路径传递。如果使用外设库开发模式,不建议使用该头文件中定义,根据需要可仿照该头文件进行内容的声明,否则符号定义可能与外设库冲突。

  8. 嵌汇编中使用全局变量时,先使用 LD R5,#global 获取变量的内存地址,再使用 LD/ST.[whb] 指令进行加载/存储;

  9. 函数功能参数规则:R0-R4 (堆栈)用于传递参数,R0、R1(堆栈)存放返回值,LR自动跟踪函数返回地址。内嵌函数可根据情况自定义;

  10. 嵌汇编的sp操作(修改,如申请空间),若汇编嵌入在C函数中,C函数的局部变量仍基于sp和编译时分配的偏移值进行操作。即嵌汇编sp的临时修改与申请须在C作用时还原。不支持使用伪指令 MOV SP,#address 的修改,拆分MOVL和MOVH实现赋值时,若中间被中断打断,中断的现场保护和中断下的函数的栈使用均会出现异常。即SP高位为0待更新的非法栈区。

6.1.2 符号与标签的定义​

内嵌汇编可以用来定义符号。例如:

__asm (".global __use_no_semihosting\n\t");

要定义标签,请在标签名称后使用:例如:

__asm ("my_label:\n\t");

6.2 从C和C++调用汇编函数​

通常,单个应用程序的所有代码都用相同的源代码编写。这通常是高级语言,如C或C++。这些代码随后被编译成KF32汇编代码。

不过,在某些情况下,你可能想从 C/C++ 代码中调用汇编代码。例如:

  • 如果你想利用现有的汇编代码,但项目的其他部分是用C或C++编写的。
  • 如果你想直接用汇编代码手动编写关键函数,这样可以生成比编译C或C++代码更优化的代码。
  • 如果你想直接与设备硬件接口,并且在底层汇编代码中比高级 C 或 C++ 更容易。

要从 C 或 C++ 中调用汇编函数:

  1. 在汇编源代码中,将代码声明为全局函数,使用 .global 和 .type:

    .globl add
    .p2align 3
    .type add,@function
    add: // 函数入口
    ADD r0, r0, r1 // 函数参数在r0和r1寄存器中,将它们加在一起并将结果放入r0
    JMP lr // 跳转到链接寄存器中保存的返回地址

    在 C 代码中,使用 extern 声明外部函数:

    #include <stdio.h>

    extern int add(int a, int b);

    int main(void)
    {
    int a = 4;
    int b = 5;
    printf("Adding %d and %d results in %d\n", a, b, add(a, b));
    return (0);
    }

    在 C++ 代码中,使用extern “C”:

    extern "C" {int add(int a, int b);}

  2. 确保您的汇编代码符合KF32架构的函数调用约定;

  3. 编译两个源文件:

    kf32clang --target=kf32r -march=kf32r main.c myadd.s -T kf32demo.ld -o main.o