OpenHarmony设备开发 轻量系统内核(LiteOS-M)内核调测(上)
内存调测
内存调测方法旨在辅助定位动态内存相关问题,提供了基础的动态内存池信息统计手段,向用户呈现内存池水线、碎片率等信息;提供了内存泄漏检测手段,方便用户准确定位存在内存泄漏的代码行,也可以辅助分析系统各个模块内存的使用情况;提供了踩内存检测手段,可以辅助定位越界踩内存的场景。
内存信息统计
基础概念
内存信息包括内存池大小、内存使用量、剩余内存大小、最大空闲内存、内存水线、内存节点数统计、碎片率等。
- 内存水线:即内存池的最大使用量,每次申请和释放时,都会更新水线值,实际业务可根据该值,优化内存池大小;
- 碎片率:衡量内存池的碎片化程度,碎片率高表现为内存池剩余内存很多,但是最大空闲内存块很小,可以用公式(fragment=100-100*最大空闲内存块大小/剩余内存大小)来度量;
- 其他参数:通过调用接口(详见内存管理章节接口说明),扫描内存池的节点信息,统计出相关信息。
功能配置
LOSCFG_MEM_WATERLINE:开关宏,默认打开;若关闭这个功能,在target_config.h中将这个宏定义为0。如需获取内存水线,需要打开该配置。
开发指导
开发流程
关键结构体介绍:
typedef struct {
UINT32 totalUsedSize; // 内存池的内存使用量
UINT32 totalFreeSize; // 内存池的剩余内存大小
UINT32 maxFreeNodeSize; // 内存池的最大空闲内存块大小
UINT32 usedNodeNum; // 内存池的非空闲内存块个数
UINT32 freeNodeNum; // 内存池的空闲内存块个数
#if (LOSCFG_MEM_WATERLINE == 1) // 默认打开,如需关闭,在target_config.h中将该宏设置为0
UINT32 usageWaterLine; // 内存池的水线值
#endif
} LOS_MEM_POOL_STATUS;
- 内存水线获取:调用LOS_MemInfoGet接口,第1个参数是内存池首地址,第2个参数是LOS_MEM_POOL_STATUS类型的句柄,其中字段usageWaterLine即水线值。
- 内存碎片率计算:同样调用LOS_MemInfoGet接口,可以获取内存池的剩余内存大小和最大空闲内存块大小,然后根据公式(fragment=100-100*最大空闲内存块大小/剩余内存大小)得出此时的动态内存池碎片率。
编程实例
本实例实现如下功能:
1.创建一个监控任务,用于获取内存池的信息;
2.调用LOS_MemInfoGet接口,获取内存池的基础信息;
3.利用公式算出使用率及碎片率。
示例代码
代码实现如下:
本演示代码在 ./kernel/liteos_m/testsuites/src/osTest.c 中编译验证,在TestTaskEntry中调用验证入口函数MemTest。
#include <stdio.h>
#include <string.h>
#include "los_task.h"
#include "los_memory.h"
#include "los_config.h"
#define TEST_TASK_PRIO 5
void MemInfoTaskFunc(void)
{
LOS_MEM_POOL_STATUS poolStatus = {0};
/* pool为要统计信息的内存地址,此处以OS_SYS_MEM_ADDR为例 */
void *pool = OS_SYS_MEM_ADDR;
LOS_MemInfoGet(pool, &poolStatus);
/* 算出内存池当前的碎片率百分比 */
float fragment = 100 - poolStatus.maxFreeNodeSize * 100.0 / poolStatus.totalFreeSize;
/* 算出内存池当前的使用率百分比 */
float usage = LOS_MemTotalUsedGet(pool) * 100.0 / LOS_MemPoolSizeGet(pool);
printf("usage = %f, fragment = %f, maxFreeSize = %d, totalFreeSize = %d, waterLine = %d\n", usage, fragment,
poolStatus.maxFreeNodeSize, poolStatus.totalFreeSize, poolStatus.usageWaterLine);
}
int MemTest(void)
{
unsigned int ret;
unsigned int taskID;
TSK_INIT_PARAM_S taskStatus = {0};
taskStatus.pfnTaskEntry = (TSK_ENTRY_FUNC)MemInfoTaskFunc;
taskStatus.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
taskStatus.pcName = "memInfo";
taskStatus.usTaskPrio = TEST_TASK_PRIO;
ret = LOS_TaskCreate(&taskID, &taskStatus);
if (ret != LOS_OK) {
printf("task create failed\n");
return -1;
}
return 0;
}
结果验证
编译运行输出的结果如下:
usage = 0.458344, fragment = 0.000000, maxFreeSize = 16474928, totalFreeSize = 16474928, waterLine = 76816
根据实际运行环境,上文中的数据会有差异,非固定结果
内存泄漏检测
基础概念
内存泄漏检测机制作为内核的可选功能,用于辅助定位动态内存泄漏问题。开启该功能,动态内存机制会自动记录申请内存时的函数调用关系(下文简称LR)。如果出现泄漏,就可以利用这些记录的信息,找到内存申请的地方,方便进一步确认。
功能配置
- LOSCFG_MEM_LEAKCHECK:开关宏,默认关闭;若打开这个功能,在target_config.h中将这个宏定义为1。
- LOSCFG_MEM_RECORD_LR_CNT:记录的LR层数,默认3层;每层LR消耗sizeof(void *)字节数的内存。
- LOSCFG_MEM_OMIT_LR_CNT:忽略的LR层数,默认4层,即从调用LOS_MemAlloc的函数开始记录,可根据实际情况调整。为啥需要这个配置?有3点原因如下:
- LOS_MemAlloc接口内部也有函数调用;
- 外部可能对LOS_MemAlloc接口有封装;
- LOSCFG_MEM_RECORD_LR_CNT 配置的LR层数有限;
正确配置这个宏,将无效的LR层数忽略,就可以记录有效的LR层数,节省内存消耗。
开发指导
开发流程
该调测功能可以分析关键的代码逻辑中是否存在内存泄漏。开启这个功能,每次申请内存时,会记录LR信息。在需要检测的代码段前后,调用LOS_MemUsedNodeShow接口,每次都会打印指定内存池已使用的全部节点信息,对比前后两次的节点信息,新增的节点信息就是疑似泄漏的内存节点。通过LR,可以找到具体申请的代码位置,进一步确认是否泄漏。
调用LOS_MemUsedNodeShow接口输出的节点信息格式如下:每1行为一个节点信息;第1列为节点地址,可以根据这个地址,使用GDB等手段查看节点完整信息;第2列为节点的大小,等于节点头大小+数据域大小;第3~5列为函数调用关系LR地址,可以根据这个值,结合汇编文件,查看该节点具体申请的位置。
node size LR[0] LR[1] LR[2]
0x10017320: 0x528 0x9b004eba 0x9b004f60 0x9b005002
0x10017848: 0xe0 0x9b02c24e 0x9b02c246 0x9b008ef0
0x10017928: 0x50 0x9b008ed0 0x9b068902 0x9b0687c4
0x10017978: 0x24 0x9b008ed0 0x9b068924 0x9b0687c4
0x1001799c: 0x30 0x9b02c24e 0x9b02c246 0x9b008ef0
0x100179cc: 0x5c 0x9b02c24e 0x9b02c246 0x9b008ef0
注意:
开启内存检测会影响内存申请的性能,且每个内存节点都会记录LR地址,内存开销也加大。
编程实例
本实例实现如下功能:构建内存泄漏代码段。
- 调用LOS_MemUsedNodeShow接口,输出全部节点信息打印;
- 申请内存,但没有释放,模拟内存泄漏;
- 再次调用LOS_MemUsedNodeShow接口,输出全部节点信息打印;
- 将两次log进行对比,得出泄漏的节点信息;
- 通过LR地址,找出泄漏的代码位置;
示例代码
代码实现如下:
本演示代码在 ./kernel/liteos_m/testsuites/src/osTest.c 中编译验证,在TestTaskEntry中调用验证入口函数MemLeakTest。
qemu平台运行时需确保target_config.h 中对应的LOSCFG_MEM_FREE_BY_TASKID为0。
由于打开内存检测后,部分平台有其他任务运行,会频繁调用内存相关打印如:psp, start = xxxxx, end = xxxxxxx,请忽略打印或删除OsStackAddrGet函数中调用的打印即可。
#include <stdio.h>
#include <string.h>
#include "los_memory.h"
#include "los_config.h"
void MemLeakTest(void)
{
LOS_MemUsedNodeShow(LOSCFG_SYS_HEAP_ADDR);
void *ptr1 = LOS_MemAlloc(LOSCFG_SYS_HEAP_ADDR, 8);
void *ptr2 = LOS_MemAlloc(LOSCFG_SYS_HEAP_ADDR, 8);
LOS_MemUsedNodeShow(LOSCFG_SYS_HEAP_ADDR);
}
结果验证
编译运行输出示例log如下:
node size LR[0] LR[1] LR[2]
0x20001b04: 0x24 0x08001a10 0x080035ce 0x080028fc
0x20002058: 0x40 0x08002fe8 0x08003626 0x080028fc
0x200022ac: 0x40 0x08000e0c 0x08000e56 0x0800359e
0x20002594: 0x120 0x08000e0c 0x08000e56 0x08000c8a
0x20002aac: 0x56 0x08000e0c 0x08000e56 0x08004220
node size LR[0] LR[1] LR[2]
0x20001b04: 0x24 0x08001a10 0x080035ce 0x080028fc
0x20002058: 0x40 0x08002fe8 0x08003626 0x080028fc
0x200022ac: 0x40 0x08000e0c 0x08000e56 0x0800359e
0x20002594: 0x120 0x08000e0c 0x08000e56 0x08000c8a
0x20002aac: 0x56 0x08000e0c 0x08000e56 0x08004220
0x20003ac4: 0x1d 0x08001458 0x080014e0 0x080041e6
0x20003ae0: 0x1d 0x080041ee 0x08000cc2 0x00000000
根据实际运行环境,上文中的数据会有差异,非固定结果
对比两次log,差异如下,这些内存节点就是疑似泄漏的内存块:
0x20003ac4: 0x1d 0x08001458 0x080014e0 0x080041e6
0x20003ae0: 0x1d 0x080041ee 0x08000cc2 0x00000000
根据实际运行环境,上文中的数据会有差异,非固定结果
部分汇编文件如下:
MemLeakTest:
0x80041d4: 0xb510 PUSH {R4, LR}
0x80041d6: 0x4ca8 LDR.N R4, [PC, #0x2a0] ; g_memStart
0x80041d8: 0x0020 MOVS R0, R4
0x80041da: 0xf7fd 0xf93e BL LOS_MemUsedNodeShow ; 0x800145a
0x80041de: 0x2108 MOVS R1, #8
0x80041e0: 0x0020 MOVS R0, R4
0x80041e2: 0xf7fd 0xfbd9 BL LOS_MemAlloc ; 0x8001998
0x80041e6: 0x2108 MOVS R1, #8
0x80041e8: 0x0020 MOVS R0, R4
0x80041ea: 0xf7fd 0xfbd5 BL LOS_MemAlloc ; 0x8001998
0x80041ee: 0x0020 MOVS R0, R4
0x80041f0: 0xf7fd 0xf933 BL LOS_MemUsedNodeShow ; 0x800145a
0x80041f4: 0xbd10 POP {R4, PC}
0x80041f6: 0x0000 MOVS R0, R0
根据实际运行环境,上文中的数据会有差异,非固定结果
其中,通过查找0x080041ee,就可以发现该内存节点是在MemLeakTest接口里申请的且是没有释放的。
踩内存检测
基础概念
踩内存检测机制作为内核的可选功能,用于检测动态内存池的完整性。通过该机制,可以及时发现内存池是否发生了踩内存问题,并给出错误信息,便于及时发现系统问题,提高问题解决效率,降低问题定位成本。
功能配置
LOSCFG_BASE_MEM_NODE_INTEGRITY_CHECK:开关宏,默认关闭;若打开这个功能,在target_config.h中将这个宏定义为1。
- 开启这个功能,每次申请内存,会实时检测内存池的完整性。
- 如果不开启该功能,也可以调用LOS_MemIntegrityCheck接口检测,但是每次申请内存时,不会实时检测内存完整性,而且由于节点头没有魔鬼数字(开启时才有,省内存),检测的准确性也会相应降低,但对于系统的性能没有影响,故根据实际情况开关该功能。
由于该功能只会检测出哪个内存节点被破坏了,并给出前节点信息(因为内存分布是连续的,当前节点最有可能被前节点破坏)。如果要进一步确认前节点在哪里申请的,需开启内存泄漏检测功能,通过LR记录,辅助定位。
注意:
开启该功能,节点头多了魔鬼数字字段,会增大节点头大小。由于实时检测完整性,故性能影响较大;若性能敏感的场景,可以不开启该功能,使用LOS_MemIntegrityCheck接口检测。
开发指导
开发流程
通过调用LOS_MemIntegrityCheck接口检测内存池是否发生了踩内存,如果没有踩内存问题,那么接口返回0且没有log输出;如果存在踩内存问题,那么会输出相关log,详见下文编程实例的结果输出。
编程实例
本实例实现如下功能:
- 申请两个物理上连续的内存块;
- 通过memset构造越界访问,踩到下个节点的头4个字节;
- 调用LOS_MemIntegrityCheck检测是否发生踩内存。
示例代码
代码实现如下:
本演示代码在 ./kernel/liteos_m/testsuites/src/osTest.c 中编译验证,在TestTaskEntry中调用验证入口函数MemIntegrityTest。
qemu平台运行时需确保target_config.h 中对应的LOSCFG_MEM_FREE_BY_TASKID为0。
由于执行时主动触发异常,执行结束后需要重启qemu(例如打开一个新的终端界面输入killall qemu-system-arm)
#include <stdio.h>
#include <string.h>
#include "los_memory.h"
#include "los_config.h"
void MemIntegrityTest(void)
{
/* 申请两个物理连续的内存块 */
void *ptr1 = LOS_MemAlloc(LOSCFG_SYS_HEAP_ADDR, 8);
void *ptr2 = LOS_MemAlloc(LOSCFG_SYS_HEAP_ADDR, 8);
/* 第一个节点内存块大小是8字节,那么12字节的清零,会踩到第二个内存节点的节点头,构造踩内存场景 */
memset(ptr1, 0, 8 + 4);
LOS_MemIntegrityCheck(LOSCFG_SYS_HEAP_ADDR);
}
结果验证
编译运行输出log如下:
/* 提示信息,检测到哪个字段被破坏了,用例构造了将下个节点的头4个字节清零,即魔鬼数字字段 */
[ERR][IT_TST_INI][OsMemMagicCheckPrint], 1664, memory check error!
memory used but magic num wrong, magic num = 0x0
/* 被破坏节点和其前节点关键字段信息,分别为其前节点地址、节点的魔鬼数字、节点的sizeAndFlag;可以看出被破坏节点的魔鬼数字字段被清零,符合用例场景 */
broken node head: 0x2103d7e8 0x0 0x80000020, prev node head: 0x2103c7cc 0xabcddcba 0x80000020
/* 节点的LR信息需要开启前文的内存泄漏检测功能才有有效输出 */
broken node head LR info:
LR[0]:0x2101906c
LR[1]:0x0
LR[2]:0x0
/* 通过LR信息,可以在汇编文件中查找前节点是哪里申请,然后排查其使用的准确性 */
pre node head LR info:
LR[0]:0x2101906c
LR[1]:0x0
LR[2]:0x0
/* 被破坏节点和其前节点的地址 */
[ERR][IT_TST_INI]Memory integrity check error, cur node: 0x2103d784, pre node: 0x0
根据实际运行环境,上文中的数据会有差异,非固定结果
异常调测
基本概念
OpenHarmony LiteOS-M提供异常接管调测手段,帮助开发者定位分析问题。异常接管是操作系统对运行期间发生的异常情况进行处理的一系列动作,例如打印异常发生时异常类型、发生异常时的系统状态、当前函数的调用栈信息、CPU现场信息、任务调用堆栈等信息。
运行机制
栈帧用于保存函数调用过程中的函数参数、变量、返回值等信息。调用函数时,会创建子函数的栈帧,同时将函数入参、局部变量、寄存器入栈。栈帧从高地址向低地址生长。以ARM32 CPU架构为例,每个栈帧中都会保存PC、LR、SP和FP寄存器的历史值。LR链接寄存器(Link Register)指向函数的返回地址,FP帧指针寄存器(Frame Point)指向当前函数的父函数的栈帧起始地址。利用FP寄存器可以得到父函数的栈帧,从栈帧中获取父函数的FP,就可以得到祖父函数的栈帧,以此类推,可以追溯程序调用栈,得到函数间的调用关系。
当系统发生异常时,系统打印异常函数的栈帧中保存的寄存器内容,以及父函数、祖父函数的栈帧中的LR链接寄存器、FP帧指针寄存器内容,用户就可以据此追溯函数间的调用关系,定位异常原因。
堆栈分析原理如下图所示,实际堆栈信息根据不同CPU架构有所差异,此处仅做示意。
图1 堆栈分析原理示意图
图中不同颜色的寄存器表示不同的函数。可以看到函数调用过程中,寄存器的保存。通过FP寄存器,栈回溯到异常函数的父函数,继续按照规律对栈进行解析,推出函数调用关系,方便用户定位问题。
接口说明
OpenHarmony LiteOS-M内核的回溯栈模块提供下面几种功能,接口详细信息可以查看API参考。
表1 回溯栈模块接口
功能分类 | 接口名 |
回溯栈接口 | LOS_BackTrace:打印调用处的函数调用栈关系。 LOS_RecordLR:在无法打印的场景,用该接口获取调用处的函数调用栈关系。 |
使用指导
开发流程
开启异常调测的典型流程如下:
- 配置异常接管相关宏。 需要在target_config.h头文件中修改配置:
配置项 | 含义 | 设置值 |
LOSCFG_BACKTRACE_DEPTH | 函数调用栈深度,默认15层 | 15 |
LOSCFG_BACKTRACE_TYPE | 回溯栈类型: 0:表示关闭该功能; 1:表示支持Cortex-m系列硬件的函数调用栈解析; 2:表示用于Risc-v系列硬件的函数调用栈解析; | 根据工具链类型设置1或2 |
2.使用示例中有问题的代码,编译、运行工程,在串口终端中查看异常信息输出。示例代码模拟异常代码,实际产品开发时使用异常调测机制定位异常问题。 本示例演示异常输出,包含1个任务,该任务入口函数模拟若干函数调用,最终调用一个模拟异常的函数。代码实现如下:
本演示代码在 ./kernel/liteos_m/testsuites/src/osTest.c 中编译验证,在TestTaskEntry中调用验证入口函数ExampleExcEntry。
#include <stdio.h>
#include "los_config.h"
#include "los_interrupt.h"
#include "los_task.h"
UINT32 g_taskExcId;
#define TSK_PRIOR 4
/* 模拟异常函数 */
UINT32 GetResultException0(UINT16 dividend){
UINT32 result = *(UINT32 *)(0xffffffff);
printf("Enter GetResultException0. %u\r\n", result);
return result;
}
UINT32 GetResultException1(UINT16 dividend){
printf("Enter GetResultException1.\r\n");
return GetResultException0(dividend);
}
UINT32 GetResultException2(UINT16 dividend){
printf("Enter GetResultException2.\r\n");
return GetResultException1(dividend);
}
UINT32 ExampleExc(VOID)
{
UINT32 ret;
printf("Enter Example_Exc Handler.\r\n");
/* 模拟函数调用 */
ret = GetResultException2(TSK_PRIOR);
printf("Divided result =%u.\r\n", ret);
printf("Exit Example_Exc Handler.\r\n");
return ret;
}
/* 任务测试入口函数,创建一个会发生异常的任务 */
UINT32 ExampleExcEntry(VOID)
{
UINT32 ret;
TSK_INIT_PARAM_S initParam = { 0 };
/* 锁任务调度,防止新创建的任务比本任务高而发生调度 */
LOS_TaskLock();
printf("LOS_TaskLock() Success!\r\n");
initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)ExampleExc;
initParam.usTaskPrio = TSK_PRIOR;
initParam.pcName = "Example_Exc";
initParam.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
/* 创建高优先级任务,由于锁任务调度,任务创建成功后不会马上执行 */
ret = LOS_TaskCreate(&g_taskExcId, &initParam);
if (ret != LOS_OK) {
LOS_TaskUnlock();
printf("Example_Exc create Failed!\r\n");
return LOS_NOK;
}
printf("Example_Exc create Success!\r\n");
/* 解锁任务调度,此时会发生任务调度,执行就绪队列中最高优先级任务 */
LOS_TaskUnlock();
return LOS_OK;
}
述代码串口终端输出异常信息如下:
LOS_TaskLock() Success!
Example_Exc create Success!
Enter Example_Exc Handler.
Enter GetResultException2.
Enter GetResultException1.
*************Exception Information**************
Type = 4
ThrdPid = 5
Phase = exc in task
FaultAddr = 0xfffffffc
Current task info:
Task name = Example_Exc
Task ID = 5
Task SP = 0x210549bc
Task ST = 0x21053a00
Task SS = 0x1000
Exception reg dump:
PC = 0x2101c61a
LR = 0x2101c64d
SP = 0x210549a8
R0 = 0x4
R1 = 0xa
R2 = 0x0
R3 = 0xffffffff
R4 = 0x2103fb20
R5 = 0x5050505
R6 = 0x6060606
R7 = 0x210549a8
R8 = 0x8080808
R9 = 0x9090909
R10 = 0x10101010
R11 = 0x11111111
R12 = 0x0
PriMask = 0x0
xPSR = 0x41000000
----- backtrace start -----
backtrace 0 -- lr = 0x2101c64c
backtrace 1 -- lr = 0x2101c674
backtrace 2 -- lr = 0x2101c696
backtrace 3 -- lr = 0x2101b1ec
----- backtrace end -----
TID Priority Status StackSize WaterLine StackPoint TopOfStack EventMask SemID CPUUSE CPUUSE10s CPUUSE1s TaskEntry name
--- -------- -------- --------- --------- ---------- ---------- --------- ------ ------- --------- -------- ---------- ----
0 0 Pend 0x1000 0xdc 0x2104730c 0x210463e8 0 0xffff 0.0 0.0 0.0 0x2101a199 Swt_Task
1 31 Ready 0x500 0x44 0x210478e4 0x21047428 0 0xffff 0.0 0.0 0.0 0x2101a9c9 IdleCore000
2 5 PendTime 0x6000 0xd4 0x2104e8f4 0x210489c8 0 0xffff 5.7 5.7 0.0 0x21016149 tcpip_thread
3 3 Pend 0x1000 0x488 0x2104f90c 0x2104e9e8 0x1 0xffff 8.6 8.6 0.0 0x21016db5 ShellTaskEntry
4 25 Ready 0x4000 0x460 0x21053964 0x2104f9f0 0 0xffff 9.0 8.9 0.0 0x2101c765 IT_TST_INI
5 4 Running 0x1000 0x458 0x210549bc 0x21053a00 0 0xffff 76.5 76.6 0.0 0x2101c685 Example_Exc
OS exception NVIC dump:
interrupt enable register, base address: 0xe000e100, size: 0x20
0x2001 0x0 0x0 0x0 0x0 0x0 0x0 0x0
interrupt pending register, base address: 0xe000e200, size: 0x20
0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
interrupt active register, base address: 0xe000e300, size: 0x20
0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
interrupt priority register, base address: 0xe000e400, size: 0xf0
0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
interrupt exception register, base address: 0xe000ed18, size: 0xc
0x0 0x0 0xf0f00000
interrupt shcsr register, base address: 0xe000ed24, size: 0x4
0x70002
interrupt control register, base address: 0xe000ed04, size: 0x4
0x1000e805
memory pools check:
system heap memcheck over, all passed!
memory pool check end!
根据实际运行环境,上文中的数据会有差异,非固定结果
定位流程
异常接管一般的定位步骤如下:
- 确认编译时关掉优化选项,否则下述的描述内容可能被优化掉。
- 打开编译后生成的镜像反汇编(asm)文件。如果默认没有生成,可以使用objdump工具生成,命令为:
- 搜索PC指针(指向当前正在执行的指令)在asm中的位置,找到发生异常的函数。 PC地址指向发生异常时程序正在执行的指令。在当前执行的二进制文件对应的asm文件中,查找PC值0x2101c61a,找到当前CPU正在执行的指令行,反汇编如下所示:
2101c60c <GetResultException0>:
2101c60c: b580 push {r7, lr}
2101c60e: b084 sub sp, #16
2101c610: af00 add r7, sp, #0
2101c612: 4603 mov r3, r0
2101c614: 80fb strh r3, [r7, #6]
2101c616: f04f 33ff mov.w r3, #4294967295 ; 0xffffffff
2101c61a: 681b ldr r3, [r3, #0]
2101c61c: 60fb str r3, [r7, #12]
2101c61e: 68f9 ldr r1, [r7, #12]
2101c620: 4803 ldr r0, [pc, #12] ; (2101c630 <GetResultException0+0x24>)
2101c622: f001 f92b bl 2101d87c <printf>
2101c626: 68fb ldr r3, [r7, #12]
2101c628: 4618 mov r0, r3
2101c62a: 3710 adds r7, #16
2101c62c: 46bd mov sp, r7
2101c62e: bd80 pop {r7, pc}
2101c630: 21025f90 .word 0x21025f90
3.可以看到:
- 异常时CPU正在执行的指令是ldr r3, [r3, #0],其中r3取值为0xffffffff,导致发生非法地址异常。
- 异常发生在函数GetResultException0中。
4.根据LR值查找异常函数的父函数。 包含LR值0x2101c64d的反汇编如下所示:
2101c634 <GetResultException1>:
2101c634: b580 push {r7, lr}
2101c636: b082 sub sp, #8
2101c638: af00 add r7, sp, #0
2101c63a: 4603 mov r3, r0
2101c63c: 80fb strh r3, [r7, #6]
2101c63e: 4806 ldr r0, [pc, #24] ; (2101c658 <GetResultException1+0x24>)
2101c640: f001 f91c bl 2101d87c <printf>
2101c644: 88fb ldrh r3, [r7, #6]
2101c646: 4618 mov r0, r3
2101c648: f7ff ffe0 bl 2101c60c <GetResultException0>
2101c64c: 4603 mov r3, r0
2101c64e: 4618 mov r0, r3
2101c650: 3708 adds r7, #8
2101c652: 46bd mov sp, r7
2101c654: bd80 pop {r7, pc}
2101c656: bf00 nop
2101c658: 21025fb0 .word 0x21025fb0
5.LR值2101c648上一行是bl 2101c60c <GetResultException0>,此处调用了异常函数,调用异常函数的父函数为GetResultException1。
6.重复步骤3,解析异常信息中backtrace start至backtrace end之间的LR值,得到调用产生异常的函数调用栈关系,找到异常原因。
Trace调测
基本概念
Trace调测旨在帮助开发者获取内核的运行流程,各个模块、任务的执行顺序,从而可以辅助开发者定位一些时序问题或者了解内核的代码运行过程。
运行机制
内核提供一套Hook框架,将Hook点预埋在各个模块的主要流程中, 在内核启动初期完成Trace功能的初始化,并注册Trace的处理函数到Hook中。
当系统触发到一个Hook点时,Trace模块会对输入信息进行封装,添加Trace帧头信息,包含事件类型、运行的cpuid、运行的任务id、运行的相对时间戳等信息;
Trace提供2种工作模式,离线模式和在线模式。
离线模式会将trace frame记录到预先申请好的循环buffer中。如果循环buffer记录的frame过多则可能出现翻转,会覆盖之前的记录,故保持记录的信息始终是最新的信息。Trace循环buffer的数据可以通过shell命令导出进行详细分析,导出信息已按照时间戳信息完成排序。
在线模式需要配合IDE使用,实时将trace frame记录发送给IDE,IDE端进行解析并可视化展示。
接口说明
OpenHarmony LiteOS-M内核的Trace模块提供下面几种功能,接口详细信息可以查看API参考。
表1 Trace模块接口说明
功能分类 | 接口名 |
启停控制 | - LOS_TraceStart:启动Trace - LOS_TraceStop:停止Trace |
操作Trace记录的数据 | - LOS_TraceRecordDump:输出Trace缓冲区数据 - LOS_TraceRecordGet:获取Trace缓冲区的首地址 - LOS_TraceReset:清除Trace缓冲区中的事件 |
过滤Trace记录的数据 | LOS_TraceEventMaskSet:设置事件掩码,仅记录某些模块的事件 |
屏蔽某些中断号事件 | LOS_TraceHwiFilterHookReg:注册过滤特定中断号事件的钩子函数 |
插桩函数 | LOS_TRACE_EASY:简易插桩 LOS_TRACE:标准插桩 |
- 当用户需要针对自定义事件进行追踪时,可按规则在目标源代码中进行插桩,系统提供如下2种插桩接口:
- LOS_TRACE_EASY(TYPE, IDENTITY, params…) 简易插桩。
- 一句话插桩,用户在目标源代码中插入该接口即可。
- TYPE有效取值范围为[0, 0xF],表示不同的事件类型,不同取值表示的含义由用户自定义。
- IDENTITY类型UINTPTR,表示事件操作的主体对象。
- Params类型UINTPTR,表示事件的参数。
- 对文件fd读写操作的简易插桩示例:
/* 假设自定义读操作为type: 1, 写操作为type: 2 */
LOS_TRACE_EASY(1, fd, flag, size); /* 在读fd文件的适当位置插入 */
LOS_TRACE_EASY(2, fd, flag, size); /* 在写fd文件的适当位置插入 */
- LOS_TRACE(TYPE, IDENTITY, params…) 标准插桩。
- 相比简易插桩,支持动态过滤事件和参数裁剪,但使用上需要用户按规则来扩展。
- TYPE用于设置具体的事件类型,可以在头文件los_trace.h中的enum LOS_TRACE_TYPE中自定义事件类型。定义方法和规则可以参考其他事件类型。
- IDENTITY和Params的类型及含义同简易插桩。
- 示例:
1.定义FS模块的类型,即FS模块的事件掩码
/* 在enum LOS_TRACE_MASK中定义, 定义规范为TRACE_#MOD#_FLAG, #MOD#表示模块名 */
TRACE_FS_FLAG = 0x4000
2.定义FS模块的具体事件类型
/* 定义规范为#TYPE# = TRACE_#MOD#_FLAG | NUMBER, */
FS_READ = TRACE_FS_FLAG | 0; /* 读文件 */
FS_WRITE = TRACE_FS_FLAG | 1; /* 写文件 */
3.定义事件参数
/* 定义规范为#TYPE#_PARAMS(IDENTITY, parma1...) IDENTITY, ... */
#define FS_READ_PARAMS(fp, fd, flag, size) fp, fd, flag, size /* 宏定义的参数对应于Trace缓冲区中记录的事件参数,用户可对任意参数字段进行裁剪 */
#define FS_READ_PARAMS(fp, fd, flag, size) /* 当定义为空时,表示不追踪该类型事件 */
4.在目标代码中插桩
/* 定义规范为LOS_TRACE(#TYPE#, #TYPE#_PARAMS(IDENTITY, parma1...)) */
LOS_TRACE(FS_READ, fp, fd, flag, size); /* 读文件操作的代码桩 */
说明:
预置的Trace事件及参数均可以通过上述方式进行裁剪,参数详见kernel\include\los_trace.h。
- Trace Mask事件过滤接口LOS_TraceEventMaskSet(UINT32 mask),其入参mask仅高28位生效(对应LOS_TRACE_MASK中某模块的使能位),仅用于模块的过滤,暂不支持针对某个特定事件的细粒度过滤。例如:LOS_TraceEventMaskSet(0x202),则实际设置生效的mask为0x200(TRACE_QUE_FLAG),QUE模块的所有事件均会被采集。一般建议使用的方法为: LOS_TraceEventMaskSet(TRACE_EVENT_FLAG | TRACE_MUX_FLAG | TRACE_SEM_FLAG | TRACE_QUE_FLAG);
- 如果仅需要简易插桩事件,通过设置Trace Mask为TRACE_MAX_FLAG即可。
- Trace缓冲区有限,事件写满之后会覆盖写,用户可通过LOS_TraceRecordDump中打印的CurEvtIndex识别最新记录。
- Trace的典型操作流程为:LOS_TraceStart、 LOS_TraceStop、 LOS_TraceRecordDump.
- 针对中断事件的Trace, 提供中断号过滤,用于解决某些场景下特定中断号频繁触发导致其他事件被覆盖的情况,用户可自定义中断过滤的规则, 示例如下:
BOOL Example_HwiNumFilter(UINT32 hwiNum)
{
if ((hwiNum == TIMER_INT) || (hwiNum == DMA_INT)) {
return TRUE;
}
return FALSE;
}
LOS_TraceHwiFilterHookReg(Example_HwiNumFilter);
则当中断号为TIMER_INT 或者DMA_INT时,不记录中断事件。
开发指导
开发流程
开启Trace调测的典型流程如下:
- 配置Trace模块相关宏。 需要在target_config.h头文件中修改配置:
配置项 | 含义 | 设置值 |
LOSCFG_KERNEL_TRACE | Trace模块的裁剪开关 | YES/NO |
LOSCFG_RECORDER_MODE_OFFLINE | Trace工作模式为离线模式 | YES/NO |
LOSCFG_RECORDER_MODE_ONLINE | Trace工作模式为在线模式 | YES/NO |
LOSCFG_TRACE_CLIENT_INTERACT | 使能与Trace IDE (dev tools)的交互,包括数据可视化和流程控制 | YES/NO |
LOSCFG_TRACE_FRAME_CORE_MSG | 记录CPUID、中断状态、锁任务状态 | YES/NO |
LOSCFG_TRACE_FRAME_EVENT_COUNT | 记录事件的次序编号 | YES/NO |
LOSCFG_TRACE_FRAME_MAX_PARAMS | 配置记录事件的最大参数个数 | INT |
LOSCFG_TRACE_BUFFER_SIZE | 配置Trace的缓冲区大小 | INT |
2.(可选)预置事件参数和事件桩(或使用系统默认的事件参数配置和事件桩)。
3.(可选)调用LOS_TraceStop停止Trace后,清除缓冲区LOS_TraceReset(系统默认已启动trace)。
4.(可选)调用LOS_TraceEventMaskSet设置需要追踪的事件掩码(系统默认的事件掩码仅使能中断与任务事件),事件掩码参见los_trace.h 中的LOS_TRACE_MASK定义。
5.在需要记录事件的代码起始点调用LOS_TraceStart。
6.在需要记录事件的代码结束点调用LOS_TraceStop。
7.调用LOS_TraceRecordDump输出缓冲区数据(函数的入参为布尔型,FALSE表示格式化输出,TRUE表示输出到windows客户端)。
上述第3-7步中的接口,均封装有对应的shell命令,对应关系如下
- LOS_TraceReset —— trace_reset
- LOS_TraceEventMaskSet —— trace_mask
- LOS_TraceStart —— trace_start
- LOS_TraceStop —— trace_stop
- LOS_TraceRecordDump —— trace_dump
编程实例
本实例实现如下功能:
- 创建一个用于Trace测试的任务。
- 设置事件掩码。
- 启动trace。
- 停止trace。
- 格式化输出trace数据。
示例代码
示例代码如下:
本演示代码在 ./kernel/liteos_m/testsuites/src/osTest.c 中编译验证,在TestTaskEntry中调用验证入口函数ExampleTraceTest。
#include "los_trace.h"
UINT32 g_traceTestTaskId;
VOID Example_Trace(VOID)
{
UINT32 ret;
LOS_TaskDelay(10);
/* 开启trace */
ret = LOS_TraceStart();
if (ret != LOS_OK) {
dprintf("trace start error\n");
return;
}
/* 触发任务切换的事件 */
LOS_TaskDelay(1);
LOS_TaskDelay(1);
LOS_TaskDelay(1);
/* 停止trace */
LOS_TraceStop();
LOS_TraceRecordDump(FALSE);
}
UINT32 ExampleTraceTest(VOID){
UINT32 ret;
TSK_INIT_PARAM_S traceTestTask = { 0 };
/* 创建用于trace测试的任务 */
memset(&traceTestTask, 0, sizeof(TSK_INIT_PARAM_S));
traceTestTask.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_Trace;
traceTestTask.pcName = "TestTraceTsk"; /* 测试任务名称 */
traceTestTask.uwStackSize = 0x800;
traceTestTask.usTaskPrio = 5;
traceTestTask.uwResved = LOS_TASK_STATUS_DETACHED;
ret = LOS_TaskCreate(&g_traceTestTaskId, &traceTestTask);
if(ret != LOS_OK){
dprintf("TraceTestTask create failed .\n");
return LOS_NOK;
}
/* 系统默认情况下已启动trace, 因此可先关闭trace后清除缓存区后,再重启trace */
LOS_TraceStop();
LOS_TraceReset();
/* 开启任务模块事件记录 */
LOS_TraceEventMaskSet(TRACE_TASK_FLAG);
return LOS_OK;
}
结果验证
输出结果如下:
***TraceInfo begin***
clockFreq = 50000000
CurEvtIndex = 7
Index Time(cycles) EventType CurTask Identity params
0 0x366d5e88 0x45 0x1 0x0 0x1f 0x4 0x0
1 0x366d74ae 0x45 0x0 0x1 0x0 0x8 0x1f
2 0x36940da6 0x45 0x1 0xc 0x1f 0x4 0x9
3 0x3694337c 0x45 0xc 0x1 0x9 0x8 0x1f
4 0x36eea56e 0x45 0x1 0xc 0x1f 0x4 0x9
5 0x36eec810 0x45 0xc 0x1 0x9 0x8 0x1f
6 0x3706f804 0x45 0x1 0x0 0x1f 0x4 0x0
7 0x37070e59 0x45 0x0 0x1 0x0 0x8 0x1f
***TraceInfo end***
根据实际运行环境,上文中的数据会有差异,非固定结果
输出的事件信息包括:发生时间、事件类型、事件发生在哪个任务中、事件操作的主体对象、事件的其他参数。
- EventType:表示的具体事件可查阅头文件los_trace.h中的enum LOS_TRACE_TYPE。
- CurrentTask:表示当前运行在哪个任务中,值为taskid。
- Identity:表示事件操作的主体对象,可查阅头文件los_trace.h中的#TYPE#_PARAMS。
- params:表示的事件参数可查阅头文件los_trace.h中的#TYPE#_PARAMS。
下面以序号为0的输出项为例,进行说明。
Index Time(cycles) EventType CurTask Identity params
0 0x366d5e88 0x45 0x1 0x0 0x1f 0x4
- Time cycles可换算成时间,换算公式为cycles/clockFreq,单位为s。
- 0x45为TASK_SWITCH即任务切换事件,当前运行的任务taskid为0x1。
- Identity和params的含义需要查看TASK_SWITCH_PARAMS宏定义:
#define TASK_SWITCH_PARAMS(taskId, oldPriority, oldTaskStatus, newPriority, newTaskStatus) \
taskId, oldPriority, oldTaskStatus, newPriority, newTaskStatus
因为#TYPE#_PARAMS(IDENTITY, parma1…) IDENTITY, …,所以Identity为taskId(0x0),第一个参数为oldPriority(0x1f)
说明:
params的个数由LOSCFG_TRACE_FRAME_MAX_PARAMS配置,默认为3,超出的参数不会被记录,用户应自行合理配置该值。
综上所述,任务由0x1切换到0x0,0x1任务的优先级为0x1f,状态为0x4,0x0任务的优先级为0x0。
文章转载自:https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/device-dev/kernel/kernel-mini-memory-debug.md/