Skip to content

LV085-汇编与C

在 C 程序和 ARM 汇编程序之间相互调用时必须遵守 ATPCS 规则,ATPCS 规定了一些函数间调用的基本规则。

一、ATPCS 规则

ATPCS 即 ARM-THUMB procedure call standard(ARM-Thumb 过程调用标准)的简称,是基于 ARM 指令集和 THUMB 指令集过程调用的规范,规定了调用函数如何传递参数,被调用函数如何获取参数,以何种方式传递函数返回值。

寄存器使用约定如下:

寄存器名称用途
R0~R3参数寄存器用于传递函数参数,无需恢复
R4~R11局部变量寄存器用于保存局部变量,需要恢复
R12临时寄存器用作函数间 scratch 寄存器
R13SP(栈指针)指向栈顶,进出函数值必须相等
R14LR(链接寄存器)保存函数返回地址
R15PC(程序计数器)存放当前指令地址,不能作他用

【详细说明】

(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

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

assembly
.global add_asm

add_asm:
    // 参数传递:
    // R0 = a (第一个参数)
    // R1 = b (第二个参数)
    
    // 计算 a + b
    ADD R0, R0, R1    // R0 = R0 + R1
    
    // 返回值通过 R0 返回
    BX LR             // 返回调用者

使用下面的命令编译:

bash
arm-linux-gnueabihf-gcc -o c_call_asm main.c add.S

执行结果如下

10 + 20 = 30

2. 实例2:多参数函数(使用栈)

这个实例展示当参数超过 4 个时,如何通过栈传递参数。

2.1 main2.c

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

assembly
.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            // 返回调用者

使用如下命令编译:

bash
arm-linux-gnueabihf-gcc -o c_call_asm2 main2.c sum.S

执行结果:

1 + 2 + 3 + 4 + 5 = 15

3. 实例3:C语言内联汇编

这个实例展示如何在 C 语言中直接嵌入汇编代码(内联汇编),实现两个整数相加。

3.1 inline_asm.c

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 内联汇编语法说明

内联汇编的基本格式:

c
__asm__ __volatile__(
    "汇编指令"              // 要执行的汇编代码
    : "输出操作数"          // 可选,用于接收结果
    : "输入操作数"          // 可选,用于传递参数
    : "被破坏的寄存器"      // 可选,列出被修改的寄存器
);

各部分说明:

  • __asm__:表示这是汇编代码
  • __volatile__:告诉编译器不要优化这段代码,确保指令按顺序执行
  • "ADD %0, %1, %2":汇编指令,其中 %0、%1、%2 是操作数的占位符
  • "=r"(result):输出操作数,"=r" 表示使用通用寄存器,result 是输出变量
  • "r"(a), "r"(b):输入操作数,"r" 表示使用通用寄存器,a 和 b 是输入变量

3.3 更复杂的内联汇编示例

c
#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;
}

编译命令:

bash
arm-linux-gnueabihf-gcc -o inline_asm inline_asm.c

执行结果:

10 + 20 = 30

3.4 内联汇编的优缺点

优点:

  • 无需单独编写汇编文件,代码集中在一个文件中
  • 可以直接访问 C 变量,方便数据交换
  • 适合优化关键代码段

缺点:

  • 代码可读性较差
  • 调试困难
  • 可移植性差(不同编译器语法可能不同)

五、汇编调用 C 实例

汇编调用 C 的调用流程:

汇编函数                  C 函数
   |                         |
   |-- 准备参数 ------------>  |
   |   R0=5                  |
   |                         |
   |-- BL square ----------->|
   |                         |-- 读取参数 x
   |                         |-- 执行乘法运算
   |                         |-- 返回结果
   |                         |
   |<-- 返回 -----------------|
   |   R0=25 (返回值)         |
   |                         |
   |-- 使用返回值              |

1. 实例 1:调用 C 函数计算平方

这个实例展示汇编如何调用 C 函数。

1.1 square.c

c
// 计算平方的函数
int square(int x)
{
    return x * x;
}

1.2 call_square.S

assembly
.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             // 系统调用

编译命令:

bash
arm-linux-gnueabihf-gcc -o asm_call_c call_square.S square.c

执行结果:

程序正常执行,R0 中保存结果 25。

2. 实例 2:调用 C 函数并传递多个参数

这个实例展示汇编如何调用 C 函数并传递多个参数。

2.1 multiply.c

c
// 计算乘积的函数
int multiply(int a, int b, int c)
{
    return a * b * c;
}

2.2 call_multiply.S

assembly
.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             // 系统调用

编译命令:

bash
arm-linux-gnueabihf-gcc -o asm_call_c2 call_multiply.S multiply.c

执行结果:

程序正常执行,R0 中保存结果 24。