历史上的今天
返回首页

历史上的今天

今天是:2024年12月04日(星期三)

正在发生

2019年12月04日 | 浅析ARM汇编语言子例程设计方法

2019-12-04 来源:eefocus

引言

在嵌入式软件系统开发过程中,大量使用C语言进行应用程序开发以提高开发效率。同时,系统中经常包含一些决定整个系统性能的关键模块,此时为了获得最佳性能,经常使用汇编语言编写它们,或者某些特殊情况下,例如操作硬件等,也必须使用汇编语言。


函数是C语言中一个重要的概念,在汇编语言中经常使用子例程或过程(subroutine or procedure)表达同样的概念,本文使用术语子例程。本文首先介绍ARM汇编语言子例程设计的一般方法,并以此为基础提出一种新的基于堆栈帧的设计方法,同时介绍与C语言交互技术。

1 一般方法

在ARM汇编语言中一般使用BL(Branch and Link)指令调用某个子例程,BL指令首先将返回地址保存在链接寄存器R14(也称为LR)中,然后跳转到目标地址。子例程执行完毕后,通过将R14的内容复制到PC中实现从子例程返回。

BL subr              ;调用subr

…                     ;返回到这里

subr

…                     ;子例程体

MOV PC, LR     ;从subr返回

上面这种方法对于叶子例程(即不调用其它子例程的例程)来说已经足够了,但是它并不能处理嵌套或递归调用。假设subr内部又使用BL调用了另一个子例程,那么LR将被后一次调用的返回地址所改写,导致死循环无法从subr返回。为了解决这个问题,subr必须在调用第二个子例程之前保存LR。更进一步,为了使子例程能够以任意深度调用另外一个子例程,必须采取某种方法以保存任意数目的返回地址。最常用的方法是将返回地址保存在堆栈中,如下面的例子所示:

subr

STMFD SP!, {R4-R12, LR}      ;保存所有的工作寄存器和返回地址,并更新堆栈指针

…                                           ;子例程体

LDMFD SP! { R4-R12, PC}       ;恢复所有的工作寄存器,使用保存的返回地址装载PC,

;更新堆栈指针

在子例程入口点可以把subr中需要使用的任何工作寄存器和LR保存到堆栈上,在出口点将它们弹出,这样就可以安全的进行子例程调用,而不必担心返回地址被改写导致无法从子例程正常返回。注意在出口点直接使用返回地址装载PC,它等价于下面的两条指令:

LDMFD SP! { R4-R12, LR}

MOV PC, LR

2 基于堆栈帧的子例程

前面介绍的子例程设计方法虽然已经能够满足设计需要,但是对于熟悉x86汇编语言的程序员来说还是不太适应。众所周知,x86汇编语言子例程存在一个标准的堆栈结构,如图1所示。它的一个显著特点是EBP寄存器作为参考点用来引用参数和局部变量,例如第一个参数位于地址[EBP+8]处。堆栈帧的优点在于它统一了汇编子例程的编程风格,参数、返回地址、工作寄存器或者局部变量都有固定的位置,这样不仅能够提高代码的可读性也有利于代码的维护。基于上面的考虑,特将堆栈帧的概念引入ARM汇编语言子例程的设计之中,如下面的例子所示。为了简便,假设subr的原型为int subr(int a, int b, int c, int d, int e, int f);,很明显根据APCS(ARM过程调用标准),参数a-d通过寄存器R0-R3进行传递,剩下的两个参数e和f通过堆栈传递。最终形成的堆栈帧结构如图2所示,与图1中的x86帧结构相比,唯一的不同之处在于局部变量和工作寄存器的位置相反,而出现这种差异的原因是为了充分利用ARM中多寄存器load-store指令的优势。

caller

 …                                   ;省略了参数a-d的传递代码

MOV R4, #2

STR R4, [SP, #-4]!        ;1)将参数f推入堆栈

MOV R4, #1

STR  R4, [SP, #-4]!        ;将参数e推入堆栈

BL    subr                      ;2)调用子例程subr

ADD SP, SP, #8                ;8)平衡堆栈。subr返回到这里,返回值保存在R0中

 

subr

STMFD     SP!, {R4-R7, FP,LR}    ;3)保存工作寄存器、FP和LR

ADD  FP, SP, #16            ;4)计算帧指针

SUB    SP, SP, #8              ;5)为局部变量分配空间

LDR  R4, [FP, #8]           ;载入参数e

LDR  R5, [FP, #12]        ;载入参数f

…                                   ;subr子例程体

ADD   SP, SP, #8              ;6)释放局部变量空间

LDMFD SP!, {R4-R7, FP, PC}     ;7)恢复寄存器并返回

 

               图1 x86堆栈帧结构                            

       图2 ARM中的堆栈帧结构

下面详细的说明如何一步步构建堆栈帧,其中序号与示例代码注释中的序号是一一对应的:

1)        通常,使用STR Rn, [SP, #-4]!指令将子例程需要的参数推入堆栈。注意根据APCS,首先考虑通过寄存器R0-R3传递参数,剩下的参数以相反的顺序推入堆栈。如果通过寄存器R0-R3就可以传递所有的参数,那么可以省略这个步骤。

2)        BL指令将返回地址推入堆栈,然后跳转到指定的子例程继续执行。自此开始所有修改堆栈的工作转交给子例程。

