微信号:emLinuxHome

介绍:百分百原创,资深嵌入式架构设计师撰文,分享嵌入式Linux和Android架构技术、系统开发、硬件体系编程、中间件、应用框架研究、系统编程等知识.

出道题考考你的嵌入式软件功底...(ABI)

2017-10-14 15:11 yueqian

一般地,在Linux或者其他操作系统中,一个模块都有一个总入口和多个具体的接口集,例如驱动、中间件等。上层应用调用总入口,并传递具体接口对应的命令号cmd和具体的接口参数,总入口负责分发调用具体的接口来实现。那么,总入口会有以下声明形式:

Drv_entry(uint8_t cmd, void*,void*,void*);

Drv_entry(void*, void*,void*, uint8_tcmd);

两者的不同,就在于cmd的位置不同,那么两者哪种声明比较好呢?如果你能立刻做出正确的选择,那么你的嵌入式软件功底是蛮不错的。如果不能,那说明你还需要好好努力。

这个问题跟ABI和编译优化有关,可以先思考,答案放文章最后。

一、ABI

API是应用编程接口,是高级语言接口。而ABI是应用二进制编程接口,在C和汇编语言混合编程中必须要非常熟练,尤其在操作系统的定制和优化里面常常需要注意和运用。纯C编程虽然可以不用在意ABI,但不熟悉会造成程序性能下降。


ABI要解决的问题包括:1.参数如何传递和返回,不同的参数类型,一个或者多个传参 2. 参数传递过程中对栈、寄存器的利用,尤其是寄存器上下文中,哪些寄存器是调用者维护,哪些是被调用者维护。

ABI是一种编译约定,并不是CPU对软件的强制规定。有一个寄存器例外,就是PC寄存器。编译器必须要使用CPU指定的寄存器作为PC寄存器。

二、ATPCS

ARM体系的ABI。

2.1寄存器约定:

1. 子程序通过寄存器R0~R3来传递参数. 这时寄存器可以记作: A1~A4 , 被调用的子程序在返回前无需恢复寄存器R0~R3的内容.

2. 在子程序中,使用R4~R11来保存局部变量.这时寄存器R4~R11可以记作:V1~V8 .如果在子程序中使用到V1~V8的某些寄存器,子程序进入时必须保存这些寄存器的值,在返回前必须恢复这些寄存器的值,对于子程序中没有用到的寄存器则不必执行这些操作.在THUMB程序中,通常只能使用寄存器R4~R7来保存局部变量.

 3.寄存器R12用作子程序间scratch寄存器,记作ip; 在子程序的连接代码段中经常会有这种使用规则.

 4.寄存器R13用作数据栈指针,记做SP,在子程序中寄存器R13不能用做其他用途. 寄存器SP在进入子程序时的值和退出子程序时的值必须相等.

 5.寄存器R14用作连接寄存器,记作lr ; 它用于保存子程序的返回地址,如果在子程序中保存了返回地址,则R14可用作其它的用途.

 6.寄存器R15是程序计数器,记作PC ; 它不能用作其他用途.

2.2参数的传递规则.

1.参数个数可变的子程序参数传递规则

 对于参数个数可变的子程序,当参数不超过4个时,可以使用寄存器R0~R3来进行参数传递,当参数超过4个时,还可以使用数据栈来传递参数. 在参数传递时,将所有参数看做是存放在连续的内存单元中的字数据。然后,依次将各名字数据传送到寄存器R0,R1,R2,R3; 如果参数多于4个,将剩余的字数据传送到数据栈中,入栈的顺序与参数顺序相反,即最后一个字数据先入栈. 

 2.参数个数固定的子程序参数传递规则

 对于参数个数固定的子程序,参数传递与参数个数可变的子程序参数传递规则不同,如果系统包含浮点运算的硬件部件,浮点参数将按照下面的规则传递: 各个浮点参数按顺序处理;为每个浮点参数分配FP寄存器;分配的方法是,满足该浮点参数需要的且编号最小的一组连续的FP寄存器.第一个整数参数通过寄存器R0~R3来传递,其他参数通过数据栈传递.

2.3参数返回规则

 1.结果为一个32位的整数时,可以通过寄存器R0返回.

 2.结果为一个64位整数时,可以通过R0和R1返回,依此类推.

三、MIPS ABI

       MIPS体系ABI

3.1寄存器约定

 


3.2传参约定

1. 参数从左到右存放在a0,a1,a2,a3寄存器中,返回值存放在v0和v1寄存器中。

2. 每个参数不论是字还是字节都用一个寄存器来传,自动进行0扩展或者有符号扩展。如果是数据结构传递需要模拟内存的存放,而不是一个字节也用一个寄存器来放。传递数据结构需要进行汇编的调试以保证正确。GL5110不允许传递数据结构,应该传递数据结构的指针。GL5110不允许使用浮点数。

3. 当参数个数超过4个时,用栈空间进行传递。事实上,a0,a1,a2,a3表示的参数在栈中也有对应的16字节空间。(是不是有点太浪费栈了...)

4. 返回32位数在v0,64位时为v0和v1。

四、51体系ABI

       有兴趣可以自己编程并切换到汇编模式看看。实践得真知。

五、开篇的答案

       有了上面的提示,应该大致知道原因了,第二种声明才是合理的。

       既然是总入口,那其会根据CMD来进行分发,大致是这样:

       Drv_entry(void*A,void*B,void*C, uint8_t cmd)

{

       Switch(cmd):

              Case CMD1: cmd1(A, B, C);break;

              Case CMD2: cmd2(A, B, C);break;

              …

}

       可以看出,总入口和调用具体接口的A、B、C参数顺序是一样的,因此其在寄存器上的传递安排也是一样的。如果使用编译优化,那上层应用调用总入口之后,A, B, C已经在对应的寄存器上,分发调用具体接口时不需要进行改变。如果将cmd作为第一个参数,那分发调用时需要调整A、B、C所在的寄存器。至于A、B、C在哪个寄存器,不同体系的ABI有不同的约定,看看上面就清楚了。

 

关注微信公众号:嵌入式企鹅圈,百分百原创,上百篇嵌入式和物联网研发分享。


 
嵌入式企鹅圈 更多文章 从需求的角度去理解嵌入式Linux:总线、设备和驱动 嵌入式系统交叉调试原理和方法 蓝牙扫描器的隐患和双芯片握手协议 语音助手的安卓客户端的设计 嵌入式企鹅圈共享资源
猜您喜欢 回到过去,找回遗失的珍宝 - TiDB 的历史读功能 免费翻墙必备方法 同桌的你——消费80后集体回忆的时刻来了 Redis 新数据结构 - Streams 不是人家装逼,是我们太low