Windows核心編程之核心總結(第四章 進程(一))(2018.6.8)

學習目標

第四章進程的學習可謂是任重而道遠,雖然不難,但知識量很多,也比較零散,需要多總結,腦海裏纔有進程的框架。所以,我把本章分爲幾個小節來講完。我還是一如既往的添加輔助性內容,希望對於小白有所幫助。而比我流弊的大有人在,大神們可以跳過輔助性內容。本小節的學習目標如下:
1.C/C++程序編譯過程
2.C/C++命令行參數的使用
3.什麼是進程
4.Windows的入口點函數
5.進程實例句柄(可執行文件實例句柄或者DLL文件實例句柄)
6.進程前一個實例句柄

C/C++程序編譯過程

C/C++的編譯、鏈接過程要把我們編寫的一個c/c++程序(源代碼)轉換成可以在硬件上運行的程序(可執行代碼),需要進行編譯和鏈接。編譯就是把文本形式源代碼翻譯爲機器語言形式的目標文件的過程。鏈接是把目標文件、操作系統的啓動代碼和用到的庫文件進行組織形成最終生成可執行代碼的過程。過程圖解如下:
Windows核心編程之核心總結(第四章 進程(一))(2018.6.8)

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()函數的參數存在如下的關係:

設命令行爲:program str1 str2 str3 str4 str5

其中program爲文件名,也就是一個由program.c經編譯、鏈接後生成的可執行文件program.exe,其後各跟5個參數。對main()函數來說,它的參數argc記錄了命令行中命令與參數的個數,共6個,指針數組的大小由參數argc的值決定,即爲char*argv[6],指針數組的取值情況如下圖所示:
Windows核心編程之核心總結(第四章 進程(一))(2018.6.8)
數組的各指針分別指向一個字符串。應當引起注意的是接收到的指針數組的各指針是從命令行的開始接收的,首先接收到的是命令,其後纔是參數。

什麼是進程

(1)進程的概念
書中原文是這樣寫的:一個進程,就是一個正在運行的程序!一個程序,可以產生多個進程。
1.一個內核對象,被系統用來管理這個進程,這個內核對象中,還包含了進程的一些策略信息。
2.一個地址空間,這個地址空間中包含了可執行代碼,動態鏈接庫模塊代碼,數據,程序動態內存分配獲取的內存,也在這個內存地址空間中。

在操作系統的相關書籍裏是這樣說的:由程序段、相關的數據段和PCB三部分構成進程,所以,其實程序段、相關的數據段就是一個地址空間,而PCB(進程控制塊)就是內核對象。
(1) 進程和線程的關係
書中原文是這樣寫的:進程是由“惰性“的,進程要做任何事情都必須讓一個線程在它的上下文中運行。該線程負責執行進程地址空間包含的代碼。事實上,一個進程可以有多個線程,所有線程都在進程的地址空間中”同時執行代碼“。…此處省略一些字...。每個進程至少要有一個線程來執行進程地址空間包含的代碼。當系統創建一個進程的時候,會自動爲進程創建第一個線程,這稱爲主線程。然後這個主線程再創建更多的線程,後者再創建更多的線程。單個CPU,爲線程分配CPU採用循環方式,爲每個線程都分配時間片;多個CPU,採取更復雜的算法爲線程分配CPU。
怎麼理解進程和線程的關係?舉個例子就十分透徹了。當雙擊一個程序,產生了一個工廠(進程)同時也產生了第一個人----廠長(primary thread:主線程),這個廠長只做一件事就是招募(創建)員工(線程),讓其他員工(線程)幫他做事。有兩種方法工廠會倒閉(進程銷燬),第一種是工廠裏的員工(線程,包括主線程)全部退出或銷燬,那麼工廠自然會倒閉(進程銷燬)。第二種方法是調用ExitProcess函數可以直接結束進程,第二種方法後面會講到,現在先了解有這一方法結束進程即可。

Windows的入口點函數

