查看: 1588|回复: 0

比特世界病虫害防治指南(四):防治总线错误

[复制链接]
  • TA的每日心情
    开心
    2024-3-26 15:16
  • 签到天数: 266 天

    [LV.8]以坛为家I

    3299

    主题

    6546

    帖子

    0

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    32024
    最后登录
    2024-4-25
    发表于 2021-6-24 10:09:06 | 显示全部楼层 |阅读模式
    大家好,很高兴咱们又见面了,只是,这次是一边高兴一边泪眼汪汪。为啥这么说,是因为从这次开始,小编要回首还要浓缩一些苦难的往事,那就是在Cortex-M比特世界一次次和病虫害做斗争的经历。哦不,咱们应该积极乐观,树立“与虫斗,其乐无穷”的正确价值观。


    有了前面三次的见面,咱们对HardFault已经一回生,二回五成熟,三回九成熟了。
    接下来就让咱们在HardFault的帮助下,让BUG们就像白墙上的苍蝇那般躲也没处躲,藏也没处藏,扫除一切害人虫,全无敌,Oh yeah!
    鸡汤喝完了,那就让咱们先从最常见面的BusFault开始吧!


    回顾一下,BusFault一般是反映了指针误用和缓冲越界访问导致的问题,并且细分为PreciseErr、ImpreciseErr、IBusErr、StkErr和UnStkErr。
    这里咱们主要分析常见的PreciseErr和ImpreciseErr,其它的也简单介绍一下。柿子要挑软的捏,那咱们也先从最简单的PreciseErr开始吧!

    排查PreciseErr
    说这个最简单,是因为它是“Precise”(精确)的,表示总能当场抓住肇事的指令,并且还能查出试图访问的错误地址。而与之相对的,还有些错误的访问动作不能当场抓到,这主要是拜现代SoC复杂的总线系统和层层写入缓冲机制所赐。


    对于数据读取,由于不拿到数据就不算执行完毕,所以不管外面的存储器时序和总线系统多复杂,花了多少个周期,都要算到这个读取指令的头上。这期间如果发生了错误,这个指令自然也跑不了,所以,数据读取的出错是能得到PreciseErr的。


    当PreciseErr发生时,Cortex-M CPU会把导致出错的访问地址记录在 SCB->BFAR寄存器中,并且CFSR中的”BFARValid”位也有效。在排查hard fault时,看到BFARValid打勾,就是件很开心的事了。


    接下来,小编还为大家演示了一个构造出来的场景,能产生ImpreciseErr,录制成了小视频给大家观看:
    在这个视频中,小编模仿了一个类似memcpy的数据拷贝循环,并且在循环中设置了断点。打开反汇编窗口,找到了一条LDR指令:
    21.png
    发现它是以r1寄存器所指向的buffer中取数据的。接下来,小编就扮演为一个害虫搞破坏,把寄存器R1改成一个无效的地址:
    22.png
    备注:小编在构造这次的PreciseErr时使用的是i.MX RT1050,对于这个器件,0x30002000是“真空”区。


    干完了坏事,就让程序全速执行。不出所料,“咸猪手”立马被CPU当场剁到:


    下面的图是在KEIL下的可视化显示:
    23.png
    图中表示,在访问0x30002000地址时发生了ImpreciseErr。怎么样,是不是非常简单?不过,这只是“万里长征”的开始,因为在实际的程序中,这种内存地址出错的根源最复杂多变。常常与缓冲区溢出、数组越界有关。也可能链接文件配置有误以致于程序访问无效地址。


    在刚才的例子中,是通过LDR指令来读取数据。还有一种情况,是被破坏的寄存器的值是当作返回地址,通过pop {…,pc}指令从栈区弹出到PC中来实现函数返回。而POP指令其实是就是LDM(一次读取多个寄存器)指令。倘若SP指针被破坏成无效地址,那么在返回时若执行到POP时,也会产生PreciseErr,并且BFAR给出的地址是栈区中的地址。

    由系统级原因引发的PreciseErr
    上面介绍的产生PreciseErr的原因,都和程序访问错误的地址有关。


    然而,在MCU上开发,系统和硬件上的问题也不可不防。比如,倘若你发现BFAR中的地址是合理的该怎么办?这时,就要突破“软件程序员”的人设,从更底层来考虑了。


    MCU上的开发不像在PC或Linux系列这样靠得住的平台上开发那么单纯,还需要考虑像内存控制器时钟,电源初始化,SDRAM的时序配置,表面上地址连续的内存是由不同的物理功能块实现的(可以各自进低功耗模式),甚至是高频干扰、电源纹波等来自硬件原因。这也是嵌入式开发,尤其是基于MCU的嵌入式开发的独特魅力,需要开发者有综合的知识面和丰富的经验。


    所以,咱们要像做中医一样做MCU的嵌入式开发,发散思维,全盘考虑,不断积累经验,争取成为国宝级的“老中医”。

    排查ImpreciseErr
    软柿子捏完了,现在要碰这个有点烫手的山芋,ImpreciseErr了。这里难就难在”Imprecise”上,不能准确地抓到现场,使肇事者得以逃逸。LDR/LDM族读取数据的指令是不会产生ImpreciseErr的,只有STR/STM族的数据写入才能导致这个问题。这主要是因为和数据读取不同,在数据写入指令看来,只要CPU中的数传单元把数送出去就算完了,就像把信封投到信箱里就算寄出了一样。


    STR/STM执行后,CPU拍拍屁股走人了,然而,对于被写入的数据来说,真正的旅行才刚刚开始,等待着它的可能是层层关卡。就拿往SDRAM中写数为例,要经历流水线写入级,Store buffer (M7特有), Write buffer,争抢主总线矩阵,进外存控制器的写入队列排队等候(中间可能还能遇到夹塞的),直到最终写入器件。时间又长又不固定,很可能到了最后几步才爆雷,这里CPU只不定已经又执行多少条指令了。


    幸好,上面的情况只是极端地点背才会发生的,大部分ImpreciseErr和肇事指令的间隔并不是很远。尤其是出现在循环体中时,常常还没离开循环体。


    既然没法当前抓住肇事者,咱们就只好扩大检查的范围了,把发生ImpreciseErr之前的几条STR族的指令都当作有嫌疑的,一个一个检查。这里尤其要注意有小循环的情况。最倒霉的情况,就是中间还有函数返回或者是跳转。这就需要额外分析程序,找出可能是从哪里跳过来的了。


    下面,小编也给大家构造一个ImpreciseErr的场景,先上视频:
    在这个视频中,一个循环体正在复制数据,由反汇编代码可知,r1指向源数据区,r2指向目的数据区。
    24.png
    这回,咱们把r2改坏,然后单步执行,结果并没有发生任何问题,说明它成功逃逸了。


    那它能逍遥法外多久呢?我们且多点几下单步按钮。在这个构造的例子中小编单步了5次才发生ImpreciseErr。而且通过查找自动入栈的PC,发现肇事者还狡猾地指向了无辜的LDR指令的头上。如果不知道这个来龙去脉,表面上看仿佛是神奇地在STR指令执行之前ImpreciseErr就“预知”到错误而发生了!其实是因为进入了下一轮循环的缘故。
    25.png
    STR/STM族的指令出问题时,一般是产生ImpreciseErr,并且“不精确”的程度一般是随器件的复杂度提升,数据流动中间环节的增多而变得更不精确。

    排查StkErr, UnStkErr
    这两种种情况只有当异常响应和返回时的栈操作才可能出现,所以基本上都和栈指针指向不能访问的区域有关。
    然而,在C语言中是没有机会直接操作SP的。就算是在汇编中,除了操作系统、高级语言之类的调度和异常处理代码,也极少手动调整SP。
    所以这种情况比较罕见,小编也还没真实遇到过。不过,我们却可以反过来“利用”它俩(以及其它的BusFault)来协助捕获栈溢出。比如,把栈区放在RAM区的最低地址段,这样当栈溢出时,向下生长的SP必定会指向虚无缥缈的地址区,接下来就等着BusFault来为你服务吧。


    排查IBusErr
    这也是一类罕见的情况,因为如果程序开始执行了,就说明程序存储区是正常的,不大会执行中途时程序存储器挂了。
    不过,如果是用函数指针来调用,而函数指针的内容指向了未映射的地址(比如说忘记了初始化),那就可能会产生IBusErr了。
    这种错误发生时,可以直接从入栈的PC中找到这个错误的地址。
    顺便说,忘记初始化的函数指针往往默认是0,这会产生InvState类型的用法错误(UsageFault),这个咱们后面再说。

    题外话——C编程与内存安全问题
    前面给大家看了很多常见的BusFault,在实际的开发中它们也是上镜最高的。归根结底, C语言中“一切相信程序员”的哲学难逃干系。


    C语言在发明的时候,全世界的电脑也少到数都数得过来,全是给最顶尖的祖师爷级的程序员用的,而且C语言也是他们发明的,祖师爷们相信自己自然没话说。


    可时过境迁,现在C语言变成给咱们很多并非科班出身的菜鸟们来用,还用在保护机制薄弱的嵌入式平台上,不常常出事才怪!首当其冲的,就是内存安全问题。


    别说是咱们,就算是在微软和Google中,内存安全的Bug也占70%以上的软件漏洞。新兴的Rust语言被寄予厚望来根治这类内存安全问题,不过,在Rust真正普及之前,咱们还是要写C代码,万事靠自觉,小编这里也奉上自己常用的C编程易错点:


    整数可以直接当作指针用
    动态申请与释放内存,注意多重释放和释放后引用的问题。


    不“卫生”的宏:展开后可以影响上文中的变量


    Union: 字段重叠,尤其是位段


    在结构体中嵌入指针,这样如果memcpy结构体则指针有了两个,但内容只有一份!


    指针的循环引用


    其实,在C编程中与内存安全(和其它很多)问题打交道,是一个“罄竹难书”的话题,著名的“MISRA C”就规定了很多禁止使用的易错情况,真是要聊起这个的话,恐怕又是另一个系列了。


    在这里,小编只是希望平时能多提高咱们的C编程水平,减少犯错,与大家共勉。




    签到签到
    回复

    使用道具 举报

    您需要登录后才可以回帖 注册/登录

    本版积分规则

    关闭

    站长推荐上一条 /4 下一条

    Archiver|手机版|小黑屋|恩智浦技术社区

    GMT+8, 2024-4-26 07:33 , Processed in 0.114091 second(s), 20 queries , MemCache On.

    Powered by Discuz! X3.4

    Copyright © 2001-2024, Tencent Cloud.

    快速回复 返回顶部 返回列表