嵌入式軟件設計中查找缺陷的幾個(gè)技巧
確保永不發(fā)生堆棧溢出的唯一途徑就是分析代碼,確定程序在各種可能情況下的最大堆棧用量,然后檢查是否分配了足夠的堆棧。測試不大可能觸發(fā)特定的瞬時(shí)輸入組合進(jìn)而導致系統出現最壞情況。
堆棧深度分析的概念比較簡(jiǎn)單:
1. 為每個(gè)獨立的線(xiàn)程建立一棵調用樹(shù)。
2. 確定調用樹(shù)中每個(gè)函數的堆棧用量。
3. 檢查每棵調用樹(shù),確定從樹(shù)根到外部“樹(shù)葉”的哪條調用路徑需要使用的堆棧最多。
4. 將每個(gè)獨立線(xiàn)程調用樹(shù)的最大堆棧用量相加。
5. 確定每個(gè)中斷優(yōu)先級內各中斷服務(wù)程序(ISR)的最大堆棧用量并計算其總和。但是,如果ISR本身沒(méi)有堆棧而使用被中斷線(xiàn)程的堆棧,則應將ISR使用的最大堆棧數加到各線(xiàn)程堆棧之上。
6. 對于每個(gè)優(yōu)先級,加上中斷發(fā)生時(shí)用來(lái)保存處理器狀態(tài)的堆棧數。
7.如果使用RTOS,則加上RTOS自身內部用途需要的最大堆棧數(與應用代碼引發(fā)的系統調用不同,后者已包含在步驟2中)。
除此之外,還有兩個(gè)重要事項需要考慮。首先,僅僅從高級語(yǔ)言源代碼建立的調用樹(shù)很可能并不完善。大部分編譯器采用運行時(shí)庫(run-time library)來(lái)優(yōu)化常用計算任務(wù),如大值整數的乘除、浮點(diǎn)運算等,這些調用只在編譯器產(chǎn)生的匯編語(yǔ)言中才可見(jiàn)。運行時(shí)庫函數本身可能使用大量的堆??臻g,在分析時(shí)必須將它們包括進(jìn)去。如果使用的是C++語(yǔ)言,則以下所有類(lèi)型的函數(方法)也都必須包含到調用樹(shù)內:結構器、析構器、重載運算符、復制結構器和轉換函數。所有的函數指針也都必須進(jìn)行解析,并且將它們調用的函數包含進(jìn)分析之中。
第二,編譯器使用一個(gè)C庫來(lái)實(shí)現memcpy()、cos()和atof ()等標準函數,而這些例程的源代碼可能無(wú)法得到。如果能夠得到它們的源代碼,就有可能確定程序用到的每個(gè)庫調用在最壞情況下的堆棧使用數量。如果這些庫只包含在目標文件中,則編譯器廠(chǎng)商必須提供每個(gè)庫例程使用的堆棧數。如果沒(méi)有這些信息,就無(wú)法通過(guò)分析來(lái)確定最壞情況下程序使用的最大堆棧數。幸運的是,許多面向嵌入式系統的編譯器廠(chǎng)商都提供這些信息。
通常,每次一個(gè)函數被調用時(shí),編譯器將使用堆棧來(lái)保存返回地址并傳遞函數參數。函數的自動(dòng)(局部)變量通常也在堆棧當中。不過(guò),由于編譯器會(huì )盡可能通過(guò)將參數或局部變量放入寄存器來(lái)優(yōu)化代碼,因此檢查匯編語(yǔ)言以精確地確定堆棧用量非常重要。編譯器也有可能在代碼中的其它地方選擇使用堆棧,如用堆棧來(lái)保存中間計算結果。
有些與編譯器一起打包銷(xiāo)售的開(kāi)發(fā)環(huán)境包含生成調用樹(shù)的工具,還有許多第三方的調用樹(shù)生成工具。但是,除非它們能夠對匯編語(yǔ)言進(jìn)行分析,否則這些工具可能會(huì )遺漏運行時(shí)庫和C庫的調用。不過(guò)無(wú)論在哪種情況下,開(kāi)發(fā)分析匯編語(yǔ)言文件并提取函數名稱(chēng)以及各函數內部調用的腳本都比較簡(jiǎn)單。分析的結果可寫(xiě)入一個(gè)文件,而這個(gè)文件能夠方便地輸入到表格之中。
確定了各個(gè)函數的堆棧用量之后,必須計算每個(gè)線(xiàn)程所需的最大堆棧數。由于一般程序通常涉及數百個(gè)函數,調用跨越多層深度,處理這些信息的一種簡(jiǎn)便方法就是采用分析表格。如表1所示,表格的各行包含了函數名稱(chēng)、該函數使用的最大堆棧數(包括調用其它函數所需的堆棧數),以及它調用的所有函數的清單。通過(guò)編程控制,這個(gè)表格從每個(gè)函數的根開(kāi)始迭代循環(huán),計算該函數及其調用的所有函數需要的堆棧。這些信息存放在堆棧路徑列中,這樣,采用每個(gè)線(xiàn)程根函數(如main)的堆棧路徑數據就可以方便地計算出需要的最大堆棧數了。這個(gè)過(guò)程包含了先前介紹的堆棧分析過(guò)程中的前四個(gè)步驟。
評論