新闻中心

EEPW首页 > 嵌入式系统 > 设计应用 > 嵌入式实时应用的高级动态代码分析(ADCA)

嵌入式实时应用的高级动态代码分析(ADCA)

—— 一种自动检测C/C++代码中内存访问错误的方法
作者:Stephan Lauterbach(劳特巴赫技术公司CTO)时间:2023-09-14来源:电子产品世界收藏

C 和C++ 程序语言功能强大,但也容易出现错误。其中一类容易出现的是内存访问错误,例如缓冲区溢出、内存泄漏等,其后果也可能是灾难性的。领先的调试器供应商Lauterbach 希望通过一项名为(Advanced Dynamic Code Analysis ,简称)的新技术来帮助嵌入式开发人员避免这类错误。

本文引用地址:http://www.eepw.com.cn/article/202309/450588.htm

1 引言

软件错误的代价可能是巨大的,甚至具有灾难性后果。例如,1996 年欧洲航天局(ESA)的阿丽亚娜5 号火箭在飞行39 秒后爆炸,造成超过3.7 亿美元的损失[1]。事故原因是内部惯性参考系统(SRI)软件异常,由于执行从64 位浮点数到16 位有符号整数值的数据转换时,浮点数的值超出了16 位有符号整数所能表示的范围,导致操作数错误。而数据转换指令(在Ada 代码中)也没有保护机制以防止操作数错误。

错误发生在控制捆绑式惯性平台对准的软件部分。操作数错误是由于内部对准函数结果(称为BH(水平偏差))的值太高引起的,该值与平台感知的水平速度有关。虽然阿丽亚娜5 号使用的SRI 设计与阿丽亚娜4号几乎相同,但由于阿丽亚娜5 号早期轨迹与阿丽亚娜4 号不同,导致水平速度值显著增加,使BH 值比预期高得多。这类错误不仅在火箭这一类的项目中可能造成灾难性后果,在日常嵌入式系统中也可能是危险的主要来源。

在1985 年至1987 年间,医疗行业发生了一起严重事故。由于多任务系统中存在竞争条件(Race Conditions),Therac-25 放射治疗机造成了大量辐射过量,导致三名患者死亡,至少三名其他患者受到严重伤害[2]。操作员原本打算使用低功率光束进行治疗,但由于没有放置扩散磁铁,实际使用的却是高功率光束,导致剂量远远超出预期。这一问题源自代码库中的竞争条件(Race Conditions),这种竞争条件在前一个模型Therac-20中就已经存在,但被硬件安全控制所阻止。整个软件系统由多个同时运行的进程组成,数据输入和键盘处理程序共享一个变量来标识数据输入是否完成。在数据输入阶段完成后,系统会进入磁铁设置阶段。但是,如果在这8 秒钟的磁铁设置阶段内,用户在数据输入阶段使用了特定的编辑序列,由于标识变量的值的影响,设置无法应用到机器硬件上。

在汽车行业,自动驾驶汽车中的软件错误也造成了多起死亡事故。例如,在2016 年,一辆汽车的传感器系统未能识别出一辆大型白色18 轮卡车/ 拖车正在穿过高速公路,汽车以全速驶入了拖车的下方[3]

为了尽可能避免这类错误,自动代码分析工具可以帮助开发人员自动检测日益复杂的软件中的错误。

2 最危险的软件缺陷

2.1 内存访问错误是软件错误的主要来源之一

美国国家网络安全卓越中心每年都会发布常见缺陷列表(CWE™),其中包括《Top 25 最危险软件缺陷》(CWE™ Top 25)[4]。该清单展示了目前最常见和影响最大的软件缺陷。

为了创建这份清单,CWE 团队利用了国家标准技术研究所(NIST)国家漏洞数据库(NVD)中的常见漏洞和暴露(CVE®)数据和与每个CVE 记录相关的常见漏洞评分系统(CVSS)的分数,包括关注网络安全和基础设施安全局(CISA)已知被利用漏洞(KEV)目录中的CVE 记录。应用了一个公式来根据普遍性和严重性对每个软件缺陷进行评分。

用于计算2022 年Top 25 的数据集包含了前两年内共37,899 条CVE 记录。2022 年,前11 大缺陷中有4个是与内存相关的错误( 图1)。

image.png

图1 2022 CWE™ Top 25

2.2 C/C++语言中典型的内存访问错误

在2022 年,前11 种软件缺陷中有四个与内存访问错误有关,如图2 所示。越界错误位居2022 年CWE™Top 25 清单之首。该错误类型一般有三种变体,但基本问题的核心都相同:程序在分配的内存区域外写入或读取了数据。

