《Windows核心程式設計》筆記(一)

吳尼瑪發表於2018-08-28

字元及字串處理

  • UTF-16將每個字元編碼為2個位元組(或者說16位)。UTF-8將一些字元編碼為1個位元組,一些字元編碼為2個位元組,一些字元編碼為3個位元組,一些字元編碼為4個位元組。UTF-32將每個字元都編碼為4個位元組。
  • C執行庫中現有的字串處理函式,在應用程式中包含StrSafe.h時,String.h也會包含進來。比如_tcscpy巨集背後的那些函式,已標記為廢棄不用。如果使用了這些函式,編譯時就會發出警告。注意,必須在包含了其他所有檔案之後,才包含StrSafe.h。
  • Windows也提供了各種字串處理函式。其中許多函式(比如lstrcat和lstrcpy)已經不贊成使用了,因為它們無法檢測緩衝區溢位問題。與此同時,ShlwApi.h定義了大量方便好用的字串函式,可以用來對作業系統有關的數值進行格式化操作,比如StrFormatKBSize和StrFormatByteSize。我們經常都要比較字串,以便進行相等性測試或者進行排序。為此,最理想的函式是CompareString(Ex)和CompareStringOrdinal。
  • 修改字串算術問題。例如,函式經常希望你傳給它緩衝區的字元數,而不是位元組數。這意味著你應該傳入_countof(szBuffer),而不是sizeof(szBuffer)。而且,如果需要為一個字串分配一個記憶體塊,而且知道字串中的字元數,那麼記住記憶體是以位元組來分配的。這意味著你必須呼叫malloc(nCharacters * sizeof(TCHAR)),而不是呼叫malloc(nCharacters)。如果出錯,編譯器不會提供任何警告或錯誤資訊。所以,最好定義一個巨集來避免犯錯:#define chmalloc(nCharacters) (TCHAR*)malloc(nCharacters * sizeof(TCHAR)).
  • 始終使用安全的字串處理函式,比如那些字尾為_s的,或者字首為StringCch的。後者主要在你想明確控制截斷的時候使用;如果不想明確控制截斷,則首選前者。

核心物件

  • 要想判斷一個物件是不是核心物件,最簡單的方式是檢視建立這個物件的函式。幾乎所有建立核心物件的函式都有一個允許你指定安全屬性資訊的引數。
  • 記住,物件控制程式碼的繼承只會在生成子程式的時候發生。假如父程式後來又建立了新的核心物件,並同樣將它們的控制程式碼設為可繼承的控制程式碼。那麼正在執行的子程式是不會繼承這些新控制程式碼的。
  • 子程式獲取繼承來的父程式控制程式碼值的方法: 1、命令列傳參 2、程式間通訊 3、通過環境變數

