ARM汇编子例程设计方法及其与C语言交互
eetrendMcu 2021-04-08

引言

在嵌入式软件系统开发过程中,大量使用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语言交互的心得,不当之处请读者指正。 

声明: 本文转载自其它媒体或授权刊载,目的在于信息传递,并不代表本站赞同其观点和对其真实性负责,如有新闻稿件和图片作品的内容、版权以及其它问题的,请联系我们及时删除。(联系我们,邮箱:evan.li@aspencore.com )
0
评论
热门推荐
  • 相关技术文库
  • 单片机
  • 嵌入式
  • MCU
  • STM
  • 单片机基础及主流厂商一览

    MCU是Microcontroller Unit 的简称,中文叫微控制器,俗称单片机 ,是把CPU的频率与规格做适当缩减,并将内存、计数器、USB、A/D转换、UART、PLC、DMA等周边接口,甚至LCD驱动电路都整合在单一芯片上,形成芯片级的计算机,为不同的应用场合做不同组合控制,

    昨天
  • 单片机的几种数字滤波算法

    单片机主要作用是控制外围的器件,并实现一定的通信和数据处理。但在某些特定场合,不可避免地要用到数学运算,尽管单片机并不擅长实现算法和进行复杂的运算。下面主要是介绍如何用单片机实现数字滤波。 在单片机进行数据采集时,会遇到数据的随机误差,随机

    前天
  • 故障排查,软件遇到IO异常怎么回事

    软件工程师在调试样板时,遇到这样一个问题,有个按键一直没有反应。他检查了按键IO的配置,确定已经是配置成了输入模式,试了很多遍,都是一样,怎么按按键,都是没有反应。 于是,必需硬件工程师出马。我的排查过程是这样的。 首先,样板断电用万用表的短路

    04-19
  • 智能化交通信号机解决方案

    面向交通信号灯行业,ZLG推出智能化交通信号机解决方案,该方案可大幅提升设备的智能化和道路使用率,改善道路拥堵,打造高效畅通的“智慧交通”。   行业背景 据公安部统计,2020年,全国机动车保有量达3.72亿辆,其中汽车2.81亿辆。百度地图发布的《2020年度

    04-16
  • 单片机如何实现Bootloader?

    去某新能源大厂出了一次差,这次出差是为了升级程序解决Bug,需要给单片机重新烧录.hex文件,用户已经将产品封装起来,无法开盖,只能使用CAN总线来更新程序,用Bootloader实现。其实就是通过上位机把.bin/hex文件以CAN通讯的方式发送给单片机并存储在规定的F

    04-13
  • 单片机应用系统的开发流程

    我们学习单片机的目的就是为了进行嵌入式系统的开发,学好单片机首先要有一个整体认识,下面将简要介绍一下单片机应用系统的开发流程,如图1所示。 图1 单片机系统开发流程 (1)明确任务 分析和了解项目的总体要求,并综合考虑系统使用环境、可靠性要求、可维

    04-12
  • 太经典了!用最少的IO口,扫最多的键

    在做项目(工程)的时候,我们经常要用到比较多的按键,而且IO资源紧张,于是我们就想方设法地在别的模块中节省IO口,好不容易挤出一两个IO口,却发现仍然不够用,实在没办法了就添加一个IC来扫键。一个IC虽然价格不高,但对于大批量生产而且产品利润低的厂家

    03-30
  • 单片机的学习方法和步骤

    注 | 文末留言有福利 作为一名电子技术从业人员,你学过单片机吗?你会运用单片机吗? 我想你一定学过,但不一定会运用。 因为学习单片机比学习其他学科需要付出更多的努力和代价,不仅要学习理论知识还要练习实际操作,而且主要是在实际操作中才能真正学到单

    03-25
  • 单片机内存的分配

    单片机执行指令过程详解 单片机执行程序的过程,实际上就是执行我们所编制程序的过程。即逐条指令的过程。计算机每执行一条指令都可分为三个阶段进行。即取指令-----分析指令-----执行指令。 取指令的任务是:根据程序计数器PC中的值从程序存储器读出现行指令

    03-22
  • 几种常用单片机之间的通信方式

    越来越多的功能各异的单片机为我们的设计提供了许多新的方法与思路。对于莫一些场合,比如:复杂的后台运算及通信与高实时性前台控制系统、软件资源消耗大的系统、功能强大的低消耗系统、加密系统等等。如果合理使用多种不同类型的单片机组合设计,可以得到极

    03-17
  • STM32F4的总线架构和STM8的中断控制

    STM32F4的总线架构 总线架构    DMA: Direct Memory Access,直接内存存取。    八条主控总线: Cortex-M4 内核I总线,D总线和S总线; DMA1存储器总线,DMA2存储器总线; DMA2外设总线; 以太网DMA总线; USB OTG HS DMA总线。 七条被控总线: 内部FLASH ICo

    03-17
  • 单片机P0口必须加上上拉电阻?

    在我们刚一开始接触到51单片机的时候对P0口必须加上上拉电阻,否则P0就是高阻态。 对这个问题可能感到疑惑,为什么是高阻态?加上拉电阻?今天针对这一概念进行简单讲解。 高阻态 高阻态这是一个数字电路里常见的术语,指的是电路的一种输出状态,既不是高电

    03-15
下载排行榜
更多
广告
X
广告