嵌入式實(shí)時(shí)應用的高級動(dòng)態(tài)代碼分析(ADCA)
C 和C++ 程序語(yǔ)言功能強大,但也容易出現錯誤。其中一類(lèi)容易出現的是內存訪(fǎng)問(wèn)錯誤,例如緩沖區溢出、內存泄漏等,其后果也可能是災難性的。領(lǐng)先的調試器供應商Lauterbach 希望通過(guò)一項名為高級動(dòng)態(tài)代碼分析(Advanced Dynamic Code Analysis ,簡(jiǎn)稱(chēng)ADCA)的新技術(shù)來(lái)幫助嵌入式開(kāi)發(fā)人員避免這類(lèi)錯誤。
本文引用地址:http://dyxdggzs.com/article/202309/450588.htm1 引言
軟件錯誤的代價(jià)可能是巨大的,甚至具有災難性后果。例如,1996 年歐洲航天局(ESA)的阿麗亞娜5 號火箭在飛行39 秒后爆炸,造成超過(guò)3.7 億美元的損失[1]。事故原因是內部慣性參考系統(SRI)軟件異常,由于執行從64 位浮點(diǎn)數到16 位有符號整數值的數據轉換時(shí),浮點(diǎn)數的值超出了16 位有符號整數所能表示的范圍,導致操作數錯誤。而數據轉換指令(在A(yíng)da 代碼中)也沒(méi)有保護機制以防止操作數錯誤。
錯誤發(fā)生在控制捆綁式慣性平臺對準的軟件部分。操作數錯誤是由于內部對準函數結果(稱(chēng)為BH(水平偏差))的值太高引起的,該值與平臺感知的水平速度有關(guān)。雖然阿麗亞娜5 號使用的SRI 設計與阿麗亞娜4號幾乎相同,但由于阿麗亞娜5 號早期軌跡與阿麗亞娜4 號不同,導致水平速度值顯著(zhù)增加,使BH 值比預期高得多。這類(lèi)錯誤不僅在火箭這一類(lèi)的項目中可能造成災難性后果,在日常嵌入式系統中也可能是危險的主要來(lái)源。
在1985 年至1987 年間,醫療行業(yè)發(fā)生了一起嚴重事故。由于多任務(wù)系統中存在競爭條件(Race Conditions),Therac-25 放射治療機造成了大量輻射過(guò)量,導致三名患者死亡,至少三名其他患者受到嚴重傷害[2]。操作員原本打算使用低功率光束進(jìn)行治療,但由于沒(méi)有放置擴散磁鐵,實(shí)際使用的卻是高功率光束,導致劑量遠遠超出預期。這一問(wèn)題源自代碼庫中的競爭條件(Race Conditions),這種競爭條件在前一個(gè)模型Therac-20中就已經(jīng)存在,但被硬件安全控制所阻止。整個(gè)軟件系統由多個(gè)同時(shí)運行的進(jìn)程組成,數據輸入和鍵盤(pán)處理程序共享一個(gè)變量來(lái)標識數據輸入是否完成。在數據輸入階段完成后,系統會(huì )進(jìn)入磁鐵設置階段。但是,如果在這8 秒鐘的磁鐵設置階段內,用戶(hù)在數據輸入階段使用了特定的編輯序列,由于標識變量的值的影響,設置無(wú)法應用到機器硬件上。
在汽車(chē)行業(yè),自動(dòng)駕駛汽車(chē)中的軟件錯誤也造成了多起死亡事故。例如,在2016 年,一輛汽車(chē)的傳感器系統未能識別出一輛大型白色18 輪卡車(chē)/ 拖車(chē)正在穿過(guò)高速公路,汽車(chē)以全速駛入了拖車(chē)的下方[3]。
為了盡可能避免這類(lèi)錯誤,自動(dòng)代碼分析工具可以幫助開(kāi)發(fā)人員自動(dòng)檢測日益復雜的軟件中的錯誤。
2 最危險的軟件缺陷
2.1 內存訪(fǎng)問(wèn)錯誤是軟件錯誤的主要來(lái)源之一
美國國家網(wǎng)絡(luò )安全卓越中心每年都會(huì )發(fā)布常見(jiàn)缺陷列表(CWE?),其中包括《Top 25 最危險軟件缺陷》(CWE? Top 25)[4]。該清單展示了目前最常見(jiàn)和影響最大的軟件缺陷。
為了創(chuàng )建這份清單,CWE 團隊利用了國家標準技術(shù)研究所(NIST)國家漏洞數據庫(NVD)中的常見(jiàn)漏洞和暴露(CVE?)數據和與每個(gè)CVE 記錄相關(guān)的常見(jiàn)漏洞評分系統(CVSS)的分數,包括關(guān)注網(wǎng)絡(luò )安全和基礎設施安全局(CISA)已知被利用漏洞(KEV)目錄中的CVE 記錄。應用了一個(gè)公式來(lái)根據普遍性和嚴重性對每個(gè)軟件缺陷進(jìn)行評分。
用于計算2022 年Top 25 的數據集包含了前兩年內共37,899 條CVE 記錄。2022 年,前11 大缺陷中有4個(gè)是與內存相關(guān)的錯誤( 圖1)。
圖1 2022 CWE? Top 25
2.2 C/C++語(yǔ)言中典型的內存訪(fǎng)問(wèn)錯誤
在2022 年,前11 種軟件缺陷中有四個(gè)與內存訪(fǎng)問(wèn)錯誤有關(guān),如圖2 所示。越界錯誤位居2022 年CWE?Top 25 清單之首。該錯誤類(lèi)型一般有三種變體,但基本問(wèn)題的核心都相同:程序在分配的內存區域外寫(xiě)入或讀取了數據。
圖2 三種常見(jiàn)越界示例
當數據大小超過(guò)所分配的內存區域時(shí),可能會(huì )發(fā)生緩沖區溢出。這種情況下,數據可能被寫(xiě)入或讀取錯誤的內存位置。此外,當程序計算數據大小或位置不正確時(shí)也可能發(fā)生緩沖區溢出。在我們的示例中,緩沖區溢出發(fā)生在程序試圖訪(fǎng)問(wèn)數組中無(wú)效索引的情況下,即索引小于0 或等于或大于數組長(cháng)度。
錯誤,每種錯誤發(fā)生的頻率都較低,但其影響也同樣嚴重,主要包括以下幾種。
● 未定義行為:程序的行為沒(méi)有被編程語(yǔ)言規范所定義。未定義行為的例子包括有符號整數溢出、空指針引用、在沒(méi)有序列點(diǎn)的表達式中多次修改同一個(gè)標量以及通過(guò)不同類(lèi)型的指針訪(fǎng)問(wèn)對象。
● 內存泄漏:當程序員分配內存但忘記使用delete()函數或delete[] 運算符釋放內存時(shí),就會(huì )發(fā)生內存泄漏。C++ 中最常見(jiàn)的內存泄漏是使用錯誤的delete 運算符。delete 運算符應用于釋放單個(gè)分配的內存空間,而delete [] 運算符應用于釋放數據值數組。
● 使用后釋放:當在釋放內存后又引用內存時(shí),會(huì )發(fā)生這種錯誤。使用先前釋放的內存可能會(huì )產(chǎn)生各種不利后果,從有效數據損壞到執行任意代碼,具體取決于缺陷的實(shí)例和時(shí)序。
● 未初始化內存讀?。喝绻麘贸绦驈奈幢惶畛涑跏贾档目蓪ぶ穬却嬷凶x取,則會(huì )發(fā)生此錯誤。錯誤可能是由于初始化順序不正確或多線(xiàn)程應用程序中的競爭條件引起的。
● 返回后使用棧:如果在聲明函數返回后訪(fǎng)問(wèn)棧變量?jì)却?,則會(huì )發(fā)生此錯誤。
圖3 列舉了這些錯誤類(lèi)型的簡(jiǎn)短代碼示例。
圖3 C/C++代碼中常見(jiàn)內存錯誤
3 目前可用的自動(dòng)代碼分析工具
為了避免C/C++ 代碼中出現內存錯誤,已經(jīng)有一些動(dòng)態(tài)代碼分析工具可以幫助檢查,如圖4 中顯示是最著(zhù)名并長(cháng)期被使用的Valgrind 和AddressSanitizer(簡(jiǎn)稱(chēng)ASan)兩個(gè)工具。兩者都支持多種CPU 架構,并以不同方式對代碼進(jìn)行檢測。但是,由于這兩種工具都會(huì )導致性能降低和內存損耗,因此它們在嵌入式實(shí)時(shí)應用中的使用受到極大的限制。
圖4 兩款常用工具比較
3.1 Valgrind
Valgrind本質(zhì)上是一種使用即時(shí)編譯技術(shù)的虛擬機,包括動(dòng)態(tài)重新編譯[5]。它首先將程序轉換為一種臨時(shí)、更簡(jiǎn)單的形式,稱(chēng)為中間表示(IR),然后工具可以自由地對IR 進(jìn)行任何轉換,最后Valgrind 將IR 轉換成機器代碼并讓主處理器運行它。
Valgrind 附帶了多個(gè)工具, 默認且最常用的是Memcheck。Memcheck 在幾乎所有指令周?chē)迦腩~外的檢測代碼,用于跟蹤數據的有效性和可尋址性。此外,Memcheck 用自己的實(shí)現替換了標準C 內存分配函數,該實(shí)現還包括在所有分配塊周?chē)O置內存保護。這個(gè)功能使Memcheck 能夠檢測到微量偏差錯誤。
Memcheck 能夠檢測并警告的問(wèn)題包括以下幾點(diǎn):
● 使用未初始化的內存;
● 在內存被釋放后讀寫(xiě);
● 越界訪(fǎng)問(wèn)malloc 分配的內存塊;
● 內存泄漏。
使用Valgrind 工具的主要代價(jià)是性能的損失。在Memcheck 下運行的程序通常比在Valgrind 外運行慢20-30 倍,并且使用更多的內存(每次分配都會(huì )有一定的內存開(kāi)銷(xiāo))。
3.2 ASan
AddressSanitizer(或ASan)是谷歌安全研究人員創(chuàng )建的一種開(kāi)源編程工具,用于識別C 和C++ 程序中的內存訪(fǎng)問(wèn)問(wèn)題[6]。它可以檢測到內存相關(guān)錯誤,例如緩沖區溢出或對懸空指針(釋放后使用)的訪(fǎng)問(wèn)等。AddressSanitizer 的實(shí)現是基于編譯器插樁和直接映射影子內存。
為了監控內存分配并識別內存泄漏,malloc 和free系列函數被替換,因此每次內存分配/ 釋放都會(huì )被工具監控。然后,每次讀取或寫(xiě)入內存訪(fǎng)問(wèn)都會(huì )被編譯為一段代碼,用來(lái)檢查該內存地址是否被標記為有害。如果是有害的,它將報告一個(gè)錯誤。
通常情況下,應用程序的虛擬地址空間被劃分為應用程序代碼使用的程序內存和存儲有害(不可尋址)內存元數據內存的影子內存。 AddressSanitizer 將每8 字節的應用程序內存映射到1 字節的影子內存中。如果一個(gè)內存地址未被標識有害(即可尋址),則影子內存中的標志位為0。如果一個(gè)內存地址為有害(即不可尋址),則影子內存中的標志位為1。這樣,AddressSanitizer 就可以識別哪些內存訪(fǎng)問(wèn)是允許的,哪些不允許并報告錯誤。與Valgrind 一樣,您也必須為ASan付出高昂的代價(jià),包括速度損失和內存需求。在A(yíng)san 下運行的程序通常比在外部運行慢2 倍,并且平均使用240%更多的內存。
4 適用于嵌入式實(shí)時(shí)系統的Lauterbach跟蹤技術(shù)
鑒于現有工具有時(shí)無(wú)法滿(mǎn)足實(shí)時(shí)系統的需求,那么使用Lauterbach 的跟蹤技術(shù)或者是一種選擇。圖5 顯示了“Lauterbach Trace Pyramid”的功能模塊,頂部是新的ADCA 技術(shù)。ADCA 基于Lauterbach 的上下文跟蹤系統(CTS),而CTS 又基于標準實(shí)時(shí)流跟蹤技術(shù)。
圖5 “Lauterbach Trace Pyramid”架構
4.1 實(shí)時(shí)流數據跟蹤Real Time Flow Trace
不斷提高的集成密度和價(jià)格壓力導致許多處理器將CPU 內核、緩存、外設、FLASH 和RAM 內存集成在一個(gè)封裝中(SoC),在許多情況下甚至不再具有外部存儲器接口。除了調試接口外,很多芯片廠(chǎng)商還在芯片上實(shí)現了具有特殊功能的跟蹤接口。這使得程序和數據以壓縮形式可以輸出到芯片外部(圖6),這種方法稱(chēng)為流跟蹤方法。
跟蹤總線(xiàn)鏈接到跟蹤接口,通過(guò)該總線(xiàn)以壓縮形式傳輸程序流和(或)數據訪(fǎng)問(wèn)信息。地址總線(xiàn)/ 數據總線(xiàn)的信息在CPU 核心處也可以直接輸出。這意味著(zhù)也可以記錄對芯片內部FLASH或RAM內存(特別是緩存)的訪(fǎng)問(wèn)。
只有少數跟蹤接口支持特定的開(kāi)/ 關(guān)切換或定義地址窗口,以控制生成跟蹤數據。因此唯一的解決方案是使用具有大型跟蹤內存的跟蹤工具,其中所有跟蹤數據都未經(jīng)過(guò)濾地被記錄起來(lái)。通過(guò)幾秒鐘的錄制時(shí)間,很可能可以在錄制數據中找到所尋求的錯誤。此外,多任務(wù)程序運行的跟蹤數據也可用于運行時(shí)統計分析和/ 或代碼覆蓋率分析。
這種跟蹤技術(shù)唯一的缺點(diǎn)是帶寬問(wèn)題,即如果芯片內部生成的跟蹤數據比通過(guò)跟蹤接口傳輸的更多,就會(huì )出數據丟失的情況。芯片制造商一般用FIFO 緩沖區和減少跟蹤數據來(lái)解決這個(gè)問(wèn)題。
實(shí)時(shí)流跟蹤雖然不是特別高尖端學(xué)科,但是,Lauterbach 的跟蹤工具提供了業(yè)界最高的數據帶寬和最豐富的數據分析功能。
圖6 流跟蹤的通用配置
4.2 TRACE32上下文跟蹤系統(CTS)
僅依靠流跟蹤數據可能需要花費大量時(shí)間分析跟蹤數據,以找出哪些指令、數據或系統狀態(tài)導致目標系統出現故障。
Lauterbach 的基于跟蹤的調試- 簡(jiǎn)稱(chēng)CTS- 允許用戶(hù)根據跟蹤緩沖區中采樣的跟蹤數據重構選定點(diǎn)的目標系統狀態(tài)(圖7)。并且從這個(gè)選定點(diǎn)開(kāi)始,可以重復在TRACE32 PowerView GUI 中調試實(shí)時(shí)記錄在跟蹤存儲器中的程序步驟。執行全功能跟蹤調試的前提條件是將程序和數據流完整記錄到跟蹤緩沖區,直到程序執行停止。
圖7 Context Tracking System (CTS)功能
使能上下文跟蹤系統(CTS)功能后,可以選擇一個(gè)記錄點(diǎn),在指令集模擬器(SIM) 中為其重構目標系統狀態(tài)。程序計數器(PC) 自動(dòng)設置在源程序中對應的位置,即該記錄點(diǎn)所跟蹤數據的地址。
現在CTS 也已經(jīng)支持所有調試命令,例如Step、Step Over Call、Go、Return 等。 CPU 指令按照它們在跟蹤存儲器中記錄的順序進(jìn)行處理。調試功能也實(shí)現了進(jìn)一步擴展,甚至可支持程序向后步進(jìn)。
由于指令集模擬器(SIM)支持單個(gè)指令的執行,因此也能夠跟蹤變量、內存和寄存器的變化等。甚至T32 SW 還可以自動(dòng)修復由于跟蹤端口帶寬限制,所產(chǎn)生跟蹤數據遺漏而導致的代碼丟失。如果只采樣讀操作以防止跟蹤端口過(guò)載,則CTS 也可以重建所有寫(xiě)操作。
此外,TRACE32? 也可以使用CTS 技術(shù)支持緩存(Cache)狀態(tài)/ 使用率分析等,該技術(shù)也是基于跟蹤記錄中捕獲的程序跟蹤數據。如果設置了MMU 架構,則緩存分析將考慮對緩存控制寄存器的所有操作:包括緩存刷新、緩存的開(kāi)啟和關(guān)閉以及緩存鎖定等。
總之,Lauterbach 的CTS 可以大大簡(jiǎn)化調試,特別是對于僅靠停止調試模式往往不夠的實(shí)時(shí)應用程序。
4.3 TRACE32 高級動(dòng)態(tài)代碼分析 (Advanced Dynamic Code Analysis ,ADCA)
ADCA 是一種先進(jìn)的CTS 模式,用于探索和修復由不同類(lèi)型的錯誤觸發(fā)的內存訪(fǎng)問(wèn)錯誤。它在啟動(dòng)代碼之后運行,捕獲有效的初始內存和寄存器狀態(tài),堆棧和數據等。
ADCA 需要完整的程序跟蹤流和足夠的數據來(lái)重建所有指針以及從啟動(dòng)代碼的完整跟蹤數據。該工具還依賴(lài)于編譯器提供的正確和完整的調試信息,并需要理解編譯器生成和優(yōu)化的結構。此外,它還需要理解特殊代碼,如中斷和RTOS 任務(wù)切換等。
ADCA 的核心功能是將靜態(tài)和動(dòng)態(tài)標簽分配給所有數據和內存地址。標簽是不可見(jiàn)的元信息,由調試器支持。根據鑰匙鎖原理檢測內存違規:具有某個(gè)標簽的數據只能訪(fǎng)問(wèn)與相同標簽關(guān)聯(lián)的內存地址(圖8)。
圖8 一對一鑰匙鎖校驗機制
運行程序并記錄跟蹤數據后,您可以運行ADCA功能并結合TRACE32?PowerView 軟件的其他相關(guān)窗口的信息,識別相關(guān)錯誤(圖9)。
圖9 TRACE32 PowerView相關(guān)窗口
Lauterbach 的ADCA 報告顯示了所有潛在內存訪(fǎng)問(wèn)錯誤的總結( 圖10)。下面將討論報告中的一個(gè)具體示例: 訪(fǎng)問(wèn)名為“check_array1”的數組以及訪(fǎng)問(wèn)沖突的內存地址(0x418f5c) 時(shí)失敗。開(kāi)發(fā)人員可以使用這些信息來(lái)分析代碼,并通過(guò)使用TRACE32?PowerView GUI中顯示的附加信息來(lái)修復錯誤。
圖10 TRACE32? ACDA 自動(dòng)檢測內存錯誤
從圖9 所示的可能性中,第一個(gè)可能的TRACE32?PowerView視圖應該是存儲器顯示( 圖11)。在顯示器的左側,可以觀(guān)察到地址418F5C 的標簽變化。結果很明顯,與數組“check_array1”( 標簽28A) 相關(guān)的最新有效內存地址是418F5B。
圖11 TRACE32? ADCA 內存視圖
在內存顯示的中間部分可以觀(guān)察到,“check_array1”的最高有效索引( 與內存地址418F58 到418F5B 和標簽28A 相關(guān)聯(lián)) 是“9”。
正如稍后將看到的,這些信息對于錯誤檢測很有價(jià)值,但當然還不足以修復錯誤。
接下來(lái)要探索的是寄存器顯示(如圖12)。顯然,寄存器R1 與標簽28A 相關(guān)聯(lián),該標簽屬于數組“check_array1”。不幸的是,R1 指向內存地址418F5C,該地址位于“check_array1”的有效地址空間之外。此時(shí),錯誤的原因已經(jīng)接近被發(fā)現,只需要找出源代碼窗口即可(如圖13)。
圖12 TRACE32? ADCA寄存器視圖
根據寄存器顯示中的信息(如圖12),可以輕松確定寄存器R1 包含Lauterbach 的ADCA 在開(kāi)始時(shí)探索到的對“check_array1 [i]”進(jìn)行寫(xiě)訪(fǎng)問(wèn)的(無(wú)效)內存地址。根據內存顯示中的信息,這種無(wú)效訪(fǎng)問(wèn)的原因是顯而易見(jiàn)的:索引變量“i”從0 增加到10,這導致最后一個(gè)循環(huán)通過(guò)“check_array1 [10]”發(fā)生內存訪(fǎng)問(wèn)違規。要修復錯誤,您只需將代碼從“i<11”更改為“i <10”即可。
圖13 TRACE32? ADCA源碼視圖
盡管目前已經(jīng)有一些成熟的代碼分析工具,但它們卻都不適用于實(shí)時(shí)的嵌入式軟件應用。而Lauterbach 高級動(dòng)態(tài)代碼分析恰恰滿(mǎn)足了嵌入式開(kāi)發(fā)人員對于實(shí)時(shí)性的要求和檢測C / C++ 中相關(guān)的潛在內存訪(fǎng)問(wèn)錯誤的需求。ADCA 是識別這些難以發(fā)現和復現的錯誤的重大進(jìn)步,也可支持多核架構。當然也有一些限制。比如某些特殊的結構和錯誤類(lèi)型無(wú)法檢測。但是,這些類(lèi)型的錯誤通常都會(huì )被編譯器檢測出來(lái)。特殊的代碼結構可能需要額外的設置來(lái)提供分析信息。
Lauterbach ADCA 技術(shù)將在2023 年的軟件版本中作為T(mén)RACE32?PowerView 軟件更新的一部分對勞特巴赫客戶(hù)免費提供。ADCA 也不會(huì )作為單獨產(chǎn)品出售,因此也無(wú)法用作第三方跟蹤工具的附加組件。希望這個(gè)功能為您的軟件開(kāi)發(fā)提供更多幫助。
參考文獻:
[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.
作者簡(jiǎn)介:
Lauterbach GmbH公司的所有者之一。自1982年加入勞特巴赫以來(lái),一直在TRACE32調試器產(chǎn)品的開(kāi)發(fā)中處于核心位置,他對復雜的軟硬件系統研發(fā)經(jīng)驗豐富。
(本文來(lái)源于EEPW 2023年9月期)
評論