对于ARM体系来说,不同语言撰写的函数之间相互调用(mix calls)遵循的是 ATPCS(ARM-Thumb Procedure Call Standard),ATPCS主要是定义了函数呼叫时参数的传递规则以及如何从函数返回。
文章目录
一、准备工作
1)创建一个新项目。
2)根据自身情况选择硬件,配置一些参数。
3)为 SOURCE GROUP 1
新建两个文件main.c(C程序)和Func.s(汇编程序)。
二、C语言调用汇编函数
C 程序调用汇编程序时,汇编程序的书写也要遵循ATPCS规则,以保证程序调用时参数正确传递。在C程序中调用汇编子程序的方法为:首先在汇编程序中使用 EXPORT伪指令声明被调用的子程序,表示该子程序将在其他文件中被调用;然后在C程序中使用extern关键字声明要调用的汇编子程序为外部函数。
1.C语言无参数调用汇编函数
Func.s代码
AREA MY_FUNCTION,CODE,READONLY
EXPORT Init_1 ;//与在c文件中定义的Init_1函数关联起来
; //高级语言中的声明和使用变量其实是对板子寄存器的使用,所以我们只需要直接使用寄存器即可
Init_1
MOV R1,#0 ; //设R1寄存器为i
MOV R2,#0 ; //设R2寄存器为j
LOOP ;// 写在最左边的是程序段的段名,执行跳转程序时用到
CMP R1,#10 ; //比较R1和10的大小
BHS LOOP_END ;// 如果R1大于等于10,则跳转到LOOP_END程序段,反之忽略该语句,直接执行下面的语句
ADD R2,#1 ; //j++
ADD R1,#1 ; //i++
B LOOP ; //循环
LOOP_END
NOP
END ; //必须空格后再写END,不然会被认为是段名,表示程序结束
main.c代码
# include<stdio.h>
extern void Init_1(void);//与在汇编文件中定义的Init_1函数关联起来
int main()
{
Init_1();
return 0;
}
在图示处设置五个断点。
点击translate
、build
,然后点击debug
,选择第一个选项开始仿真调试。
点击单步运行
,观察左边界面寄存器的变化状况。
R0、R1从0开始,每次加1直到加到10,这与编写的代码相一致。
二、C语言有参数调用汇编函数
1.函数形参小于4个
修改Func.s代码
AREA My_Function,CODE,READONLY;
EXPORT Init_1;
;
Init_1
ADD R0,#100;//将R0与100相加
END;
修改main.c代码
#include<stdio.h>
extern int Init_1(int x);
int main()
{
Init_1(10);
return 0;
}
看到这里难免有点疑惑,这里似乎没有直接的语句写明形参x的值会存放到R0,那指令ADD R0,#100
中传入的整型数x为什么可以和100相加?
设置两个断点。
然后开始仿真调试,点击单步运行
,观察左边界面寄存器的变化状况。
可以看到传入的整型数x被传递到R0。
且最后的运算结果为32位整数,则该由R0返回,这是ATPCS规定的参数的传递规则,最后R0的值为110。
2.函数形参多于4个
main.c代码(10个参数)
#include <stdio.h>
int fun(int n0,int n1,int n2,int n3,int n4,int n5,
int n6,int n7,int n8,int n9)
{
int m;
m=n0+n1+n2+n3+n4+n5+n6+n7+n8+n9;
return m;
}
void main(void)
{
int num;
num=fun(1,2,3,4,5,6,7,8,9,10);
}
为了搞清楚在ARM函数传递机制,在ubuntu18.04下安装arm-linux-gcc。安装好了后输入以下命令得到汇编文件。
arm-linux-gcc -s main.c -o main.s
输入以下命令查看汇编文件。
cat main.s
可以看出,子函数的参数值传递按顺序存放在R0,R1,R2,R3里,超过4个参数值传递放栈帧里。想详细了解C语言在ARM中函数调用时栈的变化过程,可参考下面的第一条链接。
三、汇编函数调用C函数
在汇编程序中调用C程序的方法为:首先在汇编程序中使用IMPORT伪指令事先声明将要调用的C语言函数;然后通过BL指令来调用C函数。
修改Func.s代码
AREA MY_Function,CODE,READONLY
EXPORT Init_1 ;// 与在c文件中定义的Init_1函数关联起来
IMPORT get5 ; //声明get5 为外部引用
;// 高级语言中的声明和使用变量其实是对板子寄存器的使用,所以我们只需要直接使用寄存器即可
Init_1
MOV R1,#0 ; //设R1寄存器为i
MOV R2,#0 ; //设R2寄存器为j
LOOP //写在最左边的是程序段的段名,执行跳转程序时用到
CMP R1,#10 ; //比较R1和10的大小
BHS LOOP_END ;// 如果R1大于等于10,则跳转到LOOP_END程序段,反之忽略该语句,直接执行下面的语句
ADD R2,#1 ; //j++
ADD R1,#1 ; //i++
BL get5 ; //调用get5,返回的值传入R0
B LOOP ; //循环
LOOP_END
NOP
END ;// 必须空格后再写END,不然会被认为是段名,表示程序结束
修改main.c代码
# include<stdio.h>
extern void Init_1(void);
int get5(void);
int main()
{
Init_1();
return 0;
}
int get5()
{
return 5;
}
设置断点后进入仿真调试,观察左边界面寄存器的而变化状况。
从以上过程来看,R1、R2每次循环加1,而get5( )的返回值总是被返回到R0且均为5,这是由于该返回结果为一个32位整数。
四、总结C语言与汇编语言混合编程的规则
1.寄存器的使用规则
子程序之间通过寄存器R0-R3来传递参数,当参数个数多于4个时,使用堆栈来传递参数,此时R0-R3可记作A1-A4。在子程序中,使用寄存器R4-R11保存局部变量。因此当进行子程序调用时要注意对这些寄存器的保存和恢复,此时R4-R11可记作V1-V8。寄存器R12用于保存堆栈指针SP,当子程序返回时使用该寄存器出栈,记作IP。寄存器R13用作堆栈指针,记作SP。寄存器R14称为链接寄存器,记作LR,该寄存器用于保存子程序的返回地址。寄存器R15称为程序计数器,记作PC。
2.堆栈的使用规则
ATPCS规定堆栈采用满递减类型(FD,Full Descending),即堆栈通过减小存储器地址而向下增长,堆栈指针指向内含有效数据项的最低地址。
3.参数的传递规则
1)C语言在ARM中函数调用时,如果形参个数少于或等于4,则形参由R0,R1,R2,R3四个寄存器进行传递;若形参个数大于4,大于4的部分必须通过堆栈进行传递,且压入堆栈的顺序是与函数中形参的顺序相反。
2)对于X86平台,32位程序使用栈传递,而64位程序则根据参数的个数而不同。 当参数少于6,使用寄存器传递参数;当参数大于6,多出来的参数使用栈传递。可以参考以下第二条链接验证看看。
3)子程序的返回结果为一个32位整数时,通过R0返回;返回结果为一个64位整数时,通过R0和R1返回;依此类推。结果为浮点数时,通过浮点运算部件的寄存器F0、D0或者S0返回。
五、心得体会
学习C语言和汇编语言混合编程的过程中,了解了许多与自己固有认识不同的知识,像是这其中的参数传递机制,而且这个探索过程真的很有趣!