
概述
在高質量、高安全性的產品中,軟件占據了越來越重的比例,從需求分析階段,到軟件研制,各級測試過程中,都需要盡量避免軟件問題的發生。
一方面從設計上規避,一方面對軟件產品進行充分的測試,盡可能早的發現隱藏在軟件中的缺陷,避免將軟件問題在大批量生成之后,造成嚴重的后果和損失。
軟件測試是有效發現軟件缺陷的重要方法之一,嵌入式軟件測試按照是否執行被測系統,將測試類型分為靜態測試和動態測試:
靜態測試不需要編譯執行源程序,對源程序進行詞法語法、編程規范、數據流、控制流、度量等分析,獲取程序的結構和特性,利用形式化方法驗證、證明程序是否符合安全規則,能夠較為全面的獲取程序的特征。
動態測試需要通過獲取程序的動態信息來分析軟件的缺陷,如分析程序的內存狀態、覆蓋率和執行結果,更有利于理解程序的動態行為特征。
很多代碼缺陷產生于程序運行過程中,具有隱蔽性和不可預見性,比如數組越界,動態存儲分配,內存溢出,指針非法引用,類型不一致的隱式轉換等,這些錯誤都是在編譯階段不能被編譯器檢查發現的錯誤。
在大規模,復雜度極高的軟件中,完全依賴于人工檢查方式可能會因為人為等主觀原因造成問題被遺漏無法發現。
將現有常見多發問題形成缺陷模式庫植入工具中,由工具自動化檢測代碼缺陷,能夠高效提升測試效率,避免問題重復發生,減輕測試人員工作量。
缺陷模式規則分類
規則分類
按照錯誤類別分類
為了進一步研究軟件錯誤發生的機理,分布情況,針對C語言典型缺陷模式庫中的規則,按照規則檢查類型對其進行分類。
(1)? 數檢查:除零錯誤、數組越界。
(2)? 指針使用檢查:空指針解引用。
(3)? 數據流檢查:變量定義未使用就再次賦值。
(4)? 控制流檢查:if elseif分支語句末尾缺少else分支。
(5)? 初始化檢查:使用前未初始化變量。
(6)? 類型轉換檢查:數據類型不一致引入的隱式類型轉換。
(7)? 操作符使用不當檢查:關系表達式、邏輯表達式、條件判斷語句中的控制表達式禁止為賦值表達式“=”、使用連續的比較運算,可能導致代碼執行邏輯與預期不符。
按照嚴重程度分類
根據故障出現可能引起的錯誤嚴重程度,對軟件或者系統造成影響的嚴重程度,給每條規則定義了優先級高、中、低的屬性。
(1)? 重要:除零錯誤;數組越界;空指針解引用。
(2)? 中等:使用前未初始化變量;數據類型不一致引入的隱式類型轉換;關系表達式、邏輯表達式、條件判斷語句中的控制表達式禁止為賦值表達式“=”;使用連續的比較運算,可能導致代碼執行邏輯與預期不符。
(3)? 輕微:if elseif分支語句末尾缺少else分支;變量定義未使用就再次賦值。
規則解析
數值檢查
錯誤發生在不斷運行過程中,稱為運行時錯誤,一旦程序中出現該類型錯誤,會導致計算得到的實際結果與預期結果完成不同,甚至引發計算機復位。
數值檢查作為運行時錯誤檢查的重要檢查內容,會出現在某些特定的運行條件下,即便是經過嚴格測試的程序,仍有可能存在非預期的淺通路,引發軟件不安全問題。
(1)? 數組越界
通過數組的下標來得到數組內指定索引的元素,稱作對數組的訪問。如果一個數組定義為n個元素,它占用一塊連續的內存空間,對n個元素(下標為0到n-1)的訪問都合法,如果對這n個之外的元素(如下標n)進行訪問,訪問到的是其它變量,是非法的,稱為“越界”。
數組越界在運行時的表現是不確定的,可能不會造成嚴重后果,也有可能導致程序崩潰。因此在使用數組時一定要檢查訪問是否越界,以保證程序的正確性。代碼示例見表1。
表1 ?數組越界代碼示例

(2)? 除零錯誤
出現除零操作時會導致計算結果為一個極大值,超過數據類型能夠表示的最大范圍,就會發生溢出,計算機程序對于溢出的防護處理可能是計算機復位。
因此代碼中將整數和浮點數作為分母時都應該進行保護,防止除零后數據溢出的異常情況發生。代碼示例見表2。
表2 ?除零錯誤代碼示例

空指針解引用
空指針解引用是一種常見的動態內存錯誤。指針變量可以指向堆地址、靜態變量和空地址單元,當引用指向空地址單元的指針變量時,就會產生空指針引用故障,導致不可預見的錯誤,系統崩潰或者異常復位。因此,在解引用指針前,應先判斷是否為NULL,如果是NULL則不要解引用。代碼示例見表3。
表3? 空指針解引用代碼示例

變量定義未使用就再次賦值
這條規則屬于數據流分析規則,主要關注變量值的操作邏輯是否合理正確,如果不符合邏輯,就有可能隱藏代碼問題。代碼示例見表4。
表4 ?變量定義未使用就再次賦值代碼示例表

if elseif分支語句末尾缺少else分支
這條規則屬于控制流分析規則,主要關注程序的結構。需要在代碼解析的基礎上提取程序的控制流信息,程序的控制流用于決定分配給變量的特定值可能傳播到程序的哪些部分。
如果存在某條路徑中變量值與預期不一致,就有可能隱藏代碼問題。代碼示例見表5。
表5 ?if elseif分支語句末尾缺少else分支代碼示例表