程式

  • 一般將程式定義成一個正在執行的程式的一個例項,它由以下兩個元件構成:
    • 一個核心物件,作業系統用它來管理程式。核心物件也是系統儲存程式統計資訊的地方。
    • 一個地址空間,其中包含所有執行體(executable)或DLL模組的程式碼和資料。此外,它還包含動態記憶體分配,比如執行緒堆疊和堆的分配。
  • 作業系統實際並不呼叫你所寫的入口函式。相反,它會呼叫由C/C++執行庫實現並在連結時使用-entry:命令列選項來設定的一個C/C++執行時啟動函式。該函式將初始化C/C++執行庫,使你能呼叫malloc和free之類的函式。 它還確保了在你的程式碼開始執行之前,你宣告的任何全域性和靜態C++物件都被正確地構造。
  • 一個鮮為人知的事實是,完全可以從自己的專案中移除/SUBSYSTEM連結器開關。一旦這樣做,連結器就會自動判斷應該將應用程式設為哪一個子系統。連結時,連結器會檢查程式碼中包括4個函式中的哪一個(WinMain,wWinMain,main或wmain),並據此推算你的執行體應該是哪個子系統,以及應該在執行體中嵌入哪個C/C++啟動函式。
  • 許多應用程式都會將(w)WinMain的hInstanceExe引數儲存在一個全域性變數中,使其很容易由執行體檔案的所有程式碼訪問。
  • (w)WinMain的hInstanceExe引數的實際值是一個基記憶體地址;在這個位置,系統將執行體檔案的映像載入到程式的地址空間中。
  • 可以使用C執行庫函式_chdir而不是Windows SetCurrentDirectory函式來更改當前目錄。_chdir函式在內部呼叫SetCurrentDirectory,但_chdir還可以呼叫SetEnvironmentVariable來新增或修改環境變數,從而使不同驅動器的當前目錄得以保留。
  • 注意,pszCommandLine引數被原型化為一個PTSTR。這意味著CreateProcess期望你傳入的是一個非“常量字串”的地址。在內部,CreateProcess實際上會修改你傳給它的命令列字串。但在CreateProcess返回之前,它會將這個字串還原為原來的形式。
  • 建立一個新的程式,會導致系統建立一個程式核心物件和一個執行緒核心物件。在建立時,系統會為每個物件指定一個初始的使用計數1。然後,就在CreateProcess返回之前,它會使用完全訪問許可權來開啟程式物件和執行緒物件,並將各自的與程式相關的(相對於程式的)控制程式碼放入PROCESS_INFORMATION結構的hProcess和hThread成員中。當CreateProcess在內部開啟這些物件時,每個物件的使用計數就變為2。這意味著系統要想釋放程式物件,程式必須終止(使用計數遞減1),而且父程式必須呼叫CloseHandle(使用計數再次遞減1,變成0)。
  • 許多開發人員都有這樣的一個誤解:關閉到一個程式或執行緒的控制程式碼,會強迫系統殺死此程式或執行緒。但這是大謬不然的。關閉控制程式碼只是告訴系統你對程式或執行緒的統計資料不再感興趣了。程式或執行緒會繼續執行,直至自行終止。
  • 可以使用GetCurrentProcessId來得到當前程式的ID,使用GetCurrentThreadId來獲得當前正在執行的執行緒的ID。另外,還可以使用GetProcessId來獲得與指定控制程式碼對應的一個程式的ID,使用GetThreadId來獲得與指定控制程式碼對應的一個執行緒的ID。最後,根據一個執行緒控制程式碼,你可以呼叫GetProcessIdOfThread來獲得其歸屬程式的ID。
  • TerminateProcess函式是非同步的——換言之,它告訴系統你希望程式終止,但到函式返回的時候,並不能保證程式已經被“殺死”了。所以,為了確定程式是否已經終止,應該呼叫WaitForSingleObject(詳見第9章)或者一個類似的函式,並將程式的控制程式碼傳給它。
  • Windows只允許在程式邊界上進行許可權提升。一旦程式啟動,再要求更多的許可權就已經遲了。不過,一個未提升許可權的程式可以生成另一個提升了許可權的程式,後者將包含一個COM伺服器。這個新程式將保持活動狀態。這樣一來,老程式就可以向已經提升了許可權的新程式發出IPC呼叫,而不必啟動一個新例項再終止它自身。
  • 由於管理任務必須由另一個程式或者另一個程式中的COM伺服器來執行,所以你應該在另一個應用程式中收集好需要管理員許可權的所有任務,並通過呼叫ShellExecuteEx(為lpVerb 傳遞“runas”)來提升它的許可權。然後,具體要執行的特權操作應該作為新程式的命令列上的一個引數來傳遞。
  • GetProcessElevation的helper函式能返回提升型別和一個指出你是否正在以管理員身份執行的布林值。
BOOL GetProcessElevation(TOKEN_ELEVATION_TYPE* pElevationType, BOOL* pIsAdmin) {

   HANDLE hToken = NULL;
   DWORD dwSize; 

   // Get current process token
   if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken))
      return(FALSE);

   BOOL bResult = FALSE;

   // Retrieve elevation type information 
   if (GetTokenInformation(hToken, TokenElevationType, 
      pElevationType, sizeof(TOKEN_ELEVATION_TYPE), &dwSize)) {
      // Create the SID corresponding to the Administrators group
      byte adminSID[SECURITY_MAX_SID_SIZE];
      dwSize = sizeof(adminSID);
      CreateWellKnownSid(WinBuiltinAdministratorsSid, NULL, &adminSID, 
         &dwSize);

      if (*pElevationType == TokenElevationTypeLimited) {
         // Get handle to linked token (will have one if we are lua)
         HANDLE hUnfilteredToken = NULL;
         GetTokenInformation(hToken, TokenLinkedToken, (VOID*) 
            &hUnfilteredToken, sizeof(HANDLE), &dwSize);

         // Check if this original token contains admin SID
         if (CheckTokenMembership(hUnfilteredToken, &adminSID, pIsAdmin)) {
            bResult = TRUE;
         }

         // Don't forget to close the unfiltered token
         CloseHandle(hUnfilteredToken);
      } else {
         *pIsAdmin = IsUserAnAdmin();
         bResult = TRUE;
      }
   }

   // Don't forget to close the process token
   CloseHandle(hToken);

   return(bResult);
}
複製程式碼

