Strong Zhang

程序跑飞是软件开发常见的问题,无论是在产品开发过程中,还是在批量量产后都可能会出现,本文探讨如何快速查找导致程序跑飞的代码,分析程序跑飞几种原因以及提出相关的解决方法。

如何查找非法中断的返回地址?

程序跑飞通常代码会进入非法中断,首先需要定位到运行哪行代码会导致程序跑飞,通过RPC寄存器的值可以查看非法中断的返回地址,即在代码跑飞之后,在CCS寄存器窗口查看Core Register的RPC的值。

需要注意RPC只保存了上次由LCR指令跳转过来的返回地址,对于其他跳转指令如"LB #22bit address"等,RPC则提供不了有效信息。

如果是LB指令,则需要从堆栈里查找,如TMS320C28x CPU and Instruction Set (Rev. F)手册所说,堆栈指针SP往回偏移7和8个字节的地址保存了上次跳到非法中断的返回地址。

例如下图的C代码 “(*(void (*)(void))0x0090000)()”的汇编代码包含了LCR指令,当该代码从地址0x81f7b执行LCR跳转指令时,会跳到0x90000继续运行,而0x90000是只有0xFF内容的无效指令的Flash地址,因此必然会跳到非法中断。

这时可以从RPC寄存器得到地址为0x81F7E,也就是指向"(*(void (*)(void))0x0090000)()"代码的地址。

而堆栈0xC022~0xC023(当前堆栈指针0xC02A偏移7~8个值)保存了0x0090000的地址信息,也就是执行地址为0x0090000的非法指令FFFF导致了异常。

因此,RPC寄存器只保存了LCR跳转指令的返回地址,而堆栈则是始终保存了上一个跳到非法中断的返回地址,但如果堆栈被破坏了或者返回地址存的指令本身是非法的,则RPC指针有时候可以提供更为有效的信息,在实际调试中,两者可以结合起来,帮助找到程序跑飞的原因。

不过有时候即使找到了具体哪行代码导致程序跑飞,也未必知道根本的原因,根据C28x Interrupt FAQ (ti.com)提供的原因,有下面几种情况下会导致非法中断。

下面从方便调试的角度对可能导致非法中断的原因进行分类,以提供进一步解决该类问题的思路。

程序跑飞的原因:

1. 堆栈溢出导致程序跑飞

cmd文件里面会定义分配给堆栈的RAM空间大小,但实际堆栈所占的空间是动态的,只有程序跑起来后才知道堆栈的实际占用的空间有多大,因此如果cmd文件里面定义堆栈太小,编译器是不会报错提醒的,这个时候运行代码,堆栈数据会溢出,侵占了其他RAM的空间,刷掉RAM程序或者全局变量的内容。

   Stack_Data           : origin = 0x008030, length = 0x000070
            RAM_Code             : origin = 0x0080A0, length = 0x000600
            .stack               : > Stack_Data,     fill=0xAA 

例如下面的函数updateDisplay()定义了很多临时变量,运行的时候需要动态分配大量的内存空间。

在运行updateDisplay()之前,如下面0x0080A0地址所示存放的是RAM的代码,这是初始化之后就应该固定不变的。

但跑完这个函数后,堆栈空间数据溢出,RAM代码被堆栈数据所刷新,导致程序跑飞。

解决方法:

通过在cmd文件中初始化stack堆栈空间,如初始化填充了0xAA,在代码运行起来的时候,观察有多少0xAA的值被动态更新了,这样大致能够确定代码所需要的堆栈空间,一般设置需要留一定的裕量,如果不够,可以把下面分配给堆栈的空间Stack_Data设大一些。

   Stack_Data          : origin = 0x008030, length = 0x000070
           .stack           : > Stack_Data,     fill=0xAA  

另外还需要注意的是堆栈的指针是16位的,因此存储堆栈区域的地址是不能超过0xFFFF的,否则也可能会出现程序跑飞的情况。

2. 在RAM运行的函数没有从Flash中拷贝成功

如果函数声明在RAM里面运行,但在应用程序中并没有用memcpy函数从Flash拷贝到RAM,这时候程序运行就会跑飞。

例如下面sampleADC()函数声明放在段.TI.ramfunc里面,而.TI.ramfunc在cmd文件中的定义是从Flash装载,然后在RAM里面运行,这个时候编译器会自动分配RAM空间给sampleADC函数,但不会自动把代码从Flash拷贝到RAM,

#pragma CODE_SECTION(sampleADC, ".TI.ramfunc")
 //将函数存在段.TI.ramfunc所在的RAM空间

如下图所示函数sampleADC()分配的起始RAM地址为0x0080E8,但这部分RAM空间的值都是0,即还没有从对应Flash程序中拷贝内容,运行该函数将会导致程序跑飞。

解决方法:

确保所有在RAM运行的函数调用之前,必须先用memcpy函数把代码从Flash拷贝到RAM。

    memcpy(&RamfuncsRunStart, &RamfuncsLoadStart, (size_t)&RamfuncsLoadSize);
            DELAY_US(1000);
            InitFlash();

3. 代码所占Flash空间靠近无效内存边界

对于代码空间,需要注意存储的代码在flash或者RAM的空间不能太靠近边界,如F28003x的Errata所说,例如下面所说的0x0AFFF0~0x0AFFFF是不能用的,否则有可能出现异常。

解决方法:

在cmd配置中,不使用地址为0x0AFFF0~0x0AFFFF的Flash空间。

4. 非加密区程序访问加密区的数据

如果对代码使用了DCSM/CSM模块进行加密,需要注意的是非加密区的程序不能访问加密区的数据。

另外以下几种情况容易被忽视,需要特别注意:

  1. ROM的代码一般是没有加密的,如果调用了ROM的代码,例如Flash API,IQmath库的函数,需要注意该函数访问的数据是不能加密;
  2. 对于同一工程里面划分了加密区和非加密区的Flash或者RAM,堆栈的RAM空间往往不能设置为加密,因为加密区和非加密区都会共用了一个堆栈空间。
  3. 如果在RAM跑的程序空间为非加密区,则注意不能访问加密区的数据。

解决方法:

确保非加密的程序不能访问加密区数据,特别注意ROM和部分RAM空间是不加密的,调用了这里的函数不能访问加密区的数据。

总结:

通过RPC的值或者堆栈保存下来的返回地址,可以找到具体哪行代码导致程序跑飞,以帮助进一步分析原因。

而程序跑飞的原因有很多,比如堆栈大小不够导致溢出,运行了没有被成功初始化的RAM程序,代码跑到Errata所说的禁用内存区间,或者用了DCSM加密模块而有非加密程序访问了加密的数据等等。

当然还有其他因为软件bug导致的原因,如果CCS编译之后有warning的提示,一般都要尽可能消掉,否则会存在难以排查的潜在风险。