在嵌入式軟體開發過程中,一般來說,花在測試和花在編碼的時間比為3:1(實際上可能更多)。這個比例隨著你的程式設計和測試水平的提高而不斷下降,但不論怎樣,領測國際認為軟體測試對一般人來講很重要。很多年前,一位開發人員為了在對嵌入式有更深層次的理解,向Oracle詢問了這樣的一個問題:我怎麼才能知道並懂得我的系統到底在幹些什麼呢?
Oracle面對這個問題有些吃驚,因為在當時沒有人這麼問過,而同時代的嵌入式開發人員問的最多的大都圍繞“我怎麼才能使程式跑的更快”、“什麼編譯器最好”等膚淺的問題。所以,面對這個不同尋常卻異乎成熟的問題,Oracle感到欣喜並認真回覆了他:你的問題很有深度很成熟,因為只有不斷地去深入理解才有可能不斷地提高水平。並且Oracle為了鼓勵這位執著的程式設計師,把10條關於嵌入式軟體開發測試的祕訣告訴了他。下面我們一起來看看。
這十條祕訣在業界廣為流傳,使很多人受益。本文圍繞這十條祕訣展開論述。
1.懂得使用工具
通常嵌入式系統對可靠性的要求比較高。嵌入式系統安全性的失效可能會導致災難性的後果,即使是非安全性系統,由於大批量生產也會導致嚴重的經濟損失。這就要求對嵌入式系統,包括嵌入式軟體進行嚴格的測試、確認和驗證。隨著越來越多的領域使用軟體和微處理器控制各種嵌入式裝置,對日益複雜的嵌入式軟體進行快速有效的測試愈加顯得重要。
就像修車需要工具一樣,好的程式設計師應該能夠熟練運用各種軟體工具。不同的工具,有不同的使用範圍,有不同的功能。使用這些工具,你可以看到你的系統在幹些什麼,它又佔用什麼資源,它到底和哪些外界的東西打交道。讓你鬱悶好幾天的問題可能通過某個工具就能輕鬆搞定,可惜你就是不知道。那麼為什麼那麼多的人總是在折騰個半死之後才想到要用測試工具呢?原因很多,主要有兩個。一個是害怕,另一個是惰性。害怕是因為加入測試工具或測試模組到程式碼需要技巧同時有可能引入新的錯誤,所以他們總喜歡寄希望於通過不斷地修改重編譯程式碼來消除bug,結果卻無濟於事。懶惰是因為他們習慣了使用printf之類的簡單測試手段。下面來介紹一些嵌入式常用的測試工具。
原始碼級偵錯程式[Source-levelDebugger]這種偵錯程式一般提供單步或多步除錯、斷點設定、記憶體檢測、變數檢視等功能,是嵌入式除錯最根本有效的除錯方法。比如VxWorksTornadoII提供的gdb就屬於這一種。
簡單實用的列印顯示工具[printf]printf或其它類似的列印顯示工具估計是最靈活最簡單的除錯工具。列印程式碼執行過程中的各種變數可以讓你知道程式碼執行的情況。但是,printf對正常的程式碼執行干擾比較大(一般printf佔用CPU比較長的時間),需要慎重使用,最好設定列印開關來控制列印。
ICE或JTAG偵錯程式[In- circuitEmulator]ICE是用來模擬CPU核心的裝置,它可以在不干擾運算器的正常執行情況下,實時的檢測CPU的內部工作情況。像桌面除錯軟體所提供的:複雜的條件斷點、先進的實時跟蹤、效能分析和埠分析這些功能,它也都能提供。ICE一般都有一個比較特殊的CPU,稱為外合(bond-out)CPU.這是一種被打開了封裝的CPU,並且通過特殊的連線,可以訪問到CPU的內部訊號,而這些訊號,在CPU被封裝時,是沒法 “看到”的。當和工作站上強大的除錯軟體聯合使用時,ICE就能提供你所能找到的最全面的除錯功能。但ICE同樣有一些缺點:昂貴;不能全速工作;同樣,並不是所有的CPU都可以作為外合CPU的,從另一個角度說,這些外合CPU也不大可能及時的被新出的CPU所更換。 JTAG(JointTestActionGroup)雖然它最初開發出來是為了監測IC和電路連線,但是這種序列介面擴充套件了用途,包括對除錯的支援。
ROM監視器[ROMMonitor]ROM監控器是一小程式,駐留在嵌入系統ROM中,通過序列的或網路的連線和執行在工作站上的除錯軟體通訊。這是一種便宜的方式,當然也是最低端的技術。它除了要求一個通訊埠和少量的記憶體空間外,不需要其它任何專門的硬體。並提供瞭如下功能:下載程式碼、執行控制、斷點、單步步進、以及觀察、修改暫存器和記憶體。因為ROM監控器是操作軟體的一部分,只有當你的應用程式執行時,它才會工作。如果你想檢查CPU和應用程式的狀態,你就必須停下應用程式,再次進入ROM監控器。
Data監視器[DataMonitor]這種監視器在不停止CPU執行的情況下不僅可以顯示指定變數內容,還可以收集並以圖形形式顯示各個變數的變化過程。
OS監視器[OperatingSystemMonitor]作業系統監視器可以顯示諸如任務切換、訊號量收發、中斷等事件。一方面,這些監視器能夠為你呈現事件之間的關係和時間聯絡;另一方面,還可以提供對訊號量優先順序反轉、死鎖和中斷延時等問題的診斷。
效能分析工具[Profiler]可以用來測試CPU到底耗在那裡。profiler工具可以讓你知道系統的瓶頸在那裡、CPU的使用率以及需要優化的地方。
記憶體測試工具[MemoryTeseter]可以找到記憶體使用的問題所在,比如記憶體洩露、記憶體碎片、記憶體崩潰等問題。如果發現系統出現一些不可預知的或間歇性的問題,就應該使用記憶體測試工具測測看。
執行跟蹤器[ExecutionTracer]可以顯示CPU執行了哪些函式、誰在呼叫、引數是什麼、何時呼叫等情況。這種工具主要用於測試程式碼邏輯,可以在大量的事件中發現異常的那些。
覆蓋工具[CoverageTester]主要顯示CPU具體執行了那些程式碼,並讓你知道那些程式碼分支沒有被執行到。這樣有助於提高程式碼質量並消除無用程式碼。
GUI測試工具[GUITester]很多嵌入式應用帶有某種形式的圖形使用者介面進行互動,有些系統性能測試是根據使用者輸入響應時間進行的。GUI測試工具可以作為指令碼工具有開發環境中執行測試用例,其功能包括對操作的記錄和回放、抓取螢幕顯示供以後分析和比較、設定和管理測試過程(Rational公司的robot和Mercury的Loadrunner工具是傑出的代表)。很多嵌入式裝置沒有GUI,但常常可以對嵌入式裝置進行插裝來執行GUI測試指令碼,雖然這種方式可能要求對被測程式碼進行更改,但是節省了功能測試和迴歸測試的時間。
自制工具[Home-madetester]在嵌入式應用中,有時候為了特定的目的,需要自行編寫一些工具來達到某種測試目的。本人曾經編寫的視訊流錄顯工具在測試視訊會議資料流向和變化上幫了大忙,幫公司找到了幾個隱藏很深的bug。
2.儘早發現記憶體問題
記憶體問題危害很大,不容易排查,主要有三種類型:記憶體洩露、記憶體碎片和記憶體崩潰。對於記憶體問題態度必須要明確,那就是早發現早“治療”。在軟體設計中,記憶體洩露的“名氣”最大,主要由於不斷分配的記憶體無法及時地被釋放,久而久之,系統的記憶體耗盡。即使細心的程式設計老手有時後也會遭遇記憶體洩露問題。有測試過記憶體洩露的朋友估計都有深刻地體驗,那就是記憶體洩露問題一般隱藏很深,很難通過程式碼閱讀來發現。有些記憶體洩露甚至可能出現在庫當中。有可能這本身是庫中的bug,也有可能是因為程式設計師沒有正確理解它們的介面說明文件造成錯用。
在很多時候,大多數的記憶體洩露問題無法探測,但可能表現為隨機的故障。程式設計師們往往會把這種現象怪罪於硬體問題。如果使用者對系統穩定性不是很高,那麼重啟系統問題也不大;但,如果使用者對系統穩定很高,那麼這種故障就有可能使使用者對產品失去信心,同時也意味著你的專案是個失敗的專案。由於記憶體洩露危害巨大,現在已經有許多工具來解決這個問題。這些工具通過查詢沒有引用或重複使用的程式碼塊、垃圾記憶體收集、庫跟蹤等技術來發現記憶體洩露的問題。每個工具都有利有弊,不過總的來說,用要比不用好。總之,負責的開發人員應該去測試記憶體洩露的問題,做到防患於未然。
記憶體碎片比記憶體洩露隱藏還要深。隨著記憶體的不斷分配並釋放,大塊記憶體不斷分解為小塊記憶體,從而形成碎片,久而久之,當需要申請大塊記憶體是,有可能就會失敗。如果系統記憶體夠大,那麼堅持的時間會長一些,但最終還是逃不出分配失敗的厄運。在使用動態分配的系統中,記憶體碎片經常發生。目前,解決這個問題最效的方法就是使用工具通過顯示系統中記憶體的使用情況來發現誰是導致記憶體碎片的罪魁禍首,然後改進相應的部分。
由於動態記憶體管理的種種問題,在嵌入式應用中,很多公司乾脆就禁用malloc/free的以絕後患。
記憶體崩潰是記憶體使用最嚴重的結果,主要原因有陣列訪問越界、寫已經釋放的記憶體、指標計算錯誤、訪問堆疊地址越界等等。這種記憶體崩潰造成系統故障是隨機的,而且很難查詢,目前提供用於排查的工具也很少。
總之,如果要使用記憶體管理單元的話,必須要小心,並嚴格遵守它們的使用規則,比如誰分配誰釋放。
3.深入理解程式碼優化
講到系統穩定性,人們更多地會想到實時性和速度,因為程式碼效率對嵌入式系統來說太重要了。知道怎麼優化程式碼是每個嵌入式軟體開發人員必須具備的技能。就像女孩子減肥一樣,起碼知道她哪個地方最需要減,才能去購買減肥藥或器材來減掉它。可見,程式碼優化的前提是找到真正需要優化的地方,然後對症下藥,優化相應部分的程式碼。前面提到的profile(效能分析工具,一些功能齊全IDE都提供這種內建的工具)能夠記錄各種情況比如各個任務的CPU佔用率、各個任務的優先順序是否分配妥當、某個資料被拷貝了多少次、訪問磁碟多少次、是否呼叫了網路收發的程式、測試程式碼是否已經關閉等等。
但是,profile工具在分析實時系統性能方面還是有不夠的地方。一方面,人們使用profile工具往往是在系統出現問題即CPU耗盡之後,而profile工具本身對CPU佔用較大,所以profile對這種情況很可能不起作用。根據Heisenberg效應,任何測試手段或多或少都會改變系統執行,這個對profiler同樣適用!
總之,提高執行效率的前提是你必須要知道CPU到底幹了些什麼乾的怎麼樣。
4.不要讓自己大海撈針
大海撈針只是對除錯的一種生動比喻。經常聽到組裡有人對自己正在除錯的程式碼說shit!可以理解,因為程式碼不是他寫的,他有足夠的理由去shitbug百出的程式碼,只要他自己不要寫出這種程式碼,否則有一天同組的其它人可能同樣會shit他寫的程式碼。為何會有大海撈針呢?肯定是有人把針掉到海里咯;那針為何會掉在海里呢?肯定是有人不小心或草率唄。所以當你在抱怨針那麼難找的時候,你是否想過是你自己草率地丟掉的。同樣,當你除錯個半死的時候,你是否想過你要好好反省一下當初為了尋求捷徑可能沒有嚴格地遵守好的編碼設計規範、沒有檢測一些假設條件或演算法的正確性、沒有將一些可能存在問題的程式碼打上記號呢?關於如何寫高質量請參考林銳的《高質量c++/c程式設計指南》或《關於C的0x8本“經書》。
如果你確實已經把針掉在海里是,為了防止在找到之前刺到自己,你必須要做一些防範工作,比如戴上安全手套。同樣,為了盡能地暴露和捕捉問題根源,我們可以設計比較全面的錯誤跟蹤程式碼。怎麼來做呢?儘可能對每個函式呼叫失敗作出處理,儘可能檢測每個引數輸入輸出的有效性,包括指標以及檢測是否過多或過少地呼叫某個過程。錯誤跟蹤能夠讓你知道你大概把針掉在哪個位置。
5.重現並隔離問題
如果你不是把針掉在大海了,而是掉在草堆裡,那要好辦些。因為至少我們可以把草堆分成很多塊,一塊一塊的找。對於模組獨立的大型專案,使用隔離方法往往是對付那些隱藏極深bug的最後方法。如果問題的出現是間歇性的,我們有必要設法去重現它並記錄使其重現的整個過程以備在下一次可以利用這些條件去重現問題。如果你確信可以使用記錄的那些條件去重現問題,那麼我們就可以著手去隔離問題。怎麼隔離呢?我們可以用#ifdef把一些可能和問題無關的程式碼關閉,把系統最小化到仍能夠重現問題的地步。如果還是無法定位問題所在,那麼有必要開啟“工具箱”了。可以試著用ICE或資料監視器去檢視某個可疑變數的變化;可以使用跟蹤工具獲得函式呼叫的情況包括引數的傳遞;檢查記憶體是否崩潰以及堆疊溢位的問題。
6.以退為進
獵人為了不使自己在森林裡迷路,他常常會在樹木上流下一些標記,以備自己將來有一天迷路時可以根據這些標記找到出路。對過去程式碼的修改進行跟蹤記錄對將來出現問題之後的除錯很有幫助。假如有一天,你最近一次修改的程式跑了很久之後忽然死掉了,那麼你這時的第一反映就是我到底改動了些什麼呢,因為上次修改之前是好的。那麼如何檢測這次相對於上次的修改呢?沒錯,程式碼控制系統SCS或稱版本控制系統VCS(ConcurrentVersionControl,CVS是VCS的演化版本)。將上個版本checkin下來後和當前測試版本比較。比較的工具可以是SCS/VCS/CVS自帶的diff工具或其它功能更強的比較工具,比如BeyondCompare和 ExamDiff.通過比較,記錄所有改動的程式碼,分析所有可能導致問題的可疑程式碼。
7.確定測試的完整性
你怎麼知道你的測試有多全面呢?覆蓋測試(coveragetesting)可以回答這個問題。覆蓋測試工具可以告訴你CPU到底執行了那些程式碼。好的覆蓋工具通常可以告訴你大概20%到40% 程式碼沒有問題,而其餘的可能存在bug.覆蓋工具有不同的測試級別,使用者可以根據自己的需要選擇某個級別。即使你很確信你的單元測試已經很全面並且沒有 deadcode,覆蓋工具還是可以為你指出一些潛在的問題,看下面的程式碼:if(i>=0&& (almostAlwaysZero==0 (last=i)))如果almostAlwaysZero為非0,那麼last=i賦值語句就被跳過,這可能不是你所期望的。這種問題通過覆蓋工具的條件測試功能可以輕鬆的被發現。總之,覆蓋測試對於提高程式碼質量很有幫助。
8.提高程式碼質量意味著節省時間
有研究表明軟體開發的時間超過80%被用在下面幾個方面:除錯自己的程式碼(單元測試)。除錯自己和其他相關的程式碼(模組間測試)。除錯整個系統(系統測試),更糟糕的是你可能需要花費10-200倍的時間來找一個 bug,而這個bug在開始的時候可能很容易就能找到。一個小bug可能讓你付出巨大的代價,即使這個bug對整個系統的效能沒有太大的影響,但很可能會影響讓那些你可以看得到的部分。所以我們必須要養成良好的編碼和測試手段以求更高的程式碼質量,以便縮短除錯的程式碼。
9.發現它,分析它,解決它
這世界沒有萬能的膏藥。profile再強大也有力不從心的時候;記憶體監視器再好,也有無法發現的時候;覆蓋工具再好用,也有不能覆蓋的地方。一些隱藏很深的問題即使用盡所有工具也有可能無法查到其根源,這時我們能做的就是通過這些問題所表現出來的外在現象或一些資料輸出來發現其中的規律或異常。一旦發現任何異常,一定要深入地理解並回溯其根源,直到解決為止。
10.利用初學者的思維
有人這樣說過:“有些事情在初學者的腦子裡可能有各種各樣的情況,可在專家的頭腦裡可能就很單一”。有時候,有些簡單的問題會被想的很複雜,有些簡單的系統被設計得很複雜,就是由於你的“專家思維”。當你被問題難住時,關掉電腦,出去走走,把你的問題和你的朋友甚至你的小狗說說,或許他們可以給你意想不到的啟發。
總結:嵌入式除錯也是一門藝術。就想其它的藝術一樣,如果你想取得成功,你必須具備智慧、經驗並懂得使用工具。只要我們能夠很好地領悟Oracle這十條祕訣,我相信我們在嵌入式測試方面就能夠取得成功。