使用未初始化變量
程序在執行過程中變量位于內存中,內存中供用戶使用的存儲空間分為三部分:程序區、靜態存儲區、動態存儲區。
全局變量全部存儲在靜態存儲區;動態存儲區主要存放:函數的形參、自動變量(沒有加static的局部變量)、函數調用的現場保護和返回值。
在使用動態存儲區變量前如果未對其賦初值就直接使用,由于變量值不確定,可能會發生無法預知的錯誤。代碼示例見表6。
表6 ?使用未初始化變量代碼示例表

數據類型不一致引入的隱式類型轉換
當賦值變量與被賦值變量類型不相同時,需要進行數據類型轉換的相關檢查規則。
一般情況下,數據的類型的轉換通常是由編譯系統自動進行的,不需要人工干預,這種類型轉換稱為隱式類型轉換。
但如果程序要求一定要將某一類型的數據轉換為另外一種類型,則可以利用強制類型轉換運算符進行轉換,這種強制轉換過程稱為顯式轉換。

圖1? 編譯器自動提升規則
C語言編譯器數據類型提升規則可以歸納為從長度小的數據類型向長度大的數據類型提升,稱為向上轉換;反之,稱為向下轉換。編譯器自動轉換規則見圖1所示。
如果出現向上隱式類型轉換,兩個數據進行加法運算時,小數對應到大數有效位數超出有效數據位數,會導致小數無法加到大數上,造成計算結果存在精度損失。
如果出現向下隱式類型轉換,大范圍變量的數據值超出了小范圍變量的能夠表示的有效范圍,編譯器會根據數據值在內存中的表示形式經過轉換后,將高位數據截斷丟棄造成轉換結果錯誤,因此編譯器不允許隱式向下轉換,如果必須要這么做,需要使用顯式的方法,使用顯式的方法也會存在問題,比如兩個數據相加得到的計算結果可能出現向上舍入或者向下舍入的情況。
例如,將高精度double類型變量轉換成float類型變量需要增加強制類型轉換。double型向float型轉換為向下轉換,C語言編譯器無法自動執行向下轉換的操作,因此需要采用顯式類型轉換的方式,將double類型變量顯式轉換為float類型變量。代碼示例見表7。
表7 ?類型轉換代碼示例表

操作符使用不當
(1)? 關系表達式、邏輯表達式、條件判斷語句中的控制表達式禁止為賦值表達式“=”。
在上述表達式中如果使用賦值號會導致條件判斷結果始終為真,代碼執行邏輯與預期要求不符。代碼示例見表8。
表8 ?某些表達式中禁止使用賦值表達式代碼示例表

(2)? 使用連續的比較運算,可能導致代碼執行邏輯與預期不符。
表9 ?使用連續比較運算的代碼示例表

代碼抽樣及結果分析
抽樣代碼情況及指標
抽樣20w代碼嵌入式C代碼,處理器類型有C51、GCC、DSP,選擇以下幾個指標作為對工具的衡量指標,以工具發現問題為核心指標。
(1)? 工具提示:工具提示出違反某規則的總數;
(2)? 確認為問題:工具提示違反規則位置,經人工確認確實存在程序問題;
(3)? 工具誤報:工具提示違反規則位置,經人工確認不存在違反規則或者任何錯誤;
(4)? 工具漏報:代碼違反規則要求,工具未給出提示。
工具分析結果
表10? 缺陷模式檢查結果

?建議
根據表10結果,從幾個方面:漏報規則、誤報規則、定位準確規則、提示性信息規則、代碼改進規則、未檢出規則幾個方面給出改進建議。
(1)? 漏報規則:工具能夠較好的發現整數為0做分母的“除零錯誤”,準確率較高,但是未能檢出分母為浮點數0.0的情況。在需求分析、代碼設計階段應考慮分母是否可能為零,如果可能為零,應進行除零保護。
(2)? 誤報規則:工具對于“變量賦值后未使用再次賦值”存在一定程度的誤報,該條規則給出了較多提示性內容,但并未發現真正的程序問題。建議工具對于局部變量定義時賦初值后未使用再次賦值的情況不進行提示。規則描述中也可更進一步明確什么場景下的代碼邏輯是存在缺陷的。
(3)? 定位準確規則:工具對于“數組越界”、“使用未初始化變量”、“關系表達式、邏輯表達式、條件判斷語句中的控制表達式禁止為賦值表達式“=””、“使用連續的比較運算,可能導致代碼執行邏輯與預期不符”,檢查結果較為準確。
(4)? 提示性信息規則:“if elseif分支語句末尾缺少else分支”未發現出現實質性問題,一般情況下需要結合代碼的處理邏輯分析是否存在問題。
(5)? 代碼改進規則:“將double類型數據轉換為float類型”出現問題的大多數情況是存在一定程度的數據精度損失,會造成計算結果存在一定的誤差,建議代碼不進行這類操作。
(6)? 未檢出規則:目前未見“空指針解引用”的錯誤發生,一旦發生后果比較嚴重可能造成計算機復位,開發和測試人員應引起重視,避免代碼中存在指針未判斷是否為空就直接使用的情況出現。

關注【一起學嵌入式】,回復“加群”進技術交流群。
覺得文章不錯,點擊“分享”、“贊”、“在看” 唄!