1694672244473437.png

图2 三种常见越界示例

当数据大小超过所分配的内存区域时,可能会发生缓冲区溢出。这种情况下,数据可能被写入或读取错误的内存位置。此外,当程序计算数据大小或位置不正确时也可能发生缓冲区溢出。在我们的示例中,缓冲区溢出发生在程序试图访问数组中无效索引的情况下,即索引小于0 或等于或大于数组长度。

错误,每种错误发生的频率都较低,但其影响也同样严重,主要包括以下几种。

●   未定义行为:程序的行为没有被编程语言规范所定义。未定义行为的例子包括有符号整数溢出、空指针引用、在没有序列点的表达式中多次修改同一个标量以及通过不同类型的指针访问对象。

●   内存泄漏:当程序员分配内存但忘记使用delete()函数或delete[] 运算符释放内存时,就会发生内存泄漏。C++ 中最常见的内存泄漏是使用错误的delete 运算符。delete 运算符应用于释放单个分配的内存空间,而delete [] 运算符应用于释放数据值数组。

●   使用后释放:当在释放内存后又引用内存时,会发生这种错误。使用先前释放的内存可能会产生各种不利后果,从有效数据损坏到执行任意代码,具体取决于缺陷的实例和时序。

●   未初始化内存读取:如果应用程序从未被填充初始值的可寻址内存中读取,则会发生此错误。错误可能是由于初始化顺序不正确或多线程应用程序中的竞争条件引起的。

●   返回后使用栈:如果在声明函数返回后访问栈变量内存,则会发生此错误。

图3 列举了这些错误类型的简短代码示例。

1694672334732344.png

图3 C/C++代码中常见内存错误

3 目前可用的自动代码分析工具

为了避免C/C++ 代码中出现内存错误,已经有一些动态代码分析工具可以帮助检查,如图4 中显示是最著名并长期被使用的Valgrind 和AddressSanitizer(简称ASan)两个工具。两者都支持多种CPU 架构,并以不同方式对代码进行检测。但是,由于这两种工具都会导致性能降低和内存损耗,因此它们在中的使用受到极大的限制。

1694672386513802.png

图4 两款常用工具比较

3.1 Valgrind

Valgrind本质上是一种使用即时编译技术的虚拟机,包括动态重新编译[5]。它首先将程序转换为一种临时、更简单的形式,称为中间表示(IR),然后工具可以自由地对IR 进行任何转换,最后Valgrind 将IR 转换成机器代码并让主处理器运行它。

Valgrind 附带了多个工具, 默认且最常用的是Memcheck。Memcheck 在几乎所有指令周围插入额外的检测代码,用于跟踪数据的有效性和可寻址性。此外,Memcheck 用自己的实现替换了标准C 内存分配函数,该实现还包括在所有分配块周围设置内存保护。这个功能使Memcheck 能够检测到微量偏差错误。

Memcheck 能够检测并警告的问题包括以下几点:

●   使用未初始化的内存;

●   在内存被释放后读写;

●   越界访问malloc 分配的内存块;

●   内存泄漏。

使用Valgrind 工具的主要代价是性能的损失。在Memcheck 下运行的程序通常比在Valgrind 外运行慢20-30 倍,并且使用更多的内存(每次分配都会有一定的内存开销)。

3.2 ASan

AddressSanitizer(或ASan)是谷歌安全研究人员创建的一种开源编程工具,用于识别C 和C++ 程序中的内存访问问题[6]。它可以检测到内存相关错误,例如缓冲区溢出或对悬空指针(释放后使用)的访问等。AddressSanitizer 的实现是基于编译器插桩和直接映射影子内存。

为了监控内存分配并识别内存泄漏,malloc 和free系列函数被替换,因此每次内存分配/ 释放都会被工具监控。然后,每次读取或写入内存访问都会被编译为一段代码,用来检查该内存地址是否被标记为有害。如果是有害的,它将报告一个错误。

通常情况下,应用程序的虚拟地址空间被划分为应用程序代码使用的程序内存和存储有害(不可寻址)内存元数据内存的影子内存。 AddressSanitizer 将每8 字节的应用程序内存映射到1 字节的影子内存中。如果一个内存地址未被标识有害(即可寻址),则影子内存中的标志位为0。如果一个内存地址为有害(即不可寻址),则影子内存中的标志位为1。这样,AddressSanitizer 就可以识别哪些内存访问是允许的,哪些不允许并报告错误。与Valgrind 一样,您也必须为ASan付出高昂的代价,包括速度损失和内存需求。在Asan 下运行的程序通常比在外部运行慢2 倍,并且平均使用240%更多的内存。

