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