作業

  • Windows提供了一個作業(job)核心物件,它允許你將程式組合在一起並建立一個“沙箱”來限制程式能夠做什麼。最好將作業物件想象成一個程式容器。但是,即使作業中只包含一個程式,也是非常有用的,因為這樣可以對程式施加平時不能施加的限制。
  • 建立好一個作業之後,接著一般需要限制作業中的程式能做的事情;換言之,現在要設定 一個“沙箱”。可以向作業應用以下幾種型別的限制:
    • 基本限制和擴充套件基本限制,防止作業中的程式獨佔系統資源。
    • 基本的UI限制,防止作業內的程式更改使用者介面。
    • 安全限制,防止作業內的程式訪問安全資源(檔案、登錄檔子項等)。

執行緒

  • CreateThread函式是用於建立執行緒的Windows函式。不過,如果寫的是C/C++程式碼,就絕對不要呼叫CreateThread。相反,正確的選擇是使用Microsoft C++執行庫函式_beginthreadex。如果使用的不是Microsoft C++編譯器,你的編譯器的提供商應該提供類似的函式來替代CreateThread。不管這個替代函式是什麼,都必須使用它。

  • 執行緒可以通過以下4種方法來終止執行。

    • 執行緒函式返回(這是強烈推薦的)。
    • 執行緒通過呼叫ExitThread函式“殺死”自己(要避免使用這種方法)。
    • 同一個程式或另一個程式中的執行緒呼叫TerminateThread函式(要避免使用這種方法)。
    • 包含執行緒的程式終止執行(這種方法避免使用)。
  • 終止執行緒執行的推薦方法是讓它的執行緒函式返回 。但是,務必注意ExitThread函式是用於“殺死”執行緒的Windows函式。如果你要寫C/C++程式碼,就絕對不要呼叫ExitThread。相反,應該使用C++執行庫函式_endthreadex。如果使用的不是Microsoft的C++編譯器,那麼你的編譯器提供方應該提供它們自己的ExitThread替代函式。不管這個替代函式是什麼,都必須使用它。

  • Windows提供了一些函式來方便執行緒引用它的程式核心物件或者它自己的執行緒核心物件:

    • HANDLE GetCurrentProcess();
    • HANDLE GetCurrentThread();

    這兩個函式都返回到主調執行緒的程式或執行緒核心物件的一個偽控制程式碼(pseudohandle)。它們不會在主調程式的控制程式碼表中新建控制程式碼。而且,呼叫這兩個函式,不會影響程式或執行緒核心物件的使用計數。如果呼叫CloseHandle,將一個偽控制程式碼作為引數傳入,CloseHandle只是簡單地忽略此呼叫,並返回FALSE。在這種情況下,GetLastError將返回ERROR_INVALID_HANDLE。

  • 有時或許需要一個真正的執行緒控制程式碼,而不是一個偽控制程式碼。 所謂“真正的控制程式碼”,指的是能明確、無歧義地標識一個執行緒的控制程式碼。 DuplicateHandle函式可以執行這個轉換:

BOOL DuplicateHandle(
HANDLE hSourceProcess,
HANDLE hSource,
HANDLE hTargetProcess,
PHANDLE phTarget,
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD dwOptions);
複製程式碼
  • 正常情況下,利用這個函式,你可以根據與程式A相關的一個核心物件控制程式碼來建立一個新控制程式碼,並讓它同程式B相關。因為DuplicateHandle遞增了指定核心物件的使用計數,所以在用完複製的物件控制程式碼後,有必要把目標控制程式碼傳給CloseHandle,以遞減物件的使用計數。

相關文章