创建两个线程和一个消息队列,一个线程发消息,另一个线程接收消息,消息的大小是1个字节,在这样的情况下,接收消息的线程一收到消息会出现死机问题,但如果在接收消息之前加一行log打印的代码就不会死机,非常神奇的 bug,以下是示例代码:
|
|
从这段应用代码其实看不出来有什么问题,但是死机的时候有个特征:**有时候是PC 指针异常(PC指针的值不是一个函数的入口地址),有时候是访问非法内存(访问不存在的内存地址)。**出现这样的现象,通常会往线程的栈溢出这个方向怀疑,而且还是局部变量或者buffer内存写越界导致的这类栈溢出问题。
有了问题的线索,那就查看消息队列的实现源码,做进一步分析。消息队列的收发接口 ql_rtos_queue_wait ql_rtos_queue_release 是对底层 rtos threadx 的消息队列的封装,底层 threadx 的移植代码未开源,但可以在 github 上找相近版本的源码来分析问题,threadx 消息队列代码的链接:https://github.com/azure-rtos/threadx/blob/HEAD/common/src/tx_queue_receive.c
查看源码发现,在接收msg的时候,threadx 的消息队列实现会把指针转换为 unsigned long 类型:
从下面的代码看,在拷贝 msg 数据的时候,至少会写4个字节的数据:
而测试代码中 uint8_t event 定义的数据只有1个字节大小,在接收 msg 数据的时候 event 这个变量会写越界,从而破坏线程的栈。
加一些调试代码验证这个问题,在定义 event 的前后分别增加两个 buffer,并填充一些已知数据,看接收消息列的 msg 之后是否会有数据越界写到 buffer里面,通过全局变量记录 event 和这两个 buffer 的内存地址,方便在 Trace32 中查看他们的内存数据,在接收消息之后,以访问空指针的方式触发dump,查看内存数据变化。
通过 Trace32 查看死机时候的 dump,接收 msg 的时候,写了4个字节的数据,并把 填充了 0xff 的 buffer 的数据篡改了。
再去掉刚才添加的 buffer,查看死机时候的 dump,结果也是 event 变量后面的数据被覆盖,变成 0x7e000000,这个 0x7e000000 在被改写之前应该是一个与函数地址有关的变量,他的值被改了,函数调用结束返回的时候给 PC 指针赋值了一个非法值,也就死机了。至于为什么在接收消息之前加一行打印就不会死机了呢?实际上接收消息的时候栈里面的这段数据还是会被篡改,只是这段数据没那么致命(存储的不是内存地址),变化了也不会引起死机,就如同上面验证的时候添加的 buffer,里面的数据被篡改了也无关紧要。
threadx 消息队列里面收发消息的 msg buffer 大小至少是一个 ULONG(4个字节),且得是 ULONG 大小的倍数,必须确保 msg buffer 里面有足够的空间容纳消息数据,否则会出现局部变量写越界,任务栈被破坏的问题。
总结使用 Trace32 的一些技巧:
- 局部变量越界死机的特征:有时候是 PC 指针异常( PC 指针的值不是一个函数的入口地址),有时候是访问非法内存(访问不存在的内存地址)。
- 使用全局变量保存局部变量的地址,然后在 Trace32 里面 dump 内存数据分析。
- 在问题代码上下文中主动触发 dump,可以访问空指针或者调用平台的 panic 接口,然后通过 Trace32 解析dump 进行分析验证。
Trace32 相关文章推荐: