參數有效性檢驗

參數有效性檢驗

2018-11-26天氣涼,耗時三個週末完成這篇原創文章,記錄下自己關於程序安全性方面的一些微薄見解。願自己程序員之路越走越順利,保持激情初心,不忘理想前行。

  • 問題:

    • 爲什麼要檢驗?
    • 哪些情況判爲參數失效?
    • 有哪些參數需要檢驗?
    • 怎麼檢測?
    • 在哪裏檢驗?
    • 怎麼處理?
  • 爲什麼要檢驗?
      保護程序免糟非法輸入數據的破壞,儘可能將異常數據對程序造成的影響控制在有限的範圍內。
      防禦式編程主要思想:子程序應該不因傳入錯誤數據而被破壞,哪怕是由其他子程序產生的錯誤數據。更一般地說,其核心思想是承認程序都會有問題,都需要被修改,聰明的程序員應該根據這一點來編程。
      不管進來什麼,好的程序都不會生成垃圾,而是做到“垃圾進,什麼都不出”、“進來垃圾,出去是錯誤提示”或“不許垃圾進來”。

    ——《代碼大全2》第8章 防禦式編程

  • 哪些情況判爲參數失效?

    • 參數越界失效:參數值不在預期範圍內。比如參數值超過上下限,數組下標越界。
    • 符號異常失效:指正負號異常,應該儘可能使用無符號類型。
    • 空指針失效:傳遞空指針。
    • 數據過期失效:原本實時更新的數據長期未變化。
    • 跳變失效:數據波動異常的大或小。
    • 關聯數據異常失效:指兩個以上的數據值之間有依賴和關聯,當值不符合依賴關聯性時認爲失效。
  • 有哪些參數需要檢驗?
      最佳的形式是在一開始就不引入錯誤。

    • 從數據來源講:
      • 檢查來源於外部的數據的值。
      • 檢查子程序的輸入參數的值。

      應重點檢查外部輸入數據(網絡通訊傳入、上層或下層傳入、硬件設備採集數據)外部傳入的數據對本模塊的程序員來說往往是模糊和不可操控的,內部數據也較多依賴於外部數據輸入,因此中間檢查外部數據。內部數據可以只檢查較爲複雜的算式或算結果。

  • 怎麼檢測?
      對參數進行失效檢驗意味着需要存儲有一些有關參數的信息,可以定義結構體化的參數。我們對參數進行分類如下:

    • 功能型參數。
      用於表示某項功能開關的參數,我們可以定義爲如下結構體:

       ```c
        //Type redefinition------------------------------------------------------------------------
        typedef unsigned char Byte;
        typedef Uint16        Word;
        
        //Function enum definition-----------------------------------------------------------------
        typedef enum Switch {DISABLE, ENABLE} ESwitch; //Switch variable
        
        //Functional parameter structure-----------------------------------------------------------
        typedef struct FunParameter
        {
            ESwitch eSwitch;               //Value: ENABLE or DISABLE
            ESwitch ePrevious;             //Previous value
            Word wCounter;                 //Enable or Disable time
        }TFunParameter, * pTFunParameter;
        
        //functional parameter initialization------------------------------------------------------
        //Motor head temperature
        TFunParameter tfHeadTemp = {.eSwitch = ENABLE,
                                    .ePrevious = DISABLE,
                                    .wCounter = 0
                                   };
      ```
      

      note:定義歷史值方便檢測功能開關狀態切換的跳變變化。功能有時需要記錄開啓或者關閉時間,因此直接將定時器封裝進結構體使得參數關係緊密。

        對功能型參數使用枚舉類型因此不需要檢測值越界失效和符號異常失效,重要的是檢測關聯數據異常失效。比如功能A和B爲互斥關係,一次只能有一個功能打開或兩個功能都關閉,再比如功能B開啓前應該先打開功能A。功能開關較多,關聯性強的情況下這樣的檢測變得重要。
        對於功能型參數的關聯性檢測可以放到預處理時期由編譯器檢測,不用等到外出調試才發現錯誤。預處理指令不能檢測變量,考慮到功能有效性一旦確當後不在修改,但又要滿足調試時可能存在的對功能開關的修改,可以通過以下形式實現:

        //General macro definition-----------------------------------------------------------------
        #define ENABLE                     1
        #define DISABLE                    0
        
        //The switch of Function-------------------------------------------------------------------
        #define FUN_HEAD_TEMP             ENABLE
        #define FUN_OIL_PRESS             DISABLE
        #define FUN_HEAD_TEMP_STOP        ENABLE
        
        //Functional correlation detection---------------------------------------------------------
        #if((FUN_HEAD_TEMP == FUN_OIL_PRESS) && (FUN_HEAD_TEMP == ENABLE))
            #error 油壓與油溫爲互斥功能,不能同時開啓。
        #endif
        
        #if((FUN_HEAD_TEMP_STOP == ENABLE) && (FUN_HEAD_TEMP == DISABLE))
            #error 開啓油溫停機功能必須同時開啓油溫採樣功能。
        #endif
        
        //functional parameter initialization--------------------------------------------------------
        TFunParameter tfHeadTemp = {.eSwitch = FUN_HEAD_TEMP, .ePrevious = DISABLE, .wCounter = 0};              //Motor head temperature
        TFunParameter tfOilPress = {.eSwitch = FUN_OIL_PRESS, .ePrevious = DISABLE, .wCounter = 0};              //Oile press Function
        TFunParameter tfHeadTempStop = {.eSwitch = FUN_HEAD_TEMP_STOP, .ePrevious = DISABLE, .wCounter = 0};     //Motor Closed Function
      
    • 狀態型參數。
      狀態型參數用於表示當前運行下的某種狀態,是實時的因此不像常量宏可以用預處理指令檢驗。比如電機的開關波狀態,閥門的開關狀態,氣壓狀態等。我們可以定義爲如下結構體:

        //Status enum definition-------------------------------------------------------------------
        typedef enum MotorPwm{PWM_CLOSE, PWM_OPEN}EMotorPwm;                 //Motor Pwm Status
        typedef enum AirPressure{AP_LOW, AP_NORMAL, AP_HIGH}EAirPressure;    //Air Pressure Status
        
        //Status parameter structure definition----------------------------------------------------
        typedef struct StatusParameter
        {
            Byte byStatus;                           //Current Status
            Byte byPrevious;                         //Previous value
            Byte byFrequency;                        //Status frequency 
            Word wCounter;                           //Status duration time
        }TStatusParameter, * pTStatusParameter;
        
        //Status parameter initialization----------------------------------------------------------
        //Motor Pwm Status
        TStatusParameter tsMotorPwm = {.byStatus = PWM_OPEN,
                                       .byPrevious = PWM_CLOSE,
                                       .byFrequency = 0,
                                       .wCounter = 0                   //Unit: s
                                      };
        
        //Air Pressure Status
        TStatusParameter tsAirPressure = {.byStatus = AP_NORMAL,
                                          .byPrevious = AP_NORMAL,
                                          .byFrequency = 0,
                                          .wCounter = 0                //Unit: ms
                                         };
      

    note:定義歷史值方便檢測狀態轉換的跳變變化。狀態常常需要記錄持續的時間,因此加入一個計數器可用於計時。在一段時間內有時還需要記錄狀態發生的次數,因此需要一個變量記錄狀態發生頻度。

      一種運行狀態可能存在兩種以上的狀態值,且不同的狀態值有不同的含義,因此無法用統一的枚舉類型。對於一組狀態值可以單獨使用一組枚舉表示,枚舉的參數封裝性更好,宏定義參數較“散”但省空間,綜合多種因素在狀態繁多的情況下我選擇用封裝性更好的枚舉。
      對於狀態的檢測,由於狀態是隨着程序運行實時變化的且不固定,因此無法在編譯階段對狀態值的有效性檢測。這就需要我們根據實際邏輯需要增加相應的檢測代碼。由於狀態值都通過枚舉定義好,因此不需要做參數越界失效和符號異常失效判斷,狀態值都是預知且固定的所以不需要跳變失效判斷,其他失效根據實際情況添加檢驗代碼。

    • 故障型參數。
      故障型參數用於記錄當前或歷史的故障發生情況,並決定是否要發出報警信號或停機等操作。我們可以定義爲如下結構體:

        //Unusual enum definition-------------------------------------------------------------------
        typedef enum UnusualSymbol{ABNORMAL, NORMAL}EUnusualSymbol;        //Unusual state
        
        //Unusual parameter structure definition----------------------------------------------------
        typedef struct UnusualParameter
        {
            EUnusualSymbol eUnusualSymbol;                //Unusual symbol
            EUnusualSymbol ePrevious;                     //Previous value
            EUnusualSymbol eWarningSymbol;                //Unusual Warning symbol
            Byte byFrequency;                             //Unusual frequency 
            Word wCounter;                                //Unusual duration time
        }TUnusualParameter, * pTUnusualParameter;
        
        //Unusual parameter initialization----------------------------------------------------------
        //Head temperature over
        TUnusualParameter tuHeadTempOver = {.eUnusualSymbol   = NORMAL,
                                            .ePrevious        = NORMAL,
                                            .eWarningSymbol   = NORMAL,
                                            .byFrequency      = 0,
                                            .wCounter         = 0         //Unit: ms
                                           };
      

    note:定義歷史值方便檢測故障的發生和恢復變化。對於故障常常需要檢測故障的持續時間和頻度,因此定義相關成員變量。根據故障的持續時間和頻度有時會引起其他動作,比如報警或停機,因此引入報警標誌。

      故障的狀態只需要兩種,即異常或正常,因此可以同意定義枚舉類型。
      對於故障的檢測,由於故障是在程序運行中發生,因此對故障參數有效性的檢測需要實時進行。由於故障參數有固定的枚舉類型,因此不需要越界失效、符號異常、跳變失效判斷,其他失效根據實際情況添加檢驗代碼。

    • 普通值參數。
      普通值參數是較爲龐大的一類參數,這類參數的值類型不一,值之間的關聯性可以很複雜,每種值各有特點,參數的有效性檢驗很難做統一的處理。我們可以定義爲如下結構體:

        //Sign enum definition-------------------------------------------------------------------
        typedef enum Sign{SIGN_MINUS = -1, SIGN_PLUS = 1}ESign;          //Unusual state
        typedef enum Lock{LOCK_UNLOCK = 0, LOCK_LOCK = 1}ELock;          //Resource lock
        
        //Value parameter structure definition----------------------------------------------------
        typedef struct ValueParameter
        {
            ESign eSign;                                //Value: SIGN_PLUS of SIGN_MINUS
            Word wValue;                                //Current value
            Word wPrevious;                             //Previous value
            Word wDefault;                              //Default value
            Word wCounter;                              //duration time
            Word wUpperLimit;                           //Upper limit
            Word wLowerLimit;                           //Lower limit
            Word wJumpLimit;                            //Jump limit
            Word wOverdueLimit;                         //Overdue limit
            Byte wLock;                                 //Resource lock
        }TValueParameter, * pTValueParameter;
        
        //Value parameter initialization----------------------------------------------------------
        //Motor Rpm
        TValueParameter tvMotorActualRpm = {.eSign         = SIGN_PLUS,
                                            .wValue        = 0,                //Unit: Rpm
                                            .wPrevious     = 0,
                                            .wDefault      = 1000,
                                            .wCounter      = 0,                //Unit: ms
                                            .wUpperLimit   = 2000,
                                            .wLowerLimit   = 0,
                                            .wJumpLimit    = 1000,
                                            .wOverdueLimit = 0,                //Unit: s
                                            .wLock           = LOCK_UNLOCK
                                           };
        
        //Speed
        TValueParameter tvSpeed = {.eSign         = SIGN_PLUS,
                                   .wValue        = 0,                //Unit: 0.1K/M
                                   .wPrevious     = 0,
                                   .wDefault      = 400,
                                   .wCounter      = 0,                //Unit: ms
                                   .wUpperLimit   = 200,
                                   .wLowerLimit   = 0,
                                   .wJumpLimit    = 1000,
                                   .wOverdueLimit = 0,                //Unit: s
                                   .wLock          = LOCK_UNLOCK
                                  };
      
  • 怎麼處理?

    • 用斷言檢查永遠不應該發生的錯誤
      優點:錯誤定位,給代碼調試帶來極大的便利,創建更穩定、質量更好且不易於出錯的代碼。
      缺點:頻繁的調用會極大的影響程序的性能,增加額外的開銷。
      相比於函數,調用函數需要額外的隊棧開銷,宏函數節省開銷但同一段代碼存在多個副本。使用宏函數會引起很多副作用需要小心。

      斷言使用形式如下:

        //#define NDEBUG
        
        #ifndef NDEBUG                               
        #define _assert(bExpression)                                 \
        do{                                                          \
                if (!(bExpression))                                  \
                {                                                    \
                    ErrorDeal();            /*Error deal*/           \
                }                                                    \
        }while(0)
        #else
        #define _assert(bExpression)        ((void)0)
        #endif
        
        int main()
        {
            Byte byNumber1 = 10;
            Byte byNumber2 = 0;
            Byte byResult  = 0;
            
            _assert(0 != byNumber2);        //Cannot be 0
            
            byResult = byNumber1 / byNumber2;
            printf("%d", byNumber2);
            
            system("pause");
            return 0;
        } 
      

      note:通過定義宏NDEBUG來控制斷言的開啓或關斷。用於對較爲嚴重的錯誤統一處理。另外,禁止把必須執行的代碼放在斷言中,斷言關閉後功能將失效。

      用斷言的兩種形式:
      1、斷言一直開啓。不論是調試版還是發行版都將斷言函數開啓。
      2、可以在測試時啓用斷言,而在部署時禁用斷言。同樣,程序投入運行後,最終用戶在遇到問題時可以重新起用斷言。它可以快速發現並定位軟件問題,同時對系統錯誤進行自動報警。斷言可以對在系統中隱藏很深,用其它手段極難發現的問題可以用斷言來進行定位,從而縮短軟件問題定位時間,提高系統的可測性。實際應用時,可根據具體情況靈活地設計斷言。

      前置條件斷言:代碼執行之前必須具備的特性

      後置條件斷言:代碼執行之後必須具備的特性

      前後不變斷言:代碼執行前後不能變化的特性

    • 錯誤處理代碼
        用錯誤處理代碼來處理預期會發生的狀況,用斷言來處理絕不應該發生的狀況。通常用錯誤處理來檢查有害的輸入數據。斷言檢查代碼中的bug。

        根據情形的不同,你可以返回中立值、換用下一個正確數據、返回與前次相同的值、換用最接近的有效值、在日誌文件中記錄警告信息、返回一個錯誤嗎、調用錯誤處理子程序或對象、顯示出錯信息或者關閉程序——或把這些技術結合起來使用。

      1、返回中立值
        對於一些“危害”並不嚴重的錯誤,最佳的做法就是繼續執行操作並簡單的返回一個沒有危害的數值,比如數值0或空指針。

      2、給出一個修正的數據
        對於can網絡中不斷更新但實時性要求並不高的數據,比如電機溫度,短時間內數據丟失可是使用歷史值。

      3、換用最接近的合法值
        有些情況下可以換用最接近的合法值,當值大於上限或者小於下限值時,可以直接去上下限邊界值。

      4、把警告信息記錄到日誌文件中
        在檢測到錯誤數據的時候,你可以選擇在日誌文件中記錄一條警告信息,然後繼續執行。這種方法可以同其他的錯誤處理技術結合使用。

      5、返回一個錯誤碼
        你可以決定只讓系統的某些部分處理錯誤。其他部分則不在本地(局部)處理錯誤,而只是簡單地報告有錯誤發生和發生的是何種錯誤,並信任調用鏈上游的某個子程序會處理該錯誤。通知系統其餘部分已經發生錯誤可以採用下列方法:

      • 設置一個狀態變量的值
      • 用狀態值作爲函數的返回值
      • 用語言內建的異常機制拋出一個異常

        需要決定系統裏哪部分應該直接處理錯誤,哪部分只是報告所發生的錯誤。對於安全性很重要的部分請確認調用的子程序總會檢查返回的錯誤碼。

      6、調用錯誤處理子程序或對象
        這種方法需要把錯誤處理都集中在一個全局的錯誤處理子程序或對象中,使得調試工作更爲簡單。而代價是整個程序都要知道這個集中點並與之緊密耦合。如果你想在其他系統中重用其中的某些代碼,那就得把錯誤處理代碼一併帶過去。

      7、錯誤發生時發出錯誤信息
        在調試模式下,當錯誤發生時爲了幫助調試者儘快定位問題所在位置,可以通過打印顯示或者報文將錯誤信息較爲詳細的發出。

      8、在局部處理錯誤
        針對一些設計方案可能更適合在局部立即解決所有遇到的問題,具體的錯誤處理方法需要由該模塊的設計者根據問題特點自行決定。這種方法給力模塊程序員很大的靈活度,但這樣做會導致錯誤記錄和發出錯誤信息等代碼散佈到整個系統中,從而使得設計者還得考慮錯誤記錄和錯誤發送相關的故障問題。能在局部立即解決的問題最好在內部立即解決。

      9、關閉或復位程序
        程序發生較爲嚴重的錯誤時,比如堆棧溢出導致運行環境被破壞,所依附的操作系統嚴重故障,甚至收到惡意攻擊時可能需要關閉和復位程序。

        確定一種通用的處理錯誤參數的方法,是架構層次(或稱高層次)的設計決策。如果在高層次處理錯誤,低層次只是彙報錯誤,那麼就要確保高層次真的處理有錯誤!千萬不要忽略錯誤信息,在函數的返回值返回故障信息,記得檢查函數的返回值,即使你對某個函數有足夠的自信。當有錯誤產生時,請記錄並反饋對應的故障碼和它描述的信息。

    • 異常
        如果一個子程序在運行過程中遇到了預料之外的情況,但這個情況又必須處理否則會影響功能的使用或程序的運行,這個時候需要子程序將異常拋出,把“問題”交給能解釋並處理好它的代碼。這屬於一種錯誤處理機制。
        能在局部處理的錯誤優選在局部處理,不能立即處理或局部無法處理的問題可拋出異常。不能用異常來推卸責任。異常處理函數需要了解大量子程序可能拋出何種錯誤並給出處理辦法,弱化了程序的封裝性,增加了程序的複雜程度。
        每種異常都對應一種特殊情況,因此要確保異常信息中含有爲理解異常拋出原因所需的充分信息,這對於讀取異常信息的人來說是很有價值的。比如當發生數據越界時,除了拋出對應的故障碼標識數據越界外,還應該給出上下界和非法的數據值。

    總結:斷言、錯誤處理代碼、異常都需要有完備的故障捕獲處理,以及故障信息的記錄反饋。因此有必要考慮一種方法以確保異常處理的一致性,即創建一個集中的異常報告機制。這個機制需要能對異常信息進行一個集中的格式化和存儲。c語言不像c++和java自帶異常機制,但c語言可以藉助setjmp和longjmp實現異常處理機制。實現代碼如下:

      typedef unsigned char Byte;
      typedef Uint16        Word;
      
      #define String        char *              //Don't use typedef
      
      //Exception information structure-----------------------------------------------------------
      typedef struct Exception
      {
          char * description;
      }TException, * pTException;
      
      //Exception stack structure------------------------------------------------------------
      typedef struct ExceptionFrame
      {
          struct ExceptionFrame * prev;
          jmp_buf env;                          //Use in setjmp()
          
          //Exception information
          const TException * ptException;       //About Exception
          const String strFileName;             //File name
          const String strFunctionName;         //Function name
          Word wLineNumber;                     //Line number
      }TExceptionFrame, * pTExceptionFrame;
      
      //Exception status----------------------------------------------------------------------
      typedef enum ExceptionStatus{
          EXCEPT_ENTERED = 0, 
          EXCEPT_RAISED,    
          EXCEPT_HANDLED, 
          EXCEPT_FINALIZED
      }EExceptionStatus;
      
      /************************************************************
      *                try-catch-finally definition                *
      *************************************************************/
      
      //try deal--------------------------------------------------------------------------------
      #define _try                                                                     \
      {                                                                                \
          Byte byExceptFlg;                                    /*Return by setjmp()*/  \
          TExceptionFrame tExceptionNode;                                              \
                                                                                       \
          /*Push stack*/                                                               \
          tExceptionNode.prev = ptExceptionStackTop;                                   \
          ptExceptionStackTop = &tExceptionNode;                                       \
                                                                                       \
          byExceptFlg = setjmp(tExceptionNode.env);            /*Set jump point*/      \
          if(EXCEPT_ENTERED == byExceptFlg)                                            \
          {                                                                    
              /*Here check and throw your exception*/
              //...
      
      //catch deal-------------------------------------------------------------------------------
      #define _catch(tException)                                                       \
              if(EXCEPT_ENTERED == byExceptFlg)                /*No Exception*/        \
              {                                                                        \
                  ptExceptionStackTop = ptExceptionStackTop -> prev;    /*Pop stack*/  \
              }                                                                        \
          }                                                                            \
          else if(&(tException) == tExceptionNode.ptException)                         \
          {                                                                            \
              byExceptFlg = EXCEPT_HANDLED;
               
               /*Here deal your exception*/
               //...                                            
      
      //finally deal------------------------------------------------------------------------------
      #define _finally                                                                \
              if(EXCEPT_ENTERED == byExceptFlg)                /*No Exception*/       \
              {                                                                       \
                  ptExceptionStackTop = ptExceptionStackTop -> prev;    /*Pop stack*/ \
              }                                                                       \
          }                                                                           \
          {                                                                           \
              if(EXCEPT_ENTERED == byExceptFlg)                                       \
              {                                                                       \
                  byExceptFlg = EXCEPT_FINALIZED;                                     \
              }            
              
                  /*Here must deal*/
                  //...                                                                
      
      //end try deal-------------------------------------------------------------------------------
      #define _end_try                                                                \
              if(EXCEPT_ENTERED == byExceptFlg)                /*No Exception*/       \
              {                                                                       \
                  ptExceptionStackTop = ptExceptionStackTop -> prev;    /*Pop stack*/ \
              }                                                                       \
          }                                                                           \
          if(EXCEPT_RAISED == byExceptFlg)                                            \
          {                                                                           \
                                                                                      \
          }                                                                           \
      }while(0)
      
      //_throw() deal------------------------------------------------------------------------------ 
      #define _throw(tException) ExceptRaise(&(tException), __FILE__, __FUNCTION__, __LINE__)
      
      /************************************************************
      *                Exception deal function                     *
      *************************************************************/
      pTExceptionFrame ptExceptionStackTop = NULL;            //Stack top
      
      void ExceptRaise(const TException * const ptException, 
                       const String const strFileName,
                       const String const strFunctionN  ame,
                       const wLineNumber)
      {
          pTExceptionFrame pStackTop = ptExceptionStackTop;
          
          if(NULL == ptException)                             //Check NULL exception
          {
              //Do Something
              //...
          }
          else
          {
              if(NULL == pStackTop)                           //Check stack empty
              {
                  //Do Something
                  //...
              }
              else
              {
                  //Record some information
                  pStackTop -> ptException = ptException;
                  pStackTop -> strFileName = strFileName;
                  pStackTop -> strFunctionName = strFunctionName;
                  pStackTop -> wLineNumber = wLineNumber;
                  
                  ptExceptionStackTop = ptExceptionStackTop -> prev;      //Pop stack
                  
                  longjmp(pStackTop -> env, EXCEPT_RAISED);
              }
          }
      }
                       
      
      int main()
      {
          TException A = {"A exception"};
      
          _try
          {
              _throw(A);
          }
          _catch(A)
          {
              printf("A exception\n");
          }_end_try;
          
          system("pause");
          return 0;
      }
    

    預料之外的錯誤處理。
      異常中沒有捕獲到的異常情況,我們認爲是意外異常,發生意外異常時的一種常見做法是終止程序運行。

    • 隔欄。
        數據源有兩類,一類是外部傳入一類是內部運算產生。由於外部數據的處理過程和存儲環境對本模塊設計者來說是模糊或不可見的,而內部數據處理直接或間接依賴於外部數據。因此我們假定外部數據是骯髒且不可信的,內部數據假定爲乾淨(僅對少量安全性高數據檢測)。不可信的外部數據傳入內部需要一個統一數據清理的環節,這就是隔欄的作用。

      隔欄與斷言的關係:
        隔欄的使用使斷言和錯誤處理有了清晰的區分。隔欄外部的數據應該使用錯誤處理技術,在那裏對數據做到任何假定都是不安全的。而隔欄內部的程序裏就應該使用斷言技術,因爲傳進來的數據都已在通過隔欄時被清理過了。如果隔欄內部的某個子程序檢測到了錯誤的數據,那麼應該是程序李的錯誤而不是數據裏的錯誤。

      ——《代碼大全2》

      ![Alt text](https://img-blog.csdnimg.cn/20181126234314543.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQyNDc1NzEx,size_16,color_FFFFFF,t_70)
    • 錯誤日誌處理。
        隨着項目規模的不斷擴大,日誌的應用對於維護程序的正常運行具有重要的意義。當一個程序崩潰時如果沒有拋出異常信息,對於維護人員來說無法快速定位問題,更別談去解決問題了。
      日誌時程序員解決問題的第一手資料。異常發生時,從別人口述的情況往往存在不準確或模糊,爲了更好的解決問題我們需要當時發生了什麼,用戶當時做了什麼操作,運行環境有無異常,數據有哪些變化,異常時否反覆發生,是偶發還是有必然起因。若日誌做了詳細記錄對於問題的定位和解決起到舉足輕重的作用。

      1. 日誌的作用

        1. 記錄用戶操作的審計日誌,甚至就是監管部門的要求。
        2. 記錄程序運行的流程。
        3. 記錄數據的變化。
        4. 記錄異常信息,快速定位問題根源。
        5. 數據統計和性能分析(程序性能或設備性能)。
      2. 如何做好程序日誌

        1. 日誌的可讀性
            看日誌的是程序員,但並不一定是接觸過相關模塊或看過這部分源碼的程序員。日誌應該一路瞭然,信息詳細但又不重複冗餘。日誌信息應該準確可靠,避免含糊不清的日誌。

        2. 日誌的資源消耗
            日誌的記錄毫無疑問需要消耗資源,佔用程序運行的時間資源、佔用io口寫入文件或數據庫,佔用磁盤或EE存儲。日誌信息應該簡明扼要,避免打印無意義的日誌,減少重複日誌。日誌應該做成滾動式,記錄一段時間內的日誌和錯誤等級高的日誌,防止日誌文件寫滿存儲空間。

        3. 日誌應該分等級
          日誌的等級可以分爲FATAL、ERROR、INFO、DEBUG、TRACE。

          1. FALTAL(致命)
            FALTAL是錯誤等級最高的重大錯誤,它的出現程序無法自我恢復,必須通過復位程序才能運行。該類錯誤應該記錄錯誤碼。

          2. ERROR(錯誤)
            這類錯誤雖然發生,但可以通過程序來彌補或忽略來保證程序正常運行。該類錯誤需要記錄錯誤碼。

          3. WARN(警告)
            表示會出現潛在錯誤的情形,比如參數未傳入,使用默認了參數,參數持續時長一直未變化,這類錯誤雖異常但在程序員預期之內,並且程序做了充分的彌補措施。也建議記錄錯誤碼。

          4. INFO(信息)
            用於記錄程序的運行過程,打印一些程序運行中的重要信息,但要避免濫用。

          5. DEBUG(調試)
            用於開發調試過程中,由程序員自由控制,具有較高靈活度。

          6. TRACE(跟蹤)
            一般不使用。用於跟蹤函數的調用,不含變量只反應函數調用關係。

      代碼示例:

        //Log Level definition-----------------------------------------------------------------
        typedef enum LogLevel {LOG_LEVEL_TRACE, LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_WARN, LOG_LEVEL_ERROR, LOG_LEVEL_FATAL} ELogLevel;
        
        // 定義FATAL級別輸出宏
        #define LOG_FATAL(wFaultCode, strFormat, ...)         LogPrint(LOG_LEVEL_FATAL, "[%s <%d>](%s) " "[%#08X]" strFormat "\n", __FILE__, __LINE__, __FUNCTION__, wFaultCode, ##__VA_ARGS__)
        
        // 定義ERROR級別輸出宏
        #define LOG_ERROR(wFaultCode, strFormat, ...)         LogPrint(LOG_LEVEL_ERROR, "[%s <%d>](%s) " "[%#08X]" strFormat "\n", __FILE__, __LINE__, __FUNCTION__, wFaultCode, ##__VA_ARGS__)
        
        // 定義WARN級別輸出宏
        #define LOG_WARN(wFaultCode, strFormat, ...)          LogPrint(LOG_LEVEL_WARN, "[%s <%d>](%s) " "[%#08X]" strFormat "\n", __FILE__, __LINE__, __FUNCTION__, wFaultCode, ##__VA_ARGS__)  
        
        // 定義INFO級別輸出宏
        #define LOG_INFO(strFormat, ...)          LogPrint(LOG_LEVEL_INFO, "[%s <%d>](%s) " strFormat "\n", __FILE__, __LINE__, __FUNCTION__, ##__VA_ARGS__)
        
        // 定義DEBUG級別輸出宏
        #define LOG_DEBUG(strFormat, ...)         LogPrint(LOG_LEVEL_DEBUG, "[%s <%d>](%s) " strFormat "\n", __FILE__, __LINE__, __FUNCTION__, ##__VA_ARGS__)   
        
        // 定義TRACE級別輸出宏
        #define LOG_TRACE(strFormat, ...)         LogPrint(LOG_LEVEL_TRACE, "[%s <%d>)](%s) " strFormat "\n", __FILE__, __LINE__, __FUNCTION__, ##__VA_ARGS__)
      

      輸出結果展示:

      ![Alt text](https://img-blog.csdnimg.cn/20181126234401896.PNG)
    • 錯誤碼。
        軟件運行中由於錯誤類型衆多,爲了對錯誤進行區分,系統設定了錯誤碼(error code),軟件通過內部的自檢機制超出錯誤並拋出給開發人員,開發人員通過錯誤碼快速分析錯誤原因。

        //Error description----------------------------------------------------------------------------------
        //Belongs to
        #define AIR_COMPRESSOR                          (0X01 << 24)
        #define STEERING_MOTOR                          (0X02 << 24)
        //...
        
        //Error level
        #define ERROR_LEVEL_1                           (0X01 << 16)
        #define ERROR_LEVEL_2                           (0X02 << 16)
        #define ERROR_LEVEL_3                           (0X03 << 16)
        #define ERROR_LEVEL_4                           (0X04 << 16)
        #define ERROR_LEVEL_5                           (0X05 << 16)
        #define ERROR_LEVEL_6                           (0X06 << 16)
        
        //Error categories
        #define CATEGORIES_FUNCTION                     (0X01 << 8)
        #define CATEGORIES_VALUE                        (0X02 << 8)
        //...
        
        //About function
        #define DOMAIN_ERROR                            (AIR_COMPRESSOR | ERROR_LEVEL_4 | CATEGORIES_FUNCTION | 0X01)
        #define RANGE_ERROR                             (AIR_COMPRESSOR | ERROR_LEVEL_4 | CATEGORIES_FUNCTION | 0X02)
        #define INVALID_ARGUMENT                        (AIR_COMPRESSOR | ERROR_LEVEL_4 | CATEGORIES_FUNCTION | 0X03)
        //...
        
        //About value                    
        #define OVERFLOW_ERROR                          (AIR_COMPRESSOR | ERROR_LEVEL_4 | CATEGORIES_VALUE | 0X01)
        #define UNDERFLOW_ERROR                         (AIR_COMPRESSOR | ERROR_LEVEL_4 | CATEGORIES_VALUE | 0X02)
        #define OUT_OF_BOUNDS                           (AIR_COMPRESSOR | ERROR_LEVEL_4 | CATEGORIES_VALUE | 0X03)
        #define LENGTH_ERROR                            (AIR_COMPRESSOR | ERROR_LEVEL_4 | CATEGORIES_VALUE | 0X04)
        //...
        
        //...
      
    • 調試log。
        跟蹤代碼運行軌跡,擔當開發環境中的調試利器,提高開發效率,增強查錯能力。
      區別出產品代碼與開發代碼。產品級軟件要求運行快速、節約資源、較高安全性,而開發中的軟件可以運行緩慢、資源利用奢侈、提供一些額外的輔助調試的不安全操作。開發期間犧牲一些速度和資源換取使開發順暢的內置工具,比如調試log,用於監視程序運行流程和數據。

        #include <stdio.h>
        #include <stdlib.h>
        
        typedef unsigned int Uint16;
        
        typedef unsigned char Byte;
        typedef Uint16        Word;
        
        //Function enum definition-----------------------------------------------------------------
        typedef enum Switch {DISABLE, ENABLE} ESwitch; //Switch variable
        
        #define DEBUG_LOG
        
        #ifdef DEBUG_LOG
            //Global variable-----------------------------------------------------------------
            ESwitch bDebugSwtich = DISABLE;
            
            //Switch function
            #define DEBUG_SWITCH(bSwitch)        bDebugSwtich = (bSwitch)
        
            //Basic function
            #define DEBUG_ENTER_FUN()            (DISABLE == bDebugSwtich) ? ((void)0) : printf("File: %s. -Line: %d. <-----Enter function - %s ----->\n", __FILE__, __LINE__, __FUNCTION__)
            #define DEBUG_EXIT_FUN()             (DISABLE == bDebugSwtich) ? ((void)0) : printf("File: %s. -Line: %d. <-----Exit function - %s ----->\n", __FILE__, __LINE__, __FUNCTION__)
            #define DEBUG_PRINT(strFormat, ...)  (DISABLE == bDebugSwtich) ? ((void)0) : printf("File: %s. -Line: %d. -Function: %s. --->> " strFormat "\n", __FILE__, __LINE__, __FUNCTION__, ##__VA_ARGS__)
            #define DEBUG_RETURN(Ret)            (DISABLE == bDebugSwtich) ? ((void)0) : printf("File: %s. -Line: %d. -Function: %s. ===>> Ret: %#08X\n", __FILE__, __LINE__, __FUNCTION__, Ret);
        
            //Extension function
            #define DEBUG_NUMBER(Number)         (DISABLE == bDebugSwtich) ? ((void)0) : printf("File: %s. -Line: %d. -Function: %s. --->> " #Number " = %d\n", __FILE__, __LINE__, __FUNCTION__, Number)
        #else
            //Switch function
            #define DEBUG_SWITCH(bSwitch)        ((void)0)
        
            //Basic function
            #define DEBUG_ENTER_FUN()            ((void)0)
            #define DEBUG_EXIT_FUN()             ((void)0)
            #define DEBUG_PRINT(strFormat, ...)  ((void)0)
            #define DEBUG_RETURN(Ret)            ((void)0)
            
            //Extension function
            #define DEBUG_NUMBER(Number)         ((void)0)
        #endif
        
        int main()
        {
            int a = 0;
            int b = 0;
            int sum = 0;
            int Return = 0x00000001;
            
            DEBUG_SWITCH(ENABLE);
            DEBUG_ENTER_FUN();
            
            a = 10;
            DEBUG_NUMBER(a);
            b = 2;
            DEBUG_NUMBER(b);
            sum = a + b;
            DEBUG_PRINT("%d  = %d + %d", sum, a, b);
            
            DEBUG_RETURN(Return);
            DEBUG_EXIT_FUN();
            DEBUG_SWITCH(DISABLE);
            
            system("pause");
            return Return;
        }
      

      輸出結果展示:

      ![Alt text](https://img-blog.csdnimg.cn/20181126234425503.PNG?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQyNDc1NzEx,size_16,color_FFFFFF,t_70)
    • 正確性與健壯性抉擇。

      • 正確性意味着結果永遠是正確的,如果出錯,寧願不給出結果也不要給定一個不準確的值。
      • 健壯性意味着通過一些措施,保證軟件能正常運行下去,即使有時候會有一些不準確的值出現。
    • 進攻式編程。
        它的思想主要是提倡在開發階段讓問題儘可能顯現出來,並且將問題擴大使得問題難以被忽視,這樣才有助於問題不被忽視得到解決。而在產品運行時解決問題或讓它能夠自我糾正恢復。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章