4 适用于嵌入式实时系统的Lauterbach跟踪技术

鉴于现有工具有时无法满足实时系统的需求,那么使用Lauterbach 的跟踪技术或者是一种选择。图5 显示了“Lauterbach Trace Pyramid”的功能模块,顶部是新的 技术。 基于Lauterbach 的上下文跟踪系统(CTS),而CTS 又基于标准实时流跟踪技术。

1694672565864937.png

图5 “Lauterbach Trace Pyramid”架构

4.1 实时流数据跟踪Real Time Flow Trace

不断提高的集成密度和价格压力导致许多处理器将CPU 内核、缓存、外设、FLASH 和RAM 内存集成在一个封装中(SoC),在许多情况下甚至不再具有外部存储器接口。除了调试接口外,很多芯片厂商还在芯片上实现了具有特殊功能的跟踪接口。这使得程序和数据以压缩形式可以输出到芯片外部(图6),这种方法称为流跟踪方法。

跟踪总线链接到跟踪接口,通过该总线以压缩形式传输程序流和(或)数据访问信息。地址总线/ 数据总线的信息在CPU 核心处也可以直接输出。这意味着也可以记录对芯片内部FLASH或RAM内存(特别是缓存)的访问。

只有少数跟踪接口支持特定的开/ 关切换或定义地址窗口,以控制生成跟踪数据。因此唯一的解决方案是使用具有大型跟踪内存的跟踪工具,其中所有跟踪数据都未经过滤地被记录起来。通过几秒钟的录制时间,很可能可以在录制数据中找到所寻求的错误。此外,多任务程序运行的跟踪数据也可用于运行时统计分析和/ 或代码覆盖率分析。

这种跟踪技术唯一的缺点是带宽问题,即如果芯片内部生成的跟踪数据比通过跟踪接口传输的更多,就会出数据丢失的情况。芯片制造商一般用FIFO 缓冲区和减少跟踪数据来解决这个问题。

实时流跟踪虽然不是特别高尖端学科,但是,Lauterbach 的跟踪工具提供了业界最高的数据带宽和最丰富的数据分析功能。

1694672677290939.png

图6 流跟踪的通用配置

4.2 TRACE32上下文跟踪系统(CTS)

仅依靠流跟踪数据可能需要花费大量时间分析跟踪数据,以找出哪些指令、数据或系统状态导致目标系统出现故障。

Lauterbach 的基于跟踪的调试- 简称CTS- 允许用户根据跟踪缓冲区中采样的跟踪数据重构选定点的目标系统状态(图7)。并且从这个选定点开始,可以重复在TRACE32 PowerView GUI 中调试实时记录在跟踪存储器中的程序步骤。执行全功能跟踪调试的前提条件是将程序和数据流完整记录到跟踪缓冲区,直到程序执行停止。

1694672736862482.png

图7 Context Tracking System (CTS)功能

使能上下文跟踪系统(CTS)功能后,可以选择一个记录点,在指令集模拟器(SIM) 中为其重构目标系统状态。程序计数器(PC) 自动设置在源程序中对应的位置,即该记录点所跟踪数据的地址。

现在CTS 也已经支持所有调试命令,例如Step、Step Over Call、Go、Return 等。 CPU 指令按照它们在跟踪存储器中记录的顺序进行处理。调试功能也实现了进一步扩展,甚至可支持程序向后步进。

由于指令集模拟器(SIM)支持单个指令的执行,因此也能够跟踪变量、内存和寄存器的变化等。甚至T32 SW 还可以自动修复由于跟踪端口带宽限制,所产生跟踪数据遗漏而导致的代码丢失。如果只采样读操作以防止跟踪端口过载,则CTS 也可以重建所有写操作。

此外,TRACE32® 也可以使用CTS 技术支持缓存(Cache)状态/ 使用率分析等,该技术也是基于跟踪记录中捕获的程序跟踪数据。如果设置了MMU 架构,则缓存分析将考虑对缓存控制寄存器的所有操作:包括缓存刷新、缓存的开启和关闭以及缓存锁定等。

总之,Lauterbach 的CTS 可以大大简化调试,特别是对于仅靠停止调试模式往往不够的实时应用程序。

4.3 TRACE32 (Advanced Dynamic Code Analysis ,ADCA)

ADCA 是一种先进的CTS 模式,用于探索和修复由不同类型的错误触发的内存访问错误。它在启动代码之后运行,捕获有效的初始内存和寄存器状态,堆栈和数据等。

