1. Rule: Restrict all code to very simple control flow constructs - do not use goto statements, setjmp or longjmp constructs, and direct or indirect recursion.
Rationale: Simpler control flow translates into stronger capabilities for verification and often results in improved code clarity. The banishment of recursion is perhaps the biggest surprise here. Without recursion, though, we are guaranteed to have an acyclic function call graph, which can be exploited by code analyzers, and can directly help to prove that all executions that should be bounded are in fact bounded. (Note that this rule does not require that all functions have a single point of return -although this often also simplifies control flow. There are enough cases, though, where an early error return is the simpler solution.)
規則:將所有程式碼限制為非常簡單的控制流結構 - 不要使用 goto 語句、setjmp 或 longjmp 結構以及直接或間接遞歸。
理由:更簡單的控制流程意味著更強的驗證能力,並且通常可以提高程式碼的清晰度。遞歸的廢除也許是這裡最大的驚喜。但是,如果沒有遞歸,我們可以保證有一個非循環函數呼叫圖,它可以被程式碼分析器利用,並且可以直接幫助證明所有應該有界的執行實際上都是有界的。 (請注意,此規則並不要求所有函數都有一個返回點 - 儘管這通常也會簡化控制流。不過,在許多情況下,早期錯誤返回是更簡單的解決方案。)
2. Rule: All loops must have a fixed upper-bound. It must be trivially possible for a checking tool to prove statically that a preset upper-bound on the number of iterations of a loop cannot be exceeded. If the loop-bound cannot be proven statically, the rule is considered violated.
Rationale: The absence of recursion and the presence of loop bounds prevents runaway code. This rule does not, of course, apply to iterations that are meant to be non-terminating (e.g., in a process scheduler). In those special cases, the reverse rule is applied: it should be statically provable that the iteration cannot terminate. One way to support the rule is to add an explicit upper-bound to all loops that have a variable number of iterations (e.g., code that traverses a linked list). When the upper-bound is exceeded an assertion failure is triggered, and the function containing the failing iteration returns an error. (See Rule 5 about the use of assertions.)
規則:所有循環都必須有固定的上限。檢查工具必須能夠靜態證明循環迭代次數的預設上限不能被超過。如果無法靜態證明循環界限,則認為違反了該規則。
理由:不存在遞歸且存在循環界限可以防止程式碼失控。當然,這條規則不適用於非終止的迭代(例如,在進程排程器中)。在這些特殊情況下,適用相反的規則:應該可以靜態證明迭代不能終止。支援此規則的一種方法是為所有具有可變迭代次數的循環(例如,遍歷連結清單的程式碼)添加明確的上限。當超出上限時,會觸發斷言失敗,並且包含失敗迭代的函數將傳回錯誤。 (有關斷言的使用,請參閱規則 5。)
3. Rule : Do not use dynamic memory allocation after initialization.
Rationale: This rule is common for safety critical software and appears in most coding guidelines. The reason is simple: memory allocators, such as malloc, and garbage collectors often have unpredictable behavior that can significantly impact performance. A notable class of coding errors also stems from mishandling of memory allocation and free routines: forgetting to free memory or continuing to use memory after it was freed, attempting to allocate more memory than physically available, overstepping boundaries on allocated memory, etc. Forcing all applications to live within a fixed, pre-allocated, area of memory can eliminate many of these problems and make it easier to verify memory use. Note that the only way to dynamically claim memory in the absence of memory allocation from the heap is to use stack memory. In the absence of recursion (Rule 1), an upper-bound on the use of stack memory can derived statically, thus making it possible to prove that an application will always live within its pre-allocated memory means.
規則:初始化後不要使用動態記憶體分配。
原因:此規則對於安全關鍵型軟體來說很常見,並且出現在大多數編碼指南中。原因很簡單:記憶體分配器(例如 malloc)和垃圾收集器通常具有不可預測的行為,這會嚴重影響效能。一類值得注意的編碼錯誤也源自於對記憶體分配和釋放例程的錯誤處理:忘記釋放記憶體或在釋放記憶體後繼續使用記憶體、試圖分配比物理可用記憶體更多的記憶體、超越分配記憶體的邊界等。請注意,在沒有從堆疊分配記憶體的情況下動態聲明記憶體的唯一方法是使用堆疊記憶體。在沒有遞歸(規則 1)的情況下,可以靜態得出堆疊記憶體使用上限,從而可以證明應用程式將始終存在於其預先分配的記憶體範圍內。
4. Rule: No function should be longer than what can be printed on a single sheet of paper in a standard reference format with one line per statement and one line per declaration. Typically, this means no more than about 60 lines of code per function.
Rationale: Each function should be a logical unit in the code that is understandable and verifiable as a unit. It is much harder to understand a logical unit that spans multiple screens on a computer display or multiple pages when printed. Excessively long functions are often a sign of poorly structured code.
4. 規則:任何函數的長度都不應超過標準參考格式在一張紙上列印的長度,每個語句一行,每個宣告一行。通常,這意味著每個函數的程式碼不超過 60 行。
理由:每個函數都應該是程式碼中的一個邏輯單元,作為一個單元是可以理解和驗證的。要理解跨越電腦顯示器上多個螢幕或列印時多頁的邏輯單元要困難得多。過長的函數通常是程式碼結構不良的標誌。
5. Rule: The assertion density of the code should average to a minimum of two assertions per function. Assertions are used to check for anomalous conditions that should never happen in real-life executions. Assertions must always be side-effect free and should be defined as Boolean tests. When an assertion fails, an explicit recovery action must be taken, e.g., by returning an error condition to the caller of the function that executes the failing assertion. Any assertion for which a static checking tool can prove that it can never fail or never hold violates this rule. (I.e., it is not possible to satisfy the rule by adding unhelpful “assert(true)” statements.)
Rationale: Statistics for industrial coding efforts indicate that unit tests often find at least one defect per 10 to 100 lines of code written. The odds of intercepting defects increase with assertion density. Use of assertions is often also recommended as part of a strong defensive coding strategy. Assertions can be used to verify pre- and post-conditions of functions, parameter values, return values of functions, and loop-invariants. Because assertions are side-effect free, they can be selectively disabled after testing in performance-critical code. A typical use of an assertion would be as follows:
規則:程式碼的斷言密度應該平均每個函數至少有兩個斷言。斷言用於檢查實際執行中永遠不會發生的異常情況。斷言必須始終沒有副作用,並且應該定義為布林測試。當斷言失敗時,必須採取明確的恢復措施,例如,向執行失敗斷言的函數的呼叫者傳回錯誤條件。任何靜態檢查工具可以證明其永遠不會失敗或永遠不會成立的斷言都違反了此規則。 (即,不可能透過加入無用的「assert(true)」語句來滿足規則。)
原因:工業編碼工作的統計數據表明,單元測試通常每編寫 10 到 100 行程式碼就會發現至少一個缺陷。攔截缺陷的幾率會隨著斷言密度的增加而增加。通常也建議使用斷言作為強防禦性編碼策略的一部分。斷言可用於驗證函數的前置條件和後置條件、參數值、函數的回傳值、循環不變量。因為斷言沒有副作用,所以可以在效能關鍵型程式碼中測試後選擇性地停用它們。斷言的典型用法如下:
if (!c_assert(p >= 0) == true) { return ERROR; }
with the assertion defined as follows:
#define c_assert(e) ((e) ? (true): \
tst_debugging("%s,%d: assertion '%s' failed\n",\
_ FILE_, _ LINE, #e), false)
In this definition, FILE and LINE are predefined by the macro preprocessor to produce the filename and line-number of the failing assertion. The syntax #e turns the assertion condition e into a string that is printed as part of the error message. In code destined for an embedded processor there is of course no place to print the error message itself - in that case, the call to tst_debugging is turned into a no-op, and the assertion turns into a pure Boolean test that enables error recovery from anomolous behavior.
在此定義中,FILE 和 LINE 由巨集預處理器預先定義,以產生失敗斷言的檔案名稱和行號。語法 #e 將斷言條件 e 轉換為字串,並作為錯誤訊息的一部分列印出來。在嵌入式處理器的程式碼中,當然沒有地方列印錯誤訊息本身——在這種情況下,對 tst_debugging 的呼叫將變成無操作,並且斷言將變成純布林測試,從而能夠從異常行為。
6. Rule: Data objects must be declared at the smallest possible level of scope.I
Rationale: This rule supports a basic principle of data-hiding. Clearly if an object is not in scope, its value cannot be referenced or corrupted. Similarly, if an erroneous value of an object has to be diagnosed, the fewer the number of statements where the value could have been assigned; the easier it is to diagnose the problem. The rule discourages the re-use of variables for multiple, incompatible purposes, which can complicate fault diagnosis.
規則:資料物件必須在盡可能最小的作用域層級上聲明。
理由:此規則支持資料隱藏的基本原則。顯然,如果物件不在範圍內,則其值就無法被引用或破壞。類似地,如果必須診斷物件的錯誤值,則可能分配該值的語句數量就越少;診斷問題就越容易。該規則不鼓勵將變數重複用於多個不相容的目的,這會使故障診斷變得複雜。
7. Rule: The return value of non-void functions must be checked by each calling function, and the validity of parameters must be checked inside each function.
Rationale: This is possibly the most frequently violated rule, and therefore somewhat more suspect as a general rule. In its strictest form, this rule means that even the return value of printf statements and file close statements must be checked. One can make a case, though, that if the response to an error would rightfully be no different than the response to success, there is little point in explicitly checking a return value. This is often the case with calls to printf and close. In cases like these, it can be acceptable to explicitly cast the function return value to (void) - thereby indicating that the programmer explicitly and not accidentally decides to ignore a return value. In more dubious cases, a comment should be present to explain why a return value is irrelevant. In most cases, though, the return value of a function should not be ignored, especially if error return values must be propagated up the function call chain. Standard libraries famously violate this rule with potentially grave consequences. See, for instance, what happens if you accidentally execute strlen(0), or strcat(s1, s2, -1) with the standard C string library - it is not pretty. By keeping the general rule, we make sure that exceptions must be justified, with mechanical checkers flagging violations. Often, it will be easier to comply with the rule than to explain why non-compliance might be acceptable.
規則:非void函數的傳回值必須經過每個呼叫函數的檢查,並且必須在每個函數內部檢查參數的有效性。
理由:這可能是最常被違反的規則,因此作為一般規則更令人懷疑。依照最嚴格的形式,該規則意味著甚至必須檢查 printf 語句和檔案關閉語句的回傳值。然而,有人可能會認為,如果對錯誤的回應與對成功的回應沒有什麼不同,那麼明確檢查回傳值就沒有什麼意義了。呼叫 printf 和 close 時通常就是這種情況。在這種情況下,可以接受將函數返回值明確轉換為(void) - 從而表明程式設計師明確地而不是意外地決定忽略返回值。在更可疑的情況下,應該提供註釋來解釋為什麼回傳值不相關。但是,在大多數情況下,函數的傳回值不應被忽略,特別是當錯誤傳回值必須沿著函數呼叫鏈傳播時。眾所周知,標準庫違反了這條規則,可能會造成嚴重後果。例如,如果您意外地使用標準 C 字串庫執行 strlen(0) 或 strcat(s1, s2, -1),會發生什麼情況 - 這並不好看。透過遵守一般規則,我們確保例外情況必須合理,並使用機械檢查器標記違規行為。通常,遵守規則比解釋為什麼不遵守規則是可以接受的更容易。
8. Rule: The use of the preprocessor must be limited to the inclusion of header files and simple macro definitions. Token pasting, variable argument lists (ellipses), and recursive macro calls are not allowed. All macros must expand into complete syntactic units. The use of conditional compilation directives is often also dubious, but cannot always be avoided. This means that there should rarely be justification for more than one or two conditional compilation directives even in large software development efforts, beyond the standard boilerplate that avoids multiple inclusion of the same header file. Each such use should be flagged by a tool-based checker and justified in the code.
Rationale: The C preprocessor is a powerful obfuscation tool that can destroy code clarity and befuddle many text based checkers. The effect of constructs in unrestricted preprocessor code can be extremely hard to decipher, even with a formal language definition in hand. In a new implementation of the C preprocessor, developers often have to resort to using earlier implementations as the referee for interpreting complex defining language in the C standard. The rationale for the caution against conditional compilation is equally important. Note that with just ten conditional compilation directives, there could be up to 210 possible versions of the code, each of which would have to be tested- causing a huge increase in the required test effort.
規則:預處理器的使用必須限於包含頭檔和簡單的巨集定義。不允許標記貼上、變數參數清單(省略號)和遞歸巨集呼叫。所有宏都必須擴展為完整的句法單元。使用條件編譯指令通常也是可疑的,但並非總是可以避免。這意味著,即使在大型軟體開發工作中,也很少需要使用超過一兩個條件編譯指令,除了避免多次包含同一個頭檔的標準樣板之外。每種此類用法都應由基於工具的檢查器進行標記,並在程式碼中加以證明。
原因:C 預處理器是一個強大的混淆工具,它可以破壞程式碼清晰度並迷惑許多基於文字的檢查器。即使手上有正式的語言定義,不受限制的預處理器程式碼中的構造的效果也極難破解。在 C 預處理器的新實作中,開發人員通常必須使用早期的實作作為解釋 C 標準中複雜定義語言的參考。謹慎反對條件編譯的理由也同樣重要。請注意,僅使用十個條件編譯指令,程式碼就有多達 210 個可能版本,每個版本都必須進行測試 - 這會大大增加所需的測試工作。
9. Rule: The use of pointers should be restricted. Specifically, no more than one level of dereferencing is allowed. Pointer dereference operations may not be hidden in macro definitions or inside typedef declarations. Function pointers are not permitted.
Rationale: Pointers are easily misused, even by experienced programmers. They can make it hard to follow or analyze the flow of data in a program, especially by tool-based static analyzers. Function pointers, similarly, can seriously restrict the types of checks that can be performed by static analyzers and should only be used if there is a strong justification for their use, and ideally alternate means are provided to assist tool-based checkers determine flow of control and function call hierarchies. For instance, if function pointers are used, it can become impossible for a tool to prove absence of recursion, so alternate guarantees would have to be provided to make up for this loss in analytical capabilities.
規則:應限制指針的使用。具體來說,不允許超過一個等級的取消引用。指標取消引用操作不能隱藏在巨集定義中或 typedef 宣告中。不允許使用函數指標。
原因:即使是經驗豐富的程式設計師,指針也很容易被誤用。它們會使追蹤或分析程式中的資料流變得困難,尤其是使用基於工具的靜態分析器時。同樣,函數指針也會嚴重限制靜態分析器可以執行的檢查類型,因此只有在有充分理由的情況下才應使用函數指針,並且最好提供替代方法來幫助基於工具的檢查器確定控制流和函數調用層次。例如,如果使用函數指針,工具就無法證明不存在遞歸,因此必須提供替代保證來彌補分析能力的損失。
10. Rule: All code must be compiled, from the first day of development, with all compiler warnings enabled at the compiler's most pedantic setting. All code must compile with these setting without any warnings. All code must be checked daily with at least one, but preferably more than one, state-of-the-art static source code analyzer and should pass the analyses with zero warnings.
Rationale: There are several very effective static source code analyzers on the market today, and quite a few freeware tools as well. There simply is no excuse for any software development effort not to make use of this readily available technology. It should be considered routine practice, even for non-critical code development. The rule of zero warnings applies even in cases where the compiler or the static analyzer gives an erroneous warning: if the compiler or the static analyzer gets confused, the code causing the confusion should be rewritten so that it becomes more trivially valid. Many developers have been caught in the assumption that a warning was surely invalid, only to realize much later that the message was in fact valid for less obvious reasons. Static analyzers have somewhat of a bad reputation due to early predecessors, such as lint, that produced mostly invalid messages, but this is no longer the case. The best static analyzers today are fast, and they produce selective and accurate messages. Their use should not be negotiable at any serious software project.
10. 規則:從開發的第一天起,所有程式碼都必須在編譯器最嚴格的設定下啟用所有編譯器警告進行編譯。所有程式碼都必須使用這些設定進行編譯,且無任何警告。必須每天使用至少一個(最好是多個)最先進的靜態原始碼分析器檢查所有程式碼,並且應以零警告通過分析。
理由:現今市面上有幾種非常有效的靜態原始碼分析器,還有不少免費軟體工具。任何軟體開發工作都沒有理由不利用這種現成的技術。即使對於非關鍵程式碼開發來說,這也應該被視為常規做法。即使在編譯器或靜態分析器給出錯誤警告的情況下,零警告規則仍然適用:如果編譯器或靜態分析器感到困惑,則應重寫導致困惑的程式碼,以使其變得更加有效。許多開發人員都誤以為警告肯定無效,但很久之後才意識到由於不太明顯的原因該訊息實際上是有效的。靜態分析器有點名聲不好,因為早期的前身(如 lint)大多會產生無效訊息,但現在情況已不再如此。當今最好的靜態分析儀速度很快,並且可以產生選擇性的、準確的訊息。在任何嚴肅的軟體專案中,它們的使用都是不容商榷的。