1. gzyueqian
      13352868059
      首頁 > 新聞中心 > > 正文

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

      更新時間: 2006-01-05 16:13:08來源: 粵嵌教育瀏覽量:2646

        大部分軟件開發項目依靠結合代碼檢查、結構測試和功能測試來識別軟件缺陷。盡管這些傳統技術非常重要,而且能發現大多數軟件問題,但它們無法檢查出當今復雜系統中的許多共性錯誤。本文將介紹如何避免那些隱蔽然而常見的錯誤,并介紹的幾個技巧幫助工程師發現軟件中隱藏的錯誤。

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

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

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

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

        * 堆棧溢出

        * 競爭條件

        * 死鎖

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

        * 時序問題

        * 可重入條件

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

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

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

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

        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的自動化。

        大部分軟件開發項目依靠結合代碼檢查、結構測試和功能測試來識別軟件缺陷。盡管這些傳統技術非常重要,而且能發現大多數軟件問題,但它們無法檢查出當今復雜系統中的許多共性錯誤。本文將介紹如何避免那些隱蔽然而常見的錯誤,并介紹的幾個技巧幫助工程師發現軟件中隱藏的錯誤。

        競爭條件

        當兩個或更多獨立線程同時訪問同一資源時,就出現了競爭條件。競爭條件的影響多種多樣,取決于具體的情況。清單1解釋了一個潛在的競爭條件。函數Update_Sensor()通過調用get_raw()來讀取傳感器的原始數據。在處理過程中,該數據被乘上一個定標因子,并加上一個偏移量。處理是在該數據的一個臨時副本上進行的,然后,該臨時副本被寫入共享變量。

        如果在數據寫入之前,使用shared_sensor的另一個線程或ISR先占(preempt)了這個線程,它將得到原來的傳感器讀數。使用臨時副本可以防止先占線程讀取只經過部分處理的數據。不過,如果這些代碼在一個數據總線不足32位的處理器上運行,就會存在競爭條件。

        在一個8位或16位的處理器上,向shared_sensor的寫入操作并不是一次性完成的。在8位處理器上,寫入32位浮點值可能需要四條指令,在16位處理器上可能需要兩條指令。如果在對shared_sensor進行連續寫入中途Update_Sensor()被先占,則先占線程將從由一部分老數據和一部分新數據組成的shared_sensor讀取一個數值。根據應用的具體情況,這有可能造成嚴重的后果。解決的辦法是鎖定調度程序,或在更新共享變量期間禁止中斷。

        消除競爭條件通常很簡單,但找出隱藏在代碼中的競爭條件則需要仔細的分析。

        對于由一個循環程序和不同ISR組成的簡單系統,分析競爭條件很簡單,只需檢查每個ISR并識別它引用的所有共享變量。共享變量通常是這些系統中的全局數據,一旦這些共享變量被找出來之后,就可以檢查它們在代碼中的各次使用情況。每次訪問都必須按需要進行保護,以避免潛在的沖突。在簡單設計中,一般通過在關鍵代碼段周圍禁止中斷來實現保護。遵守下列規則可幫助避免競爭問題:

        * 如果一個ISR對共享數據進行寫入,則該ISR之外的每次可中斷的讀操作都必須予以保護。

        * 如果一個ISR對共享數據進行寫入,則該ISR之外的任何讀-修-寫操作都必須予以保護。

        * 如果一個ISR讀取共享數據,則對該數據的可中斷寫操作必須予以保護。

        * 如果一個ISR和其它代碼都要檢查一個硬件狀態標志,以便在使用某資源之前確定其可用性,如:

        if (!resource_busy)

        {

        // Use resource

        }

        則從檢查標志之時開始,到硬件設置標志表示資源不可用為止,必須采取保護措施。

        對于使用了優先級不同的多個線程的更為復雜的系統,其分析也非常相似。上述規則仍然適用于ISR使用的所有數據。此外,還必須識別出每個線程使用的共享數據。首先從系統中優先級的線程開始,找出它與任何優先級較低的線程共享的所有數據,然后按照上述四條規則進行保護。對于軟件使用的其它每個優先級,再重復這一過程。

        注意,如果系統采用了一種循環調度算法,則特定優先級內的所有線程可在任意時刻相互先占。這意味著前述四條分析規則在考慮較低優先級的線程之外,還必須考慮同一優先級的所有線程。

        多線程系統通常使用某種類型的操作系統,它能夠提供多種保護選擇。可以使用互斥或信號量,或者鎖定調度器。有時也可使用其它進程間通信(IPC)基本技術:通過向消息隊列發送消息(而非修改共享變量)來表示數據已經改變。在許多情況下,由單一線程來管理共享資源,它負責處理所有的讀寫請求,并在內部防止訪問沖突。

        在復雜的代碼中辨認潛在的競爭條件可能是一項乏味而又耗時的工作。相應的輔助工具從用來識別全局數據訪問的簡單腳本到先進的動態分析程序如Polyspace Verifier。雖然比較困難,但詳盡的代碼分析是識別這類錯誤的途徑。測試不大可能能夠建立重復觸發競爭條件所需的精確時序序列。

        死鎖

        在共享資源的系統中,防止訪問沖突極為重要,但這有可能導致另一個問題:死鎖。當通過"鎖定"一個資源來防止任何其它線程訪問這個資源,以避免競爭條件時,必須對設計進行評估,確保不會發生死鎖。死鎖測試通常沒有什么效果,因為只有某種特定順序的資源鎖定才可能產生死鎖,而一般的測試不大可能導致這種順序。

        死鎖只不過是多線程環境中一個鎖定資源的問題。以下四個條件必須同時具備,才會發生死鎖。防止其中任何一個條件出現都可以排除死鎖的可能性:

        * 相互排除---每次只有一個線程可以使用某個鎖定的資源;

        * 非先占---其它線程不能強迫另一個線程釋放資源;

        * 保持并等待---線程在等待需要的其它任何資源時,保持它們已經鎖定的資源;

        * 循環等待---存在一個線程循環鏈,其中每個線程保持鏈中下一個線程所需要的資源。

        線程1首先鎖定Buf資源,在保持Buf時,指向Bus,然后是Mux。如果線程1一直運行到結束,它終將釋放所有這些資源。線程2運行時,必須指向Bus、Sem,是Mux。線程3運行時,需要Sem和Buf。

        在這個設計實例中,無法保證任何一個線程能夠在另一個線程開始執行之前結束。如果一個線程不能得到需要的某個資源,它將掛起執行(阻塞),直到該資源有效為止。在系統運行過程中,各線程都將對資源進行鎖定或解鎖。由于各線程運行和指向其資源的相對時序各不相同,有可能出現由于各個線程正在等待被其它線程保持的資源,導致所有線程都無法運行的情況。例如,如果線程1保持Buf,線程2保持Bus,而線程3已經取得了Sem,則系統將發生死鎖。因為按照從Buf到Bus到Sem,再回到Buf的線程分配箭頭,循環等待條件得到了滿足。

        潛在死鎖問題識別出來之后,通常很容易進行修復。在圖2中,對線程3進行了修改,使其在得到Sem之前首先設法指向Buf。這樣,循環等待的條件就被打破了,系統將不會再受到死鎖的影響。

        一些操作系統過多地使用消息傳遞來進行線程間通信和同步。在這些類型的系統中,當某線程向另一個線程傳遞消息時,發送線程將阻塞,直到從接收線程收到響應為止。接收線程通常將一直阻塞到從其它某個線程接收到一個消息為止。這些結構中也會發生死鎖。為了給一個基于消息的操作系統建立一張資源分配圖,我們利用消息通道來模擬分配的資源。圖3是一個例子。線程2建立了通道T2 Ch,當它未因為等待這個通道上的一個消息而阻塞時,線程2就將"鎖定"這個通道。當它阻塞并等待一個消息時,另一個線程可在這個通道上向它發送一個消息,并且這個消息將立即被接收到。

        現在考慮下面這個系統:線程1指向Mutex并在通道T2 Ch上向線程2發送消息。在線程2中的某個地方,線程2在通道T3 Ch上向線程3發送消息。線程3也在通道T4 Ch上向線程4發送消息。在線程4中的某個地方,它也嘗試指向Mutex,如果得不到,它就將阻塞。顯然,各資源之間存在一條循環路徑,這表明有可能發生死鎖。例如,如果某一時刻線程1保持Mutex而線程4嘗試指向它,線程4就將在Mutex上阻塞。然后當線程3嘗試在通道T4 Ch上向線程4發送一個消息時,線程3將阻塞,等待來自線程4的應答(因為線程4是由于等待Mutex而阻塞,不是為了等待這個消息)。類似地,當線程2嘗試向線程3發送一個消息時,將被阻塞;線程1嘗試向線程2發送一個消息時也將阻塞,由于它仍然保持著Mutex,所以系統將發生死鎖。

        對付死鎖的容易的辦法是通過設計進行避免。采用以下任何一條設計約束都可排除死鎖出現的可能性:

        * 任意時刻線程鎖定的資源不超過一個。 * 線程開始執行前就完全分配它所需的全部資源。 * 指向多個資源的線程必須按照一種系統范圍的預設順序來鎖定(并釋放)這些資源。

        如果無法通過設計來避免死鎖,則應該建立資源分配圖。檢查資源分配圖可以識別潛在的死鎖。通過仔細跟蹤系統中的所有線程和它們鎖定的共享資源,可以維護資源分配圖并周期性地進行檢查,及時發現循環等待的特征。

        建立資源分配圖需要識別每個受保護的共享資源,以及指向其中某一資源的所有線程。如果使用一個操作系統,可以采用下面的過程步驟:

        1. 識別所有可能阻塞的系統調用,如Mutex_Lock(),每個受保護的共享資源總是有一些與訪問它有關的阻塞調用。

        2. 識別出獲取共享資源的阻塞調用之后,在源代碼中查找它們的各次調用情況。

        3. 對于每次調用,記錄下指向資源的線程名稱和該資源的名稱。通常調用本身將受保護的資源作為一個參數來傳遞,調用在源代碼中所處的位置表明了哪個線程需要該資源。通過這種方式,可以識別出所有受保護的資源以及分配資源的線程。

        4. 建立資源分配圖,并檢查是否有任何資源存在循環路徑。當線程和共享資源較少時,畫出資源分配圖比較簡單。在較為復雜的系統中,將這些信息輸入分析表格,并編寫一個宏來檢查線程和資源分配結構,以識別潛在的死鎖。編寫好宏之后,就可以快速地對資源分配變化進行重新評估。編寫宏時,可以忽略不會導致死鎖的資源之間的循環。在表2所示的例子中,各種資源之間有許多循環,但只有線程6和線程7之間可能存在死鎖。

        在一些類型的系統中,預先確定每一個共享資源并建立分配圖是不實際或不可能的。此時可以增加一些額外的代碼,以便在系統運行時檢測出潛在的死鎖。許多不同的算法都致力于優化這個檢測過程,但本質上它們幾乎都動態地建立某種資源分配圖。只要有線程請求、分配或釋放資源,分配圖就會被修改和檢測,以確定是否存在表明潛在死鎖的循環路徑。

        檢測到某個死鎖之后,的克服方法是強迫線程釋放關鍵的資源。通常,這意味著中斷正保持著所需資源的線程。對于某些應用,這種方法可能是無法接受的。另一個有趣的解決方案是在運行時收集資源分配情況并進行事后分析處理,以確定在程序運行過程中是否有死鎖情況發生。盡管這種方法并不能防止在運行時發生死鎖,但它確實有助于在死鎖出現后發現問題并進行修復。

        還有一些工具也可以用來幫助發現代碼中的死鎖。例如,Solaris程序設計員可以采用 Sun公司的LockLint工具來對代碼進行統計分析。它可以發現對鎖定技術的不一致用法,識別引起競爭條件和死鎖的許多原因。

      免費預約試聽課

      亚洲另类欧美综合久久图片区_亚洲中文字幕日产无码2020_欧美日本一区二区三区桃色视频_亚洲AⅤ天堂一区二区三区

      
      

      1. 亚洲欧美另类综合偷拍 | 在线观看黄V免费网站免费 亚洲视频在线香蕉 | 中文国产特黄特色在线视频 | 亚洲精品一级在线播放 | 日韩一本一区二区三四区 | 中文字幕国产第一页首页 |