ADCA 需要完整的程序跟踪流和足够的数据来重建所有指针以及从启动代码的完整跟踪数据。该工具还依赖于编译器提供的正确和完整的调试信息,并需要理解编译器生成和优化的结构。此外,它还需要理解特殊代码,如中断和RTOS 任务切换等。

ADCA 的核心功能是将静态和动态标签分配给所有数据和内存地址。标签是不可见的元信息,由调试器支持。根据钥匙锁原理检测内存违规:具有某个标签的数据只能访问与相同标签关联的内存地址(图8)。

1694672848554055.png

图8 一对一钥匙锁校验机制

运行程序并记录跟踪数据后,您可以运行ADCA功能并结合TRACE32®PowerView 软件的其他相关窗口的信息,识别相关错误(图9)。

1694672879695858.png

图9 TRACE32 PowerView相关窗口

Lauterbach 的ADCA 报告显示了所有潜在内存访问错误的总结( 图10)。下面将讨论报告中的一个具体示例: 访问名为“check_array1”的数组以及访问冲突的内存地址(0x418f5c) 时失败。开发人员可以使用这些信息来分析代码,并通过使用TRACE32®PowerView GUI中显示的附加信息来修复错误。

1694672952387739.png

图10 TRACE32® ACDA 自动检测内存错误

从图9 所示的可能性中,第一个可能的TRACE32®PowerView视图应该是存储器显示( 图11)。在显示器的左侧,可以观察到地址418F5C 的标签变化。结果很明显,与数组“check_array1”( 标签28A) 相关的最新有效内存地址是418F5B。

1694672985682842.png

图11 TRACE32® ADCA 内存视图

在内存显示的中间部分可以观察到,“check_array1”的最高有效索引( 与内存地址418F58 到418F5B 和标签28A 相关联) 是“9”。

正如稍后将看到的,这些信息对于错误检测很有价值,但当然还不足以修复错误。

接下来要探索的是寄存器显示(如图12)。显然,寄存器R1 与标签28A 相关联,该标签属于数组“check_array1”。不幸的是,R1 指向内存地址418F5C,该地址位于“check_array1”的有效地址空间之外。此时,错误的原因已经接近被发现,只需要找出源代码窗口即可(如图13)。

1694673067603810.png

图12 TRACE32® ADCA寄存器视图

根据寄存器显示中的信息(如图12),可以轻松确定寄存器R1 包含Lauterbach 的ADCA 在开始时探索到的对“check_array1 [i]”进行写访问的(无效)内存地址。根据内存显示中的信息,这种无效访问的原因是显而易见的:索引变量“i”从0 增加到10,这导致最后一个循环通过“check_array1 [10]”发生内存访问违规。要修复错误,您只需将代码从“i<11”更改为“i <10”即可。

1694673100682024.png

图13 TRACE32® ADCA源码视图

尽管目前已经有一些成熟的代码分析工具,但它们却都不适用于实时的嵌入式软件应用。而Lauterbach 恰恰满足了嵌入式开发人员对于实时性的要求和检测C / C++ 中相关的潜在内存访问错误的需求。ADCA 是识别这些难以发现和复现的错误的重大进步,也可支持多核架构。当然也有一些限制。比如某些特殊的结构和错误类型无法检测。但是,这些类型的错误通常都会被编译器检测出来。特殊的代码结构可能需要额外的设置来提供分析信息。

Lauterbach ADCA 技术将在2023 年的软件版本中作为TRACE32®PowerView 软件更新的一部分对劳特巴赫客户免费提供。ADCA 也不会作为单独产品出售,因此也无法用作第三方跟踪工具的附加组件。希望这个功能为您的软件开发提供更多帮助。

参考文献:

[1] https://www.bugsnag.com/blog/bug-day-ariane-5-disaster, Retrieved 12 Feb 2023.

[2] https://pvs-studio.com/en/blog/posts/0438/, Retrieved12 Feb 2023.

[3] https://www.businessinsider.com/details-aboutthe-fatal-tesla-autopilot-accident-released-2017-6,Retrieved 12 Feb 2023.

[4] https://cwe.mitre.org/top25/archive/2022/2022_cwe_top25.html, Retrieved 12 Feb 2023.

[5] https://valgrind.org/docs/manual/mc-manual.html,Retrieved 12 Feb 2023.

[6] https://clang.llvm.org/docs/AddressSanitizer.html,Retrieved 12 Feb 2023.

作者简介:

1694673865321202.png

Lauterbach GmbH公司的所有者之一。自1982年加入劳特巴赫以来,一直在TRACE32调试器产品的开发中处于核心位置,他对复杂的软硬件系统研发经验丰富。

(本文来源于EEPW 2023年9月期)



评论


技术专区

关闭