内存相关问题

内存是嵌入式系统的宝贵资源,在没有复杂操作系统进行内存管理的情况下,程序直接访问实地址,因此经常会遇到各种问题。本文介绍了我在调试过程中积累的内存方面问题的一些案例。

1.变量初始化

不同编译器对未初始化的全局变量会做不同处理,有些会将其分配在bss段,即初始化为零,而有些将其分配在data段,并且不做显式初始化,则上电为随机值。而同样处理器在生成输出文件的ABI格式不同时也有可能会做不同处理。
在某驱动代码中定义了一个结构体,该结构体中嵌套另一个结构体,其中有一个标志位字段。在定义时未做初始化,而代码中依靠该标志位做分支。在ELF格式下TI的C6000编译器v7.4.2将其初始化为零,然而同样的编译器当生成COFF格式代码时却未做初始化。因此某次代码总是运行出错,断点单步调试才发现这个低级错误。

2.DDR未初始化

变量和代码在链接文件中分配到DDR中,但DDR却未进行初始化,因此访问自然出错。这发生在对链接文件和对启动过程不熟悉的情况下。

《DDR引发的问题(上)》中深入一步研究了变量初始化的问题,《DDR引发的问题(下)》进一步说明了一个DDR未初始化引发的问题。

3.多核共享变量初始化冲突

C6678的8个核都可以任意访问片内的RAM,因此需要在逻辑上为每个核划分内存区,相互之间不重叠,体现在链接控制文件中。但是有些情况下多核要共享一部分内存,这时对这块内存的初始化时序要特别注意。一是如果用变量来访问一个内存地址,不同工程中同一个变量要定义到同一位置。二是要避免多次初始化造成的不一致。三是在访问时要加互斥保护。
某次设计中,C6678上电后由主核配置多核导航器,然后启动从核。这里主从核共享了几个变量,但都对其做了初始化和访问,结果从核后启动后将主核写入的数据覆盖,导致主核后续初始化SRIO模块时再访问该变量失败。
最终是通过用SRIO的源代码替换库,单步进入初始化代码中,配合Memory Browser看内存值找到了问题。解决方案是在启动时多核间进行同步,避免时序上的冲突。

4.共享代码malloc错误。

为了节省内存,采用共享代码的方式。即一个工程用不同的链接控制文件编译多次,链接控制文件里代码段分配同样的内存地址,数据段分配不同的内存地址,生成的多个输出文件供多个核使用,期望达到节省内存的目的。简单做了一个实验,该效果可以实现。但实际应用到一个复杂的代码时,发现执行失败。
具体调试发现代码中调用了malloc函数,但所有的核申请到的空间都指向了同一区域,即核7的heap区中,而核7是最后一个启动的核。
这时怀疑代码被覆盖,将几个编译输出文件的代码段进行对比,发现果然有若干字节不同。这证明了同样的代码,同样的优化选项,同样的编译指令,在链接文件不同时生成的代码指令也不一定相同。
从原理上分析,malloc是C库函数,它是先占用了一大块静态空间,在sysmem段,然后用自己的分配算法向上层提供申请和释放的接口。不同链接文件中sysmem段的地址不同,而在指令中有一些是地址相关代码,其在编译期有些字段先置空,到链接期再根据链接控制文件填补进去。这样不同的链接控制文件生成的代码当然有可能不同。
实际上仔细研究链接过程是一件很有趣的事,《程序员的自我修养——链接、装载与库》中有详细的说明,看完会有茅塞顿开的感觉。
最终该问题的解决方法是把malloc代码全部修改为预定义的静态数组,实际上是避开了该问题。

5.复位不清空内存

XX板上C6455由DM8168通过HPI接口来下载启动。在启动过程中有一些交互是通过双方在C6455的指定内存中写指定数据来完成的。如DM8168写入A值表示某步骤已完成,C6455读该地址直到发现该处值为A再继续向下运行。
某次重启C6455后发现启动过程不正常。最后是通过在启动过程中架上仿真器查看C6455的内存发现了问题原因。原来是DM8168先复位C6455,然后直接向其内存中下载解析后的启动文件,最后写入一些配置数据后向C6455的某个内存地址写入A表示配置完成。然而复位动作并未清除C6455的内存,该地址中仍然遗留着上次写入的A值,但实际上本次配置数据还未曾写入。因此C6455使用了上次的配置,导致双方信息不匹配。最后的解决方案是在每次启动C6455前先把内存全部清空一遍再下载新启动文件和配置数据。

6.堆栈相关

内存受限是嵌入式系统的一大特点。因此一方面要节省内存,另一方面要提防因为节省导致的栈溢出等负作用。前文《C6414上移植LwIP》介绍过两个栈溢出导致的案例,这里再补充几个堆空间不足相关的案例。

在SYS/BIOS操作系统中,创建进程需要指定一个进程栈大小,该空间是从系统一个堆中获取的,堆的值在配置文件中配置,最终在编译后作为.far数据段的一个全局变量。堆空间还被系统中邮箱、信号量、时钟等多个模块申请。
在多次调试中,均发生过系统崩溃的现象,最后追究原因是该堆空间配置不足。解决起来也有开源节流两个方法,一是增加该空间,二是尽量减少使用。两者都需要精细化堆的确切使用量。这里使用CCS的ROV功能,一方在在系统启动过程中查看哪些动作占用了多少堆,二是长期运行后查看每个任务的栈峰值使用量,这样就能评估出系统准确的堆需求,指导工程配置。

7.Cache一致性

内存可以被CPU以外的多种其他设备读写,如DMA控制器。这时如果芯片硬件具有自动一致性感知,则没有问题,否则有可能会引发Cache不一致的情况。例如C6678对于片内的4MB共享内存就不具备硬件的自动一致性,这时每个核都维护着自己独立的Cache,核A对该地址的修改只在Cahce里,并未真正写内存,而另一个核B访问该地址时只能访问到本核Cache中的原值,也非内存中的真实值,从而产生错误。同理DMA对该内存的修改也不能影响到Cache中数据,因此CPU不能获得该地址内的真实值。
解决方法一方面对于公共内存,读写前要进行互斥保护,另一方面CPU读时先将缓存内值置无效,写后手动写回到内存中。
曾经在C6678上发生过IPC(核间通信)传输错误的问题,最终发现是由于IPC库里一个Cache一致性Bug导致的,这个案例今后再专门介绍。

你可能感兴趣的:(内存,嵌入式系统)