创建两个线程和一个消息队列,一个线程发消息,另一个线程接收消息,消息的大小是1个字节,在这样的情况下,接收消息的线程一收到消息会出现死机问题,但如果在接收消息之前加一行log打印的代码就不会死机,非常神奇的 bug,以下是示例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static uint8_t _wait_queue(uint32_t timeout)
{
    uint8_t ret;
    uint8_t event;
    ret = ql_rtos_queue_wait(queue, &event, 1, timeout);
    if( ret != QUEC_SUCCESS )
        return -1;
    return event;
}

static void _recv_task(void * argv)
{
    uint8_t ret = 0;
    while(1)
    {
        ret = _wait_queue(QL_WAIT_FOREVER);
        printf("====== ret is :%d\n",ret);
        ret = _wait_queue(1000);
        printf("====== ret is :%d\n",ret);
    }
}

static void _send_task(void * argv)
{
    uint8_t event = 1;
    while(1)
    {
        ql_rtos_queue_release(queue, 1, &event, QL_WAIT_FOREVER);
        printf("======== send queue success\r\n");
        ql_rtos_task_sleep_s(1);
    }
}

从这段应用代码其实看不出来有什么问题,但是死机的时候有个特征:**有时候是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 的一些技巧:

  1. 局部变量越界死机的特征:有时候是 PC 指针异常( PC 指针的值不是一个函数的入口地址),有时候是访问非法内存(访问不存在的内存地址)。
  2. 使用全局变量保存局部变量的地址,然后在 Trace32 里面 dump 内存数据分析。
  3. 在问题代码上下文中主动触发 dump,可以访问空指针或者调用平台的 panic 接口,然后通过 Trace32 解析dump 进行分析验证。

Trace32 相关文章推荐:

  1. 使用 Trace32 分析内存溢出死机问题
  2. 用 Trace32 分析死机问题
  3. FreeRTOS 中的栈溢出检测机制