嵌入式軟件Bug從哪來,怎么去
1.軟件問題從哪來
軟件缺陷問題千千萬萬,主要是需求、實現(xiàn)、和運行環(huán)境三方面。
1.1 需求描述偏差
客戶角度的描述,在經(jīng)過業(yè)務(wù)對接、產(chǎn)品經(jīng)理的轉(zhuǎn)述,最終呈現(xiàn)的軟件需求可能已經(jīng)偏離了原始的述求,開發(fā)人員基于自身經(jīng)驗的理解偏差,開發(fā)過程缺乏有效的溝通及監(jiān)督,導致最終的軟件功能與客戶的核心訴求存在偏差。
1.2 異常處理機制不完善
嵌入式軟件必定是運行在特定的硬件設(shè)備,硬件本身或環(huán)境問題等特殊干擾,開發(fā)人員因經(jīng)驗不足缺乏風險評估,面對電腦是無法全方位猜測、模擬各種異常環(huán)境下的差異,最終導致設(shè)備在特定場景下運行異常。
1.3 軟件開發(fā)能力不足
嵌入式系統(tǒng)的復雜度與開發(fā)人員的能力矛盾,導致軟件本身的邏輯存在缺陷。
2.軟件開發(fā)與軟件問題
關(guān)于軟件bug的來源,排除不可控的外界因素,與軟件開發(fā)人員相關(guān),或者開發(fā)人員可以減少問題的發(fā)生的可能,從軟件開發(fā)角度解決的方案如下:
2.1 重視需求分析
軟件開發(fā)就是寫程序,并設(shè)法使之運行,這是個錯誤的想法。軟件結(jié)果與客戶期望不一致,需求問題不全是軟件開發(fā)的鍋。大多數(shù)情況下客戶的原始述求不會直接到軟件開發(fā),軟件開發(fā)沒法去反訴找客戶確認,只能通過軟件的實現(xiàn)形式去甄別不合理的,或者針對客觀環(huán)境、研發(fā)團隊的基礎(chǔ)去評估風險。
比如客戶要求可以設(shè)備可以定時1秒采集一次溫度,精度要求0.0001攝氏度;或者要求數(shù)據(jù)采集持續(xù)采集24h后,每天12:00準點TCP上報后臺服務(wù)器。這就需要考慮溫度傳感器的精度、RTC喚醒以及TCP聯(lián)網(wǎng)時間、24小時采樣數(shù)據(jù)的存儲。如果硬件資源或者客觀環(huán)境無法實現(xiàn),盲目承諾客戶,或者開始編碼,最終結(jié)果可想而知。
軟件開發(fā)是個人的任務(wù),但開發(fā)前多溝通確認,進行風險評估反饋,減少開發(fā)的無用功,也是對開發(fā)人員的基本要求。
2.2 積累行業(yè)經(jīng)驗
嵌入式產(chǎn)品都是針對某個細分行業(yè),見多識廣,才能預判可能出現(xiàn)的異常,開發(fā)階段有針對性的去處理,或者提前告知使用者去規(guī)避。有時候經(jīng)驗比技術(shù)能力重要。
2.3 提高開發(fā)水平
軟件開發(fā)水平,首先是個人能力,熟悉軟件SDK的應(yīng)用,相關(guān)的操作系統(tǒng)、設(shè)計模式、調(diào)試方法等。軟件開發(fā)能力大多數(shù)情況下決定軟件質(zhì)量和可維護性,這是個長期學習提高的過程,如果一定要提供捷徑,那就是多閱讀優(yōu)秀的開源代碼。
2.4 先設(shè)計再編碼
軟件開發(fā)不能隨心所欲,先明確方案和大概的實現(xiàn)流程,胸有成竹,然后再開始編碼,完善細節(jié)。這理論沒毛病,但真正執(zhí)行起來卻比較難,大多數(shù)情況下都只在乎軟件出結(jié)果,而實際上方案不合理,后期修修補補更浪費時間。如果制度和時間不允許,個人在紙上畫畫框圖和結(jié)構(gòu),先構(gòu)思再開發(fā)也能彌補,起碼不至于南轅北轍。
2.5 編碼規(guī)范
編碼規(guī)范是軟件開發(fā)團隊合作的標準,嵌入式行業(yè)可以參考“華為技術(shù)C語言編程規(guī)范”,但實際開發(fā)過程,和前面的先設(shè)計再編碼一樣,各種不可控因素,比如項目進度壓力和開發(fā)者水平與認知的差異,導致有編碼規(guī)范卻無法嚴格執(zhí)行。隨著軟件工程規(guī)模的擴大,軟件交期、代碼同步、重構(gòu)或交接,其風險也逐漸放大。存在編碼規(guī)則并不能解決問題,只有強制執(zhí)行才有意義。
2.6 代碼缺陷靜態(tài)檢查與單元測試
軟件質(zhì)量是項目成敗的關(guān)鍵點之一,在開發(fā)周期有限,人力資源不足的情況下,使用工具實現(xiàn)代碼自動掃描,分析出潛在隱患點,可從源頭減少軟件bug,比如cppCheck、PC-lint等,實現(xiàn)代碼自動靜態(tài)分析,或者人工視檢,有效規(guī)避簡單的軟件風險。
如果可能,最佳的選擇是單元測試,單元測試比可交付成果本身更重要,文檔注釋不全時,單元測試就是設(shè)計文檔;單元測試定義的API和用法,以及可能的使用風險點,就是最佳的參考范例;不足100%的覆蓋率就是玩忽職守,開發(fā)人員應(yīng)該全權(quán)負責測試自己造出的產(chǎn)品。依靠后期的黑盒測試發(fā)現(xiàn)問題,其消耗的人力物力,是編寫單元測試的幾倍,而且單元測試可以反復的自動測試。不過這種情況更多的是存在于開發(fā)理論中。可以參考微信公眾號 嵌入式系統(tǒng) 的《代碼的保養(yǎng)》第二章。
3.前期減少問題
軟件問題的解決,有些不是個人能解決的,需要協(xié)調(diào)溝通,或者與研發(fā)團隊的整體風格、制度有關(guān)。個人能決定的是軟件具體邏輯,這也是體現(xiàn)個人技術(shù)能力的重點。
3.1 C語言基礎(chǔ)
- 多看優(yōu)秀代碼,學習其技巧。
- 使用帶參數(shù)檢測的接口,比如優(yōu)先選擇snprintf,少用sprintf,其它str前綴的如strncmp也是,但要明白這類接口和memcmp區(qū)別。不同的編譯器表現(xiàn)不一致,平時也要多關(guān)注。
在GCC中編譯運行(設(shè)備):
char str[5];
int ret = snprintf(str, 3, "%s", "abcdefg");
//ret = 7 ,str = ab
char str[99];
int ret = snprintf(str, 99, "%s", "abcdefg");
//ret = 7 ,str = abcdefg
注:snprintf的返回值為字符串的長度,且寫入的字符串后面帶有‘\0’結(jié)束符。
在VC中編譯運行:
char str[5];
int ret = snprintf(str, 3, "%s", "abcdefg");
//ret = -1 ,str = abc [后面不會自動補\0結(jié)束符]
- 注意函數(shù)返回類型,避免類型強制轉(zhuǎn)換導致調(diào)用判斷異常,有些編譯器對隱示類型轉(zhuǎn)換直接報錯,因為它確實存在風險。
- 合理的使用sizeof、struct、union、weak等關(guān)鍵字,增加代碼的可讀性和可擴展性。
- 參數(shù)使用前,如數(shù)組小標,指針變量使用前必須先判斷是否合法。
- 浮點數(shù)不能直接進行==和!=比較,等等,這些細節(jié)太多,可以參考《C陷阱與缺陷》。
- 講的都會,說的都對,但真實際寫代碼,就容易各種小問題,主要還是態(tài)度問題,缺乏自我檢查、自測的步驟,依靠測試發(fā)現(xiàn)bug去驅(qū)動研發(fā)調(diào)試修復是大忌。
3.2 動態(tài)內(nèi)存
- 盡量做到申請與釋放在同一個函數(shù),申請內(nèi)存后,先判斷是否申請成功,再進行其它操作。
- 內(nèi)存申請與釋放之間有特殊情況return,要注意釋放。
- 釋放結(jié)構(gòu)體指針前,注意該變量內(nèi)部是否還有指針變量動態(tài)申請空間,先釋放內(nèi)部,再釋放外部。
- 關(guān)于內(nèi)存申請與釋放,或使用越界是C語言的劣勢,如果設(shè)備堆空間足夠大,可以在申請時額外多申請固定空間,記錄申請函數(shù)、長度、并在首尾標記,后續(xù)釋放時檢查內(nèi)存區(qū)首尾標記是否被覆蓋;或者查詢是哪些函數(shù)申請的內(nèi)存始終沒有被釋放。
3.3 跨平臺問題
- 使用系統(tǒng)API前先判斷自身傳入?yún)?shù)的有效性和范圍等是否符合要求,一般系統(tǒng)API是庫文件,使用錯誤更難發(fā)現(xiàn)問題。
- 針對不同的平臺常用的接口,務(wù)必增加適配層隔離,便于調(diào)試和后續(xù)移植。比如有的平臺中斷(SDK提供的中斷回調(diào)不一定是硬件中斷)不支持串口日志。
3.4RTOS系統(tǒng)特性
- 多任務(wù)的競爭,在RTOS系統(tǒng)中,需要注意全局函數(shù)、全局變量的使用,避免互相競爭影響,對公共函數(shù)盡量做到可重入設(shè)計,具體實現(xiàn)方案請關(guān)注微信公眾號 嵌入式系統(tǒng) 的《基于RTOS的軟件開發(fā)理論》 。
- 中斷與任務(wù)的調(diào)度關(guān)系 請關(guān)注微信公眾號 嵌入式系統(tǒng) 的《基于RTOS的軟件開發(fā)理論》。
- 合理分配任務(wù)棧空間和消息隊列的深度,函數(shù)內(nèi)部盡量少用大數(shù)組。
3.5 個人素養(yǎng)
軟件編碼完成,不是能編譯就收工了,其功能是否符合預期,開發(fā)人員自己檢查是最高效的,很多問題都是開發(fā)不仔細,或者很簡單的C基礎(chǔ)應(yīng)用錯誤,這不是技術(shù)問題而是心態(tài)。可以多看看開源代碼,或者《C專家編程》等。
4.后期解決問題
如果軟件問題不可避免,該如何去修復解決呢?
一般來說100%出現(xiàn)的問題都比較容易解決,找到相關(guān)代碼仔細檢查或者加點日志就能發(fā)現(xiàn)問題。難處理的是小概率出現(xiàn)的問題,穩(wěn)定復現(xiàn)它就是成功的一半。
4.1 問題復現(xiàn)
穩(wěn)定復現(xiàn)問題才能快速對問題進行定位、解決以及驗證,如何提高復現(xiàn)的概率?
- 模擬復現(xiàn)條件,問題只在特定的條件下出現(xiàn),對于依賴外部輸入的條件難以滿足,可以考慮程序里預設(shè)直接進入對應(yīng)狀態(tài),或者軟件內(nèi)部進行極端的壓力測試。
- 提高相關(guān)代碼執(zhí)行頻率,進行某個操作才可能出現(xiàn)異常,人工持續(xù)操作,或者軟件頻繁執(zhí)行相應(yīng)的功能,提高問題點的執(zhí)行頻率,加快復現(xiàn)速度。
- 增大測試樣本量 ,個別樣機難出現(xiàn),如果條件允許,可以使用多個設(shè)備同時進行測試。一般情況下試產(chǎn)就是為了發(fā)現(xiàn)這類問題。
4.2 問題定位
縮小排查范圍,確認引入問題的函數(shù)或代碼片段。
- 打印日志 ,日志是最直接、簡單的調(diào)試方法,在問題的可疑點增加日志輸出,以此來追蹤程序執(zhí)行流程以及關(guān)鍵變量的值,觀察是否與預期相符。
- 版本回退,使用版本管理工具時可以通過不斷回退版本,驗證前面版本的情況,定位首次引入該問題的版本,針對該版本的改動進行排查。
- 二分注釋,“二分注釋”類似二分查找法的方式注釋掉部分代碼,以此判斷問題是否由注釋掉的這部分代碼引起。具體為將與問題不相干的部分代碼注釋掉一半,看問題是否解決,未解決則注釋另一半,如果解決則繼續(xù)將注釋范圍縮小一半,以此類推逐漸縮小問題的范圍,確定是哪一塊代碼導致這個問題。
- 硬件協(xié)助,借助示波器、邏輯分析儀分析波形,必要時也請硬件協(xié)助分析;問題樣機與正常樣機的主控對調(diào),看問題是否隨芯片走。尤其是涉及驅(qū)動方面的問題,比如充電、中斷、復位、外設(shè)通信調(diào)試異常時。
- 仿真調(diào)試 ,在線調(diào)試可以起到和打印LOG類似的作用,適合排查程序崩潰類的BUG,當程序陷入異常中斷候可以直接STOP查看call stack以及內(nèi)核寄存器的值,快速定位問題點,不過這需要硬件支持。
- 三板斧,使用最多的是前面三種方法,這三板斧足以應(yīng)付大部分業(yè)務(wù)邏輯問題;偶爾請硬件協(xié)助解決驅(qū)動問題,日常開發(fā)中的問題都能解決。個別系統(tǒng)層面或者架構(gòu)不合理導致的深沉問題,要么花時間死磕coredunmp,要么聯(lián)系原廠FAE協(xié)助,一般芯片方案商都提供技術(shù)支持。
4.3 問題修復與回歸測試
- 縮小范圍確定問題代碼,再排查具體的函數(shù),修復問題點。
- 有些問題屬于架構(gòu)層面,比如和RTOS相關(guān)的競爭關(guān)系,這種就無法定位到具體問題代碼點,只能在宏觀上依靠經(jīng)驗或操作系統(tǒng)理論去解決。
- 解決后需要進行回歸測試,確認問題是否不再出現(xiàn),也要確認修改不會引入其他新問題。
4.4 復盤
- 一般情況下最后發(fā)現(xiàn)原因都是很簡單的幾句話,比如數(shù)據(jù)越界或者循環(huán)體多執(zhí)行一次,看起來都是很簡單的基礎(chǔ)用法,因為一句錯誤可能需要幾周時間來發(fā)現(xiàn)解決,為什么當初寫錯而且沒檢查發(fā)現(xiàn)呢?
- 總結(jié)問題產(chǎn)生的原因及解決方法,今后如何防范,對其他平臺否值得借鑒,做到舉一反三,從失敗中吸取經(jīng)驗。
5.心得
業(yè)務(wù)指示開發(fā)、測試驅(qū)動開發(fā),這一荒謬方法論,體現(xiàn)在部門合作與職責不清,整體就是效率低下、互相推諉,在這樣的環(huán)境下開發(fā)軟件也很累。