第四章進程的學習可謂是任重而道遠,雖然不難,但知識量不少,也比較零散,須要多總結,腦海裏纔有進程的框架。因此,我把本章分爲幾個小節來說完。我仍是一如既往的添加輔助性內容,但願對於小白有所幫助。而比我流弊的大有人在,大神們能夠跳過輔助性內容。本小節的學習目標以下:
1.C/C++程序編譯過程
2.C/C++命令行參數的使用
3.什麼是進程
4.Windows的入口點函數
5.進程實例句柄(可執行文件實例句柄或者DLL文件實例句柄)
6.進程前一個實例句柄c++
C/C++的編譯、連接過程要把咱們編寫的一個c/c++程序(源代碼)轉換成能夠在硬件上運行的程序(可執行代碼),須要進行編譯和連接。編譯就是把文本形式源代碼翻譯爲機器語言形式的目標文件的過程。連接是把目標文件、操做系統的啓動代碼和用到的庫文件進行組織造成最終生成可執行代碼的過程。過程圖解以下:算法
C/C++語言中的main函數,常常帶有參數argc,argv,以下:編程
int main(int argc, char** argv) int main(int argc, char* argv[])
從函數參數的形式上看,包含一個整型和一個指針數組。當一個C/C++的源程序通過編譯、連接後,會生成擴展名爲.EXE的可執行文件,這是能夠在操做系統下直接運行的文件,換句話說,就是由系統來啓動運行的。對main()函數既然不能由其它函數調用和傳遞參數,就只能由系統在啓動運行時傳遞參數了。在操做系統環境下,一條完整的運行命令應包括兩部分:命令與相應的參數。其格式爲:命令參數1參數2....參數n¿此格式也稱爲命令行。命令行中的命令就是可執行文件的文件名,其後所跟參數需用空格分隔,併爲對命令的進一步補充,也便是傳遞給main()函數的參數。
命令行與main()函數的參數存在以下的關係:windows
設命令行爲:program str1 str2 str3 str4 str5
其中program爲文件名,也就是一個由program.c經編譯、連接後生成的可執行文件program.exe,其後各跟5個參數。對main()函數來講,它的參數argc記錄了命令行中命令與參數的個數,共6個,指針數組的大小由參數argc的值決定,即爲char*argv[6],指針數組的取值狀況以下圖所示:
數組的各指針分別指向一個字符串。應當引發注意的是接收到的指針數組的各指針是從命令行的開始接收的,首先接收到的是命令,其後纔是參數。數組
(1)進程的概念
書中原文是這樣寫的:一個進程,就是一個正在運行的程序!一個程序,能夠產生多個進程。
1.一個內核對象,被系統用來管理這個進程,這個內核對象中,還包含了進程的一些策略信息。
2.一個地址空間,這個地址空間中包含了可執行代碼,動態連接庫模塊代碼,數據,程序動態內存分配獲取的內存,也在這個內存地址空間中。
在操做系統的相關書籍裏是這樣說的:由程序段、相關的數據段和PCB三部分構成進程,因此,其實程序段、相關的數據段就是一個地址空間,而PCB(進程控制塊)就是內核對象。
(1) 進程和線程的關係
書中原文是這樣寫的:進程是由「惰性「的,進程要作任何事情都必須讓一個線程在它的上下文中運行。該線程負責執行進程地址空間包含的代碼。事實上,一個進程能夠有多個線程,全部線程都在進程的地址空間中」同時執行代碼「。…此處省略一些字...。每一個進程至少要有一個線程來執行進程地址空間包含的代碼。當系統建立一個進程的時候,會自動爲進程建立第一個線程,這稱爲主線程。而後這個主線程再建立更多的線程,後者再建立更多的線程。單個CPU,爲線程分配CPU採用循環方式,爲每一個線程都分配時間片;多個CPU,採起更復雜的算法爲線程分配CPU。
怎麼理解進程和線程的關係?舉個例子就十分透徹了。當雙擊一個程序,產生了一個工廠(進程)同時也產生了第一我的----廠長(primary thread:主線程),這個廠長只作一件事就是招募(建立)員工(線程),讓其餘員工(線程)幫他作事。有兩種方法工廠會倒閉(進程銷燬),第一種是工廠裏的員工(線程,包括主線程)所有退出或銷燬,那麼工廠天然會倒閉(進程銷燬)。第二種方法是調用ExitProcess函數能夠直接結束進程,第二種方法後面會講到,如今先了解有這一方法結束進程便可。框架
Windows支持兩種類型的應用程序:GUI程序(圖形用戶界面程序)和CUI程序(控制檯用戶界面程序)。當咱們用Visual Studio來建立一個應用程序項目時,集成開發環境會設置各類連接器開關,使連接器將子系統的正確C/C++運行啓動函數嵌入最終生成的可執行文件中。對於GUI程序,連接器開關是/SUBSYSTEM:CONSOLE;對於CUI程序,連接器開關是/SUBSYSTEM:WINDOWS。在學習C與C++時,當運行一個可執行文件,咱們都認爲系統調用的第一個函數是入口點函數(例如:main函數),但其實操做系統實際並不調用咱們寫的入口點函數(例如:main函數),實際最早調用的是C/C++運行庫的啓動函數。應用程序類型和相應的入口函數:ide
應用程序類型 | 入口點函數 | 嵌入可執行文件的啓動函數 |
---|---|---|
處理ANSI字符和字符串的GUI應用程序 | _tWinMain (WinMain) | WinMainCRTStartup |
處理Unicode字符和字符串的GUI應用程序 | _tWinMain (wWinMain) | wWinMainCRTStartup |
處理ANSI字符和字符串的CUI應用程序 | _tmain (Main) | mainCRTStartup |
處理Unicode字符和字符串的CUI應用程序 | _tmain (Wmain) | wmainCRTStartup |
要生成一個可執行文件,必須通過編譯連接過程。當在連接成可執行文件時,若是系統發現該項目指定了/SUBSYSTEM:WINDOWS連接器開關,連接器就會在程序代碼中尋找WinMain或wWinMain函數,若是沒有找到這兩個函數(要麼入口點函數寫成main或wmain函數或者沒有寫入口點函數),連接器將返回一個「unresolved external symbol「(沒法解析的外部符號錯誤);若是找到了這兩個函數,則根據具體狀況(是Unicode字符集仍是多字節字符集)選擇WinMainCRTStartup或 wWinMainCRTStartup啓動函數,再將啓動函數嵌入到可執行文件中。相似地,若是系統發現該項目指定了/SUBSYSTEM:CONSOLE連接器開關,連接器就會在程序代碼中尋找main或wmain函數,若是沒有找到這兩個函數(要麼入口點函數寫成WinMain或wWinMain函數或者沒有寫入口點函數),連接器將返回一個「unresolved external symbol「(沒法解析的外部符號錯誤);若是找到了這兩個函數,則根據具體狀況(是Unicode字符集仍是多字節字符集)選擇mainCRTStartup或 wmainCRTStartup啓動函數,再將啓動函數嵌入到可執行文件中。
到目前爲止,就生成了一個可執行文件。那接下來說講當運行了一個可執行文件,啓動函數作了什麼?函數
全部C/C++運行庫啓動函數所作的事情基本都是同樣的,區別就在於它們要處理的是ANSI字符串,仍是Unicode字符串;以及在初始化C運行庫以後,它們調用的是哪個入口點函數。 這些C運行時庫函數,主要完成如下任務: 1. 獲取進程命令行指針; 2. 獲取進程環境變量指針; 3. 初始化C/C++運行時庫的全局變量,若是你包含了頭Stdlib.h,那麼你就能夠訪問這些變量!初始化malloc函數的內存堆; 4. 爲C++全局類,調用構造函數。
注意:malloc 函數,不要輕易使用?由於這個函數通常來講,最終會調用windows API函數,咱們直接調用virtualAlloc的windowsAPI函數,效率會高!
讓咱們看下啓動函數都初始化哪些全局變量,下面圖示:
好了,咱們知道了啓動函數都作了些什麼。當全部這些初始化操做完成後,C / C + +啓動函數就調用應用程序的進入點函數。若是源文件寫了一個_tWinMain,而且定義了_UNICODE(即項目屬性設置爲Unicode字符集),它將如下面的形式被調用 :學習
GetStartupInfo(&StartupInfo); int nMainRetVal = wWinMain((HINSTANCE)&__ImageBase, NULL, pszCommandLineUnicode, (StartupInfo.dwFlags & STARTF_USESHOWWINDOW) ? StartupInfo.wShowWindow:SW_SHOWDEFAULT);
若是沒有定義_UNICODE(即項目屬性設置爲多字節字符集),它將如下面的形式被調用 :測試
GetStartupInfo(&StartupInfo); int nMainReLVal = WinMain((HINSTANCE)&__ImageBase, NULL, pszCommandLineANSI, (StartupInfo.dwFlags & STARTF_USESHOWWINDOW) ? Startupinfo.wShowWindow:SW_SHOWDEFAULT);
注意,上面的__ImageBase是一個連接器定義的僞變量,代表可執行文件被映射到進程地址空間的某個起始位置。
若是源文件寫了一個_tmain,而且定義了_UNICODE(即項目屬性設置爲Unicode字符集),它將如下面的形式被調用 :
int nMainRetVal = wmain(argc, wargv, wenviron);
若是沒有定義_UNICODE(即項目屬性設置爲多字節字符集),它將如下面的形式被調用 :
int nMainRetVal = main(argc, argv, environ);
童鞋們確定好奇爲何在啓動函數調用入口函數時,傳入的參數不是全局變量argc、argv或 __wargv(這三個全局變量都有雙下劃線,排版問題因此沒顯示出來)等。那咱們就進行源碼剝析的測試:我先寫了個CUI的程序,只有一個_tmain函數,而後調試,查看堆棧,雙擊我下方藍色區域,看下執行到哪,會發現跳轉到了入口函數的調用處,看來沒錯,參數確實是argc等。
接着咱們,看看這些argc, argv, environ到底在哪被賦值了,其實在本頭文件上方的一個函數(_wgetmainargs)調用就被賦值了,可是因爲我查看不到這個函數(_wgetmainargs)的定義,因此我猜想是函數裏面就使用了咱們以前所講的雙下劃線的全局變量。總結一句話,微軟的Windows真是太封閉了,源碼沒放出來真是難受呀。
當進入點函數返回時,啓動函數便調用C運行期的exit函數,將返回值(nMainRetVal)傳遞給它。Exi t 函數負責下面的操做:
1.調用由_onexit函數的調用而註冊的任何函數。
咱們通過前面的學習都瞭解了,當運行一個程序時,會生成一個進程,而後進程有兩個部分,其中一個部分就是進程地址空間,加載到進程地址空間的每個可執行文件或者DLL文件都被賦予一個獨一無二的實例句柄。這兩種實例句柄分別來表示裝入後的可執行文件,或者DLL,此時咱們把這個可執行文件或者DLL叫作進程地址空間中的一個模塊!進程實例句柄的本質,就是當前模塊載入進程地址空間的起始地址。進程實例句柄的類型是HINSTANCE。學過Windows程序設計的童鞋都知道實例句柄的用處,在程序中不少地方,都被使用,尤爲是在裝入某一個資源的時候:
LoadIcon( HINSTANCE hInstance; PCTSTR pszIcon);
(1)因爲常常在程序的其餘地方須要使用到這個進程實例句柄,因此能夠考慮將hInstance參數保存在一個全局變量,但俗話說得好,能不用全局變量就別用全局變量。爲了迎合俗話,下面給出幾個獲取進程實例句柄的方法:
下面是GetModuleHandle()函數簽名:
HMODULE WINAPI GetModuleHandle(
__in_opt LPCTSTR lpModuleName//模塊名稱,其實就是可執行文件或者DLL文件的名稱。
);
GetModuleHandle()函數獲取的就是進程模塊(可執行文件模塊或DLL文件模塊)在進程地址空間中的首地址!這個函數的使用注意事項:
實際上,不論是(w)WinMain函數的第一個參數,仍是GetModuleHandle函數獲取的進程實例句柄,這個進程實例句柄都是指可執行文件或DLL文件模塊載入進程地址空間的基地址。基地址默認是0x00400000,能夠在項目->屬性->連接器->高級處的基址、隨機基址進行調整設置,先將隨機基址設爲否,再在基址填寫「0x00100000」,這樣每次運行應用程序,可執行文件或DLL文件都在0x00100000基址處開始。 下面對GetModuleHandle函數的使用進行測試:
#include<windows.h>
#include<tchar.h>
int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE, PTSTR pszCmdLine, int nCmdShow)
{
//(1)測試點1:GetModuleHandle函數的使用,參數是模塊文件名
//windows程序中,通常都會有Kernel32.dll這個模塊,那麼如今咱們就得到這個模塊的句柄;
HMODULE hModule1 = GetModuleHandle(L"Kernel32.dll");//Kernel32.dll動態連接庫文件通常在程序中都會被嵌入到進程的地址空間去。
HMODULE hModule2 = GetModuleHandle(NULL);
HMODULE hModule3 = GetModuleHandle(L"Win32Project28.exe");
//hInstance、hModule2和hModule3的值都是相等,由於GetModuleHandle(NULL)返回的是主調進程的可執行文件的實例句柄值。
Return 0;
}
(2)若是要獲取進程模塊的文件名是什麼?能夠調用GetModuleFileHandle函數。 函數簽名:
DWORD GetModuleFileName(
HMODULE hInstance,//進程句柄
PTSTR pszPath,//文件名
DWORD cchPath);//pszPath指向的內存的大小
在函數簽名咱們能夠看到,HMODULE是什麼類型的數據?在16位Windows中,HINSTANCE和HMODULE表明的是不一樣類型的數據。而如今的VS編譯器有着這樣的一條語句:typedef HINSTANCE HMODULE;說明其實如今的HINSTANCE和HMODULE都是同一個東西。 下面對GetModuleFileName函數的使用進行測試:
#include<windows.h>
#include<tchar.h>
int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE, PTSTR pszCmdLine, int nCmdShow)
{
//(2)測試點2:GetModuleFileName函數的使用,
//參數1是模塊(加載到進程地址空間的每個可執行文件或者DLL文件都屬於一個模塊)的實例句柄
//參數2是模塊文件的名稱(絕對地址)
//參數3是文件名的大小,能夠設置爲MAX_PATH->最大的路徑長度
TCHAR path1[MAX_PATH];
TCHAR path2[MAX_PATH];
GetModuleFileName(hModule1, path1, MAX_PATH);
GetModuleFileName(hModule2, path2, MAX_PATH);
Return 0;
}
(3)若是本身的代碼位於一個DLL文件中,那麼想知道這個DLL文件被裝入進程控件後的模塊地址怎麼辦?注意,下面兩種方法的使用有兩種狀況,因爲__ImageBase和GetModuleHandleEx函數都是返回當前模塊(調用函數所在模塊,例以下方的_tWinMain函數)的基地址,因此,若是下面兩種方法在可執行文件的代碼中使用,那麼返回的就是可執行文件的基地址。而若是下面兩種方法或函數在DLL文件的代碼中使用,那麼返回的就是DLL模塊的基地址。舉個例子:
#include<windows.h>
#include<tchar.h>
extern "C" HANDLE __ImageBase;
int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE, PTSTR pszCmdLine, int nCmdShow)
{
__ImageBase;
HMODULE hModule4;
GetModuleHandleEx(
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
(PCTSTR)_tWinMain, &hModule4);//獲取函數_tWinMain函數在哪一個模塊中運行。
return 0;
}
測試結果圖以下,__ImageBase和hModule4的值是相等的。 ![](https://s1.51cto.com/images/blog/201806/08/77cbbda21b83aba24a9bf91fa7f16235.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) # 進程前一個實例句柄 如前所述,C/C++運行庫啓動函數老是向(w)WinMain的hPrevInstance參數傳遞NULL。該參數是用於16位系統,於是仍然將其保留爲(w)WinMain的一個參數,目的只是方便咱們移植16位Windows應用程序,因此絕對不要在本身的代碼中引用這個參數。咱們可使用UNREFERENCED_PARAMETER宏來消除「參數沒有被引用到」的警告。