嵌入式操作系统漫议:任务调用栈

在进行嵌入式系统设计时,很重要的一个方面是决定整个系统由几个任务构成、任务的优先级以及任务调用栈(call stack)的大小。本篇主要介绍调用栈的相关概念。

调用栈是用于保存任务运行时的临时变量的内存区域,以堆栈的形式管理。图1是一个调用栈的结构示意图。调用栈中主要保存函数的形参、局部变量和返回地址。因此调用栈的大小主要根据该任务函数的局部变量所占空间的大小和函数的调用深度来决定。

在很多MCU架构(包括ARM系列)中,调用栈按内存地址递减的方向生长,也即栈底在高地址,栈顶在低地址。每次函数调用时,系统将返回地址、寄存器值、函数参数等压入堆栈,同时在堆栈中为局部变量留出空间,而这构成一个栈帧,对应一次函数调用。因此,在多次函数调用时,多个栈帧从高地址区往低地址区排列,最近的栈帧在低地址区,靠近栈顶。

 

1 调用栈结构

对调用栈的操作有压栈(push)和弹栈(pop)两种。其中的数据遵循先进后出(FILO)的原则。图2是一个压栈和弹栈的操作示意图。栈指针总是指向堆栈中下一个可以保存的位置,当压入0x5678时,栈指针自动往下移一个字(32位MCU的话,4个字节)。继续压入0x9abc,栈指针继续下移一个字。此时执行pop操作,则后压入的0x9abc先被弹出,栈指针上移一个字。此时继续执行pop,则0x5678被弹出。因此在堆栈中数据是先进后出。

 

2 栈操作示意图

 

调用侧:

function1(prt1, usC1, usC2, 1, 0)

MOVS r3,#0x01

MOV  r2,r11

MOV  r1,r8

STR  r0,[sp,#0x00]

MOV  r0,r5

BL.W function1 (0x08006A48)

 

被调用侧:

function1(void *pHead, uint16_t usV1, uint16_t usV2, uint8_t ucV3, uint8_t ucV4)

PUSH    {r4-r8,lr}

MOV     r4,r0

MOV     r5,r1

MOV     r7,r2

MOV     r6,r3

LDR     r8,[sp,#0x18]


在ARM系列MCU中,提供了13个通用寄存器(R0-R12)。在函数调用时,编译器一般优先使用这些通用寄存器来传递函数参数和保存局部变量。因此在压栈时,用于传递函数参数的寄存器是不需要压入调用栈的。下面示例的汇编代码是Keil编译的一个函数调用的参数传递过程。

 

这是调用一个有5个参数的函数(function1)的调用侧和被调用侧与函数调用相关的代码片段。通过分析前面的代码,在调用侧,r11存有usC2,r8存有usC1,r5存有ptr1,r0被设为0。在调用函数function1时,从左到右将ptr1、usC1、usC2、1分别保存到r0-r3之中,而将最后一个参数0压入堆栈。而在被调用侧,则首先将r4-r8及lr(返回地址寄存器)压入堆栈,然后将r0-r3分别赋给了r4-r7(注意并不是按顺序赋值的,r2赋给了r7,r3赋给了r6),然后将最后一个参数从堆栈中取出,赋给r8。此处,有几点需要注意的,一是将多少个寄存器压入堆栈取决于被调用函数中会用到几个寄存器,只需要将可能修改的寄存器压栈即可。二是从堆栈取数的时候,因为是取堆栈中间的一个数,因此通过从栈指针的偏移量来计算。因为压入了24个字节,故而需要将SP偏移24个字节来取得最后一个函数参数。还有,因为ARM的堆栈是向下生长的,因此需要加24个字节。

调用栈的大小在任务创建时指定,而且在任务生存期内不再变化。如果指定一个过大的调用栈,会造成内存的浪费,而在嵌入式系统中,内存资源是非常宝贵的;但如果指定的调用栈过小,则可能导致堆栈越界的问题,会导致调用栈中的内容被覆盖。调用栈越界会表现出各种各样稀奇古怪的问题,比如系统崩溃、变量值发生了莫名其妙的改变等等。而且问题的发生可能不具有确定性。因此这种问题的定位和解决往往会花费大量时间。因此为了防止和快速定位堆栈越界问题,从芯片、操作系统到集成开发环境提供了各种各样的调试工具。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值