??
字號:
);[/code]
這個函數的作用是鎖定某個進程的某些存儲頁面(不讓換出),其輸入參數之一就是指向該進程的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中的第一個成分,二者實際上是一回事),而不會直接訪問它的具體成分。
[code]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);[/code]
此外,就像進程有“進程環境塊”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進程卻可以為別的進程分配空間?;蛘哒f,在存儲空間的分配上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的二進制映像裝入和啟動”)是比較復雜、效率也比較低的。
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -