(4)進程
進程是程式啟動並執行一個執行個體,由以下兩部分組成:
- 進程核心對象:作業系統用它來管理和統計進程資訊;
- 進程地址空間:所有進程執行所需要的代碼和資料存在這個地址空間中。
進程是惰性的,進程要做任何事都需要通過線程在其上下文環境中執行來實現。當一個進程建立後,作業系統也同時為其建立一個主線程(primary thread),主線程又會建立其他線程。當進程中所有的線程都停止時,作業系統也同時銷毀該進程。
Windows中可能的進程入口函數如下:
WinMain/wWinMain是視窗進程程式的入口,相應的C執行階段程式庫入口微WinMainCRTStartup/wWinMainStartup;同理,main/wmain是控制台進程程式的入口,相應的C執行階段程式庫入口為mainCRTStartup/wmainCRTStartup。
通過編譯器的串連開關/SubSystem:Console和/SubSystem:Winodws可以設定程式使用哪個子系統,事實上可以不設定這個值,Visual Studio的編譯器會根據我們所提供入口函數來確定到底連結到哪個子系統。程式啟動並執行時候,Windows作業系統的載入程式(loader)會檢查執行檔案鏡像中的檔案頭,從而獲得該子系統值。
Windows載入程式到進程後,即相應的CRT啟動函數,這些啟動函數的用途可總結如下:
- 擷取只想新進程的完整命令列;
- 獲得指向進程環境變數的指標;
- 初始化C/C++執行階段程式庫的全域變數。可以包含stdlib.h以訪問這些變數,所有這些變數總結如;
- 初始化CRT記憶體配置函數(malloc/calloc)和CRT IO所使用的堆(heap);
- 調用所有全域和靜態C++對象的建構函式。(Microsoft不推薦使用這些CRT變數,因為使用它們的代碼有可能在CRT初始化前運行,最好調用相應的Windows API)
如果是控制台程式可以將入口函數
_tmain(int argc, TCHAR* argv[])
換成
_tmain(int argc, TCHAR* argv[], TCHAR* env[])
Windows會這樣調用
int nMainRetVal = _tmain(argc, argv, envp);
第三個參數指向進程環境變數。
入口函數返回後,CRT啟動函數將調用CRT函數exit,並將(nMainRetVal)作為參數傳給它,exit函數執行以下任務:
- 調用_onexit函數所註冊的任何一個函數;
- 調用所有全域和靜態C++對象的解構函式;
- 在Debug版中,如果設定了_CRTDBG_LEAK_CHECK_DF標誌,就通過調用_CrtDumpMemoryLeaks來產生記憶體流失報告;
- 調用Windows作業系統的ExitProcess,並傳入nMainRetVal,這會導致作業系統kill該進程,並設定其對出代碼。
進程執行個體控制代碼
// 連接器偽變數,指示正在啟動並執行代碼所在檔案(exe/dll)被載入到應用程式總哪個位置
EXTERN_C
const
IMAGE_DOS_HEADER __ImageBase;
void DumpModule()
{
//返回進程可執行檔的執行個體控制代碼
HMODULE hApp=GetModuleHandle(NULL);
TCHAR szModuleName[MAX_PATH]={};
GetModuleFileName(hApp, szModuleName, MAX_PATH);
_tprintf(_T("GetModuleHandle(NULL) returned -> address:0x%x with name: %s\r\n"), hApp, szModuleName);
//當前執行代碼所在檔案的執行個體控制代碼
HINSTANCE hCurrentModule=(HINSTANCE)&__ImageBase;
GetModuleFileName(hCurrentModule, szModuleName, MAX_PATH);
_tprintf(_T("__ImageBase returned -> address:0x%x with name: %s\r\n"), hCurrentModule, szModuleName);
HMODULE hCurrentExecutingModule=NULL;
//獲得DumpModule函數所在檔案的執行個體控制代碼
DWORD r = GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (PCTSTR)DumpModule, &hCurrentExecutingModule);
GetModuleFileName(hCurrentExecutingModule, szModuleName, MAX_PATH);
_tprintf(_T("GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS) returned -> address:0x%x with name: %s\r\n"),
hCurrentExecutingModule, szModuleName);
}
前一個進程的執行個體
wWinMain(
_In_
HINSTANCE hInstance,
_In_opt_
HINSTANCE hPrevInstance,
_In_
LPWSTR lpCmdLine,
_In_
int nShowCmd
);
hPrevInstance為前一個執行個體的控制代碼,但在Win32始終為NULL,其存在只是相容Win16。Win16時代所有進程共用同一進程空間,為了指示程式的不同執行個體知道某些事已經被前一個執行個體做了,如註冊Window
Class,所以需要前一執行個體控制代碼;但Win32開始,每個進程有自己獨享的進程空間,因而不再需要hPrevInstance,但為了讓老代碼還能運行,因此保留該參數,並永遠設為NULL。
進程的命令列
分解命令列參數代碼
Int nNumArgs;
PTSTR *ppArgv = CommandLineArgv(GetCommandLine(), &nNumArgs);
//use the arguments
If(*ppArgv[1] == _T('x')){
…
}
//free heap
HeapFree(GetProcessHeap(), 0, ppArgv);
進程環境變數
GetEnvironmentStrings和FreeEnvironmentStrings來操作進程環境變數
PTSTR pEnvBlock = GetEnviromentStrings();
// 注意處理=::=::以及空格
FreeEnvironmentStrings(pEnvBlock);
其他動作環境變數的函數
GetEnvironmentVariable
ExpandEnvironmentStrings
SetEnvironmentVariable
進程的關聯性(親緣性)
通常進程可以在任何CPU上執行,然而也可以強制其在可用的CPU某個子集上執行,稱為進程的關聯性(processor affinity),子進程將繼承父進程關聯性。
進程錯誤模式
進程目前的目錄
GetCurrentDirectory
SetCurrentDirctory
系統版本
GetVersion返回MS-DOS和Windows版本號碼,該函數的Windows版本好高位元組和低位元組反了
GetVersionEx返回更加詳細的資訊
VerifyVersionInfo可以測試系統詳細的版本資訊
CreateProcess函數
pszApplicationName: 如果不為NULL,則使用該參數,但必須包含完整的路徑和副檔名;
pszCommandLine:如果pszApplicationName為NULL,則使用該參數,可以不包含副檔名,預設為.exe,如果不包含路徑,CreateProcess將在以下目錄尋找:
- 主調進程的exe所在目錄;
- 主調進程目前的目錄;
- Windows系統目錄,即GetSystemDirectory返回的System32目錄;
- Windows目錄;
- PATH環境變數中列出的目錄。
PsaProcess, psaThread, bInheritHandles可指定進程的安全描述參數和是否繼承父進程所擁有的可繼承核心對象。
fdwCreate包含了建立進程的標誌位。
PvEnvironment包含了傳給新進程的環境變數,如果為NULL則從父進程繼承。
pszCUrDir允許父進程設定子進程當前磁碟機和目錄。
PsiStartInfo必須初始化各個成員為0,並設定好結構體大小,否則可能導致建立進程失敗,即STARTUPINFO si = {sizeof(si)};
ppiProcInfo為系統返回給我們的參數,包含了被建立進程和其主線程的資訊。
終止進程
終止進程有四種方式:
- 進程從main函數退出(推薦)
- 主動調用ExitProcess:顯示的調用ExitProcess會導致全域,靜態等C++對象解構函式不被調用,並且C運行時的許多其他清理工作也無法完成;
- 其他進程調用TerminalProcess:被kill的進程本身不會完成清理工作,但windows作業系統會保證這些清理工作,從而系統資源都會釋放,也就是說進程在結束後不會遺漏任何資源:
- 進程的記憶體釋放
- 所有開啟的檔案都關閉
- 所有的核心對象都減少引用數
- 所有的使用者物件和GDI對象都銷毀(區分核心,使用者和GDI對象的標準:看其Create*或其他動作函數來自於哪個dll,kernel32.dll/user.dll/gdi.dll)
- 進程所有線程"自然死亡":幾乎不會發生,除非被其他人調用了TerminalThread或者主動調用了ExitThread,進程的退出會被設為最後一個線程的結束代碼。
進程終止時會做以下動作:
- 終止任何的遺留線程;
- 釋放所指派的使用者對象,GDI對象,關閉所引用的核心對象;
- 進程結束代碼由STILL_ACTIVE變為傳遞給ExitProcess和TerminalProcess的參數(GetExitCodeProcess可獲得該退出碼);
- 進程核心對象的狀態變成已觸發狀態;
- 進程核心對象引用計數減1(進程核心對象的壽命有可能比進程本身要長,因此其他進程就有機會獲得進程終止後的結束代碼,和統計資訊)。
子進程
CreateProcess建立子進程後,可以通過Proces_Information來操作子進程,如果不需要的話,應立即CloseHandle來關閉子進程主線程(hThread)和子進程本身(hProcess)的控制代碼。WaitForSingleObject(hProcess, INFINITE)可以等待子進程結束。
進程安全
Windows Vista起引入了UAC(user account control),每次進程需要訪問一個受保護或者具有更高系統完整性層級的資源時候,都需要使用者的確認(即彈出UAC對話方塊,這點被證明很招人煩,微軟在後續的Windows版本也大大降低了彈窗的頻率)。即使一個使用者是以管理員身份登入(大部分Windows使用者都是這麼乾的),預設情況下,啟動的進程也只是擁有一個被篩選過的安全性權杖。
自動提升許可權:程式可以嵌入清單資源(RT_MANIFEST)並在trustinfo段聲明requestedExecutionLevel資訊
手動提升許可權:調用ShellExecuteEx設定lpVerb = "runas",lpFile="可執行檔"參數來申請提升許可權,當然這需要使用者的確認
許可權上下文:
下面的函數可以用來擷取許可權上下文:
- OpenProcessToken
- GetTokenInformation
- CrateWellKnownSid
- CheckTokenMembership:
- IsUserAdmin:判斷當前進程是否以管理員身份運行
枚舉系統中的進程
兩套枚舉系統中進程的API:
- PDH.dll中Process32First, Process32Next
- PSAPI.dll中EnumProcesses
完整性層級(integrity level)
進程令牌還包含有進程的系統完整性層級,每個核心對象也有其相應的系統完整性層級,有了這兩個資訊,Windows系統就能在一個進程訪問一個核心對象的時候比較其完整性層級,完整性層級低的進程無法修改和刪除完整性高的對象,至於是否能讀,取決於和對象相關的ACE(access control entry)的資源原則設定。
由於完整性層級比較發生在ACL(access control list)之前,所以如果其完整性層級較要訪問的資源低,即使進程擁有訪問資源的許可權,訪問也會被拒絕。例如,一個進程從網上下載和執行代碼,這種設計就尤為重要。
GetProcessIntegrityLevel可以獲得進程的完整新層級。