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

來源:互聯網
上載者:User

標籤:Windows核心編程之核心總結

學習目標

第四章進程的學習可謂是任重而道遠,雖然不難,但知識量很多,也比較零散,需要多總結,腦海裡才有進程的架構。所以,我把本章分為幾個小節來講完。我還是一如既往的添加輔助性內容,希望對於小白有所協助。而比我流弊的大有人在,大神們可以跳過輔助性內容。本小節的學習目標如下:
1.C/C++程式編譯過程
2.C/C++命令列參數的使用
3.什麼是進程
4.Windows的進入點函數
5.進程執行個體控制代碼(可執行檔執行個體控制代碼或者DLL檔案執行個體控制代碼)

C/C++程式編譯過程

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

設命令列為: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的進入點函數

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函數,效率會高!
讓我們看下啟動函數都初始化哪些全域變數,下面圖示:


好了,我們知道了啟動函數都做了些什麼。當所有這些初始化操作完成後,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真是太封閉了,源碼沒放出來真是難受呀。

進程執行個體控制代碼(可執行檔執行個體控制代碼或者DLL檔案執行個體控制代碼)

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

LoadIcon(    HINSTANCE hInstance;    PCTSTR pszIcon);

(1)由於經常在程式的其他地方需要使用到這個進程執行個體控制代碼,所以可以考慮將hInstance參數儲存在一個全域變數,但俗話說得好,能不用全域變數就別用全域變數。為了迎合俗話,下面給出幾個擷取進程執行個體控制代碼的方法:

1.  (w)WinMain函數的第一個參數,可執行檔的執行個體控制代碼會在啟動函數調用入口函數 (w)WinMain時傳入。2.  GetModuleHandle()函數返回指定檔案名稱的執行個體控制代碼

下面是GetModuleHandle()函數簽名:

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

GetModuleHandle()函數擷取的就是進程模組(可執行檔模組或DLL檔案模組)在進程地址空間中的首地址!這個函數的使用注意事項:

1.  如果這個函數的參數是NULL的話,那麼這個函數只返回當前可執行檔模組地址!!2.  在DLL中,調用GetModuleHandle,參數為NULL,那麼這個函數返回的不是DLL模組的地址,而是當前可執行檔模組地址!3.  這個函數只檢查本進程地址空間,不檢查別的進程的地址空間。例如:如果一個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的值是相等的。

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

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.