Windows的进程创建和映像装入
生活随笔
收集整理的這篇文章主要介紹了
Windows的进程创建和映像装入
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
關于Windows的進程創建和映像裝入的過程,“Microsoft Windows Internals 4e”一書的第六章中有頗為詳細的說明。本文就以此為依據,夾譯、夾敘、夾議地作一介紹。書中說,創建進程的過程分成六個階段,發生于操作系統的三個部分中,那就是:Windows客戶端即某個應用進程的包括Kernel32.dll在內的動態連接庫,Windows的“執行體”、即內核(確切地說是內核的上層),以及Windows子系統的服務進程Csrss中。這六個階段是:
? ? ? ?1. 打開目標映像文件。
? ? ? ?2. 創建Windows的“執行體進程對象”,也就是內核中的“進程控制塊”數據結構。
? ? ? ?3. 創建該進程的初始(第一個)線程,包括其堆棧、上下文、以及“執行體線程對象”,即內核中的“線程控制塊”數據結構。
? ? ? ?4. 將新建進程通知Windows子系統。
? ? ? ?5. 啟動初始線程地運行(除非因為參數中的CREATE_SUSPENDED標志位為1而一創建便被掛起)。
? ? ? ?6. 在新進程和線程的上下文中完成用戶空間的初始化,包括裝入所需的DLL,然后開始目標程序的運行。
下面分階段敘述。
第一階段:打開目標映像文件
? ? ? ?在Win32位API中,創建進程是由CreateProcess()完成的。這實際上是個宏定義,根據不同的情況定義成CreateProcessA ()或CreateProcessW()之一,這兩個函數都在kernel32.dll中(可以用工具depends觀察)。兩個函數的區別僅在于字符串的表達,前者采用ASCII字符,而后者采用“寬字符”、即Unicode。實際上Windows的內部都采用寬字符,所以前者只是把字符串轉換成寬字符格式,然后調用后者。
? ? ? ?可以在Windows上運行的可執行軟件有好幾類,處理的方法自然就不一樣:
? ? ? ?● Windows的32位.exe映像,直接運行。
? ? ? ?● Windows的16位.exe映像,啟動ntvdm.exe,以原有命令行作為參數。
? ? ? ?● DOS的.exe、.com、或.pif映像,啟動ntvdm.exe,以原有命令行作為參數。
? ? ? ?● DOS的.bat或.cmd批命令文件(腳本),啟動cmd.exe,以原有命令行作為參數。
? ? ? ?● POSIX可執行映像,啟動posix.exe,以原有命令行作為參數。
? ? ? ?● OS/2可執行映像,啟動os2.exe,以原有命令行作為參數。
? ? ? ?這里面最重要的當然是32位的.exe映像,而最后兩類現在已經很少見了。從對于除32位.exe以外的各種映像的處理,讀者不妨對比一下Wine對.exe映像的處理,看看這里有著什么樣的相似性。
? ? ? ?當然,我們在這里只關心32位.exe映像。對于這一類映像,CreateProcess()首先打開映像文件,再為其(分配)創建一個 “Section”、即內存區間。創建內存區間的目的當然是要把映像文件影射到這個區間,不過此時還不忙著映射,還要看看。看什么呢?首先是看已經打開的目標文件是否一個合格的.exe映像(萬一是DLL映像?)。還要看的事就有點出乎讀者意外了,看的是在“注冊表”中的這個路徑:
? ? ? ?HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options
? ? ? ?用depends可以看到,ntdll.dll中有個函數LdrQueryImageFileExecuti onOption s(),就是專門干這個事的。
? ? ? ?如果上述路徑下有以目標映像文件的文件名和擴展名為“鍵”的表項,例如“image.exe”,而表項中又有名為“Debugger”的值,那么這個值 (一個字符串)就替換了原來的目標文件名,變成新的目標映像名,并重新執行上述的第一階段操作。這樣做的目的當然是為調試程序提供方便,但是我們不妨設想:如果黑客或某個木馬程序設法在注冊表中加上了一個表項?這時候用戶以為是啟動了程序A,而實際啟動的卻是B!。
第二階段:創建內核中的進程對象
? ? ? ?我們知道,Linux上的每個進程(線程)都有一個“進程控制塊”、即task_struct數據結構,與具體進程/線程有關的絕大部分信息都集中存儲在這個數據結構中。而Windows則有所不同。首先,Windows的進程和線程各有不同的“對象”、即數據結構,從概念上把線程和進程分離開來。線程是具體的(執行)上下文,是CPU調度的單位和目標,而進程只是若干共享地址空間和特性(如調度優先級)的線程的集合。于是,進程有進程的數據結構,線程有線程的數據結構。這就好像是對一組task_struct數據結構“提取公因子”所形成的結果,這個舉措是很好理解的。進一步,Windows又把本可集中存儲的的進程數據結構也拆分成好幾個對象,有的在內核中,有的則在用戶空間。
? ? ? ?內核中與進程有關的對象有:
? ? ? ?● EPROCESS。即struct _EPROCESS,在“Internals”書中也稱為“Process Block”。它代表著Windows的一個進程,‘E’表示“Executive”,微軟把Windows內核中的上層稱為“Executive”、以區別于下層的設備驅動和內存管理等成分、一般翻譯成“執行體”。“Executive”也有“管理”、“運行”的意思(所以CEO就是“總裁”)。
? ? ? ?● KPROCESS。這是EPROCESS內部的一個成分,其名稱就叫“Pcb”。
? ? ? ?● W32PROCESS。下面將要講到,在用戶空間有個“Windows子系統”的服務進程csrss。這個服務進程為系統中的每個Windows應用進程都維持著一個數據結構,其中包含了一些與窗口和圖形界面有關的信息。而對于窗口和圖形界面的操作原來也是由csrss在“客戶”進程的請求下實現的。但是,為了提高效率,后來把這部分功能移到了內核中。與此相應,有關數據結構的一部分也需要移到內核中,就成了W32PROCESS。
? ? ? ?既然KPROCESS是EPROCESS一部分,實際上內核中與進程有關的對象實際上只有兩種,就是EPROCESS和W32PROCESS。不過這里沒有包括“打開對象表”,那也是每個進程都有的(Linux內核中的“打開文件表”也在進程控制塊的外面)。
? ? ? ?用戶空間與進程有關的對象有:
? ? ? ?● 如上所述,把W32PROCESS數據結構移入內核以后,csrss仍需要為每個Windows進程保持一些別的信息,所以csrss內部仍有按進程的相應數據結構。
? ? ? ?● PEB(Process Environment Block)、即“進程環境塊”。PEB中記錄著進程的運行參數、映像裝入地址等等信息。PEB在用戶空間中的位置是固定的,總是在 0x7ffdf000。在Windows中,用戶空間和系統空間的分界線是2GB、即0x80000000,所以PEB在靠近用戶空間頂端的地方。
? ? ? ?“Internals”書中并未給出有關數據結構的定義,但是通過Debug手段給出了EPROCESS的內部結構:
? ? ?+0x000? ? ?Pcb? ? ? ? ? ? ? ? ? ? ? ? ? ?: _KPROCESS
? ? ?+0x06c? ? ?ProcessLock? ? ? ? ? ?: _EX_PUSH_LOCK
? ? ?+0x070? ? ?CreateTime? ? ? ? ? ? ?: _LARGE_INTEGER
? ? ?+0x078? ? ?ExitTime? ? ? ? ? ? ? ? ?: _LARGE_INTEGER
? ? ?+0x080? ? ?RundownProtect? ? ?: _EX_RUNDOWN_REF
? ? ?+0x084? ? ?UniqueProcessId ? ? ?: Ptr32Void
? ? ?+0x088? ? ?ActiveProcessLinks? ? ?: _LIST_ENTRY
? ? ?+0x090? ? ?QuotaUsage? ? ? ? ? ? ?: [3] Uint4B
? ? ?+0x09c? ? ?QuotaPeak? ? ? ? ? ? ? ?: [3] Uint4B
? ? ?+0x0a8? ? ?CommitCharge? ? ? ? ?: Uint4B
? ? ?+0x0ac? ? ?PeakVirtualSize ? ? ?: Uint4B
? ? ?+0x0b0? ? ?VirtualSize? ? ? ? ? ?? ? ?: Uint4B
? ? ?+0x0b4? ? ?SessionProcessLinks : _LIST_ENTRY
? ? ?+0x0bc? ? ?DebugPort? ? ? ? ? ? ? ?: Ptr32Void
? ? ?+0x0c0? ? ?ExceptionPort? ? ? ?? ? ?: Ptr32Void
? ? ?+0x0c4? ? ?ObjectTable? ? ? ? ? ?? ? ?: Ptr32_HANDLE_TABLE
? ? ?+0x0c8? ? ?Token? ? ? ? ? ? ? ? ? ? ? ?: _EX_FAST_REF
? ? ?+0x0cc? ? ?WorkingSetLock? ? ?: _FAST_MUTEX
? ? ?+0x0ec? ? ?WorkingSetPage? ? ?: Uint4B
? ? ?+0x0f0? ? ?AddressCreationLock : _FAST_MUTEX
? ? ?+0x110? ? ?HyperSpaceLock? ? ?: Uint4B
? ? ?+0x114? ? ?ForkInProgress? ? ?? ? ?: Ptr32_ETHREAD
? ? ?+0x118? ? ?HardwareTrigger ? ? ?: Uint4B
? ? ? ?可見,EPROCESS的第一個成分是Pcb,其類型是_KPROCESS、即KPROCESS,這是一個大小為0x6c的數據結構。書中也給出了它的內部結構。
? ? ? ?“Undocumented Windows 2000 Secrets”一書也以Debug手段給出了這個數據結構的內部結構,但是列出的結構與此有很大的不同,也許是因為版本的關系。從所列的內容看,似乎 “Secrets”一書倒是正確的,因為那里所列的EPROCESS結構中有關于虛存的成分Vm,是一個大小為0x50的數據結構,而這里沒有,但是虛存 (地址空間)顯然是進程的主要資源,所以EPROCESS數據結構中理應有它的位置。由此看來,“Secrets”一書所述更接近于桌面和服務器系統的現實,而“Internals”書中所列可能更接近于不帶MMU的嵌入式系統。而且,“Secrets”一書還在附錄C中給出了通過逆向工程手段得到的 EPROCESS和PEB等數據結構的定義(代碼),這當然是很有價值的。
? ? ? ?那么,如果確有不同版本的EPROCESS,這會有什么影響呢?首先,用戶空間的應用程序不能直接訪問內核中的EPROCESS數據結構,所以具體的 EPROCESS數據結構屬于內核的內部實現,只要內核中的各種成分、各個環節都配套成龍,“自圓其說”,就沒有什么問題,這跟Linux內核中一些條件編譯和裁剪的效果是類似的。可是,另一方面,對于可以動態裝入的.sys模塊,如果在模塊中需要訪問這些數據結構,那就可能有問題了,因為.sys模塊都是以二進制映像的形式提供的,不像在Linux中那樣可以由源代碼重新編譯。怎么辦呢?我們可以到Windows的DDK中去找找答案。
? ? ? ?在DDK的.h文件中,有函數IoGetCurrentProcess()的申明:
NTKERNELAPI
PEPROCESS
IoGetCurrentProcess(
? ? ? ?VOID
);
? ? ? ?這個函數是內核為.sys模塊提供的支撐函數,相當于由Linux內核導出的函數。其返回值類型是PEPROCESS,就是指向EPROCESS數據結構的指針。顯然,這跟Linux內核中的current相似,調用的目的是獲取當前進程的EPROCESS數據結構(指針)。但是,DDK的.h文件中卻并未給出EPROCESS數據結構的定義,所以調用這個函數所得到的僅僅是個指針,實際上與void*并無區別。這意味著在.sys模塊中是不允許直接訪問其內部成分的。那么,.sys模塊如何使用這個指針呢?下面就是一個例子,還是在DDK中:
NTKERNELAPI
VOID
MmProbeAndLockProcessPag es (
? ? ? ?IN OUT PMDL MemoryDescriptorList,
? ? ? ?IN PEPROCESS Process,
? ? ? ?IN KPROCESSOR_MODE AccessMode,
? ? ? ?IN LOCK_OPERATION Operation
? ? ? ?);
? ? ? ?這個函數的作用是鎖定某個進程的某些存儲頁面(不讓換出),其輸入參數之一就是指向該進程的EPROCESS結構的指針。當然,這個函數也是由內核提供的 (屬于我們所說的設備驅動界面)。所以指針的提供者和使用者都是內核,只要這二者配套即可,.sys模塊在這里只不過是傳遞了一下,所以也不會有問題。
? ? ? ?假定proc是指向進程控制塊的指針,并且進程控制塊中有個成份X,是個整數,那么在Linux的動態安裝模塊中可以直接用“proc->X”訪問這個成分,但是在Windows的.sys模塊中則只能通過類似于get_X()、set_X()一類的支撐函數訪問這個成分。將數據結構的內容跟對于這些內容的操作(method)相分離,正是“對象”與“數據結構”的區別所在。而將數據結構的內容“封裝”起來,也正是微軟所需要的,因為它不愿意公開這些數據結構。
? ? ? ?對于兼容內核的開發,這意味著我們不必拘泥于采用與Windows完全一致的EPROCESS數據結構(盡管“Secrets”的附錄C已經給出了它的定義),一些內部的操作和處理也不必完全與之相同,而只要與DDK所規定的界面相符就可以了。
? ? ? ?了解了有關的進程對象,我們可以言歸正傳了。
? ? ? ?所謂創建內核中的進程對象,實際上就是創建以EPROCESS為核心、為基礎的相關數據結構,這就是系統調用NtCreateProcess()要做的事情,主要包括:
? ? ? ?● 分配并設置EPROCESS數據結構。
? ? ? ?● 其他相關的數據結構的設置,例如“打開對象表”。
? ? ? ?● 為目標進程創建初始的地址空間。
? ? ? ?● 對目標進程的“內核進程塊”KPROCESS進行初始化。
? ? ? ?● 將系統DLL的映像映射到目標進程的(用戶)地址空間。
? ? ? ?● 將目標進程的映像映射到其自身的用戶空間。
? ? ? ?● 設置好目標進程的“進程環境塊”PEB。
? ? ? ?● 映射其他需要映射到用戶空間的數據結構,例如與“當地語言支持”、即NLS有關的數據結構。
? ? ? ?● 完成EPROCESS創建,將其掛入進程隊列(注意受調度的是線程隊列而不是進程隊列)。
? ? ? ?這里將系統DLL、實際上是ntdll.dll、映射到目標進程的用戶空間是很關鍵的。這是因為,除別的、主流的功能和作用外,ntdll.dll同時也起著相當于Linux中ELF“解釋器”的作用,也擔負著為目標映像建立動態連接的任務。
? ? ? ?值得注意的是,NtCreateProcess()與CreateProcess()不同。CreateProcess()創建一個進程并使其(初始線程)運行,除非在創建時就指定要將其掛起。而NtCreateProcess(),則只是在內核中創建該進程的EPROCESS數據結構并為其建立起地址空間。這只是個空殼架子,因為沒有線程就談不上運行,調度的目標是線程而不是線程。而且,對NtCreateProcess()的調用還有個條件,那就是目標映像已經被映射到一個存儲區間(Section)中。
第三階段:創建初始線程
? ? ? ?如上所述,進程只是個空架子,實際的運行實體是里面的線程。所以下一步就是創建目標進程的初始線程,即其第一個線程。
? ? ? ?與EPROCESS相對應,線程的數據結構是ETHREAD,并且其第一個成分是數據結構KTHREAD,稱為TCB。同樣,“Internals”和 “Secrets”兩本書中所列的ETHREAD內部結構有所不同,后者的附錄C中給出了通過逆向工程得到的ETHREAD數據結構定義。
? ? ? ?同樣,從Windows DDK中申明的一些函數也可以看出,.sys模塊只是傳遞ETHREAD指針或KTHREAD指針(由于KTHREAD是ETHREAD中的第一個成分,二者實際上是一回事),而不會直接訪問它的具體成分。
PKTHREAD NTAPI KeGetCurrentThread();
NTKERNELAPI KPRIORITY
KeQueryPriorityThread (IN PKTHREAD Thread);
NTKERNELAPI LONG
KeSetBasePriorityThread (IN PKTHREAD Thread, IN LONG Increment);
NTKERNELAPI PDEVICE_OBJECT
IoGetDeviceToVerify(IN PETHREAD Thread);
? ? ? ?此外,就像進程有“進程環境塊”PEB一樣,線程也有“線程環境塊”TEB,KTHREAD結構中有個指針指向其存在于用戶空間的TEB。前面講過, PEB在用戶空間的位置是固定的,PEB下方就是TEB,進程中有幾個線程就有幾個TEB,每個TEB占一個4KB的頁面。
? ? ? ?這個階段的操作是通過系統調用NtCreateThread()完成的,主要包括:
? ? ? ?● 創建和設置目標線程的ETHREAD數據結構,并處理好與EPROCESS的關系(例如進程塊中的線程計數等等)。
? ? ? ?● 在目標進程的用戶空間創建并設置目標線程的TEB。
? ? ? ?● 將目標線程在用戶空間的起始地址設置成指向Kernel32.dll中的BaseProcessStart()或BaseThreadStart(),前者用于進程中的第一個線程,后者用于隨后的線程。用戶程序在調用NtCreateThread()時也要提供一個用戶級的起始函數(地址), BaseProcessStart()和BaseThreadStart()在完成初始化時會調用這個起始函數。ETHREAD數據結構中有兩個成份,分別用來存放這兩個地址。
? ? ? ?● 設置目標線程的KTHREAD數據結構并為其分配堆棧。特別地,將其上下文中的斷點(返回點)設置成指向內核中的一段程序KiThreadStartup,使得該線程一旦被調度運行時就從這里開始執行。
? ? ? ?● 系統中可能登記了一些每當創建線程時就應加以調用的“通知”函數,調用這些函數。
第四階段:通知Windows子系統
? ? ? ?Windows、確切地說是Windows NT、當初的設計目標是支持三種不同系統的應用軟件。第一種是Windows本身的應用軟件,即所謂“Native”Windows軟件,這是微軟開發 Windows NT的真正目的。第二種是OS/2的應用軟件,這是因為當時微軟與IBM還有合作關系。第三種是與Unix應用軟件相似、符合POSIX標準的軟件,那是因為當時美國的軍方采購有這樣的要求。不過實際上微軟對后兩種應用的支持從一開始就是半心半意的,后來翅膀長硬了,就更不必勉為其難了。但是,盡管如此,當初在設計的時候還是考慮了對不同“平臺”的支持,即在同一個內核的基礎上配以不同的外圍軟件,形成不同的應用軟件運行環境,微軟稱之為“子系統(Subsystem)”。于是,在Windows內核上就有了所謂“Windows子系統”、“OS/2子系統”、和“POSIX子系統”。當然,時至今日,實際上只剩下Windows子系統了。
? ? ? ?那么,所謂子系統是怎樣構成的呢?“Internals”書中闡明了Windows子系統的構成,說這是由下列幾個要素構成的。
? ? ? ?一、子系統進程csrss.exe。包括了對下列成分和功能的支持:
? ? ? ? ? ? ? ?● 控制臺(字符型)窗口的操作。面向控制臺/終端的應用本身不支持窗口操作(例如窗口的移動、大化/小化、遮蓋等等),但是又需要在窗口中運行,所以需要有額外的支持。
? ? ? ? ? ? ? ?● 進程和線程的管理。例如彈出一個對話窗,說某個進程沒有響應,讓使用者選擇是否結束該進程的運行,等等。每個Windows進程/線程再創建/退出時都要向csrss.exe進程發出通知。
? ? ? ? ? ? ? ?● DOS軟件和16位Windows軟件在(32位)Windows上的運行。
? ? ? ? ? ? ? ?● 其它。包括對當地語言(輸入法)的支持。
? ? ? ?這個進程之所以叫csrss,是“C/S Run-time SubSystem”的意思,csrss是Windows子系統的服務進程。其實三個子系統都是C/S結構,但是OS/2子系統的服務進程稱為 os2ss,POSIX子系統的服務進程稱為Psxss。之所以如此,據“Internals”說,是因為最初時三個子系統的服務進程是合在一起的,就叫 csrss,后來才把那兩個子系統移了出來另立門戶,但剩下的還繼續叫csrss。
? ? ? ?二、內核中的圖形設備驅動、即Win32k.sys模塊。其功能包括:
? ? ? ? ? ? ? ?● 視窗管理,控制著窗口的顯示和各種屏幕輸出(例如光標),還擔負著從鍵盤、鼠標等設備接收輸入并將它們分發給各個具體應用的任務。
? ? ? ? ? ? ? ?● 為應用軟件提供一個圖形函數庫。
? ? ? ?三、若干“系統DLL”,如Kernel32.dll、Advapi32.dll、User32.dll、以及Gdi32.dll。
? ? ? ?上述的第二個要素Win32k.sys原先也是和csrss.exe合在一起的,這部分功能也由服務進程在用戶空間提供。應用進程通過進程間通信向 csrss發出圖形操作請求,由csrss完成有關的圖形操作。但是后來發現頻繁的進程間通信和調度成了瓶頸,所以就把這一部分功能剝離出來,移進了內核,這就是Win32k.sys。這一來,對于一般的32位Windows應用而言,留給csrss、或者說必須要通過csrss辦的事就很少了。但是,盡管如此,在創建WIndows進程時還是要通知csrss,因為它擔負著對所有WIndows進程的管理。另一方面,csrss在接到通知以后就會在屏幕上顯示那個沙漏狀的光標,如果這是個有窗口的進程的話。
? ? ? ?注意這里向csrss發出通知的是父進程、即調用CreateProcess()的進程,而不是新創建出來的進程,它還沒有開始運行。
? ? ? ?至此CreateProcess()的操作已經完成,從CreateProcess()返回就退出了kernel32.dll,回到了應用程序或更高層的 DLL中。這四個階段都是立足于父進程的用戶空間,在整個過程中進行了多次系統調用,每次系統調用完成后都回到用戶空間中。例如,在第二階段中就調用了 NtCreateProcess(),第三階段中就調用了NtCreateThread(),而整個創建進程的過程包括了許多次系統調用(有些系統調用屬于細節,所以上面并未提及)。
? ? ? ?其實Linux的進程創建也不是一次系統調用就可完成的,典型的過程就包括fock()、execve()等系統調用,但是在Windows上就更多了。這跟Windows的整個系統調用界面的設計有關。以用戶空間的內存分配為例,Linux的系統調用brk()只有一個參數,那就是區間的長度,但是 Windows的系統調用NtAllocateVirtualMemory()卻有6個參數,其第一個參數是ProcessHandle,這是標志著一個已打開進程對象的Handle。這說明什么呢?這說明Linux進程只能為自己分配空間,而Windows進程卻可以為別的進程分配空間。或者說,在存儲空間的分配上Linux進程是“自力更生”的,而Windows進程卻可以“包辦代替”。
? ? ? ?這對于系統設計的影響可能遠超讀者的想像。就拿為子進程的第一個線程分配用戶空間堆棧而言,既然Linux進程(線程)只能為自己分配空間,而用戶空間堆棧又必須在進入用戶空間運行之前就已存在,那就只好在內核中完成用戶空間堆棧的分配。相比之下,Windows進程可以為別的進程分配空間,于是就可以由父進程在用戶空間中為子進程完成這些操作。這樣,有些事情Linux只能在內核中做,而Windows可以在用戶空間做。有些人稱Windows為“微內核”,這或許也是個原因。而Windows的CreateProcess()中包含著更多的系統調用,也就很好理解了。
? ? ? ?現在,雖然父進程已經從庫函數CreateProcess()中返回了,子進程的運行卻還未開始,它的運行還要經歷下面的第五和第六兩個階段。
第五階段:啟動初始線程
? ? ? ?新創建的線程未必是可以被立即調度運行的,因為用戶可能在創建時把標志位CREATE_ SUSPENDED設成了1。如果那樣的話,就需要等待別的進程通過系統調用恢復其運行資格以后才可以被調度運行。否則現在已經可以被調度運行了。至于什么時候才會被調度運行,則就要看優先級等等條件了。而一旦受調度運行,那就是以新建進程的身份在運行、與CreateProcess()的調用者無關了。
? ? ? ?如前所述,當進程的第一個線程首次受調度運行時,由于線程(系統空間)堆棧的設置,首先執行的是KiThreadStartup。這段程序把目標線程的IRQL從DPC級降低到APC級,然后調用內核函數PspUserThreadStartup()。
? ? ? ?最后,PspUserThreadStartup()將用戶空間ntdll.dll中的函數LdrInitializeThunk()作為APC函數掛入 APC隊列,再企圖“返回到”用戶空間。Windows的APC跟Linux的signal機制頗為相似,相當于用戶空間的“中斷服務”。所以,在返回用戶空間的前夕,就會檢查APC函數的存在并加以執行(如果存在的話)。
? ? ? ?于是,此時的CPU將兩次進入用戶空間。第一次是因為APC請求的存在而進入用戶空間,執行APC函數LdrInitializeThunk(),執行完畢以后仍回到系統空間。然后,第二次進入用戶空間才是“返回”用戶空間。返回到用戶空間的什么地方呢?前面已經講到,返回到Kernel32.dll中的 BaseProcessStart()或BaseThreadStart(),對于進程中的第一個線程是BaseProcessStart()。至于用戶程序所提供的(線程)入口,則是作為參數(函數指針)提供給BaseProcessStart()或BaseThreadStart()的,這兩個函數都會使用這指針調用由用戶提供的入口函數。
第六階段:用戶空間的初始化和DLL的連接
? ? ? ?用戶空間的初始化和DLL的連接是由LdrInitializeThunk()作為APC函數的執行來完成的。
? ? ? ?在應用軟件與動態連接庫的連接這一點上,我們已經看到,不管是Linux、Windows、還是Wine,都是一致的,那就是在用戶空間完成:
? ? ? ?● Linux的.so模塊連接由“解釋器”在用戶空間完成。“解釋器”相當于一個不需要事先連接的動態庫,因為它的入口是固定的。“解釋器”的映像是由內核裝入用戶空間的。
? ? ? ?● Windows的DLL連接由ntdll.dll中的LdrInitializeThunk()在用戶空間完成。在此之前ntdll.dll與應用軟件尚未連接,但是已經被映射到了用戶空間。函數LdrInitializeThunk()在映像中的位置是系統初始化時就預先確定并記錄在案的,所以在進入這個函數之前也不需要連接。
? ? ? ?● Wine的動態庫連接分兩種情況。一種是ELF格式的.so模塊,另一種是PE格式的DLL。二者的連接都是在用戶空間完成的,前者仍由ELF解釋器ld-linux.so.2完成,后者則由工具軟件wine-kthread完成。后者的具體調用路徑是:
main() > wine_init() > __wine_process_init() > __wine_kernel_init() >
wine_switch_to_stack() > start_process() > LdrInitializeThunk()
? ? ? ?這在“Wine的二進制映像裝入和啟動”那篇漫談中已經講到過了。注意這里最終完成DLL連接的函數也叫LdrInitializeThunk(),顯然Wine的作者對于Windows的這一套是清楚的。
? ? ? ?1. 打開目標映像文件。
? ? ? ?2. 創建Windows的“執行體進程對象”,也就是內核中的“進程控制塊”數據結構。
? ? ? ?3. 創建該進程的初始(第一個)線程,包括其堆棧、上下文、以及“執行體線程對象”,即內核中的“線程控制塊”數據結構。
? ? ? ?4. 將新建進程通知Windows子系統。
? ? ? ?5. 啟動初始線程地運行(除非因為參數中的CREATE_SUSPENDED標志位為1而一創建便被掛起)。
? ? ? ?6. 在新進程和線程的上下文中完成用戶空間的初始化,包括裝入所需的DLL,然后開始目標程序的運行。
下面分階段敘述。
第一階段:打開目標映像文件
? ? ? ?在Win32位API中,創建進程是由CreateProcess()完成的。這實際上是個宏定義,根據不同的情況定義成CreateProcessA ()或CreateProcessW()之一,這兩個函數都在kernel32.dll中(可以用工具depends觀察)。兩個函數的區別僅在于字符串的表達,前者采用ASCII字符,而后者采用“寬字符”、即Unicode。實際上Windows的內部都采用寬字符,所以前者只是把字符串轉換成寬字符格式,然后調用后者。
? ? ? ?可以在Windows上運行的可執行軟件有好幾類,處理的方法自然就不一樣:
? ? ? ?● Windows的32位.exe映像,直接運行。
? ? ? ?● Windows的16位.exe映像,啟動ntvdm.exe,以原有命令行作為參數。
? ? ? ?● DOS的.exe、.com、或.pif映像,啟動ntvdm.exe,以原有命令行作為參數。
? ? ? ?● DOS的.bat或.cmd批命令文件(腳本),啟動cmd.exe,以原有命令行作為參數。
? ? ? ?● POSIX可執行映像,啟動posix.exe,以原有命令行作為參數。
? ? ? ?● OS/2可執行映像,啟動os2.exe,以原有命令行作為參數。
? ? ? ?這里面最重要的當然是32位的.exe映像,而最后兩類現在已經很少見了。從對于除32位.exe以外的各種映像的處理,讀者不妨對比一下Wine對.exe映像的處理,看看這里有著什么樣的相似性。
? ? ? ?當然,我們在這里只關心32位.exe映像。對于這一類映像,CreateProcess()首先打開映像文件,再為其(分配)創建一個 “Section”、即內存區間。創建內存區間的目的當然是要把映像文件影射到這個區間,不過此時還不忙著映射,還要看看。看什么呢?首先是看已經打開的目標文件是否一個合格的.exe映像(萬一是DLL映像?)。還要看的事就有點出乎讀者意外了,看的是在“注冊表”中的這個路徑:
? ? ? ?HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options
? ? ? ?用depends可以看到,ntdll.dll中有個函數LdrQueryImageFileExecuti onOption s(),就是專門干這個事的。
? ? ? ?如果上述路徑下有以目標映像文件的文件名和擴展名為“鍵”的表項,例如“image.exe”,而表項中又有名為“Debugger”的值,那么這個值 (一個字符串)就替換了原來的目標文件名,變成新的目標映像名,并重新執行上述的第一階段操作。這樣做的目的當然是為調試程序提供方便,但是我們不妨設想:如果黑客或某個木馬程序設法在注冊表中加上了一個表項?這時候用戶以為是啟動了程序A,而實際啟動的卻是B!。
第二階段:創建內核中的進程對象
? ? ? ?我們知道,Linux上的每個進程(線程)都有一個“進程控制塊”、即task_struct數據結構,與具體進程/線程有關的絕大部分信息都集中存儲在這個數據結構中。而Windows則有所不同。首先,Windows的進程和線程各有不同的“對象”、即數據結構,從概念上把線程和進程分離開來。線程是具體的(執行)上下文,是CPU調度的單位和目標,而進程只是若干共享地址空間和特性(如調度優先級)的線程的集合。于是,進程有進程的數據結構,線程有線程的數據結構。這就好像是對一組task_struct數據結構“提取公因子”所形成的結果,這個舉措是很好理解的。進一步,Windows又把本可集中存儲的的進程數據結構也拆分成好幾個對象,有的在內核中,有的則在用戶空間。
? ? ? ?內核中與進程有關的對象有:
? ? ? ?● EPROCESS。即struct _EPROCESS,在“Internals”書中也稱為“Process Block”。它代表著Windows的一個進程,‘E’表示“Executive”,微軟把Windows內核中的上層稱為“Executive”、以區別于下層的設備驅動和內存管理等成分、一般翻譯成“執行體”。“Executive”也有“管理”、“運行”的意思(所以CEO就是“總裁”)。
? ? ? ?● KPROCESS。這是EPROCESS內部的一個成分,其名稱就叫“Pcb”。
? ? ? ?● W32PROCESS。下面將要講到,在用戶空間有個“Windows子系統”的服務進程csrss。這個服務進程為系統中的每個Windows應用進程都維持著一個數據結構,其中包含了一些與窗口和圖形界面有關的信息。而對于窗口和圖形界面的操作原來也是由csrss在“客戶”進程的請求下實現的。但是,為了提高效率,后來把這部分功能移到了內核中。與此相應,有關數據結構的一部分也需要移到內核中,就成了W32PROCESS。
? ? ? ?既然KPROCESS是EPROCESS一部分,實際上內核中與進程有關的對象實際上只有兩種,就是EPROCESS和W32PROCESS。不過這里沒有包括“打開對象表”,那也是每個進程都有的(Linux內核中的“打開文件表”也在進程控制塊的外面)。
? ? ? ?用戶空間與進程有關的對象有:
? ? ? ?● 如上所述,把W32PROCESS數據結構移入內核以后,csrss仍需要為每個Windows進程保持一些別的信息,所以csrss內部仍有按進程的相應數據結構。
? ? ? ?● PEB(Process Environment Block)、即“進程環境塊”。PEB中記錄著進程的運行參數、映像裝入地址等等信息。PEB在用戶空間中的位置是固定的,總是在 0x7ffdf000。在Windows中,用戶空間和系統空間的分界線是2GB、即0x80000000,所以PEB在靠近用戶空間頂端的地方。
? ? ? ?“Internals”書中并未給出有關數據結構的定義,但是通過Debug手段給出了EPROCESS的內部結構:
? ? ?+0x000? ? ?Pcb? ? ? ? ? ? ? ? ? ? ? ? ? ?: _KPROCESS
? ? ?+0x06c? ? ?ProcessLock? ? ? ? ? ?: _EX_PUSH_LOCK
? ? ?+0x070? ? ?CreateTime? ? ? ? ? ? ?: _LARGE_INTEGER
? ? ?+0x078? ? ?ExitTime? ? ? ? ? ? ? ? ?: _LARGE_INTEGER
? ? ?+0x080? ? ?RundownProtect? ? ?: _EX_RUNDOWN_REF
? ? ?+0x084? ? ?UniqueProcessId ? ? ?: Ptr32Void
? ? ?+0x088? ? ?ActiveProcessLinks? ? ?: _LIST_ENTRY
? ? ?+0x090? ? ?QuotaUsage? ? ? ? ? ? ?: [3] Uint4B
? ? ?+0x09c? ? ?QuotaPeak? ? ? ? ? ? ? ?: [3] Uint4B
? ? ?+0x0a8? ? ?CommitCharge? ? ? ? ?: Uint4B
? ? ?+0x0ac? ? ?PeakVirtualSize ? ? ?: Uint4B
? ? ?+0x0b0? ? ?VirtualSize? ? ? ? ? ?? ? ?: Uint4B
? ? ?+0x0b4? ? ?SessionProcessLinks : _LIST_ENTRY
? ? ?+0x0bc? ? ?DebugPort? ? ? ? ? ? ? ?: Ptr32Void
? ? ?+0x0c0? ? ?ExceptionPort? ? ? ?? ? ?: Ptr32Void
? ? ?+0x0c4? ? ?ObjectTable? ? ? ? ? ?? ? ?: Ptr32_HANDLE_TABLE
? ? ?+0x0c8? ? ?Token? ? ? ? ? ? ? ? ? ? ? ?: _EX_FAST_REF
? ? ?+0x0cc? ? ?WorkingSetLock? ? ?: _FAST_MUTEX
? ? ?+0x0ec? ? ?WorkingSetPage? ? ?: Uint4B
? ? ?+0x0f0? ? ?AddressCreationLock : _FAST_MUTEX
? ? ?+0x110? ? ?HyperSpaceLock? ? ?: Uint4B
? ? ?+0x114? ? ?ForkInProgress? ? ?? ? ?: Ptr32_ETHREAD
? ? ?+0x118? ? ?HardwareTrigger ? ? ?: Uint4B
? ? ? ?可見,EPROCESS的第一個成分是Pcb,其類型是_KPROCESS、即KPROCESS,這是一個大小為0x6c的數據結構。書中也給出了它的內部結構。
? ? ? ?“Undocumented Windows 2000 Secrets”一書也以Debug手段給出了這個數據結構的內部結構,但是列出的結構與此有很大的不同,也許是因為版本的關系。從所列的內容看,似乎 “Secrets”一書倒是正確的,因為那里所列的EPROCESS結構中有關于虛存的成分Vm,是一個大小為0x50的數據結構,而這里沒有,但是虛存 (地址空間)顯然是進程的主要資源,所以EPROCESS數據結構中理應有它的位置。由此看來,“Secrets”一書所述更接近于桌面和服務器系統的現實,而“Internals”書中所列可能更接近于不帶MMU的嵌入式系統。而且,“Secrets”一書還在附錄C中給出了通過逆向工程手段得到的 EPROCESS和PEB等數據結構的定義(代碼),這當然是很有價值的。
? ? ? ?那么,如果確有不同版本的EPROCESS,這會有什么影響呢?首先,用戶空間的應用程序不能直接訪問內核中的EPROCESS數據結構,所以具體的 EPROCESS數據結構屬于內核的內部實現,只要內核中的各種成分、各個環節都配套成龍,“自圓其說”,就沒有什么問題,這跟Linux內核中一些條件編譯和裁剪的效果是類似的。可是,另一方面,對于可以動態裝入的.sys模塊,如果在模塊中需要訪問這些數據結構,那就可能有問題了,因為.sys模塊都是以二進制映像的形式提供的,不像在Linux中那樣可以由源代碼重新編譯。怎么辦呢?我們可以到Windows的DDK中去找找答案。
? ? ? ?在DDK的.h文件中,有函數IoGetCurrentProcess()的申明:
NTKERNELAPI
PEPROCESS
IoGetCurrentProcess(
? ? ? ?VOID
);
? ? ? ?這個函數是內核為.sys模塊提供的支撐函數,相當于由Linux內核導出的函數。其返回值類型是PEPROCESS,就是指向EPROCESS數據結構的指針。顯然,這跟Linux內核中的current相似,調用的目的是獲取當前進程的EPROCESS數據結構(指針)。但是,DDK的.h文件中卻并未給出EPROCESS數據結構的定義,所以調用這個函數所得到的僅僅是個指針,實際上與void*并無區別。這意味著在.sys模塊中是不允許直接訪問其內部成分的。那么,.sys模塊如何使用這個指針呢?下面就是一個例子,還是在DDK中:
NTKERNELAPI
VOID
MmProbeAndLockProcessPag es (
? ? ? ?IN OUT PMDL MemoryDescriptorList,
? ? ? ?IN PEPROCESS Process,
? ? ? ?IN KPROCESSOR_MODE AccessMode,
? ? ? ?IN LOCK_OPERATION Operation
? ? ? ?);
? ? ? ?這個函數的作用是鎖定某個進程的某些存儲頁面(不讓換出),其輸入參數之一就是指向該進程的EPROCESS結構的指針。當然,這個函數也是由內核提供的 (屬于我們所說的設備驅動界面)。所以指針的提供者和使用者都是內核,只要這二者配套即可,.sys模塊在這里只不過是傳遞了一下,所以也不會有問題。
? ? ? ?假定proc是指向進程控制塊的指針,并且進程控制塊中有個成份X,是個整數,那么在Linux的動態安裝模塊中可以直接用“proc->X”訪問這個成分,但是在Windows的.sys模塊中則只能通過類似于get_X()、set_X()一類的支撐函數訪問這個成分。將數據結構的內容跟對于這些內容的操作(method)相分離,正是“對象”與“數據結構”的區別所在。而將數據結構的內容“封裝”起來,也正是微軟所需要的,因為它不愿意公開這些數據結構。
? ? ? ?對于兼容內核的開發,這意味著我們不必拘泥于采用與Windows完全一致的EPROCESS數據結構(盡管“Secrets”的附錄C已經給出了它的定義),一些內部的操作和處理也不必完全與之相同,而只要與DDK所規定的界面相符就可以了。
? ? ? ?了解了有關的進程對象,我們可以言歸正傳了。
? ? ? ?所謂創建內核中的進程對象,實際上就是創建以EPROCESS為核心、為基礎的相關數據結構,這就是系統調用NtCreateProcess()要做的事情,主要包括:
? ? ? ?● 分配并設置EPROCESS數據結構。
? ? ? ?● 其他相關的數據結構的設置,例如“打開對象表”。
? ? ? ?● 為目標進程創建初始的地址空間。
? ? ? ?● 對目標進程的“內核進程塊”KPROCESS進行初始化。
? ? ? ?● 將系統DLL的映像映射到目標進程的(用戶)地址空間。
? ? ? ?● 將目標進程的映像映射到其自身的用戶空間。
? ? ? ?● 設置好目標進程的“進程環境塊”PEB。
? ? ? ?● 映射其他需要映射到用戶空間的數據結構,例如與“當地語言支持”、即NLS有關的數據結構。
? ? ? ?● 完成EPROCESS創建,將其掛入進程隊列(注意受調度的是線程隊列而不是進程隊列)。
? ? ? ?這里將系統DLL、實際上是ntdll.dll、映射到目標進程的用戶空間是很關鍵的。這是因為,除別的、主流的功能和作用外,ntdll.dll同時也起著相當于Linux中ELF“解釋器”的作用,也擔負著為目標映像建立動態連接的任務。
? ? ? ?值得注意的是,NtCreateProcess()與CreateProcess()不同。CreateProcess()創建一個進程并使其(初始線程)運行,除非在創建時就指定要將其掛起。而NtCreateProcess(),則只是在內核中創建該進程的EPROCESS數據結構并為其建立起地址空間。這只是個空殼架子,因為沒有線程就談不上運行,調度的目標是線程而不是線程。而且,對NtCreateProcess()的調用還有個條件,那就是目標映像已經被映射到一個存儲區間(Section)中。
第三階段:創建初始線程
? ? ? ?如上所述,進程只是個空架子,實際的運行實體是里面的線程。所以下一步就是創建目標進程的初始線程,即其第一個線程。
? ? ? ?與EPROCESS相對應,線程的數據結構是ETHREAD,并且其第一個成分是數據結構KTHREAD,稱為TCB。同樣,“Internals”和 “Secrets”兩本書中所列的ETHREAD內部結構有所不同,后者的附錄C中給出了通過逆向工程得到的ETHREAD數據結構定義。
? ? ? ?同樣,從Windows DDK中申明的一些函數也可以看出,.sys模塊只是傳遞ETHREAD指針或KTHREAD指針(由于KTHREAD是ETHREAD中的第一個成分,二者實際上是一回事),而不會直接訪問它的具體成分。
PKTHREAD NTAPI KeGetCurrentThread();
NTKERNELAPI KPRIORITY
KeQueryPriorityThread (IN PKTHREAD Thread);
NTKERNELAPI LONG
KeSetBasePriorityThread (IN PKTHREAD Thread, IN LONG Increment);
NTKERNELAPI PDEVICE_OBJECT
IoGetDeviceToVerify(IN PETHREAD Thread);
? ? ? ?此外,就像進程有“進程環境塊”PEB一樣,線程也有“線程環境塊”TEB,KTHREAD結構中有個指針指向其存在于用戶空間的TEB。前面講過, PEB在用戶空間的位置是固定的,PEB下方就是TEB,進程中有幾個線程就有幾個TEB,每個TEB占一個4KB的頁面。
? ? ? ?這個階段的操作是通過系統調用NtCreateThread()完成的,主要包括:
? ? ? ?● 創建和設置目標線程的ETHREAD數據結構,并處理好與EPROCESS的關系(例如進程塊中的線程計數等等)。
? ? ? ?● 在目標進程的用戶空間創建并設置目標線程的TEB。
? ? ? ?● 將目標線程在用戶空間的起始地址設置成指向Kernel32.dll中的BaseProcessStart()或BaseThreadStart(),前者用于進程中的第一個線程,后者用于隨后的線程。用戶程序在調用NtCreateThread()時也要提供一個用戶級的起始函數(地址), BaseProcessStart()和BaseThreadStart()在完成初始化時會調用這個起始函數。ETHREAD數據結構中有兩個成份,分別用來存放這兩個地址。
? ? ? ?● 設置目標線程的KTHREAD數據結構并為其分配堆棧。特別地,將其上下文中的斷點(返回點)設置成指向內核中的一段程序KiThreadStartup,使得該線程一旦被調度運行時就從這里開始執行。
? ? ? ?● 系統中可能登記了一些每當創建線程時就應加以調用的“通知”函數,調用這些函數。
第四階段:通知Windows子系統
? ? ? ?Windows、確切地說是Windows NT、當初的設計目標是支持三種不同系統的應用軟件。第一種是Windows本身的應用軟件,即所謂“Native”Windows軟件,這是微軟開發 Windows NT的真正目的。第二種是OS/2的應用軟件,這是因為當時微軟與IBM還有合作關系。第三種是與Unix應用軟件相似、符合POSIX標準的軟件,那是因為當時美國的軍方采購有這樣的要求。不過實際上微軟對后兩種應用的支持從一開始就是半心半意的,后來翅膀長硬了,就更不必勉為其難了。但是,盡管如此,當初在設計的時候還是考慮了對不同“平臺”的支持,即在同一個內核的基礎上配以不同的外圍軟件,形成不同的應用軟件運行環境,微軟稱之為“子系統(Subsystem)”。于是,在Windows內核上就有了所謂“Windows子系統”、“OS/2子系統”、和“POSIX子系統”。當然,時至今日,實際上只剩下Windows子系統了。
? ? ? ?那么,所謂子系統是怎樣構成的呢?“Internals”書中闡明了Windows子系統的構成,說這是由下列幾個要素構成的。
? ? ? ?一、子系統進程csrss.exe。包括了對下列成分和功能的支持:
? ? ? ? ? ? ? ?● 控制臺(字符型)窗口的操作。面向控制臺/終端的應用本身不支持窗口操作(例如窗口的移動、大化/小化、遮蓋等等),但是又需要在窗口中運行,所以需要有額外的支持。
? ? ? ? ? ? ? ?● 進程和線程的管理。例如彈出一個對話窗,說某個進程沒有響應,讓使用者選擇是否結束該進程的運行,等等。每個Windows進程/線程再創建/退出時都要向csrss.exe進程發出通知。
? ? ? ? ? ? ? ?● DOS軟件和16位Windows軟件在(32位)Windows上的運行。
? ? ? ? ? ? ? ?● 其它。包括對當地語言(輸入法)的支持。
? ? ? ?這個進程之所以叫csrss,是“C/S Run-time SubSystem”的意思,csrss是Windows子系統的服務進程。其實三個子系統都是C/S結構,但是OS/2子系統的服務進程稱為 os2ss,POSIX子系統的服務進程稱為Psxss。之所以如此,據“Internals”說,是因為最初時三個子系統的服務進程是合在一起的,就叫 csrss,后來才把那兩個子系統移了出來另立門戶,但剩下的還繼續叫csrss。
? ? ? ?二、內核中的圖形設備驅動、即Win32k.sys模塊。其功能包括:
? ? ? ? ? ? ? ?● 視窗管理,控制著窗口的顯示和各種屏幕輸出(例如光標),還擔負著從鍵盤、鼠標等設備接收輸入并將它們分發給各個具體應用的任務。
? ? ? ? ? ? ? ?● 為應用軟件提供一個圖形函數庫。
? ? ? ?三、若干“系統DLL”,如Kernel32.dll、Advapi32.dll、User32.dll、以及Gdi32.dll。
? ? ? ?上述的第二個要素Win32k.sys原先也是和csrss.exe合在一起的,這部分功能也由服務進程在用戶空間提供。應用進程通過進程間通信向 csrss發出圖形操作請求,由csrss完成有關的圖形操作。但是后來發現頻繁的進程間通信和調度成了瓶頸,所以就把這一部分功能剝離出來,移進了內核,這就是Win32k.sys。這一來,對于一般的32位Windows應用而言,留給csrss、或者說必須要通過csrss辦的事就很少了。但是,盡管如此,在創建WIndows進程時還是要通知csrss,因為它擔負著對所有WIndows進程的管理。另一方面,csrss在接到通知以后就會在屏幕上顯示那個沙漏狀的光標,如果這是個有窗口的進程的話。
? ? ? ?注意這里向csrss發出通知的是父進程、即調用CreateProcess()的進程,而不是新創建出來的進程,它還沒有開始運行。
? ? ? ?至此CreateProcess()的操作已經完成,從CreateProcess()返回就退出了kernel32.dll,回到了應用程序或更高層的 DLL中。這四個階段都是立足于父進程的用戶空間,在整個過程中進行了多次系統調用,每次系統調用完成后都回到用戶空間中。例如,在第二階段中就調用了 NtCreateProcess(),第三階段中就調用了NtCreateThread(),而整個創建進程的過程包括了許多次系統調用(有些系統調用屬于細節,所以上面并未提及)。
? ? ? ?其實Linux的進程創建也不是一次系統調用就可完成的,典型的過程就包括fock()、execve()等系統調用,但是在Windows上就更多了。這跟Windows的整個系統調用界面的設計有關。以用戶空間的內存分配為例,Linux的系統調用brk()只有一個參數,那就是區間的長度,但是 Windows的系統調用NtAllocateVirtualMemory()卻有6個參數,其第一個參數是ProcessHandle,這是標志著一個已打開進程對象的Handle。這說明什么呢?這說明Linux進程只能為自己分配空間,而Windows進程卻可以為別的進程分配空間。或者說,在存儲空間的分配上Linux進程是“自力更生”的,而Windows進程卻可以“包辦代替”。
? ? ? ?這對于系統設計的影響可能遠超讀者的想像。就拿為子進程的第一個線程分配用戶空間堆棧而言,既然Linux進程(線程)只能為自己分配空間,而用戶空間堆棧又必須在進入用戶空間運行之前就已存在,那就只好在內核中完成用戶空間堆棧的分配。相比之下,Windows進程可以為別的進程分配空間,于是就可以由父進程在用戶空間中為子進程完成這些操作。這樣,有些事情Linux只能在內核中做,而Windows可以在用戶空間做。有些人稱Windows為“微內核”,這或許也是個原因。而Windows的CreateProcess()中包含著更多的系統調用,也就很好理解了。
? ? ? ?現在,雖然父進程已經從庫函數CreateProcess()中返回了,子進程的運行卻還未開始,它的運行還要經歷下面的第五和第六兩個階段。
第五階段:啟動初始線程
? ? ? ?新創建的線程未必是可以被立即調度運行的,因為用戶可能在創建時把標志位CREATE_ SUSPENDED設成了1。如果那樣的話,就需要等待別的進程通過系統調用恢復其運行資格以后才可以被調度運行。否則現在已經可以被調度運行了。至于什么時候才會被調度運行,則就要看優先級等等條件了。而一旦受調度運行,那就是以新建進程的身份在運行、與CreateProcess()的調用者無關了。
? ? ? ?如前所述,當進程的第一個線程首次受調度運行時,由于線程(系統空間)堆棧的設置,首先執行的是KiThreadStartup。這段程序把目標線程的IRQL從DPC級降低到APC級,然后調用內核函數PspUserThreadStartup()。
? ? ? ?最后,PspUserThreadStartup()將用戶空間ntdll.dll中的函數LdrInitializeThunk()作為APC函數掛入 APC隊列,再企圖“返回到”用戶空間。Windows的APC跟Linux的signal機制頗為相似,相當于用戶空間的“中斷服務”。所以,在返回用戶空間的前夕,就會檢查APC函數的存在并加以執行(如果存在的話)。
? ? ? ?于是,此時的CPU將兩次進入用戶空間。第一次是因為APC請求的存在而進入用戶空間,執行APC函數LdrInitializeThunk(),執行完畢以后仍回到系統空間。然后,第二次進入用戶空間才是“返回”用戶空間。返回到用戶空間的什么地方呢?前面已經講到,返回到Kernel32.dll中的 BaseProcessStart()或BaseThreadStart(),對于進程中的第一個線程是BaseProcessStart()。至于用戶程序所提供的(線程)入口,則是作為參數(函數指針)提供給BaseProcessStart()或BaseThreadStart()的,這兩個函數都會使用這指針調用由用戶提供的入口函數。
第六階段:用戶空間的初始化和DLL的連接
? ? ? ?用戶空間的初始化和DLL的連接是由LdrInitializeThunk()作為APC函數的執行來完成的。
? ? ? ?在應用軟件與動態連接庫的連接這一點上,我們已經看到,不管是Linux、Windows、還是Wine,都是一致的,那就是在用戶空間完成:
? ? ? ?● Linux的.so模塊連接由“解釋器”在用戶空間完成。“解釋器”相當于一個不需要事先連接的動態庫,因為它的入口是固定的。“解釋器”的映像是由內核裝入用戶空間的。
? ? ? ?● Windows的DLL連接由ntdll.dll中的LdrInitializeThunk()在用戶空間完成。在此之前ntdll.dll與應用軟件尚未連接,但是已經被映射到了用戶空間。函數LdrInitializeThunk()在映像中的位置是系統初始化時就預先確定并記錄在案的,所以在進入這個函數之前也不需要連接。
? ? ? ?● Wine的動態庫連接分兩種情況。一種是ELF格式的.so模塊,另一種是PE格式的DLL。二者的連接都是在用戶空間完成的,前者仍由ELF解釋器ld-linux.so.2完成,后者則由工具軟件wine-kthread完成。后者的具體調用路徑是:
main() > wine_init() > __wine_process_init() > __wine_kernel_init() >
wine_switch_to_stack() > start_process() > LdrInitializeThunk()
? ? ? ?這在“Wine的二進制映像裝入和啟動”那篇漫談中已經講到過了。注意這里最終完成DLL連接的函數也叫LdrInitializeThunk(),顯然Wine的作者對于Windows的這一套是清楚的。
????通過以上的敘述,我們可以看到Windows的進程創建過程與Linux有較大的不同,但是裝入PE映像和實現DLL連接的過程卻與Linux的對應過程相似,只是把“解釋器”集成到了“系統DLL”里面,并且是作為APC函數執行的,其他就沒有太大的區別了。但是,如果跟Wine的PE映像裝入過程相比,則顯然Wine的過程(見“Wine的二進制映像裝入和啟動”)是比較復雜、效率也比較低的。
原文鏈接:http://blog.chinaunix.net/uid-20476365-id-1942481.html
總結
以上是生活随笔為你收集整理的Windows的进程创建和映像装入的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 超好用的刷题神器!
- 下一篇: 图灵学院Java架构师五期笔记