Windows支持兩種類型的應用程序:GUI程序(圖形用戶界面程序)和CUI程序(控制檯用戶界面程序)。當我們用Visual Studio來創建一個應用程序項目時,集成開發環境會設置各種鏈接器開關,使鏈接器將子系統的正確C/C++運行啓動函數嵌入最終生成的可執行文件中。對於GUI程序,鏈接器開關是/SUBSYSTEM:CONSOLE;對於CUI程序,鏈接器開關是/SUBSYSTEM:WINDOWS。在學習C與C++時,當運行一個可執行文件,我們都認爲系統調用的第一個函數是入口點函數(例如:main函數),但其實操作系統實際並不調用我們寫的入口點函數(例如:main函數),實際最先調用的是C/C++運行庫的啓動函數。應用程序類型和相應的入口函數:

應用程序類型 入口點函數 嵌入可執行文件的啓動函數
處理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函數,效率會高!
讓我們看下啓動函數都初始化哪些全局變量,下面圖示:
Windows核心編程之核心總結(第四章 進程(一))(2018.6.8)
Windows核心編程之核心總結(第四章 進程(一))(2018.6.8)
好了,我們知道了啓動函數都做了些什麼。當所有這些初始化操作完成後,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等。
Windows核心編程之核心總結(第四章 進程(一))(2018.6.8)
接着我們,看看這些argc, argv, environ到底在哪被賦值了,其實在本頭文件上方的一個函數(_wgetmainargs)調用就被賦值了,但是由於我查看不到這個函數(_wgetmainargs)的定義,所以我猜測是函數裏面就使用了我們之前所講的雙下劃線的全局變量。總結一句話,微軟的Windows真是太封閉了,源碼沒放出來真是難受呀。
Windows核心編程之核心總結(第四章 進程(一))(2018.6.8)
當進入點函數返回時,啓動函數便調用C運行期的exit函數,將返回值(nMainRetVal)傳遞給它。Exi t 函數負責下面的操作:
1.調用由_onexit函數的調用而註冊的任何函數。

  1. 爲所有全局的和靜態的C++類對象調用析構函數。
  2. 調用操作系統的ExitProcess函數,將nMainRetVal傳遞給它。這使得該操作系統能夠撤消進程並設置它的exit代碼。

    進程實例句柄(可執行文件實例句柄或者DLL文件實例句柄)

    我們經過前面的學習都瞭解了,當運行一個程序時,會生成一個進程,然後進程有兩個部分,其中一個部分就是進程地址空間,加載到進程地址空間的每一個可執行文件或者DLL文件都被賦予一個獨一無二的實例句柄。這兩種實例句柄分別來表示裝入後的可執行文件,或者DLL,此時我們把這個可執行文件或者DLL叫做進程地址空間中的一個模塊!進程實例句柄的本質,就是當前模塊載入進程地址空間的起始地址。進程實例句柄的類型是HINSTANCE。學過Windows程序設計的童鞋都知道實例句柄的用處,在程序中很多地方,都被使用,尤其是在裝入某一個資源的時候:

    LoadIcon(
    HINSTANCE hInstance;
    PCTSTR pszIcon);

    (1)由於經常在程序的其他地方需要使用到這個進程實例句柄,所以可以考慮將hInstance參數保存在一個全局變量,但俗話說得好,能不用全局變量就別用全局變量。爲了迎合俗話,下面給出幾個獲取進程實例句柄的方法:

  3. (w)WinMain函數的第一個參數,可執行文件的實例句柄會在啓動函數調用入口函數 (w)WinMain時傳入。
  4. GetModuleHandle()函數返回指定文件名的實例句柄
    下面是GetModuleHandle()函數簽名:

    HMODULE WINAPI GetModuleHandle(
    __in_opt LPCTSTR lpModuleName//模塊名稱,其實就是可執行文件或者DLL文件的名稱。
    );

    GetModuleHandle()函數獲取的就是進程模塊(可執行文件模塊或DLL文件模塊)在進程地址空間中的首地址!這個函數的使用注意事項:
  5. 如果這個函數的參數是NULL的話,那麼這個函數只返回當前可執行的模塊地址!!
  6. 在DLL中,調用GetModuleHandle,參數爲NULL,那麼這個函數返回的不是DLL模塊的地址,而是當前可執行的模塊地址!
  7. 這個函數只檢查本進程地址空間,不檢查別的進程的地址空間。例如:如果一個ComDlg32.dll文件被載入了另一個B進程地址空間,那麼 這個函數在A進程地址空間的代碼中調用這個函數,這個函數不檢查B的進程地址空間,所以在A進程地址空間沒找到就返回NULL。
    實際上,不管是(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宏來消除“參數沒有被引用到”的警告。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章