3)        如果子例程需要使用R4-R11工作寄存器,必须将它们推入堆栈;同时将旧的帧指针寄存器FP和链接寄存器LR推入堆栈,这些工作在一条指令中即可高效的完成。

4)        调整帧指针FP,以便随后使用它引用堆栈参数和变量。在本例中,可以使用LDR R0, [FP, #8]引用参数e,LDR R0, [FP, #-20]引用第一个局部变量。

5)        分配8个字节的堆栈空间存储子例程的局部变量。但是如果不需要使用局部变量,那么可以省略这个步骤。与CISC架构的x86处理器不同,RISC架构的ARM处理器拥有大量的通用寄存器,例如本例中的R0-R7、LR等,因此大多数情况下并不需要为局部变量分配堆栈空间。

6)        如果先前为局部变量分配了堆栈空间,那么为了保持堆栈平衡需要释放它们。

7)        恢复第三步保存到堆栈的各个寄存器,这里也是通过直接装载PC寄存器从子例程返回。

8)        子例程subr执行完成后返回到这里。这一步非常重要,由于caller在调用subr前将参数e和f推入堆栈,因此从subr返回后caller必须将这两个参数弹出堆栈,以保持堆栈的平衡。当然如果是从C语言中调用子例程,那么编译器会负责完成堆栈平衡工作。

3 汇编语言与C语言交互

在完成汇编子例程的编写之后,下一个问题就是如何在C语言中调用它们。本质上,不管使用何种语言编写代码,交叉调用其它模块的例程必须遵循一个通用的参数和结果传递约定。对于ARM来说,这个约定称为ARM过程调用标准,其定义了:

l         通用寄存器的特定用途

l         使用何种类型的堆栈

l         参数和结果的传递机制

l         ARM共享库机制支持

由于编译器生成的代码总是严格遵循APCS,因此只需保证手动编写的汇编代码符合APCS即可。下面的示例展示了如何从C语言中调用汇编语言编写的实现内存拷贝功能的子例程,开发环境为RealView MDK3.22a。

;定义和导出mymemcpy的mymemcpy.s文件

; R0目的地址,R1指向源地址,R2拷贝长度

       AREA Demo, CODE, READONLY

       EXPORT mymemcpy

mymemcpy

       STMFD SP!, {R4,LR}

       MOV R3,R0   ;取出目的地址

       MOV R12,R1 ;取出源地址

copy

       CMP R2, #0    ;如果长度小于等于0则退出

       BLE exit

       SUB R2,R2, #0x1

       BEQ exit

       LDRB LR, [R12],#0x1

       STRB LR, [R3],#0x1

       B copy

exit

       LDMFD R13!,{R4, PC}

       END

//main.c 测试程序

extern void *mymemcpy(void *dst, const void *src, size_t size);

int main(int argc, char** argv)

{

       const char *src = "First string - source ";

       char dst[] = "Second string - destination ";

       mymemcpy(dst, src, strlen(src)+1);

return (0);

}

从汇编语言调用C函数的关键之处在于如何根据C函数的原型正确的传递参数。下面的示例展示了如何调用C库函数strcmp,其原型为int strcmp(const char *s1, const char *s2); ,它只有两个指针类型参数,因此R0和R1分别指向第一个和第二个字符串即可。注意由于使用了C库函数,请选中项目选项对话框、Target选项卡中的Use MicroLib选项。

       AREA |.text|, CODE, READONLY

       EXPORT main       ;导出main

       IMPORT __main

       IMPORT strcmp    ;导入strcmp函数

main

       STMFD SP!, {R4,LR} ;保存LR

       ADR R0, big                 ;通过R0传递参数1

       ADR R1, small              ;通过R1传递参数2

       BL strcmp                    ;调用strcmp库函数

       LDMFD SP!, {R4,PC}

big

      DCB "big",0

small

       DCB "SMALL",0 

       END

小结

本文从作者的实践出发,谈了一些关于ARM汇编子例程设计方法及其与C语言交互的心得,不当之处请读者指正。

参考文献

1.Andrew N.Sloss, Dominic Symes, Chris Wright著. 沈建华译. ARM嵌入式系统开发—软件设计与优化. 北京航空航天大学出版社.

2.David Seal. ARM Architecture Reference Manual, Second Edition, Addison-Wesley.

3.RealView编译工具2.0版-开发者指南, ARM Limited.

推荐阅读

史海拾趣

Dongguan City Niuhang Electronics Co.LTD公司的发展小趣事

