一、目的
一般来说ARM的IDE/调试器不提供UCOSII多任务任务调用函数调用关系的查询,本文旨在提供一种查看UCOSII任务调用堆栈的方法。
二、UCOSII任务切换简介
UCOSII任务切换方式有两种,一种是通过触发软中断来切换,另一种是直接跳转(分中断里跳转和任务执行中跳转)。无论怎样切换,都是把当前执行的任务内容压入当前任务的堆栈中去,然后设置任务SP的值为优先级最高的任务的堆栈,然后出栈恢复寄存器为最高优先级的任务挂起前的值,亦即还原任务状态,让最高优先级的任务跑下去。而保存任务挂起前的寄存器的值的堆栈,就是我们跟踪任务的调用关系的工具。
无论哪种方式的切换,最终都是要切换到要执行的任务,亦即把要执行的任务的堆栈出栈,这样,我们可以获知寄存器在任务堆栈里的位置,我们看切换最后面的一点代码就可以获知。例如,我们可以从下面代码获知,挂起任务堆栈里的顺序是:
CPSR,R0,R1...R12,LR,PC。
LDMFD SP!, {R0} ; Pop new task's CPSR,
MSR SPSR_cxsf, R0
; Pop new task's context.
LDMFD SP!, {R0-R12, LR}
LDMFD SP!, {PC}^
UCOSII的任务堆栈保存在每个任务控制块里面,如下所示,其成员OSTCBStkPtr就是任务堆栈指针:
/*
*********************************************************************************************************
* TASK CONTROL BLOCK
*********************************************************************************************************
*/
typedef struct os_tcb {
OS_STK *OSTCBStkPtr; /* Pointer to current top of stack */
#if OS_TASK_CREATE_EXT_EN > 0u
void *OSTCBExtPtr; /* Pointer to user definable data for TCB extension */
OS_STK *OSTCBStkBottom; /* Pointer to bottom of stack */
INT32U OSTCBStkSize; /* Size of task stack (in number of stack elements) */
INT16U OSTCBOpt; /* Task options as passed by OSTaskCreateExt() */
INT16U OSTCBId; /* Task ID (0..65535) */
#endif
struct os_tcb *OSTCBNext; /* Pointer to next TCB in the TCB list */
struct os_tcb *OSTCBPrev; /* Pointer to previous TCB in the TCB list */
#if (OS_EVENT_EN)
OS_EVENT *OSTCBEventPtr; /* Pointer to event control block */
#endif
#if (OS_EVENT_EN) && (OS_EVENT_MULTI_EN > 0u)
OS_EVENT **OSTCBEventMultiPtr; /* Pointer to multiple event control blocks */
#endif
#if ((OS_Q_EN > 0u) && (OS_MAX_QS > 0u)) || (OS_MBOX_EN > 0u)
void *OSTCBMsg; /* Message received from OSMboxPost() or OSQPost() */
#endif
#if (OS_FLAG_EN > 0u) && (OS_MAX_FLAGS > 0u)
#if OS_TASK_DEL_EN > 0u
OS_FLAG_NODE *OSTCBFlagNode; /* Pointer to event flag node */
#endif
OS_FLAGS OSTCBFlagsRdy; /* Event flags that made task ready to run */
#endif
INT32U OSTCBDly; /* Nbr ticks to delay task or, timeout waiting for event */
INT8U OSTCBStat; /* Task status */
INT8U OSTCBStatPend; /* Task PEND status */
INT8U OSTCBPrio; /* Task priority (0 == highest) */
INT8U OSTCBX; /* Bit position in group corresponding to task priority */
INT8U OSTCBY; /* Index into ready table corresponding to task priority */
OS_PRIO OSTCBBitX; /* Bit mask to access bit position in ready table */
OS_PRIO OSTCBBitY; /* Bit mask to access bit position in ready group */
#if OS_TASK_DEL_EN > 0u
INT8U OSTCBDelReq; /* Indicates whether a task needs to delete itself */
#endif
#if OS_TASK_PROFILE_EN > 0u
INT32U OSTCBCtxSwCtr; /* Number of time the task was switched in */
INT32U OSTCBCyclesTot; /* Total number of clock cycles the task has been running */
INT32U OSTCBCyclesStart; /* Snapshot of cycle counter at start of task resumption */
OS_STK *OSTCBStkBase; /* Pointer to the beginning of the task stack */
INT32U OSTCBStkUsed; /* Number of bytes used from the stack */
#endif
#if OS_TASK_NAME_EN > 0u
INT8U *OSTCBTaskName;
#endif
#if OS_TASK_REG_TBL_SIZE > 0u
INT32U OSTCBRegTbl[OS_TASK_REG_TBL_SIZE];
#endif
INT8U need_free_stack;
} OS_TCB;
三、函数调用指令简介
函数调用一般都是在函数开始处把函数里用到的寄存器和LR压入栈,函数内容处理完后,再出栈到寄存器和PC,返回调用处。
四、方法
通过二、三的介绍,我们知道对于不是正在执行的任务,其每一级的执行状态是保存到其自身的堆栈里的,调用关系完全可以通过任务堆栈来跟踪。具体操作如下:
a.通过反编译工具把axf文件转换成汇编文件,用以查看压入栈的寄存器个数和地址对应的函数名;
b.遍历任务控制块(保存在OSTCBPrioTbl里),打印其堆栈指针;
c.根据二里介绍查看任务堆栈寄存器位置和个数的方法,打印出任务挂起压入堆栈的寄存器,找到切换前的PC;
d.查看PC值处的汇编代码,可以找到它是在哪里(哪个函数)被切换的出去的;
e.如果需要跟踪它是被哪个函数调用的,可以向上查找,统计被压入栈的寄存器个数,直至函数入口处,确定LR在这一块里偏移;
f.打印栈里该函数压入栈的寄存器出来,根据压入栈LR的偏移,获取LR的值,就可以确定它是被哪个函数调用的;
g.重复e-f,可以找出所有的调用关系。
五、注意事项
大部分UCOSII的堆栈都是配置为向下增长的,所以知道压入栈的个数后,获取寄存器的值用的偏移要按照出栈的顺序来。