嵌入式軟件設計中查找缺陷的幾個技巧(上)

結構測試或白盒測試能有效地發現代碼中的邏輯、控制流、計算和數據錯誤。這項測試要求對軟件的內部工作能夠一覽無遺(因此稱爲"白盒"或"玻璃盒"),以便了解軟件結構的詳細情況。它檢查每個條件表達式、數學操作、輸入和輸出。由於需要測試的細節衆多,結構測試每次檢查一個軟件單元,通常爲一個函數或類。

 

代碼審查也使用與實現缺陷和潛在問題查找同樣複雜的技術。與白盒測試一樣,審查通常針對軟件的各個單元進行,因爲一個有效的審查過程要求的是集中而詳盡的檢查。表1:堆棧分析表格的一部分。

 

與審查和白盒測試不同,功能測試或黑盒測試假設對軟件的實現一無所知,它測試由受控輸入所驅動的輸出。功能測試由測試人員或開發人員所編寫的測試過程組成,它們規定了一組特定程序輸入對應的預期程序輸出。測試運行之後,測試人員將實際輸出與預期輸出進行比較,查找問題。黑盒測試可以有效地找出未能實現的需求、接口問題、性能問題和程序最常用功能中的錯誤。

 

雖然將這些技術結合起來可以找出隱藏在一個特定軟件程序中的大部分錯誤,但它們也有侷限。代碼審查和白盒測試每次只針對一小部分代碼,忽視了系統的其它部分。黑盒測試通常將系統作爲一個整體來處理,忽視了實現的細節。一些重要的問題只有在集中考察它們在整個系統內相互作用時的細節才能被發現;傳統的方法無法可靠地找出這些問題。必須整體地檢查軟件系統,查找具體問題的特定原因。由於詳盡徹底地分析程序中的每個細節和它與代碼中所有其它部分之間的相互作用通常是不大可能的,因此分析應該針對程序中已經知道可能導致問題的特定方面。本文將探討其中三個潛在的問題領域:

 

* 堆棧溢出


* 競爭條件


* 死鎖

 

讀者可在網上閱讀本文的第二部分,它將探討下列問題:

 

* 時序問題


* 可重入條件

 

在採用多任務實時設計技術的系統中,以上所有問題都相當普遍。

 

堆棧溢出

 

處理器使用堆棧來存儲臨時變量、向被調函數傳遞參數、保存線程“狀態”,等等。如果系統不使用虛擬內存(換句話說,它不能將內存頁面轉移到磁盤上以釋放內存空間供其它用途),堆棧將固定爲產品出廠時的大小。如果由於某種原因堆棧越出了編程人員所分配的數量範圍,程序將變得不確定。這種不穩定可能導致系統發生嚴重故障。因此,確保系統在最壞情況下能夠分配到足夠的堆棧至關重要。

 

確保永不發生堆棧溢出的唯一途徑就是分析代碼,確定程序在各種可能情況下的最大堆棧用量,然後檢查是否分配了足夠的堆棧。測試不大可能觸發特定的瞬時輸入組合進而導致系統出現最壞情況。

 

堆棧深度分析的概念比較簡單:


1. 爲每個獨立的線程建立一棵調用樹。


2. 確定調用樹中每個函數的堆棧用量。


3. 檢查每棵調用樹,確定從樹根到外部“樹葉”的哪條調用路徑需要使用的堆棧最多。


4. 將每個獨立線程調用樹的最大堆棧用量相加。


5. 確定每個中斷優先級內各中斷服務程序(ISR)的最大堆棧用量並計算其總和。但是,如果ISR本身沒有堆棧而使用被中斷線程的堆棧,則應將ISR使用的最大堆棧數加到各線程堆棧之上。


6. 對於每個優先級,加上中斷髮生時用來保存處理器狀態的堆棧數。


7.如果使用RTOS,則加上RTOS自身內部用途需要的最大堆棧數(與應用代碼引發的系統調用不同,後者已包含在步驟2中)。

 

除此之外,還有兩個重要事項需要考慮。首先,僅僅從高級語言源代碼建立的調用樹很可能並不完善。大部分編譯器採用運行時庫(run-time library)來優化常用計算任務,如大值整數的乘除、浮點運算等,這些調用只在編譯器產生的彙編語言中才可見。運行時庫函數本身可能使用大量的堆棧空間,在分析時必須將它們包括進去。如果使用的是C++語言,則以下所有類型的函數(方法)也都必須包含到調用樹內:結構器、析構器、重載運算符、複製結構器和轉換函數。所有的函數指針也都必須進行解析,並且將它們調用的函數包含進分析之中。

 

第二,編譯器使用一個C庫來實現memcpy()、cos()和atof ()等標準函數,而這些例程的源代碼可能無法得到。如果能夠得到它們的源代碼,就有可能確定程序用到的每個庫調用在最壞情況下的堆棧使用數量。如果這些庫只包含在目標文件中,則編譯器廠商必須提供每個庫例程使用的堆棧數。如果沒有這些信息,就無法通過分析來確定最壞情況下程序使用的最大堆棧數。幸運的是,許多面向嵌入式系統的編譯器廠商都提供這些信息。

 

