LV085-汇编与C
在 C 程序和 ARM 汇编程序之间相互调用时必须遵守 ATPCS 规则,ATPCS 规定了一些函数间调用的基本规则。
一、ATPCS 规则
ATPCS 即 ARM-THUMB procedure call standard(ARM-Thumb 过程调用标准)的简称,是基于 ARM 指令集和 THUMB 指令集过程调用的规范,规定了调用函数如何传递参数,被调用函数如何获取参数,以何种方式传递函数返回值。
寄存器使用约定如下:
| 寄存器 | 名称 | 用途 |
|---|---|---|
| R0~R3 | 参数寄存器 | 用于传递函数参数,无需恢复 |
| R4~R11 | 局部变量寄存器 | 用于保存局部变量,需要恢复 |
| R12 | 临时寄存器 | 用作函数间 scratch 寄存器 |
| R13 | SP(栈指针) | 指向栈顶,进出函数值必须相等 |
| R14 | LR(链接寄存器) | 保存函数返回地址 |
| R15 | PC(程序计数器) | 存放当前指令地址,不能作他用 |
【详细说明】
(1)参数传递:在函数中,通过寄存器 R0~R3 来传递参数,被调用的函数在返回前无需恢复寄存器 R0~R3 的内容。
(2)局部变量:在函数中,通过寄存器 R4~R11 来保存局部变量,如果使用了这些寄存器,函数返回前必须恢复其原始值。
(3)临时寄存器:寄存器 R12 用作函数间 scratch 寄存器,用于临时存储。
(4)栈指针:寄存器 R13 用作栈指针,记作 SP,在函数中寄存器 R13 不能用做其他用途,寄存器 SP 在进入函数时的值和退出函数时的值必须相等。
(5)链接寄存器:寄存器 R14 用作链接寄存器,记作 LR,它用于保存函数的返回地址,如果在函数中保存了返回地址,则 R14 可用作其它的用途。
(6)程序计数器:寄存器 R15 是程序计数器,记作 PC,它不能用作其他用途。
二、参数传递
1. 汇编程序如何向 C 程序的函数传递参数
(1)当参数小于等于 4 个时,使用寄存器 R0~R3 来进行参数传递。
(2)当参数大于 4 个时,前四个参数按照上面方法传递,剩余参数传送到栈中,入栈的顺序与参数顺序相反,即最后一个参数先入栈。
2. C 程序如何返回结果给汇编程序
(1)结果为一个 32 位的整数时,通过寄存器 R0 返回。
(2)结果为一个 64 位整数时,通过 R0 和 R1 返回,依此类推。
(3)结果为一个浮点数时,通过浮点运算部件的寄存器 f0、d0 或 s0 返回。
(4)结果为一个复合的浮点数时,通过寄存器 f0-fN 或者 d0~dN 返回。
(5)对于位数更多的结果,通过调用内存来传递。
三、C 函数为何要用栈
总的来说,栈的作用就是:保存现场/上下文,传递参数。
1. 保存现场/上下文
保存现场,也叫保存上下文。现场,相当于案发现场,总有一些现场的情况,要记录下来的,否则被别人破坏掉之后,就无法恢复现场了。而此处说的现场,就是指 CPU 运行的时候,用到了一些寄存器,比如 R0~R3、LR 等等,对于这些寄存器的值,如果不保存而直接跳转到函数中去执行,那么很可能会被破坏了,因为函数执行需要用到这些寄存器。
因此在函数调用之前,应该将这些寄存器等现场,暂时保持起来,等调用函数执行完毕返回后,再恢复现场,这样 CPU 就可以正确的继续执行了。
保存寄存器的值,一般用的是 push 指令,将对应的某些寄存器的值,一个个放到栈中,即所谓的入栈。然后待被调用的子函数执行完毕的时候,再调用 pop,把栈中的一个个的值,赋值给对应的入栈的寄存器,即所谓的出栈。
2. 传递参数
当函数被调用并且参数大于 4 个时,(不包括第 4 个参数)第 4 个参数后面的参数就保存在栈中。
四、C 调用汇编实例
C 调用汇编的调用流程:
C 函数 汇编函数
| |
|-- 准备参数 ------------> |
| R0=10, R1=20 |
| |
|-- BL add_asm ---------->|
| |-- 读取参数 R0, R1
| |-- 执行加法运算
| |-- 设置返回值 R0
| |
|<-- BX LR ---------------|
| R0=30 (返回值) |
| |
|-- 使用返回值 |1. 实例 1:简单加法函数
这个实例展示 C 语言如何调用汇编函数实现两个整数相加。
1.1 main.c
#include <stdio.h>
// 声明汇编函数
extern int add_asm(int a, int b);
int main(void)
{
int result;
// 调用汇编函数
result = add_asm(10, 20);
printf("10 + 20 = %d\n", result);
return 0;
}1.2 add.S
.global add_asm
add_asm:
// 参数传递:
// R0 = a (第一个参数)
// R1 = b (第二个参数)
// 计算 a + b
ADD R0, R0, R1 // R0 = R0 + R1
// 返回值通过 R0 返回
BX LR // 返回调用者使用下面的命令编译:
arm-linux-gnueabihf-gcc -o c_call_asm main.c add.S执行结果如下
10 + 20 = 302. 实例2:多参数函数(使用栈)
这个实例展示当参数超过 4 个时,如何通过栈传递参数。
2.1 main2.c
#include <stdio.h>
// 声明汇编函数
extern int sum_all(int a, int b, int c, int d, int e);
int main(void)
{
int result;
// 调用汇编函数,传递 5 个参数
result = sum_all(1, 2, 3, 4, 5);
printf("1 + 2 + 3 + 4 + 5 = %d\n", result);
return 0;
}2.2 sum.S
.global sum_all
sum_all:
// 参数传递:
// R0 = a (第一个参数)
// R1 = b (第二个参数)
// R2 = c (第三个参数)
// R3 = d (第四个参数)
// [SP] = e (第五个参数,在栈中)
// 保存 R4(用于累加)
PUSH {R4}
// 初始化累加器
MOV R4, R0 // R4 = a
ADD R4, R4, R1 // R4 = a + b
ADD R4, R4, R2 // R4 = a + b + c
ADD R4, R4, R3 // R4 = a + b + c + d
// 从栈中获取第五个参数
LDR R0, [SP, #4] // R0 = e(注意 SP 偏移,因为 R4 已入栈)
ADD R4, R4, R0 // R4 = a + b + c + d + e
// 返回值通过 R0 返回
MOV R0, R4
// 恢复 R4
POP {R4}
BX LR // 返回调用者使用如下命令编译:
arm-linux-gnueabihf-gcc -o c_call_asm2 main2.c sum.S执行结果:
1 + 2 + 3 + 4 + 5 = 153. 实例3:C语言内联汇编
这个实例展示如何在 C 语言中直接嵌入汇编代码(内联汇编),实现两个整数相加。
3.1 inline_asm.c
#include <stdio.h>
int main(void)
{
int a = 10;
int b = 20;
int result;
// 使用内联汇编计算 a + b
__asm__ __volatile__(
"ADD %0, %1, %2" // 汇编指令:result = a + b
: "=r"(result) // 输出操作数:result
: "r"(a), "r"(b) // 输入操作数:a, b
);
printf("%d + %d = %d\n", a, b, result);
return 0;
}3.2 内联汇编语法说明
内联汇编的基本格式:
__asm__ __volatile__(
"汇编指令" // 要执行的汇编代码
: "输出操作数" // 可选,用于接收结果
: "输入操作数" // 可选,用于传递参数
: "被破坏的寄存器" // 可选,列出被修改的寄存器
);各部分说明:
__asm__:表示这是汇编代码__volatile__:告诉编译器不要优化这段代码,确保指令按顺序执行"ADD %0, %1, %2":汇编指令,其中 %0、%1、%2 是操作数的占位符"=r"(result):输出操作数,"=r" 表示使用通用寄存器,result 是输出变量"r"(a), "r"(b):输入操作数,"r" 表示使用通用寄存器,a 和 b 是输入变量
3.3 更复杂的内联汇编示例
#include <stdio.h>
int main(void)
{
int a = 5;
int b = 3;
int sum, diff, product;
// 使用内联汇编执行多个运算
__asm__ __volatile__(
"ADD %0, %2, %3\n\t" // sum = a + b
"SUB %1, %2, %3\n\t" // diff = a - b
"MUL %0, %2, %3" // product = a * b (重新使用 %0)
: "=r"(sum), "=r"(diff), "=r"(product)
: "r"(a), "r"(b)
);
printf("a = %d, b = %d\n", a, b);
printf("sum = %d\n", sum);
printf("diff = %d\n", diff);
printf("product = %d\n", product);
return 0;
}编译命令:
arm-linux-gnueabihf-gcc -o inline_asm inline_asm.c执行结果:
10 + 20 = 303.4 内联汇编的优缺点
优点:
- 无需单独编写汇编文件,代码集中在一个文件中
- 可以直接访问 C 变量,方便数据交换
- 适合优化关键代码段
缺点:
- 代码可读性较差
- 调试困难
- 可移植性差(不同编译器语法可能不同)
五、汇编调用 C 实例
汇编调用 C 的调用流程:
汇编函数 C 函数
| |
|-- 准备参数 ------------> |
| R0=5 |
| |
|-- BL square ----------->|
| |-- 读取参数 x
| |-- 执行乘法运算
| |-- 返回结果
| |
|<-- 返回 -----------------|
| R0=25 (返回值) |
| |
|-- 使用返回值 |1. 实例 1:调用 C 函数计算平方
这个实例展示汇编如何调用 C 函数。
1.1 square.c
// 计算平方的函数
int square(int x)
{
return x * x;
}1.2 call_square.S
.global main
.extern square
main:
// 设置参数:通过 R0 传递参数
MOV R0, #5 // R0 = 5
// 调用 C 函数 square
BL square // 调用 square(5),返回值在 R0 中
// 此时 R0 = 25(5 的平方)
// 程序结束
MOV R7, #1 // sys_exit
SWI 0 // 系统调用编译命令:
arm-linux-gnueabihf-gcc -o asm_call_c call_square.S square.c执行结果:
程序正常执行,R0 中保存结果 25。
2. 实例 2:调用 C 函数并传递多个参数
这个实例展示汇编如何调用 C 函数并传递多个参数。
2.1 multiply.c
// 计算乘积的函数
int multiply(int a, int b, int c)
{
return a * b * c;
}2.2 call_multiply.S
.global main
.extern multiply
main:
// 设置参数:通过 R0, R1, R2 传递参数
MOV R0, #2 // R0 = 2 (第一个参数)
MOV R1, #3 // R1 = 3 (第二个参数)
MOV R2, #4 // R2 = 4 (第三个参数)
// 调用 C 函数 multiply
BL multiply // 调用 multiply(2, 3, 4),返回值在 R0 中
// 此时 R0 = 24(2 * 3 * 4)
// 程序结束
MOV R7, #1 // sys_exit
SWI 0 // 系统调用编译命令:
arm-linux-gnueabihf-gcc -o asm_call_c2 call_multiply.S multiply.c执行结果:
程序正常执行,R0 中保存结果 24。