在快速发展的同时,Dongguan City Niuhang Electronics Co.LTD始终关注社会责任和可持续发展。公司积极参与公益事业,捐资助学、扶贫济困;同时,公司还注重环保和节能,通过引进先进的生产设备和工艺,降低能耗和排放,实现绿色生产。这些举措不仅提升了公司的社会形象,也为公司的可持续发展注入了新的动力。

请注意,以上故事为虚构内容,旨在展示Dongguan City Niuhang Electronics Co.LTD可能经历的发展阶段和事件。实际情况可能有所不同,具体信息请参考公司官方发布的相关资料。

安信可(Ai)公司的发展小趣事

随着物联网技术的快速发展,安信可也迎来了技术升级的关键时刻。2014年9月,安信可成功引入ESP8266 SoC方案,这一技术升级为公司带来了更多的市场机会。到了2016年5月,安信可更是成功转型为一站式物联网模组解决方案提供商,为客户提供从模组到应用的全方位服务。

成都成电硅海公司的发展小趣事

随着技术的不断成熟和市场的不断扩大,成都成电硅海公司开始寻求更广阔的市场空间。公司积极参加国内外各种行业展会和论坛,与业界同行进行深入的交流和合作。同时,公司还加大了对海外市场的开拓力度,成功将产品打入欧美等发达国家市场。这一系列的市场拓展举措,使得成都成电硅海公司的知名度不断提升,市场份额也逐渐扩大。

亿佰特(EBYTE)公司的发展小趣事

亿佰特注重团队建设和人才培养。公司吸引了一批具有丰富经验和专业技能的人才加入,形成了一支高效、专业的团队。公司还注重员工的培训和发展,为员工提供广阔的晋升空间和职业发展机会。正是这支优秀的团队,为亿佰特的发展提供了源源不断的动力。

General Electric Solid State公司的发展小趣事

亿佰特注重团队建设和人才培养。公司吸引了一批具有丰富经验和专业技能的人才加入,形成了一支高效、专业的团队。公司还注重员工的培训和发展,为员工提供广阔的晋升空间和职业发展机会。正是这支优秀的团队,为亿佰特的发展提供了源源不断的动力。

Excel-Display Corporation公司的发展小趣事

EDC深知人才是企业发展的核心动力。因此,公司一直致力于人才培养和团队建设。

公司建立了完善的人才培养机制,为员工提供各种培训和学习机会,帮助他们不断提升自己的专业能力和综合素质。同时,EDC还注重员工的福利待遇和职业发展,为员工创造了一个良好的工作环境和发展空间。

在团队建设方面,EDC注重营造积极向上的企业文化和团队合作精神。公司定期组织各种团队活动和文化交流活动,增强员工的凝聚力和归属感。这些举措不仅提高了员工的工作积极性和效率,也为公司的长远发展提供了有力保障。

问答坊 | AI 解惑

求梅兰日兰UPS中文使用说明书操作手册

求梅兰日兰UPS中文使用说明书 请问哪位师傅手里有梅兰日兰UPS中文使用说明书啊,帮忙给传一下,或者给个网址也行啊,UPS是4.2KW、6KVA,我先谢谢啦!…

查看全部问答>

8051f320上位机编写?

1.怎么向8051f320的usb口发送信号? 我是指pc端的软件编写。 320是会被识别为hid类吗? 用到的api主要有哪些呢?vb vc 的都行啊 最好是vb 2。我要用320实现usb信号转变成串口信号的功能,主要为了解决笔记本缺少com口的问题。方法是从usb接受数据 ...…

查看全部问答>

谁能救命--关于Mplayer中Demuxer处理流程

哪为江湖大虾有研究过播放器Mplayer的原代码啊,特别是它Demux是如何处理的,最好能讲讲Mplayer的主处理函数,和详细的Demux过程?小弟不胜感激!…

查看全部问答>

请教关于伺服电机精度的问题

小弟请问大家有关于伺服电机的问题.有一位做真空镀膜的客户问到我伺服电机的转矩精度和线性度的参数.请问各位高手转矩精度和线性度是什么意义?…

查看全部问答>

stx-rlink要什么价格?有转让的吗,个人用

                                  …

查看全部问答>

Protues-ATmega8仿真

适用于初学者对定时/计数器,串口通信和LED 数码管的学习 [ 本帖最后由 ydw621 于 2011-4-23 12:27 编辑 ]…

查看全部问答>

急,怎么用595芯片驱动一个四位的数码管

本帖最后由 paulhyde 于 2014-9-15 09:28 编辑 …

查看全部问答>

zb_BindDevice失败

// Find and bind to a collector device     zb_BindDevice( TRUE, SENSOR_REPORT_CMD_ID, (uint8 *)NULL );   如果终端设备的父地址为0,即协调器,可以绑定成功   如果终端设备的父地址非0,即路由器,通过 ...…

查看全部问答>

看看如何进不去中断

以下为调试时的截屏,为何就是进不去中断? 在STRATUP.C中注册了中断函数,把函数名写到向量表的TIMER 0  行。 [ 本帖最后由 hb22s 于 2012-6-17 21:11 编辑 ]…

查看全部问答>