通常,每次一個函數被調用時,編譯器將使用堆棧來保存返回地址並傳遞函數參數。函數的自動(局部)變量通常也在堆棧當中。不過,由於編譯器會儘可能通過將參數或局部變量放入寄存器來優化代碼,因此檢查彙編語言以精確地確定堆棧用量非常重要。編譯器也有可能在代碼中的其它地方選擇使用堆棧,如用堆棧來保存中間計算結果。

 

有些與編譯器一起打包銷售的開發環境包含生成調用樹的工具,還有許多第三方的調用樹生成工具。但是,除非它們能夠對彙編語言進行分析,否則這些工具可能會遺漏運行時庫和C庫的調用。不過無論在哪種情況下,開發分析彙編語言文件並提取函數名稱以及各函數內部調用的腳本都比較簡單。分析的結果可寫入一個文件,而這個文件能夠方便地輸入到表格之中。

 

確定了各個函數的堆棧用量之後,必須計算每個線程所需的最大堆棧數。由於一般程序通常涉及數百個函數,調用跨越多層深度,處理這些信息的一種簡便方法就是採用分析表格。如表1所示,表格的各行包含了函數名稱、該函數使用的最大堆棧數(包括調用其它函數所需的堆棧數),以及它調用的所有函數的清單。通過編程控制,這個表格從每個函數的"根"開始迭代循環,計算該函數及其調用的所有函數需要的堆棧。這些信息存放在堆棧路徑列中,這樣,採用每個線程根函數(如main)的堆棧路徑數據就可以方便地計算出需要的最大堆棧數了。這個過程包含了先前介紹的堆棧分析過程中的前四個步驟。

 

有時候,採用堆棧深度分析過程可能是無法做到,或者是不實際的。如果無法得到運行時庫或C庫的源代碼,而編譯器廠商又沒有提供任何堆棧使用信息,就不可能進行完整的堆棧分析。在這種情況下,有兩種選擇:


1. 在測試期間,觀察堆棧所能達到的深度,並保證有較大的堆棧空間餘量。


2. 檢測堆棧溢出,並採取改進措施。

 

觀察堆棧深度的方法很簡單:

 

* 向整個內存堆棧區寫入一個特定的數據圖案符號,如55AA。


* 在預期使用最大堆棧空間的條件下運行系統。


* 使用仿真器或其它工具檢查堆棧存儲區,看有多少符號圖案由於堆棧的使用而被改寫了。

 

當然,這些步驟並不能保證在一些不同條件下不會需要更多的堆棧,但確實可以表明所需要的最小堆棧數。

 

使用帶內存管理單元(MMU)的處理器時,有可能檢測出運行時的堆棧溢出現象。MMU將內存劃分爲多個區域,用一個受保護的內存段來“警戒”堆棧區域。發生堆棧溢出時,處理器將訪問這個受保護段。這個操作將引發一個異常事件(如產生SIGSEGV信號),可被程序捕獲到。創建線程時,與實時POSIX標準兼容的RTOS提供有這種堆棧警戒功能選項,大大簡化了編程人員的工作。GNU工具等其它開發環境包含有編譯器開關,可在程序中添加實現堆棧警戒功能所需的代碼,但它們仍然依靠底層操作系統來有效地處理堆棧溢出。但是,按照這種方式檢測溢出還只是問題的一部分。爲了使這類設計更爲有效,系統必須能夠從堆棧溢出中恢復過來並繼續正確地工作。

 

在一個對安全或任務要求嚴格的應用中,系統運行時在測試或檢測堆棧溢出期間監視堆棧的深度可能並不是一項足夠的風險控制措施。對於一些應用,必須確保系統絕對不會越出所分配的堆棧範圍;只有通過完整的堆棧深度分析才能證明這一點。這意味着,如果整個程序在同一內存空間運行,則必須對所有代碼執行這項分析。不過,如果使用MMU,分析常可簡化。在設計系統時,可將所有關鍵代碼置於一個或多個獨立線程內,而這些線程分別在各自的保護內存段中運行。這樣,只要對這些關鍵線程進行堆棧使用分析就可以了。當然,這項簡化設計假定當非關鍵線程溢出其堆棧並失效時,關鍵線程仍可正確執行。

 

由於分析工作所需的堆棧使用數據來自彙編語言清單,因此修改代碼時,相應模塊的堆棧使用信息必須予以更新。如果使用不同的編譯器版本,或者改變了優化設置,也必須複覈整個分析過程。在理想情況下,編譯器將提供每個函數(如果不是每個線程的話)的堆棧使用數量,因爲它擁有計算需要的所有信息。例如,瑞薩公司提供有Call Walker,這是該公司高性能的Embedded Workshop開發環境的一部分。這個工具可以圖形化地顯示每個函數使用的調用樹和堆棧,包括運行時庫和C庫的函數。Call Walker也能找出使用堆棧數量最大的路徑。使用這樣的工具可以實現步驟1到步驟3的自動化。

 

作者:Sean M. Beatty


Email: [email protected]

發佈了9 篇原創文章 · 獲贊 3 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章