昨天同學接到了騰訊的電面,有一題問到了CreateProcess創建進程的具體實現過程,他答得不怎么好吧應該是,
為了以防萬一,也為了深入學習一下,今天我翻閱了好多資料,整理了一下,寫篇博客,也算是加深理解吧
1.函數原型:
BOOL
WINAPI
CreateProcessW(
LPCWSTR lpApplicationName,
LPWSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCWSTR lpCurrentDirectory,
LPSTARTUPINFOW lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
)
2.參數意義:
第一參數:lpApplicationName
指向一個NULL結尾的、用來指定可執行模塊的字符串。
這個參數可以被設為NULL,在這種情況下,可執行模塊的名字必須處於 lpCommandLine 參數最前面並由
空格符與后面的字符分開。
第二參數:lpCommandLine
指向一個以NULL結尾的字符串,該字符串指定要執行的命令行。
這個參數可以為空,那么函數將使用lpApplicationName參數指定的字符串當做要運行的程序的命令行。
如果lpApplicationName和lpCommandLine參數都不為空,那么lpApplicationName參數指定將要被運行的模塊,lpCommandLine參數指定將被運行的模塊的命令行。新運行的進程可以使用
GetCommandLine函數獲得整個命令行。C語言程序可以使用argc和argv參數。
第三參數:lpProcessAttributes
指向一個SECURITY_ATTRIBUTES結構體,這個
結構體決定是否返回的句柄可以被子進程繼承。如果lpProcessAttributes參數為空(NULL),那么句柄不能被繼承。
在Windows NT中:
SECURITY_ATTRIBUTES結構的lpSecurityDescriptor成員指定了新進程的
安全描述符,如果參數為空,新進程使用默認的安全描述符。
第四參數:lpThreadAttributes
同lpProcessAttribute,不過這個參數決定的是線程是否被繼承.通常置為NULL.
第五參數:bInheritHandles
指示新進程是否從調用進程處繼承了句柄。
如果參數的值為真,調用進程中的每一個可繼承的打開句柄都將被子進程繼承。被繼承的句柄與原進程擁有完全相同的值和訪問權限。
第六參數:dwCreationFlags
指定附加的、用來控制優先類和進程的創建的標志。以下的創建標志可以以除下面列出的方式外的任何方式組合后指定。
⑴值:CREATE_DEFAULT_ERROR_MODE
含義:新的進程不繼承調用進程的錯誤模式。CreateProcess函數賦予新進程當前的默認錯誤模式作為替代。應用程序可以調用
SetErrorMode函數設置當前的默認錯誤模式。
這個標志對於那些運行在沒有硬件錯誤環境下的多線程
外殼程序是十分有用的。
對於CreateProcess函數,默認的行為是為新進程繼承調用者的錯誤模式。設置這個標志以改變默認的處理方式。
⑵值:CREATE_NEW_CONSOLE
含義:新的進程將使用一個新的控制台,而不是繼承
父進程的控制台。這個標志不能與DETACHED_PROCESS標志一起使用。
⑶值:CREATE_NEW_PROCESS_GROUP
含義:新進程將是一個
進程樹的根進程。進程樹中的全部進程都是根進程的子進程。新進程樹的
用戶標識符與這個進程的標識符是相同的,由lpProcessInformation參數返回。進程樹經常使用GenerateConsoleCtrlEvent函數允許發送CTRL+C或CTRL+BREAK信號到一組控制台進程。
⑷值:CREATE_SEPARATE_WOW_VDM
如果被設置,新進程將會在一個私有的虛擬DOS機(VDM)中運行。另外,默認情況下所有的16位Windows應用程序都會在同一個共享的VDM中以
線程的方式運行。單獨運行一個16位程序的優點是一個應用程序的崩潰只會結束這一個VDM的運行;其他那些在不同VDM中運行的程序會繼續正常的運行。同樣的,在不同VDM中運行的16位Windows應用程序擁有不同的輸入隊列,這意味着如果一個程序暫時失去響應,在獨立的VDM中的應用程序能夠繼續獲得輸入。
⑸值:CREATE_SHARED_WOW_VDM
如果WIN.INI中的Windows段的DefaultSeparateVDM選項被設置為真,這個標識使得CreateProcess函數越過這個選項並在共享的虛擬DOS機中運行新進程。
⑹值:CREATE_SUSPENDED
含義:新進程的
主線程會以暫停的狀態被創建,直到調用
ResumeThread函數被調用時才運行。
⑺值:CREATE_UNICODE_ENVIRONMENT
含義:如果被設置,由lpEnvironment參數指定的環境塊使用Unicode
字符,如果為空,環境塊使用ANSI字符。
⑻值:DEBUG_PROCESS
含義:如果這個標志被設置,調用進程將被當做一個
調試程序,並且新進程會被當做被調試的進程。系統把被調試程序發生的所有調試事件通知給調試器。
如果你使用這個標志創建進程,只有調用進程(調用CreateProcess函數的進程)可以調用
WaitForDebugEvent函數。
⑼值:DEBUG_ONLY_THIS_PROCESS
含義:如果此標志沒有被設置且調用進程正在被調試,新進程將成為調試調用進程的調試器的另一個調試對象。如果調用進程沒有被調試,有關調試的行為就不會產生。
⑽值:DETACHED_PROCESS
含義:對於控制台進程,新進程沒有訪問
父進程控制台的權限。新進程可以通過
AllocConsole函數自己創建一個新的控制台。這個標志不可以與CREATE_NEW_CONSOLE標志一起使用。
〔11〕值:CREATE_NO_WINDOW
含義:系統不為新進程創建CUI窗口,使用該標志可以創建不含窗口的CUI程序。
第七參數:lpEnvironment
指向一個新進程的環境塊。如果此參數為空,新進程使用調用進程的環境。
一個環境塊存在於一個由以NULL結尾的字符串組成的塊中,這個塊也是以NULL結尾的。每個字符串都是name=value的形式。
與其使用應用程序提供的環境塊,不如直接把這個參數設為空,系統驅動器上的當前目錄信息不會被自動傳遞給新創建的進程。對於這個情況的探討和如何處理,請參見注釋一節。
環境塊可以包含Unicode或ANSI字符。如果lpEnvironment指向的環境塊包含Unicode
字符,那么dwCreationFlags字段的CREATE_UNICODE_ENⅥRONMENT標志將被設置。如果塊包含ANSI字符,該標志將被清空。
請注意一個ANSI環境塊是由兩個零字節結束的:一個是字符串的結尾,另一個用來結束這個快。一個Unicode環境塊是由四個零字節結束的:兩個代表字符串結束,另兩個用來結束塊。
第八參數:lpCurrentDirectory
指向一個以NULL結尾的字符串,這個字符串用來指定子進程的
工作路徑。這個字符串必須是一個包含驅動器名的
絕對路徑。如果這個參數為空,新進程將使用與調用進程相同的驅動器和目錄。這個選項是一個需要啟動應用程序並指定它們的驅動器和工作目錄的
外殼程序的主要條件。
第九參數:lpStartupInfo
第十參數:lpProcessInformation
指向一個用來接收新進程的識別信息的
PROCESS_INFORMATION結構體。
下面是CreateProcess的一個簡單實例,打開了一個notepad:
#include<stdio.h> #include<windows.h> int main(int argc,char*argv[]) { char szCommandLine[]="notepad"; STARTUPINFO si={sizeof(si)}; PROCESS_INFORMATION pi; si.dwFlags=STARTF_USESHOWWINDOW;//指定wShowWindow成員效 si.wShowWindow=TRUE;//此成員設為TRUE的話則顯示新建進程的主窗口 BOOL bRet=CreateProcess( NULL,//不在此指定可執行文件的文件名 szCommandLine,//命令行參數 NULL,//默認進程安全性 NULL,//默認進程安全性 FALSE,//指定當前進程內句柄不可以被子進程繼承 CREATE_NEW_CONSOLE,//為新進程創建一個新的控制台窗口 NULL,//使用本進程的環境變量 NULL,//使用本進程的驅動器和目錄 &si, &pi); if(bRet) { //不使用的句柄最好關掉 CloseHandle(pi.hThread); CloseHandle(pi.hProcess); printf("新進程的ID號:%d\n",pi.dwProcessId); printf("新進程的主線程ID號:%d\n",pi.dwThreadId); } getchar(); return 0; }
3.CreateProcess通過內核創建進程的步驟,大致分為六個階段:
在Windows中,進程是不活動的,只是作為線程的容器,現代操作系統將線程作為最小調度單位,進程作為資源分配的最小單位。
所以,CreateProcess作為一個相對高層的函數,要先通過系統調用NtCreateProcess()創建進程(容器),成功以后就立即通過系統調用NtCreateThread()創建其第一個線程。
第一階段:打開目標映像文件
對於32位exe映像,CreateProcess先打開其映像文件,在為其創建一個Section即文件映射區,將文件內容映射進來,前提是目標文件是一個合格的EXE文件(PE文件頭部檢測);
第二階段:創建內核中的進程對象
實際上就是創建以EPROCESS為核心的相關數據結構,這就是系統調用NtCreateProcess()要做的事情,主要包括:
①分配並設置EPROCESS數據結構;
②其他相關的數據結構的設置,如句柄表等等;
③為目標進程創建初始的地址空間;
④對EPROCESS進行初始化;
⑤將系統Dll映射到目標用戶空間,如ntdll.dll等
⑥設置目標進程的PEB;
⑦將其他需要映射到用戶空間,如與”當地語言支持“即NLS有關的數據結構;
⑧完成EPROCESS創建,將其掛入進程隊列並插入創建者的句柄表
第三階段:創建初始線程
前面說過,進程只是一個容器,干活兒是里面的線程,所以下一步就是創建目標進程的初始線程
與EPROCESS對應,線程的數據結構是ETHREAD,與進程環境塊PEB對應,線程也有線程環境塊TEB;
PEB在用戶空間的位置大致是固定的,在7ffd0000左右,PEB的下方就是TEB,進程有幾個線程就有幾個TEB,每個TEB占一個4KB的頁面;
這個階段是通過調用NtCreateThread()完成的,主要包括:
①創建和設置目標線程的ETHREAD數據結構,並處理好與EPROCESS的關系(例如進程塊中的線程計數等等)。
②在目標進程的用戶空間創建並設置目標線程的TEB。
③將目標線程在用戶空間的起始地址設置成指向Kernel32.dll中的BaseProcessStart()或BaseThreadStart(),前者用於進程中的第一個線程,后者用於隨后的線程。
用戶程序在調用NtCreateThread()時也要提供一個用戶級的起始函數(地址), BaseProcessStart()和BaseThreadStart()在完成初始化時會調用這個起始函數。
ETHREAD數據結構中有兩個成份,分別用來存放這兩個地址。
④調用KeInitThread設置目標線程的KTHREAD數據結構並為其分配堆棧和建立執行環境。
特別地,將其上下文中的斷點(返回點)設置成指向內核中的一段程序KiThreadStartup,使得該線程一旦被調度運行時就從這里開始執行。
⑤系統中可能登記了一些每當創建線程時就應加以調用的“通知”函數,調用這些函數。
第四階段:通知windows子系統
關於windows子系統 http://book.51cto.com/art/201011/235712.htm
每個進程在創建/退出的時候都要向windows子系統進程csrss.exe進程發出通知,因為它擔負着對windows所有進程的管理的責任,
注意,這里發出通知的是CreateProcess的調用者,不是新建出來的進程,因為它還沒有開始運行。
至此,CreateProcess的操作已經完成,但子進程中的線程卻尚未開始運行,它的運行還要經歷下面的第五和第六階段。
第五階段:啟動初始線程
新創建的線程未必是可以被立即調度運行的,因為用戶可能在創建時把標志位CREATE_ SUSPENDED設成了1;
如果那樣的話,就需要等待別的進程通過系統調用恢復其運行資格以后才可以被調度運行。否則現在已經可以被調度運行了。至於什么時候才會被調度運行,則就要看優先級等等條件了。
第六階段:用戶空間的初始化和Dll連接
DLL連接由ntdll.dll中的LdrInitializeThunk()在用戶空間完成。在此之前ntdll.dll與應用軟件尚未連接,但是已經被映射到了用戶空間(第二階段第⑤步)
函數LdrInitializeThunk()在映像中的位置是系統初始化時就預先確定並記錄在案的,所以在進入這個函數之前也不需要連接。