轉自:http://www.makaidong.com/%E5%8D%9A%E5%AE%A2%E5%9B%AD%E6%96%87/71405.shtml
"C++Windows核心編程讀書筆記":
這篇筆記是我在讀《windows核心編程》第5版時做的記錄和總結(部分章節是第4版的書),沒有摘抄原句,包含了很多我個人的思考和對實現的推斷,因此不少條款和windows實際機制可能有出入,但應該是合理的。開頭幾章由於我追求簡潔,往往是很多單獨的字句,后面的內容更為連貫。
海量細節。
第1章 錯誤處理
1. getlasterror返回的是最后的錯誤碼,即更早的錯誤碼可能被覆蓋。
2. getlasterror可能用於描述成功的原因(createvent)。
3. vs監視窗口err,hr。
4. formatmessage。
5. setlasterror。
第2章 字符和字符串處理
1. ansi版本的api全部是包裝unicode版本來的,在傳參和返回是多了一次編碼轉換。
2. ms的c庫的ansi和unicode版本之間也是沒有互相調用關系的,沒有編碼轉換開銷。
3. 寬字符函數:_tcscpy,_tcscat,_tcslen。
4. unicode宏是windows api使用的,而ms的c庫中,對於非標准的東西用_前綴區分,所以_unicode宏是ms的c api使用的。
5. ms提供的避免緩沖區溢出攻擊的函數在<strsafe.h>文件中,包括stringcbcat和stringcchcat等函數(其中cb表示count of byte,cch表示count of character,都用於表示衡量目標緩沖大小的單位);另外<tchar.h>中有_tcscpy_s等_s后綴的函數。在源串過短時,<strsafe.h>的函數截斷,<tchar.h>的函數斷言。
6. 要想接管crt的錯誤處理(比如assert),使用_set_invalid_parameter_handler設置自己的處理函數,然后使用_crtsetreportmode(_crt_assert, 0);來禁止crt彈出對話框。
7. windows也提供了字符串處理函數,但lstrcat、lstrcpy(針對t字符的)已經過時了,因為沒考慮緩沖區溢出攻擊。考慮使用strformatkbsize、strformatbytesize、comparest ring(有很多比較選項)、comparest ringordinal(相當於_tcscmp)。
8. getthreadlocale返回線程的語言信息:lcid(locale id),供很多函數使用(包括使用comparest ring針對語言來比較的時候)。
9. 寬字節轉多字節widechartomultibyte,反之multibytetowidechar。其中,在寬字節轉多字節的時候,如果有unicode字符在多字節編碼中沒有對應項,那寬字節會被替換成參數lpdefaultchar,並且lpuseddefaultchar會被標記為true。當用這兩個函數計算結果串的大小時,返回的是字符數。
10. istextunicode。
第3章 內核對象
1. 簡單區分內核對象和其他對象的開發方法 :創建需要安全信息的多半是內核對象。
2. 每個進程有一個內核對象表,表的每一項是一個簡單結構,包括真實內核對象地址和訪問權限等。用戶代碼持有的內核對象句柄其實是對象表中對應項的索引。因此如果closehandle關閉一個對象后沒有清空變量,且在對象表的同樣位置恰好又創建了一個新的內核對象,對之前沒清空的無效變量的訪問會造成bug。(比如對同一個句柄多調用了一次closehandle導致另一個內核對象被關閉。)
3. 進程退出時,會釋放各種內存、內核對象、gdi對象等。
4. 跨進程使用內核對象的理由:跨進程傳輸:用文件映像對象實現共享內存、郵件槽和命名管道實現數據通信、信號量和互斥量進行同步等。
5. 跨進程使用內核對象的三種方式:對象句柄繼承、命名內核對象、復制對象句柄。
6. 對象句柄繼承:創建內核對象的時候可以指定security_attributes. binherithandle表示可繼承(任何時候可以使用sethandleinformation修改可繼承性等屬性),創建子進程時指定createprocess的參數binherithandles為true,則子進程從父進程的對象表中拷貝所有可繼承的對象到自己的對象表的相同表位置中(並增加引用計數),因為表項結構被完全拷貝且內核對象實際地址在地址空間后2g的內核地址段中,所以拷貝過來的表項完全有效,進而父子進程的可繼承內核對象的句柄值完全相同,於是只要以任何方式將要繼承的對象的句柄值跨進程交給子進程(創建子進程時的命令行參數、環境變量、共享內存、消息等手段),則后者可以使用。
7. 命名內核對象:要訪問已經存在的命名內核對象,可以使用createxxx或者openxxx,后者在對象不存在的時候返回null。如果打開了一個已經存在的命名對象,在打開時為api指定的對象名以外的參數被忽略。注意,一個進程打開同一對象兩次,除了增加引用兩次外,返回的句柄值是不同的,需要分別關閉一次,即打開和關閉完全對稱(很合理的行為)。在vista及以上的系統開發,對象名可以包括在命名空間下,避免被低授權用戶訪問。
8. 復制對象句柄:duplicatehandle。
第4章 進程
1. 進程是執行文件的運行時形態。包括兩部分:內核數據(對應內核對象)、地址空間(包括執行文件代碼和棧堆等動態內存)。
2. 把vc的“系統開發-子系統開發”值刪除掉,即不指定控制台或gui,則編譯器會根據代碼中存在main或者winmain來自動選擇子系統開發(這里不談unicode了),很方便。
3. 啟動程序:根據子系統開發執行maincrtstartup/winmaincrtstartup,在該函數中干幾件事(1)准備命令行和環境變量(用於char *argv[]和char *env[])(2)初始化crt的全局變量(包括_osver、_winmajor、_winver、__argc、_environ等)(3)初始化crt運行庫的內存分配(malloc、free)、io函數等(4)初始化全局對象調用c++構造函數。
4. 退出程序:main返回后maincrtstartup會調用exit,exit干以下幾件事:(1)執行通過_onexit注冊的函數(2)執行全局對象的c++析構函數(通過atexit注冊的)(3)判斷_crtdumpmemoryleaks設置的內存泄漏檢測標志,嘗試檢測內存泄漏(4)調用exitprocess。
5. hinstance和hmoudle完全相同,都是表示映像文件加載到內存后的基址(鏈接器中可以配置)。getmodulehandle傳入文件名可以獲得模塊基址;傳入null可以得到執行文件的hinstance(即使調用者位於某個模塊中同樣返回應用開發程序基址);getmodulehandleex可以根據函數地址得到模塊基址
6. 訪問環境變量:char *env[]參數、getenvironmentstrings、getenvironmentvariable、expandenvironmentstrings(將一個使用了類似”%userprofile%”環境變量的字符串中的變量替換成值)。
7. 系統開發環境變量:hkey_local_machine\system\currentcontrolset\control\session manager\enviroment。用戶環境變量:hkey_current_user\enviroment。
8. 修改環境變量后可以通知相關的系統開發窗口(如控制面板等):sendmessage(hwnd_broadcast, wm_settingchange, 0, (lparam) “enviroment”)。
9. 可以設置特定線程在一個cpu核心集合上執行。
10. seterrormode。設置該進程如何響應各種錯誤。
11. 關於相對路徑:在通過getenvironmentstrings返回的環境變量中,有一部分不是真正的環境變量,比如“=c:=c:\windows”“ =f:=f:\projects\test05”,他們表示一種進程相關配置“本進程在特定驅動開發器下對應的當前文件夾”。一個進程除了有以上配置外,還有一個當前驅動開發 器,最終getcurrentdirectory返回的當前路徑就是當前驅動開發 器+當前驅動開發 器對應的當前文件夾。使用setcurrentdirectory會改變該驅動開發 器的當前文件夾,還會改變進程的當前驅動開發 器(但這個api的改變並不會在getenvironmentstrings上體現出來,使用c函數_chdir可以同時改變兩者,故c函數更優)。進程剛啟動時,如果不考慮從父進程繼承的環境,則只有進程當前驅動開發 有當前文件夾,其他驅動開發 都無配置。使用相對路徑訪問文件的時候,其絕對路徑可以用getfullpathname得到。”文件名”這樣的相對路徑的絕對路徑是getcurrentdirectory() + “文件名”;”驅動開發 器盤符:文件名”(注意不是”驅動開發符:/文件名”)這樣的相對路徑的絕對路徑就是”該驅動開發 器的當前文件夾”(如果無配置,則是根目錄) + “文件名”。
看如下代碼:
_chdir("d:/downloads"); // 修改d:的當前路徑為downloads,且進程當前驅動開發 器為d:
_chdir("f:/projects"); // 修改f:的當前路徑為projects,且進程當前驅動開發 器為f:
std::ofstream("1.txt"); // 當前驅動開發 器是f:,所以絕對路徑是f:/projects/1.txt
std::ofstream("d:1.txt"); // d:的當前路徑是downloads,所以絕對路徑是d:/downloads/1.txt
這種行為從cmd的cd命令也可以看得出點端倪。
歸納:相對路徑訪問文件的時候,首先將相對路徑展開成絕對路徑,使用getfullpathname,后者分兩步:首先判斷是否包含驅動開發 器(以x:開頭),如果沒有,則在開頭添加進程當前驅動開發 器;然后檢查是否以”x:/”開頭,如果沒有,則將”x:”展開成”x:/” + “對應驅動開發 的當前文件夾”。兩步過后得到絕對路徑。
12. getversionex獲取系統開發版本信息。verifyversioninfo檢測當前系統開發是否滿足版本需要。
13. createprocess的參數:關於lpapplicationname和lpcommandline,有兩種用法:(1)前者指定應用開發程序路徑,后者指定參數(第一個參數前面要有一個空格,似乎底層會直接連接兩個串)(2)前者為null,后者指定路徑和參數,空格隔開。常用第二種開發方法 。注意,lpcommandline中由於是用空格分隔參數的,所以對其中含有空格的路徑一定要用內層引號括起來。另外createprocessw有一個奇怪的行為,它會修改參數lpcommandline(似乎只在lpapplicationname為空的時候會修改),所以使用unicode版本的時候傳入的該參數不能是常字符串(如l”nodepad 1.txt”),而應該另外准備緩沖傳給該api供其修改,因為ansi版本是調用unicode版本的且在編碼轉換的時候內置了緩沖,所以createprocessa的lpcommandline參數可以是常串(最終api會修改轉換編碼的臨時緩沖)。默認情況下,cui的cui型子進程會和父進程共享控制台,在參數dwcreationflags中添加detached_process或create_new_console標志可以阻止這種行為。在dwcreationflags中添加create_new_process_group標志,可以控制進程組的組織,用戶按下ctrl+c的時候同一進程組的所有進程得到通知。lpenvironment指定為null的時候,底層為用getenvironmentstrings來填充。lpcurrentdirectory為null的時候,子進程繼承父進程的當前目錄。lpstartupinfo不能為空,至少要初始化結構為0並將cb賦為sizeof。使用startupinfoex結構作為lpstartupinfo參數,還可以具體指定子進程要繼承哪些父進程的可繼承內核對象(即使binherithandles參數為false)。
14. cmd進程輸入命令行前顯示的路徑,就是其當前路徑(getcurrentdirectory)。在createprocess時,cmd沒有設置子進程當前路徑,而資源管理器將路徑設置成子進程鏡像目錄。因為cmd的子進程會繼承cmd的當前路徑(lpcurrentdirectory為空的結果),因此最好在用cmd啟動程序的時候先將cmd的當前路徑設置為新進程的鏡像路徑。
15. 進程和線程結束后,句柄對象被標記為激活, waitforsingleobject會返回。
16. createprocess后,可以使用waitforinputidle或類似函數來等待新進程初始化環境完畢開始運行。
17. wow64:windows 32 on windows 64。所有64位windows運行着這個虛擬機,用來執行32位程序。判斷一個32位程序是否是運行在64位系統開發的32位虛擬機中:iswow64process。
18. 父進程創建子進程時使用的lpstartupinfo,在子進程中可以使用getstartupinfo來查詢。
19. 創建一個子進程時,進程和主線程本身的存在就有了引用1,而調用createprocess的父進程又會有他們的引用所以計數到了2。要完全銷毀進程和線程,需要計數為0,所以除了需要進程本身結束外,引用的該進程的其他線程也要釋放引用。當然,createprocess過后父進程馬上closehandle並不會結束子進程,只是釋放自己的引用,使其計數為1,這是正常的行為。要確保某個進程或線程不被銷毀,不調用closehandle即可。如果進程本身已經退出了,但還有其他進程引用它,則它的地址空間被回收,只有內核對象還存在(比如這時再對句柄使用api查看內存,則內存信息為空),這也是為什么可以查看已經退出的進程的退出碼的原因(退出碼保存在內核對象中)。
20. 進程和線程的id位於同一個系統開發頂層名空間。即任意進程的任意線程id絕不可能和任意進程id相同。這個id會被系統開發循環利用。
21. getprocessidofthread。
22. 進程只有在它所有線程都結束后才會結束。exitprocess會殺死所有線程,所以可以直接結束進程,在主線程中調用exitthread只會結束主線程(即,主線程創建一個死循環線程后自己_exitthreadex,這個進程不會退出。)。main返回后crt調用exit后者再調用exitprocess,所以在main中return可以直接結束進程。
23. 通過exitprocess或exitthread(單線程時)結束進程,由於這些api比crt更底層,他們只能保證正確的釋放windows資源(內存、內核對象引用),並不保證釋放c++資源(crt底層資源、全局對象的析構函數),故一定要從main中返回自然的結束進程(其他原因在后面章節說明)。terminateprocess也出於相同的原因應該避免使用。
24. createprocess創建的子進程會繼承父進程的security token權限,而shellexecuteex可以提高子進程的權限(令lpverb參數為”runas”)。資源管理器使用前者創建子進程,所以通過它開打的程序都具有和資源管理器相同的權限。
25. 關於vista及更高系統開發的uac(user account control):vista以前的系統開發如果以管理員賬號登陸,資源管理器(explorer)會獲得一個管理員權限的security token,然后從資源管理器打開的子進程都會繼承這個最高權限,這種行為非常危險。vista以后,即使以管理員賬號登陸,資源管理器仍然只持有一個一般權限的token(filtered token),子進程如果想提升權限,有兩種途徑:(1)用戶“以管理員身份運行”啟動該進程(2)子進程自己提出請求要求用戶提升權限(子進程是安裝程序、或者子進程配置有.manifest文件說明權限需求)。另外,在很多軟件開發中出現有小盾牌圖標的按鈕,也是要求提高權限,點擊過后會結束當前進程,重啟一個高權限進程(如資源管理器中“顯示所有用戶的進程”按鈕)。其實這三種提高權限都是父進程調用了shellexecuteex。
26. isuseranadmin判斷當前用戶是否是管理員。在vista及以上的系統開發中,即使是管理員,進程也有可能因為篩選token而不具備最高權限。
27. 枚舉所有進程:process32first、process32next、enumprocesses。
28. 可以從hmoudle中讀取image_dos_header和image_nt_headers,進而從這些pe頭中取得模塊的推薦加載地址等信息。
29. peb(process enviroment block)包含了進程的啟動命令行、當前路徑等數據。該字段可以通過ntqueryinformationprocess的process_basic_information參數取得。
30. 可以通過windbg的dt命令,查看一些結構的具體成員布局,如peb等。
31. windows完整性機制(windows integrity mechanism):這是uac之外的另一套安全機制,windows通過在系統開發訪問控制表(sacl, system access control list)中增加訪問控制項(ace, access control entry)實現,每一種受保護的資源都有對應的完整性級別(integrity level),每個進程都有一個基於token計算的完整性級別,如果進程的級別小於資源的級別,則不能訪問資源。提升token權限之前的進程級別為中,提升后為高,而像ie這樣可以能執行網絡代碼的進程為低。可以通過gettokeninfomation查看一些和完整性級別相關的策略。窗口系統開發也根據完整性級別,拒絕低級別者向高級別使用postmessage、sendmessage等api。
32. vista以上有一些進程是特殊的受保護進程,toolhelp api對他們無效,因此無法查看進程信息。
33. getprocesstime查看進程時間,getprocessiocounters查看io次數。
34. getprocessimagefilename返回內核格式的文件名。
第5章 作業
1. job(作業),也就是進程組的概念,添加進同一個作業的進程能夠通過作業內核對象來集中控制,設置一些額外的屬性等。添加進一個作業就不能再移出。
2. isprocessinjob、createjobobject、openjobobject。
3. 作業內核對象在它內部的所有進程都結束后才會被銷毀。
4. 細節:當客戶的作業句柄變量都被關閉后,即使作業對象還存在
5. vista以上,通過任務管理器創建的進程,都被添加進了一個獨立的作業;從命令行(cmd)創建的進程則不然。
6. 能夠對作業添加的限制:基本限制(限制進程時間、優先級、物理內存占用等)、擴展限制(基礎限制之上,還能限制內存使用總量,以及查看峰值內存使用)、ui限制(限制關機/重啟、訪問剪切板、切換桌面、改變顯示器設置、訪問作業外進程的句柄等)、安全限制(安全限制一旦設置,則不能修改)。setinformationjobobject、queryinformationjobobject用於設置和查詢限制。
7. assignprocesstojobobject添加進程到作業。
8. 父進程位於某一作業中,子進程創建后也自動加入同一作業。除非作業的基本限制中包含job_object_limit_breakaway_ok(允許進程時脫離作業),並且createprocess時指定create_breakaway_from_job標記。
9. terminatejobobject強制結束作業,同時結束作業內所有進程(等價於對作業內每個進程調terminateprocess)。
10. queryinformationjobobject除了查看作業限制外,也可以查看作業信息,包括總進程數、活躍進程數、總時間、總io次數、進程id列表等。
11. 作業結束后(所有內部進程結束),內核對象處於激活態,waitforsingleobject返回。
12. 作業通知機制:將作業對象和io完成端口綁定,作業中的事件(進程結束、時間到期、內存達到限制等)將通過完成端口事件來通知。
第6章 線程基礎
1. 像進程一樣,線程在數據上也分為兩個部分:線程內核對象(包括統計信息)、棧。(進程的兩個部分是,內核對象和地址空間)。
2. 比起exitthread和terminatethread,應該讓線程的主函數返回來結束線程,否則一些棧對象不能正常析構(這里不再考慮crt函數)。
3. 在c/c++編程中不要使用createthread、exitthread,應該使用編譯器廠商提供的包裝函數,如ms的_beginthreadex、_endthreadex。因為使用前者,c/c++的crt不能正常初始化和釋放線程相關資源(c/c++中有一些全局變量如errno和一些有內部狀態的函數strtok、asctime都需要通過tls來正確實現,畢竟c庫函數的誕生早於多線程)。事實上,如果在c/c++中使用了createthread和endthread,部分有內部狀態的函數還是可以正常使用的,因為這些函數內部會嘗試取得tls,發現還未分配的話會自動分配,crt的dll版本庫也會在得到線程退出通知時嘗試釋放tls,只是因為這份tls是中途分配的信息不夠全面,部分狀態函數還是會有問題,因此在c/c++中還是要盡量使用后者。
4. 線程棧最大為createthread的dwstacksize參數和/stack鏈接選項(vc中默認為1mb)兩者中的較大值。
5. terminatethread的一些細節:該函數是異步的,函數返回時,線程還沒有結束,需要waitforsingleobject;dllmain不會收到被terminate線程的結束通知。
6. 只有當線程函數結束(正常返回或exit掉)后,該線程的棧空間才會被回收(也就是說terminatethread函數剛返回時被殺死線程棧空間還在,直到線程對象處於激活態)。
7. 對進程中的各個線程來說,exitprocess和terminateprocess都將導致對線程的terminatethread調用,因此進程的main函數結束前,盡量確保工作線程都正常退出。
8. 大部分的資源都是進程相關的,窗口句柄和hook句柄是線程相關的,線程退出時會釋放他們(在c/c++中還有crt的tls變量)。
9. getcurrentprocess、getcurrentthread返回的都是偽句柄,如果想要把這個句柄保存下來在其他線程、進程中使用的話,是有歧義的,可以用保存id來代替,如果一定要保存句柄的話,兩種開發方法 :(1)duplicatehandle(2)先getcurrentthreadid,再openthread。
第7章 線程調度、優先級和關聯性
1. windows線程調度的時間間隔(發生上下文切換的時間片)大概是15毫秒(getsystemtimeadjustment的lptimeincrement參數)。
2. 每個線程都有一個掛起計數,當計數非0的時候,該線程不參與線程調度。createthread、createprocess傳入特定的參數可以使計數初始化為1。suspendthread可以增加計數,resumethread可以減少計數,兩者都返回新的掛起計數。顯然線程無法對自身調用resumethread。
3. 調試進程的waitfordebugevent返回后,被調試進程的所有線程被掛起,直到調試進程調用continuedebugevent。
4. sleep的休眠時間可能不精確,取決於線程調度時間片大小(一般是15毫秒左右)以及其他線程的運行情況。
5. sleep(0)和switchtothread的區別在於:如果存在另一個更低優先級的線程,前者不會將cpu讓出,而后者會。即如果存在多個線程,switchthread總是讓出cpu。
6. yieldprocessor用於支持超線程技術的cpu切換超線程。
7. getthreadtimes、getprocesstimes返回指定線程或進程的內核代碼時間和用戶代碼時間(兩者都是絕對的cpu執行代碼時間,不包括調度開發過程 中的中斷時間以及主動的sleep或者wait時間)。因此在對代碼段計時的時候,使用getthreadtimes明顯優於gettickcount等,因為后者得出的時間包括了其他線程的時間片。
8. 用於計時,最基本的有clock、gettickcount、timegettime等;為了地提高精度,可以使用queryperformancecounter;為了去掉因線程調度中斷的時間和sleep、wait的時間,可以使用getthreadtimes、getprocesstimes等。在vista以上的系統開發中,有新的機制,可以使用readtimestampcounter(對應gettickcount)、querythreadcycletime(不考慮中斷休眠,對應getthreadtimes)、queryprocesscycletime等。對於沒有考慮線程調度影響的函數,可以先用setthreadpriority提高優先級盡量獨占時間片。應該確保每次調用queryperformancecounter的時候在同一cpu核心上,使用setthreadaffinitymask。
9. 線程上下文(context)保存在線程的內核對象數據中,主要包括線程相關的cpu寄存器狀態等。上下文有兩份,分別記錄內核和用戶模式,getthreadcontext只能返回用戶模式上下文,在調用該函數前應該確保用戶上下文不再改變了,即線程正處於內核態或者雖然在用戶態但已經調用過suspendthread。
10. 先suspendthread、再setthreadcontext改變線程上下文,可以改變執行流等,一般用於調試器 “跳到指定位置執行” 的功能等。
11. 高優先級線程可以被調度時(沒有sleep、wait等),低優先級線程得不到時間片;即使低優先級線程正在執行,一旦有高優先級線程可以調度,前者會被中斷並讓出cpu資源。
12. setpriorityclass設置進程的優先級類,setthreadpriority設置線程的相對優先級(相對於進程優先級類),二者共同決定線程的實際優先級(這個映射根據windows版本不同而異,是一個0~31的整數,用戶不可訪問)。將線程的實際優先級設置為最高(31)是危險的,因為它將搶占系統開發資源,導致io不能響應等。
13. 當線程有io事件或消息到來時,操作系統會暫時提高線程的優先級;或者線程可調度但長時間(數秒)都得不到時間片的時候,系統開發也會暫時提高線程優先級。可以設置是否允許系統開發自動提升優先級:setprocesspriorityboost、setthreadpriorityboost。
14. 特定類型計算機的幾個相關cpu核心之間可以共享內存緩存等,因此windows支持設置線程關聯cpu核心setprocessaffinitymask、setthreadaffinitymask。當然這組api也可以用於為特定線程提供專用cpu資源以提高性能開發。子進程默認繼承父進程的核心關聯設置。
15. setthreadidealprocessor設置線程最多可以使用的閑置cpu數量。該設置會覆蓋affinitymask。
16. 進程的默認affinitymask可以在鏡像文件頭中設置(因為沒有鏈接選項只有手工寫文件):imageload->getimageconfiginformation->ilcd.processaffinitymask->setimageconfiginformation->imageunload。
第8章 用戶模式下的線程同步
1. interlocked系列函數:interlockedincrement(對應++)、interlockedexchangeadd(對應+=)、interlockedexchange(對應=)、interlockedcompareexchange(cas)。
2. _aligned_malloc可以指定分配內存的對齊邊界。
3. spinlock(自旋鎖)是cas的應用開發。使用自旋鎖的時候因為有while(true) { …; sleep(0); }這樣的循環,因此線程優先級不能太高,使用setthreadpriorityboost來禁用優先級提升,避免被自動提升后不會讓出cpu(或者使用switchtothread)。自旋鎖適用於單個線程不會占用資源太久的情況(因為一個線程占有資源期間,其他線程在循環檢測浪費cpu)。
4. cas(interlockedcompareexchange)必須是原語!必須!用c++編寫的cas是不行的。
5. initializeslisthead、interlockedpushentryslist、querydepthslist等api可以以interlocked的方式操作一個單鏈表。
6. cacheline:是cache和內存通信的基本單位,可能是32/64字節等,cpu讀寫內存的時候會先將對應的cacheline加載進cache,修改完成后flush到內存上。因此數據組織為cacheline size對齊、以及將只讀和讀寫數據分別組織到不同的cacheline都能提高效率。多個cpu(或者具有獨立cache的多個cpu核心)訪問同一地址時,該地址附近的數據會被多個cache映射成各自的cacheline,如果其中某個cpu修改了其cacheline的數據,該cpu會通知其他cpu更新各自的cacheline,這種行為會影響性能,故盡量避免跨線程共享數據以及利用affinitymask盡量使用同一個cpu。
7. getlogicalprocessorinformation提供cpu描述信息(比如能夠查詢到包括4個cpu核心,3級cache,1、2級cache為各個核心獨有,3級cache為共享cache,其cache line size為64字節等)。
8. 所有線程都處於等待狀態數分鍾后,電源管理器介入。
9. volatile的作用:編譯器不會將變量優化成寄存器變量,即每次讀寫都會訪問內存。對struct應用開發該關鍵字會影響每個字段。
10. critical_section內部記錄了擁有訪問權的線程以及引用次數。tryentercriticalsection如果返回true,則已經增加了計數需要對稱調用leavecriticalsection。
11. critical_section在實現上結合了spinlock(自旋鎖),調用entercriticalsection時發現資源正被占用需要切換到內核態休眠之前(切換到內核態開銷很大,高達數千cpu周期),可以嘗試進行一定次數的循環判斷。使用initializecriticalsectionandspincount可以啟用結合自旋鎖功能(作為參考,用於保護進程堆的cs的spincount為4000),使用setcriticalsectionspincount可以修改旋轉次數。當spincount為1的時候,關鍵段內部用於休眠和喚醒的事件對象會第一時間創建,而不是等到entercriticalsection的時候才創建。建議總是啟用自旋鎖。
12. slim reader/writer lock是性能比關鍵段更好的選擇,相比后者,它的缺陷是不能遞歸加鎖、且沒有trylock。initializesrwlock、acquiresrwlockshared(申請讀鎖)、acquiresrwlockexclusive(申請寫鎖)。
13. 在都能完成任務的情況下,性能從高到底依次是:無鎖、volatile、interlocked、srw、critical_section、內核對象(因為切換到內核態開銷很大)。
14. sleepconditionvariablecs、sleepconditionvariablesrw用法:已經獲得鎖(cs、srw)的線程開始在一個conditionvariable對象上睡眠,同時釋放鎖;如果其他線程wakeup這個conditionvariable對象,則函數返回true,且再度獲得鎖;如果超時,返回false,不會獲得鎖。應用開發:消費者獲得鎖后發現沒有產品於是開始休眠等待生產者產出產品后喚醒。
15. 技巧:按資源的邏輯個數而不是對象個數來組織鎖;需要加多層鎖的時候,總是按固定順序,比如按鎖的地址大小來依次加鎖,避免死鎖;通過拷貝資源等方式來減小鎖粒度。
第9章 用內核對象進行線程同步
1. 內核對象用於線程同步更靈活比如可以設置等待時間以及跨進程等,但開銷更大(需要切換到內核模式)。
2. 內核對象中都有一個表示觸發狀態的boolean值。
3. 進程和線程對象在結束前是非觸發,結束后是觸發狀態,其他時候不會再改變。
4. 文件對象有正在處理的異步io請求時處於非觸發,其他時候觸發。
5. 控制台輸入句柄在沒有輸入的時候非觸發。
6. 內核對象觸發后,wait在上面的線程被喚醒,決定哪一個線程首先被喚醒的規則基本上就是等待順序的先入先出,和線程的優先級等無關。
7. pulseevent會在event對象上產生一個觸發脈沖。近似於setevent(h);resetevent(h);兩句。
8. waitabletimer在平時處於非觸發,第一次時間到或者之后周期性時間到都會處於觸發狀態。另外在setwaitabletimer的時候可以傳入回調指定在觸發的時候往apc(asynchronous procedure call)隊列中加入回調,但必須定時器觸發時線程正處於alertable(使用sleepex等帶ex的api)狀態下才會入隊列(避免因為回調處理太慢及其他因素導致過量入隊)。一般定時器的apc和waitfor兩種模式不混用。setwaitabletimer指定第一次的時間時,正數表示絕對時間(systemtimetofiletime得到),負數表示相對時間。每次調用setwaitabletimer會自動取消上次調用的設置,故兩次調用間不必cancelwaitabletimer。該定時器和基於消息的settimer定時器建議適時選用。
9. semaphore的當前計數非0時處於觸發。releasesemaphore增加計數發現達到最大時會返回false,waitfor減少計數到0的時候會休眠。
10. mutex和criticalsection在使用上完全相同,都記錄了owner線程和遞歸次數。由於criticalsection和mutex記錄了owner線程,因此需要該線程來釋放計數,如果在計數減少到0前線程退出了,則同步對象處於abandoned(遺棄)狀態。對於abandoned的情況,系統開發能檢測到發生在mutex上的問題,並在底層自動釋放計數,只是waitfor會返回wait_abandoned表示mutex對象的計數是由系統開發自動回收的,該mutex保護的資源可能處在未定義狀態。而cs的計數不會被自動釋放,一旦abandoned則cs永遠的失效了。
11. waitforinputidle:進程中創建第一個窗口的線程的消息隊列中沒有需要處理的輸入消息后返回。
12. msgwaitformultipleobjects:等待的內核對象觸發后或者線程的消息隊列中有相應消息后返回。
13. signalobjectandwait增加一個對象計數的同時原子地等待另一個對象。能夠增加計數的對象只限於event(setevent)、mutex(releasemutex)、semaphore(releasesemaphore),而等待的對象類型不限。使用:客戶端填充好請求於是通知服務端准備處理並等待服務端處理完畢。
14. 在vista以上可以通過wct(等待鏈遍歷,wait chain traversal)相關api來追蹤死鎖。openthreadwaitchainsession、getthreadwaitchain。
第10章 同步設備i/o與異步設備i/o
1. 打開設備的方式:文件-createfile,參數時路徑名或unc路徑名。目錄-createfile,參數為路徑名或unc路徑名,另外指定file_flag_backup_semantics允許改變目錄屬性。邏輯磁盤驅動開發 器-createfile,參數為””” \\.\x:”,打開后可以格式化和檢測大小等。物理磁盤驅動開發 器-createfile,參數為””” \\.\physicaldrivex”,(其中x為012等)。串口-createfile,參數為”” comx”。並口-createfile,參數為”” lptx”。郵件槽服務器開發-createmailslot,參數為”\\.\mailslot\abcd”。郵件槽客戶端-createfile,參數為””\\servername\mailslot\abcd””。命名管道服務器開發-createnamedpipe,參數為”\\.\pipe\abcd “。命名管道客戶端-createfile,參數為””\\servername\pipe\abcd “。匿名管道-createpipe。套接字-socket、accept、acceptex。控制台-createconsolescreenbuffer、getstdhandle。前面的設備路徑規則:””””\\服務器開發\設備”,其中如果在本機的話,服務器開發就是”” .”。
2. setcommconfig可以設置串口波特率等屬性。
3. setmailslotinfo可以設置超時。
4. 一般用closehandle關閉設備。closesocket關閉套接字。
5. getfiletype可以返回設備的類型:file_type_disk-磁盤文件;file_type_char-字符文件,包括控制台和打印機等;file_type_pipe-命名管道或匿名管道。
6. 多次createfile打開同一個文件得到的是不同的內核對象,各自維護自己的文件指針等數據; duplicatehandle得到的多個句柄仍然標志的是同一個對象。
7. createfile的dwsharemode參數:0表示獨占,如果文件已經被打開,則本次打開失敗;如果本次打開成功,在關閉前不能在其他地方打開同一個文件。file_share_read,如果本次打開前已經有寫句柄,本次打開失敗;如果本次打開成功,在關閉前在其他地方不能打開寫句柄。file_share_write也類似。file_share_delete表示,如果本次打開成功,其他地方又刪除了文件,則刪除時只是打上刪除標記,待這里的句柄關閉后才真正刪除。
8. createfile的dwflagsandattributes參數:(1)關於內置緩沖。內置緩沖至少有兩個作用,首先,加速,頻繁的小字節塊訪問會被緩沖為少數大字節塊的設備讀寫;其次,最底層設備訪問需要按一定的字節塊對齊(文件無緩沖讀寫需要按磁盤扇區大小對齊),緩沖屏蔽了這個限制,方便上層使用。file_flag_no_buffering,底層不提供緩沖,需要上層自己提供緩沖,緩沖區首地址、文件讀寫偏移/指針、讀寫字節數三者都必須按磁盤扇區大小對齊(扇區大小可以通過getdiskfreespace獲得,比如512字節)。文件太大有可能打開失敗,也需要指定這個標記。當有緩沖時,file_flag_sequential_scan承諾會連續訪問(不會用setfilepointer),因此底層可以嘗試緩沖更多連續內容;file_flag_random_acess表示會隨機訪問,因此底層會盡量不要緩沖太多(緩沖的作用還剩下避免要求扇區對齊)。file_flag_write_through,表示寫文件不使用緩沖,這樣避免在數據flush到文件前對象就被非法關閉導致數據丟失。(2)其他標志。(1)file_flag_delete_on_close,關閉文件的時候刪除,適合臨時文件。file_flag_overlapped異步io。
9. createfile的dwflagsandattributes參數:只在創建文件的時候有效,用於指定archive、encrypted(加密)、hidden、readonly、system、temporary等屬性
10. createfile的hfiletemplate參數:只在創建新文件時有效,傳入另一個文件句柄的話,系統開發會忽略dwflagsandattributes參數和直接使用該句柄對應的dwflagsandattributes。
11. file_attribute_temporary和file_flag_delete_on_close標記結合適用於臨時文件,前者會讓系統開發盡量將文件維護在內存而不是磁盤中,后者會在關閉句柄時刪除文件。
12. 獲取文件大小:getfilesizeex、getcompressedfilesize(尤其針對壓縮屬性的文件)分別返回邏輯大小和磁盤上的實際大小。
13. setfilepointerex可以超出文件實際大小,超出后,除非寫文件或者setendoffile否則文件不會變大。
14. setendoffile是減小文件的唯一手段。
15. flushfilebuffers。
16. 在vista以上,可以用cancelsynchronousio來中止一個線程的同步io。
17. 異步io的實際訪問設備順序不一定和請求順序(api調用順序)相同(比如驅動開發 會根據磁盤磁頭位置選擇先處理距離最近的io請求)。
18. 對異步io的文件發出io請求有可能是同步操作,因為可能數據正好在底層緩沖中可以立即完成。
19. 關於取消異步io請求:(1)cancelio取消調用線程在指定設備上的異步io請求。(2)線程結束會取消該線程的所有異步請求。(3)關閉設備會取消所有該設備的請求。(4)cancelioex能取消調用線程以外線程在指定設備上的特定請求。(5)cancelioex能取消特定設備的所有請求。
20. overlapped結構的internal表示錯誤碼,internalhigh表示傳輸的字節。由於異步io跟文件指針無關(文件指針來不及修改),所以偏移存儲在該結構中。
21. getoverlappedresult函數實現為,訪問結構的internal、internalhigh字段,另外如果結構的hevent為空嘗試wait設備否則wait事件(函數參數bwait為true的時候)。
22. queueuserapc向線程的apc隊列拋出一個用戶自定義函數。
23. queueuserworkitem向線程池拋出任務。
24. 異步io有四種方式得到完畢通知:(1)設備內核對象觸發。(2)overlapped的hevent內核對象觸發。(3)apc回調(readfileex)。(4)io完成端口。
25. 異步io-設備內對象觸發:對file_flag_overlapped的文件使用readfile,將overlapped的hevent設置為空,io完成時設備句柄將觸發,因此只能同時進行一次io(瓶頸)。可以一個線程請求,另一線程響應完成。
26. 異步io-事件內核對象的觸發:將overlapped的hevent設置為事件以獲得通知。可以用setfilecompletionnotificationmodes來避免io完成時去觸發設備對象。可以一個線程請求,另一線程響應完成。
27. 異步io-apc隊列:readfileex后使用sleepex等讓線程進入alertable狀態。同一個線程發出請求和響應完成(瓶頸)。
28. 異步io-io完成端口:步驟(1)createiocomplitionport創建完成端口,指定活躍線程數(建議為cpu核心數)。(2)用createiocomplitionport向完成端口添加異步設備。(3)創建完成端口服務線程(建議為cpu核心*2個,或者動態估計),初始化后使用getqueuedcompletionstatus使線程和完成端口綁定並休眠。(4)執行異步io,io完成后底層會用postqueuedcompletionstatus令正在getqueuedcompletionstatus上休眠的服務線程蘇醒響應。細節:可以在overlapped的hevent指定一個值為hevent | 1的數,令io完成后不發出完成通知(即不post)。可以使用getqueuedcompletionstatusex來一次響應多個請求。完成端口服務線程中,使用getqueuedcompletionstatus休眠的線程叫等待線程,從getqueued…返回的線程叫釋放線程(活躍線程),活躍線程如果因其他原因(如sleep、wait)再掛起叫暫停線程,完成端口能夠檢測到各個線程的數量,會控制getqueuedcompletionstatus的返回以使活躍線程盡量逼近創建完成端口時指定的數目。默認情況下異步io即使同步完成,也會post…,可以使用setfilecompletionnotificationmodes來禁用post…。對於完成事件的響應是先入先出的,但服務線程的激活卻是后入先出的(盡量激活相同線程,其他線程長期休眠其棧內存可以換出到頁面文件提高性能開發)。
第11章 線程池的使用(第4版)
1. messagebox彈出的對話框是可用修改的,findwindow找到后,0x0000ffff是靜態文本框的控件id等,因此很容易實現倒計時自動關閉的消息框。
2. 從win2000開始提供的線程池主要有4種用法:(1)異步調用函數(queueuserworkitem)。(2)定時器回調(createtimerqueuetimer)。(3)內核對象觸發后回調(regis地理信息系統 terwaitforsingleobject)。(4)內置iocp實現(bindiocompletioncallback)。
3. 線程池模塊下有幾種底層線程:(1)可變數量的長任務線程,用於執行標記為wt_executelongfunction的長時間回調。(2)1個timer線程。所有createtimerqueuetimer調用都被轉發為在timer線程上創建以apc方式通知的waitabletimer,這個線程除了刪除和創建waitabletimer外,就是在alertable態下休眠等待定時器的apc。由於這個線程一旦創建就貫穿進程生命期不會銷毀,因此wt_executeinpersistentthread標志的線程池回調也由本線程執行。(3)多個wait線程。服務於regis地理信息系統 terwaitforsingleobject,每個線程用waitformultipleobjects等待最多63(maximum_wait_objects減去一個用於維護對象數組的工作對象)個內核對象,對象觸發后執行回調。(4)可變數量的io線程。由於發出異步io請求(readfileex)后,一旦請求線程結束,請求將被撤銷,因此請求被驅動開發 執行完畢之前io請求線程一定要存在,而線程池內的線程大都會根據cpu繁忙情況動態創建和刪除,因此線程池中有一部分線程被賦予了特殊行為,他們會檢測自己執行回調時發出的異步io請求是否完成,如果沒有,就不會結束運行,這些追蹤自身發起的異步io請求執行情況的特殊線程叫做io線程。因此只能在線程池的io線程上執行異步io調用。(5)可變數量的非io線程。線程池內部實現了一個io完成端口,服務於bindiocompletioncallback,其中iocp的服務線程(在getqueuedcompletionstatus上休眠)由於數量會根據cpu情況動態調整,不應用開發於執行異步io,故叫非io線程。
4. 四種用法中,如果flags參數指定的回調執行線程與默認線程不符,底層可以使用queueuserworkitem來切換線程。比如createtimerqueuetimer用法的默認線程肯定是timer線程,發現wt_executelongfunction標記后,使用queue…來切換到專門執行長任務的線程避免阻塞timer線程影響定時器功能。
5. 用法1-異步函數調用:queueuserworkitem 。flags參數為0(wt_executedefault)的時候回調交給非io線程執行(通過postqueuedcompletionstatus通知非io線程)。還可以指定wt_executeiniothread交給io線程、指定wt_executeinpersistentthread交給timer線程、指定wt_executelongfunction交給長任務線程等。
6. 用法2-定時器回調:createtimerqueue-創建專用timerqueue。deletetimerqueueex-刪除專用timerqueue,參數completionevent是用於接受刪除queue完畢通知的事件對象,如果設置為null表示不接受通知,設置為invalid_handle_value表示阻塞等待刪除完成。注意不能在timer線程上的回調中以invalid_handle_value為參數調用deletetimerqueueex,因為后者實現為向timer線程拋出一個要求維護timer列表的apc,在線程的apc回調中拋出新的apc並且還阻塞等待,結果就是死鎖。createtimerqueuetimer-創建具體的timer對象,timerqueue參數指定為null表示在默認的queue上創建對象,適用於timer對象不多的用法。使用wt_executeintimerthread標記即要求在timer線程上執行回調,因不必切換線程效率較高,注意回調不能過長影響timer線程的功能。changetimerqueuetimer-改變timer對象的一些參數。deletetimerqueuetimer-刪除timer對象,注意使用invalid_handle_value參數造成死鎖的可能。
7. 用法3-等待內核對象觸發回調:regis地理信息系統 terwaitforsingleobject-在內核對象觸發或超時后執行回調。標記wt_executeinwaitthread表示在wait線程上執行,效率較高。wt_executeonlyonce只執行一次回調,適用於進程/線程句柄這種觸發后不再重置的對象。pulseevent的脈沖可能不會被wait線程檢測到(線程剛好在干其他事)。unregis地理信息系統 terwaitex-取消回調,注意invalid_handle_value參數可能的死鎖。
8. 用法4-內置iocp實現:bindiocompletioncallback。將異步io設備和內置的io完成端口管理起來,異步完成后執行回調。標志只能為0,默認在非io線程(iocp的服務線程)上執行,如果需要切換線程,手工queueuserworkitem。
第11章 windows線程池
1. visita以上的新線程池框架下四種用法:(1)異步調用函數(trysubmitthreadpoolcallback、createthreadpoolwork)。(2)定時器回調(createthreadpooltimer)。(3)內核對象觸發后回調(createthreadpoolwait)。(4)內置iocp實現(createthreadpoolio)。
2. 新線程池的實現包括iocp。
3. 用法1-異步函數調用:trysubmitthreadpoolcallback-通過iocp的post…提交一個回調到線程池。使用work對象允許一次創建多次提交效率更高:createthreadpoolwork、submitthreadpoolwork、waitforthreadpoolworkcallbacks、closethreadpoolwork。其中waitfor可以等待所有提交項被執行完畢,或者取消掉進入隊列但還沒開始執行的項。注意不應該在回調中waitfor,可能死鎖。
4. 用法2-定時器回調:createthreadpooltimer、closethreadpooltimer -創建/刪除。setthreadpooltimer-設置timer參數。起始時間為-1表示立即開始。如果將起始時間設置為null,表示停止timer,停止后用isthreadpooltimerset判斷返回false。另外mswindowlength表示允許回調觸發時間有一個向后的波動(0~mswindowlength),這樣底層可以在這個波動范圍內將多個回調連續執行,避免多次wait和wakeup(比如timer a、b分別在5、6秒后執行,a的波動為2秒,這樣系統開發可以連續執行a、b回調,不必在兩者之間插入sleepex導致額外的線程切換開銷)。
5. 用法3-等待內核對象觸發回調:createthreadpoolwait、closethreadpoolwait、waitforthreadpoolwaitcallbacks類似前面。setthreadpoolwait指定要等待的內核對象,每次調用只會導致執行一次回調,除非再set…(即如果wait進程句柄,進程結束后只會執行一次回調,想要多執行需要再調用set…)。pulseevent的脈沖有可能不會觸發回調。
6. 用法4-內置iocp實現:createthreadpoolio、closethreadpoolio同前面。每次異步io請求之前(readfileex)需要調用startthreadpoolio。發出io請求后停止回調用cancelthreadpoolio。
7. 對於新線程池回調中的參數ptp_callback_instance,可以執行一些操作:leavecriticalsectionwhencallbackreturns、releasemutextwhencallbackreturns、releasesemaphorewhencallbackreturns、seteventwhencallbackreturns-這些函數都近似等價於在回調的最后一行釋放相關資源(模仿raii?),不過以上api只有最后一次調用有效(即只能注冊一個資源)。freelibrarywhencallbackreturns-回調返回后釋放某個dll,當回調代碼本身位於要釋放的dll中時有價值。callbackmayrunlong-通知線程池回調可能執行較長時間,返回true表示當前線程池有空閑線程,否則表示線程池緊張,建議將剩余執行任務拆分以減少回調時間。disassociatecurrentthreadfromcallbacks-一般回調返回后,回調就和執行線程解除關系了,那些waitforthreadpool…callbacks就能返回,而這個disassociate函數就是為了在回調結束前提前打上脫離關系的標記,影響包括waitforthreadpool…的函數等。
8. 定制私有線程池:createthreadpool、closethreadpool、setthreadpoolthreadmaximum、setthreadpoolthreadminimum-創建線程池對象,設置線程數量范圍。注意如果數量上下界相同,那么在線程池中的線程一旦創建就不會銷毀,可以用來進行異步io調用等。initializethreadpoolenviroment、destroythreadpoolenviroment-構建環境。setthreadpoolcallbackpool-將線程池對象置入環境。setthreadpoolcallbackrunslong-標記環境對應的線程池用於執行長任務。setthreadpoolcallbacklibrary-標記環境對應的線程池中有任務執行期間,該dll一直在內存中。
9. 線程池清理組(cleanupgroup):一個waitforthreadpool…callbacks+closethreadpool…的可選替代方案。createthreadpoolcleanupgroup、closethreadpoolcleanupgroup-創建/刪除。setthreadpoolcallbackcleanupgroup-將清理組置入環境。closethreadpoolcleanupgroupmembers-用來在線程池關閉前清理資源,一旦調用該函數就不必再 “遍歷每種資源(work、timer、wait、io)依次調用waitforthreadpool…callbacks、closethreadpool…”,即該函數調用后,所有以前的線程池組件都被銷毀了,句柄也失效。如果該函數的bcancelpendingcallbacks參數為true,那些還在線程池中排隊的任務直接取消不再執行,但會通過setthreadpoolcallbackcleanupgroup注冊的函數通知每個被直接取消掉的任務。
第12章 纖程
1. 纖程其實就是windows在用戶模式實現的協程(coroutine)。
2. 將線程自身轉化為纖程:convertthreadtofiber-它會創建相應的結構保存當前線程的各種寄存器等數據。convertthreadtofiberex-默認的結構中是不包含浮點寄存器的,使用這個api傳入fiber_flag_float_switch可以保證浮點運算正確。convertfibertothread-當不使用纖程后,應該用這種方式還原為線程。
3. createfiber、createfiberex:創建一個包括獨立棧和寄存器記錄結構的新纖程,后一個函數能夠指定初始化的棧物理內存、虛擬內存以及浮點寄存器支持標志。不使用這種纖程后,在其他纖程中使用deletefiber來結束create出來的纖程。
4. 從纖程函數中返回會結束當前線程(當然也結束該線程上所有其他纖程)。
5. switchtofiber-切換纖程。
6. fls支持(fiber local storage):flsalloc-可以指定一個回調,這個回調在flsfree或纖程銷毀時以flsgetvalue的返回值為參數被執行,可用於清理等。flsgetvalue、flssetvalue。
7. isthreadafiber-判斷當前是否在某個纖程的上下文中執行。getcurrentfiber-返回當前纖程上下文。getfiberdata-返回當前纖程主函數的參數。
第13章 windows內存體系結構
1. 在32位系統開發上,虛擬地址空間大致分為4段(64位系統開發也分為4段,只是大小不同):(1)0x00000000~0x0000ffff,空指針賦值區,輔助調試,禁止任何方式的訪問。(2)0x00010000~0x7ffeffff,用戶模式分區,各進程單獨維護,同一地址值在不同的進程可以有不同解釋,各種映像文件(dll、exe)和內存映射文件也載入本區,近2g。(3)0x7fff0000~0x7fffffff,64k禁入分區。(4)0x80000000~0xffffffff,內核模式分區,系統開發存放內核代碼、設備驅動開發 代碼、輸入輸出高速緩存、進程頁表等,2g。
2. 32位系統開發可以配置系統開發參數讓進程用戶模式分區達到3g,內核減小為1g。內核內存減小,會影響可以創建的總線程、內核對象數量等。(visita系統開發以上,使用bcdedit /set increaseuserva 3072;xp使用…)。
3. 鏈接選項-啟用大地址(/largeaddressaware):因為過去32位系統開發用戶地址空間固定為2g(直到可以設置用戶地址最大到3g),所以有慣用法依賴於這種行為(系統開發對地址參數會先&0x7fffffff的行為)擅自將地址最高位用於其他目的,為了兼容大量的這種用法並且又允許選擇使用3g用戶內存,ms增加了這個鏈接選項。如果開啟,表示承諾不使用最高位,想要訪問超過2g的用戶地址;關閉,表示只使用2g內存,最高位可能有其他解釋(在實際的系統開發實現上,如果用戶地址最高位非0會報錯)。64位系統開發中,為了便於大量32位程序向64位移植(32位程序中有大量用法如:int i = (int)p; …; int *p = (int)i;),系統開發默認程序只使用2g用戶空間,所以分配的用戶地址總是小於2g,直到開啟該連接選項。總之,無論32位或64位系統開發,如果只使用2g,關閉選項,否則開啟。
4. virtualalloc的mem_reserve參數表示要預定一段空間(如線程棧,即使大部分時候棧都很小,但也需要預留1m左右),叫區域(region)。用戶代碼申請預留的起始地址必須按allocation granularity(分配粒度,因cpu而異,但當前cpu大都為64kb)對齊,系統開發的預留申請無限制(如peb占用的內存是系統開發申請的)。預留的大小必須按頁面大小對齊(x86、x64cpu的頁面大小為4kb)。virtualalloc的mem_commit參數表示將區域commit給虛擬存儲器,系統開發會在使用時將對應的頁緩存到物理存儲器。
5. 在操作系統內存管理模型中,虛擬地址用於訪問虛擬存儲器,后者存放於磁盤上,主存作為虛擬存儲器和cpu之間的緩存(dram)被叫做物理存儲器。當cpu要訪問內存時,首先,檢查該虛擬地址是否對應合法的虛擬存儲器(是否commit),如果否則報錯表示無效地址,如果是,然后判斷該虛擬頁(vp,virtual page)是否被緩存到內存,即是否有對應的物理頁(pp, physical page),如果否則產生缺頁錯誤(page fault)進而判斷主存中是否有閑置頁面,如果沒有閑置頁面,則嘗試釋放一個物理頁,先判斷要釋放的物理頁是否被修改,如果被修改了則flush到對應的虛擬頁上然后釋放物理頁,有了閑置的物理頁后,將虛擬地址對應的虛擬頁緩存到空閑的物理頁上進而更新虛擬地址到物理地址的映射表,然后cpu的mmu(memory management unit,內存管理單元)將虛擬地址翻譯為物理地址,再判斷該地址對應的內容是否已經在cache上,如果否則cache miss然后再將對應的cache line緩存到cache中,最后讀取到cpu寄存器中。在windows中,虛擬存儲器對應的磁盤空間進一步細分到頁交換文件(page file)、映像文件(exe、dll)、內存映射文件(mapped file)中,后兩者被當做虛擬存儲器的時候還可以在多個進程間共享(寫時拷貝),由於存在共享機制因而windows的虛擬存儲器占的磁盤空間遠小於所有進程提交的用戶模式內存之和。
6. virtualalloc、virtualprotect等函數可以設置頁保護屬性:page_execute(只能運行代碼不能讀寫)、page_execute_read(只讀和運行代碼)、page_noaccess等。其中page_writecopy、page_execute_writecopy屬性表示頁面可以被多個進程共享,直到被修改,修改時是先拷貝到進程私有頁中再修改私有頁,這是copy-on-write。reserve狀態下的保護屬性會被commit下的屬性覆蓋,但兩者都可以在virtualquery中查詢到。
7. 在cpu體系結構中,cpu要訪問的數據需要按數據大小對齊(word 地址按2對齊,dword 地址按4對齊),否則會產生異常。修復數據未對齊異常有幾種途徑:(1)x86 cpu會自動進行其他硬件 修復,訪問沒對齊的數據只是更慢。(2)seterrormode傳入sem_noalignmentfaultexcept參數,通知windows通過軟件開發修復未對齊問題。(3)編譯選項__unaligned會自動產生額外代碼修復問題。綜上,后兩種軟件開發修復方案適用於非x86 cpu速度更慢,最好還是按數據大小對齊內存。
第14章 探索虛擬內存
1. 工作集(working set):緩存到主存中的那些頁面。
2. 32位系統開發中的32位程序和64位系統開發中的64位程序,都用getsysteminfo來獲取系統開發信息,而64位系統開發中的32位程序(iswow64process返回true)用getnativesysteminfo。獲取處理器信息用getlogicalprocessorinformation,獲取內存信息用globalmemorystatus。
3. system_info(getsysteminfo)各字段的解釋:dwpagesize-頁面大小。lpminimumapplicationaddress、lpmaximumapplicationaddress-用戶模式內存大小,32位系統開發中是0x0001000到0x7ffeffff。dwactiveprocessormask-cpu掩碼,同affinitymask。dwnumberofprocessors-處理器個數。wprocessorarchitecture、wprocessorlevel、wprocessorrevision-決定cpu型號。
4. memorystatus(globalmemorystatus)各字段的解釋:dwmemoryload-內存管理系統開發負載的大致估計,0~100,可以忽略。dwtotalphys、dwavailphys-系統開發總的物理內存和剩余物理內存。dwtotalpagefile、dwavailpagefile-系統開發總的頁交換文件和剩余頁交換文件。dwtotalvirtual-系統開發各進程最大用戶模式內存(32位是2g-128k)。dwavailvirtual-當前進程剩余用戶模式內存。
5. process_memory_counters_ex(getprocessmemoryinfo)各字段的解釋:pagefaultcount-缺頁錯誤數。workingsetsize-工作集,即當前進程物理內存占用。pagefileusage-當前進程的頁交換文件占用(包括全部的類型為private內存塊和部分的image、mapped塊,后者在寫拷貝后其虛擬存儲器才轉移到page file中)。privateusage-當前進程私有的內存占用,其虛擬存儲器位於頁交換文件中,虛擬存儲器中除去這部分其他的都位於共享文件中了(一般值等於pagefileusage)。
6. numa(非統一內存訪問,non-uniform memory access。一種分布式計算機系統開發內存模型)機器中的內存管理:globalmemorystatusex獲取各節點總內存。getnumahighestnodenumber-獲取系統開發中總的節點個數。getnumaavailablememorynode-獲取某節點的內存。getnumanodeprocessormask-獲取某節點的cpu掩碼。getnumaprocessornode-判斷某cpu位於的哪個節點。
7. virtualquery可以查詢某地址所在的內存塊(內存塊是具有相同狀態、保護屬性和類型的連續頁面),也提供了一些信息指出該內存塊在reserve的時候其virtualalloc起始地址和保護屬性等。
8. memory_basic_information(virtualquery)各字段解釋:baseaddress-內存塊起始地址。regionsize-內存塊長度(jeffrey把reserve的叫區域把這兒的叫內存塊,而windows只把這兒叫region,我姑且同前者的概念)。state-塊狀態,可以是free、commited、reserved。protect-保護屬性,狀態是commited時有效。type-類型,可以是private(私有內存,虛擬存儲器在頁交換文件)、image(在寫拷貝之前,其虛擬存儲器就是映像文件(exe、dll),寫拷貝(修改dll代碼或全局變量等)之后虛擬存儲器轉移到page file)、mapped(類似image,寫拷貝之前虛擬存儲器是內存映射文件),狀態是commited時有效。allocationbase、allocationprotect-reserve時候的基地址和保護屬性,狀態非free的時候字段有效。
9. windows進程內存布局:分成很多內存塊,其中部分內存塊屬於同一個區域(reserve的region)。如果要實現內存搜索的功能,可以用virtualquery遍歷各塊,在commited的塊中搜索。
10. 線程棧的內存塊具有page_guard保護屬性。
11. 一個進程內存使用的統計分析:輸出見最后(單位均為kb)。第一部分是用virtualquery遍歷各塊進行統計,可見進程commit了5.8m內存到虛擬存儲器,其中4.5m是映像文件(部分在pagefile中),1.1m是內存映射文件(部分在pagefile中),159k是私有內存(全部在pagefile中)。第二部分使用getprocessmemoryinfo,可見進程占用主存(物理存儲器)1.6m,虛擬存儲器中有425k在pagefile中(包括第一部分中全部的private和部分的image、mapped),也說明進程使用的5.8m內存中有5.4m是共享的(5.8-425)。第三部分用globalmemorystatus看出該進程可用虛擬內存為2g。
virtualquery :
commitedbytes = 5865.47
readallowedbytes = 5865.47
imagebytes = 4517.89
mappedbytes = 1187.84
privatebytes = 159.744
getprocessmemoryinfo :
workingsetsize = 1658.88
pagefileusage = 425.984
privateusage = 425.984
globalmemorystatus :
dwavailvirtual = 2.13826e+006
第15章 在應用開發程序中使用虛擬內存
1. 用virtualalloc來reserve區域的時候:pvaddress為空表示由系統開發分配區域起始地址,同時使用mem_top_down標志,提示系統開發優先選擇高地址,適用於長時間占用的內存。自定義起始地址的時候,實際reserve到的區域會包含自定義的范圍(自定義的起始地址+長度),即返回的地址可能比自定義起始地址小,同時保證該區域起始地址與系統開發的分配粒度對齊,長度與分頁大小對齊。如果找不到這樣長的閑置連續空間,返回null。reserve和commit的保護屬性相同,性能更好。
2. 用virtualalloc來commit內存塊的時候:實際提交的塊會包含自定義范圍,並且起始地址和長度都與頁面大小對齊。提交的塊不應該跨兩個區域。
3. virtualallocexnuma,適用於numa機器。
4. 在visita以上的系統開發中,可以分配大頁面,大頁面是常駐內存的需要有鎖定頁面的權限(lock pages in memory),同時要求在virtualalloc時滿足三個條件:(1)大小必須與getlargepageminimum對齊(自然該函數必須返回非0)。(2)同時reserve和commit。(3)保護屬性必須是page_readwr ite。
5. 在需要使用有空洞的大段連續內存的時候,有一個技巧:reserve一大段,根據需要commit。因為只有commit了才占用虛擬存儲器,因此很節省內存。
6. virtualfree可以反提交和釋放內存,其中mem_release時的長度參數必須為0,表示釋放整個區域。
7. virtualprotect改變保護屬性,注意一次調用不要跨多個區域。
8. virtualalloc的mem_reset標志,表示願意暫時放棄一段內存的當前內容,如果系統開發的物理內存使用緊張,reset的這段內存對應的物理內存可能會被挪用,直到再次訪問這段內存。
9. 即使通過virtualalloc來commit了,只要沒有訪問過這段地址,系統開發也不會分配內存。即如果commit的1.5g內存不讀寫,開銷很小。
10. 地址窗口擴展(awe,address windowing extension):可以指定一段地址直接映射到物理內存,具有常駐內存和增加可用內存量的優點。以mem_physic調用virtualalloc來指定要用於映射的虛擬地址段,然后allocateuserphysicalpages分配物理頁面,再mapuserphysicalpages將虛擬地址段和分配的物理頁面關聯,之后隨意讀寫,使用完畢后以null作為參數調用mapuserphysicalpages解除關聯,最后freeuserphysicalpages、virtualfree釋放物理頁面和地址段。一段虛擬地址可以通過map和unmap輪流訪問多段物理內存,明顯增加了進程可訪問的內存總量。awe也要求用戶有鎖定頁面的權限。
第16章 線程棧
1. 鏈接選項“/statck:reserve[,commit]”可以在pe文件中記錄默認的線程棧保留大小和提交大小,實際棧大小還要結合_beginthreadex時的參數。
2. page_guard屬性的作用:第一次訪問具有該屬性的頁面,會觸發一個status_guard_page_violation異常,同時該屬性被自動抹除,於是后續的訪問正常。即該屬性用於首次訪問的通知。
3. 默認條件下,線程棧創建時先reserve一塊1mb的內存,棧底的兩塊頁面被提交,其中較低地址的那塊頁面具有page_guard屬性,被稱為保護頁面(guard page)。當棧的調用層次變深需要更多內存時,系統開發去掉當前保護頁面的page_guard屬性並提交下一個頁面作為保護頁面(實現方式見條款4)。這個開發過程 進行下去,棧頂所在的提交頁面之后始終有一塊被提交的保護頁面,直到棧的調用層次足夠深,當倒數第二個頁面被提交並需要標記為保護頁面的時候,這個標記行為終止並拋出exception_stack_overflow異常。棧最低地址的一個頁面始終處於reserve狀態,用來隔離棧和棧下方的內存空間,避免非法的棧操作訪問越界。捕獲了棧溢出結構化異常的線程由於沒有了保護頁面,需要調用_resetstkoflw來重新標記保護頁,否則下次調用層次太深的時候會因為沒有保護頁不觸發棧溢出異常直接訪問到最低地址的reserve頁,造成非法訪問錯誤。
4. 棧上reserve頁被從高到底依次commit的方式:當位於棧頂的函數幀在保護頁面中時,訪問保護頁內存會觸發異常,系統開發捕獲異常,提交下一頁,並判斷下一頁是否是倒數第二頁,是的話拋出棧溢出異常,否則將一下頁標記為保護頁。如果棧頂函數幀很大(比如包含大數組),跨越多個分頁,由於函數內部可能先訪問函數幀中最低地址的reserve頁的內存,引起非法訪問錯誤,於是c++編譯器對這種棧幀大於1個分頁的函數進行了特殊處理:編譯器會在大棧幀函數的開始插入_chkstk,后者會沿大棧幀的底部向頂部依次訪問每個分頁,連續推動保護頁,保證后來函數體中的隨機訪問都作用在commit分頁上。
5. debug版本程序在調用函數前,會備份當前棧的上下文,在函數返回后對比新的棧數據和備份數據,判斷是否有棧上的越界錯誤。release版本程序開啟/gs開關后能起到類似的效果。
第17章 內存映射文件
1. 內存映射文件的主要應用開發場合:(1)映射到映像文件(exe、dll),加速進程啟動。(2)映射到數據文件,代替標准的文件io。(3)共享內存。
2. 當dll被loadlibray時如果發現預定基地址已經被占用時,可能會加載失敗(構建dll時指定了/fixed鏈接選項),至少也會重定位,后者會占用額外存儲空間和增加dll載入時間。
3. 段的大小都按頁大小對齊。
4. 使用dumpbin.exe /headers可以查看pe文件的各種段。常見段:.bss-未經初始化的全局變量等數據。.crt-只讀的c運行時數據。.data-已初始化的全局變量。.debug-調試信息。.didata-延遲導入名字表(delay imported names table)。.idata-導入名字表。.edata-導出名字表。.rdata-只讀的運行時數據。.reloc-重定位表信息。.rsrc-資源。.text-代碼段。.textbss-啟用增量鏈接(incremental linking)時c++編譯器生成。.tls-線程本地存儲。.xdata-異常處理表。
5. 默認情況下.data段的頁面具有寫拷貝屬性,因此pe文件的一個實例修改全局變量並不會影響其他進程實例。
6. 使用#pragma data_seg(“mydataseg1”); #pragma data_seg();可以聲明一個新的數據段,其中初始化的變量會自動加入該段。沒有初始化的變量可以通過__declspec(allocate(“mydataseg1”)) int g_i;來加入數據段。用#pragma comment(linker, “/section:mydataseg1, rws”)來為段指定屬性,”s”表示shared,它通過去掉段頁面的寫拷貝保護屬性,來達到多進程共享的效果。
7. createfilemapping:參數fdwprotect的page_readonly、page_writecopy等很容易理解,另外還有幾種屬性:sec_commit-默認值。sec_image-表示該文件是映像文件,該文件被映射到內存時,系統開發會對其中不同的段添加對應的保護屬性。sec_nocache-無cache,驅動開發 開發開發人員用。sec_large_pages-大頁面支持,類似virtualalloc那邊。sec_reserve-通過這個標記映射的內存沒有是沒有被提交的,直到再調用virtualalloc來commit才能訪問這些頁面。參數dwmaximumsizehigh、dwmaximumsizelow表示要求的最大文件大小,尤其在共享內存對應的虛擬存儲器在頁交換文件中時特別有意義(hfile參數為invalid_handle_value的情況),如果映射的可寫磁盤文件本身的大小沒有達到這個值,文件也會被自動擴大。如果最大大小為0,表示使用磁盤文件本身大小。
8. mapviewoffile:創建映射對象的一個視圖,多個視圖之間的數據是嚴格同步的,因為同一個映射對象的多個視圖盡管虛擬地址段不同,但都映射到同一個虛擬存儲器上。該函數返回后,內存已經被commit(除非createfilemapping時指定sec_reserve參數)。參數dwfileoffsethigh、dwfileoffsetlow、dwnumberofbytestomap共同決定要把文件的哪部分映射到內存,offset必須與分配粒度對齊,size為0的時候表示范圍從offset直到文件尾。對返回的地址virtualquery會得到map的區域。
9. unmapviewoffile:釋放映射的內存區域。
10. flushviewoffile:將緩存中已修改的數據flush到文件中,如果沒修改被直接丟棄。注意如果映射頁面具有寫保護屬性,緩沖中的數據最多被flush到page file中。如果是映射到遠程文件,該函數只保證數據被flush到網上,而遠程的文件不一定會被修改,除非createfile時指定了file_flag_write_through。
11. 注意,雖然createfilemapping會增加文件對象計數,mapviewoffile會增加映射對象的計數(也就是說,在unmapviweoffile之前這兩個內核對象就可以被closehandle了),但是如果太早關閉映射對象,其他地方要打開映射對象時會失敗(即openfilemapping失敗或者createfilemapping的lasterror不是error_already_exists),也就是說,內核通過視圖對映射對象的引用,不能被用戶模式代碼檢測到,因此最好還是按傳統順序先unmapviewoffile再closehandle。
12. numa支持:createfilemappingnuma、mapviewoffileexnuma。
13. 打開同一個磁盤文件的多個文件內核對象,由於各自擁有獨立緩沖區,因此文件內容在不同對象間不保證實時同步。
14. 映射到同一文件的多個映射對象的視圖不保證數據的實時同步。
15. mapviewoffileex:參數pvbaseaddress非空的時候可以指定映射內存的起始地址。系統開發映射exe和dll的時候就這么干的。
16. 各種跨進程通訊手段的通訊雙方都位於本機時,這些通訊方式最終都實現為內存映射文件。
17. 要映射到磁盤文件時,一定要判斷createfile的返回值,因為如果打開文件失敗,invalid_handle_value句柄會讓createfilemapping創建映射到pagefile的對象,沒有報錯卻是歧義。
18. 對應virtualalloc那“reserve一大段內存再小塊commit”的用法,內存映射文件中實現如下:以sec_reserve為參數createfilemapping,之后mapviewoffile得到reserve的區域,最后確保訪問前要先用virtualalloc來commit。注意這樣commit的共享內存不能virtualfree。
第18章 堆
1. 堆適合分配小內存塊,不需要按分配粒度或者頁大小對齊。堆在最初只是預定了一塊區域,在客戶分配時將預定的區域提交,在客戶釋放后可能反提交。
2. 關於默認堆:getprocessheap返回,用戶模式代碼無法銷毀它,在進程結束后由系統開發銷毀。進程可以通過鏈接選項“/heap:reserve[,commit]”來設置默認堆大小。因為默認堆屬於進程,所以在dll中不應設置該鏈接選項。windows的ansi版api向unicode版轉化的時候從默認堆分配字符串緩存,localalloc、globalalloc也從默認堆分配內存。默認堆對外界訪問進行了同步,即沒有使用heap_no_serialize標記。
3. 使用獨立堆的一些好處:(1)寫堆內存出錯后,不會影響其他堆的數據。(2)對特定類型數據使用獨立堆的話,由於分配塊大小相同,具有速度快、無碎片的優點。(3)相關數據使用獨立的堆,在訪問這些數據時訪問的頁面更集中,減少pagefault。(4)對特定線程上的邏輯結構使用獨立堆,不必加鎖,提高性能開發。
4. heapcreate:參數fdwoption,如果在創建堆的時候指定了部分標志(如heap_no_serialize標志等),以后每次訪問堆這些標志都生效;如果創建的時候沒有指定,那后續的每次訪問可以單獨指定標志。 heap_no_serialize-訪問堆的時候不加鎖。heap_generate_exceptions-分配內存失敗的時候拋出異常,默認行為是返回null。heap_create_enable_execute-可以在堆內存上放置代碼來執行。參數dwinitalsize-初始堆大小。參數dwmaximumsize-如果非0,表示如果堆內存使用量達到這個值后再分配會失敗;為0,表示堆會自動增大,直到內存用盡。
5. heapalloc、heapsize、heapfree、heapdestroy,容易理解。
6. heaprealloc:heap_zero_memory-增大內存時,增加的字節初始化為0。heap_realloc_in_place_only-要求不移動開發其他 起始地址的情況下改變大小,需要增大時如果當前位置剩余空間不足會返回null。
7. heapsetinformation:標記heapenableterminationoncorruption-visita以上使用。默認情況下,堆內存被破壞后只在調試器中觸發一個斷言然后繼續執行,這個標記允許發現堆破壞就拋出異常。該標記影響進程中所有堆,無法清空標記。標記heapcompatibilityinformation-值為2的時候,表示啟用低碎片堆(lowfragmentation heap)算法,啟用該算法的堆針對內存碎片問題優化有更好的性能。
8. heap32listfirst、heap32listnext-遍歷快照(createtoolhelp32snapshot)中的堆。heap32first、heap32next-遍歷指定堆中的塊。getprocessheaps-獲得包括默認堆在內的所有堆句柄。heapvalidate-檢查指定堆中所有塊或者單個塊的有效性。heapcompact-將堆中閑置塊合並,並反提交。heaplock、heapunlock-鎖定堆。heapwalk-遍歷指定堆中的塊,建議先鎖堆。
第19章 dll基礎
1. kernel32.dll-管理內存、線程、進程。user32.dll-窗口和消息。gdi32.dll-繪制圖像文字。comdlg32.dll-常用對話框。comctl32.dll-常用控件。
2. dll函數分配的內存應該由dll自己提供的函數釋放:主要是針對通過c/c++函數(malloc、new)分配的內存,因為當dll和dll的使用者都在引用靜態庫版本的crt時(或有一方在引用靜態庫crt),多個靜態庫版crt中有多份crt堆的管理數據(全局變量),如果從一個管理器分配資源交給另一個管理器釋放,顯然會錯誤。因此,如果所有模塊都使用dll版crt就不會有錯(因為只有一份全局crt堆管理數據),或者改用heapalloc(getprocessheap(),…)也不會錯(顯然dll中和exe中訪問到的默認堆是同一個),當然最佳做法還是dll同時提供匹配的釋放函數。
3. .lib文件中只包含函數、變量和類型的符號名。由於模塊中只包含要引用的模塊名而沒有路徑,所以主模塊被載入后需要按一定的搜索順序搜索被引用模塊再載入,同時這也意味着修改.lib中的符號名,搜索dll時也會搜索新名稱。
4. dll的導出段中按符號名順序列出了導出項,每一項包括符號名和rva(relative virtual address,用於指出該符號在dll模塊中相對於模塊基址的地址)。模塊可以包含多個導入段,每個導入段指出該段要依賴的dll名以及需要的符號,導入符號對應的實際地址在dll被載入后填充,其值為dll基址+rva。
5. 在為dll的導出函數指定名稱的時候,最好使用.def文件,其次可以選擇鏈接選項#pragma comment(linker, “/export:myfunc=_myfunc@”)。
6. dumpbin.exe的/exports能夠查看導出段,/imports能夠查看導入段。
7. 關於msvc編譯器對符號改名的策略:c語言下默認不改變函數名,因此c++下使用了extern “c”的__cdecl也不會改名。
第20章 dll高級技術
1. 加載一個dll,系統開發至少會干幾件事:(1)將不同段的分頁分別映射並賦予不同的保護屬性。(2)檢查dll依賴的其他dll依次加載。(3)執行dllmain。
2. loadlibraryex:dwflags參數-don’t_resolve_dll_references-將dll映射到內存后,對於條款1中的三件事,只做按段分配保護屬性這件。load_library_as_datafile-比起上個標志,連三件事中僅剩的一件也省了,只是映射文件,用做數據文件。可以加載exe然后讀取其中的資源。load_library_as_datafile_exclusive-以獨占方式映射數據文件。load_library_image_source-在as_datafile的基礎上,將導出段的所有rva轉換成va。load_with_altered_search_path-可以調整dll路徑的搜索方式。load_ignore_code_authz_level-安全相關,該安全方案被后來的uac取代。
3. setdlldirectory:設置加載dll時的搜索路徑,dll在搜索進程的當前路徑過后就會搜索這里。當路徑為空串(””\0”)的時候,表示搜索的時候跳過當前路徑,當路徑為null的時候恢復默認搜索方式。
4. freelibraryandexitthread適用於一個場合:要調用freelibrary的代碼正是位於dll中。
5. loadlibrary和loadlibraryex返回的地址不等價,不能混用。如先以load_library_as_data_file做參數調用loadlibraryex,再用loadlibrary加載同一個dll,返回值是不同的。
6. getprocaddress。
7. 名為dllmain的函數不存在的時候,系統開發會使用默認入口。
8. dllmain的fdwr eason參數:dll_process_attach-dll第一次被加載的時候傳入,對於隱式加載的dll是主線程執行,而顯式加載的dll由loadlibrary線程執行,用於執行dll初始化操作。返回false,程序會報錯表示加載dll失敗。dll_process_detach-隱式卸載的時候由主線程執行,顯示卸載的時候由freelibrary線程執行,負責清理資源。dll_thread_attach-線程在創建時,會檢查進程已經加載的dll,然后依次通知每個dll的dllmain函數。進程啟動時會先創建主線程,再加載各個dll,因此這時主線程調用dllmain只會傳入dll_process_attach而不是dll_thread_attach。dll_thread_detach-線程退出的時候檢測所有已經加載的dll依次調用dllmain。
9. disablethreadlibrarycalls:聲明線程在創建和退出的時候不用通知指定dll的dllmain函數。
10. 所有dll的dllmain的調用被加載鎖(loader lock,進程唯一的)序列化了。避免同時創建多個線程以dll_thread_attach調用dllmain時產生競爭。
11. 對於c++編寫的dll,實質上系統開發通知的是__dllmaincrtstartup,當fdwr eason是dll_process_attach和dll_process_detach時,它會調用全局變量的構造或析構函數,然后再調用dllmain。
12. 延遲載入是指直到使用dll導出的函數時系統開發才加載dll和查找函數。優點:加速進程啟動、讓為高版本系統開發設計的程序在低版本系統開發中也能使用部分功能、特殊的設計用途等。部分dll不能延遲加載:導出了數據的dll(因為延遲加載利用的是getprocaddress等功能)、kernal32.dll。另外在dllmain中也不該使用延遲載入的dll函數。
13. 延遲加載的使用:在linker-input-delay loaded dlls中指定要延遲載入的dll。如果要hook延遲加載開發過程 以及停用延遲加載的dll,需要再導入delayimp庫和開啟linker-advanced-delay loaded dll的support unload。
14. 延遲加載的細節:模塊引用的dll要延遲加載的話,會刪除該dll的idata段,改為包含didata段,對延遲加載函數的調用會跳轉到__delayloadhelper2函數中,該函數會確保該dll已經被加載,然后檢查didata中對應函數的表項是否非空,為空的話用getprocaddress查找並填充didata項,下次使用就不用再查找。用__funloaddelayloadeddll2卸載延遲加載dll,以便之后再次使用延遲函數能夠保證正常,該函數會清空didata中已經填充的各項。__pfndlinotifyhook2、__pfndilfailurehook2是延遲加載開發過程 的hook函數指針。
15. 函數轉發器:#pragma comment(linker, “/export:somefunc=dlla.someotherfunc”)。
16. hkey_local_machine\system\currentcontrolset\control\session manager\knowndlls包括一些影響loadlibrary路徑查找的信息。
17. 關於模塊基地址重定位:dll中的代碼訪問dll中的全局變量時用的是絕對地址,同時會增加一個reloc段(重定位段)記錄所有引用絕對地址的代碼,如果dll最終加載的位置不是默認基址,之前使用的絕對地址需要根據reloc的記錄被修正,這就是重定位開發過程 。可見如果進程加載的時候,多個dll基址發生沖突,需要被重定位,修復絕對地址的操作增加了加載時間,同時也會因為修改image內存頁造成寫拷貝,增加了系統開發的虛擬內存占用。最理想的情況下,所有使用dll的進程都不需要重定位,這就需要安排合理的基址,可以使用rebase.exe開發工具或者rebaseimage函數。使用dumpbin.exe /headers命令可以查看包括基址的信息。使用/fixed開關刪除reloc段,禁用重定位。
18. 關於模塊的綁定:默認情況下,模塊的引入段,會在進程加載模塊后被填入導入函數的絕對地址,因此包含引入段的模塊會發生內存頁的寫拷貝。使用bind.exe開發工具,可以在映像文件中的idata段填入絕對地址和對應dll的時間戳,當進程加載時,發現被依賴dll沒有被重定位(即基址和默認基址相同)且時間戳和綁定的dll相同,那么idata段就可以不用修改直接使用綁定值,避免了寫拷貝。可以使用bind.exe開發工具或bindimageex函數來綁定模塊。綁定操作應該在軟件開發每次升級后執行。
19. 其他綜合 討論重定位和綁定:一個dll中,引入段最終包含所依賴的dll的函數地址,如果所依賴的dll沒有被重定位,那引入段不用被修改避免了寫拷貝;dll內部的全局變量是用絕對地址訪問,如果dll本身沒有被重定位,這些絕對地址不用被修改也避免了寫拷貝。因此用rebase.exe開發工具合理安排所有dll的基址,然后在用bind.exe開發工具寫入導入函數地址,能提升性能和減少內存占用。
第21章 線程局部存儲區
1. 動態tls:每個線程都有一個內部dword 數組用於存放用戶數據,ms保證數組至少有tls_minimum_available(64)個元素。用tlsalloc申請一個空閑索引,調用tlssetvalue、tlsgetvalue時傳入這個索引可以訪問每個線程上的用戶數組,用tlsfree釋放索引,windows會保證被釋放的索引在各個線程上的數據都被清零。dll中使用動態tls的標准方式:dll_process_attach的時候tlsalloc一個索引;在dll_process_detach的時候用tlsfree釋放;在dll功能函數內部檢測tlsgetvalue返回的指針是否為空,為空的話分配一塊內存包含dll要使用的所有線程相關數據;在dll_thread_detach中檢測tlsgetvalue返回值非空則釋放掉。
2. 靜態tls:聲明為__declspec(thread)的靜態變量會保存在模塊的tls段中, 每個線程在創建的時候會根據當前所有模塊的tls段總大小分配一塊內存與線程對象關聯,這塊線程相關內存的大小也會隨loadlibrary、freelibrary增刪包含tls段的dll進行調整。靜態tls只在vista以上才被完美實現。考慮這樣一種實現:每個模塊都有一個動態tls索引(__tls_index),每個線程的該索引下保存的是malloc出來的特定模塊的tls段數據,可以認為系統開發是通過1節中描述的慣用法實現靜態tls的。
第22章 dll注入和api攔截
1. 利用注冊表注入dll:hkey_local_machine\software\microsoft\windows nt\currentversion\windows\下的appinit_dlls可以填一系列要加載的dll,僅當loadappinit_dlls為1的時候。工作原理是:任何gui程序在加載user32.dll時,它的dllmain會先嘗試加載注冊表項appinit_dlls中的dll。因此這種開發方法 會影響所有的gui程序。
2. 利用鈎子來注入dll:以參數wh_getmessage來調用setwindowshookex,線程id為0的時候會注入系統開發中所有有消息循環的進程。
3. 利用遠程線程注入dll:用createremotethread在目標進程中創建一個線程,線程函數就是loadlibrary,線程函數的參數是要注入的dll名。具體步驟:(1)用virtualallocex在目標進程中分配內存,用writeprocessmemory寫入要注入dll名的字符串,這樣就在目標進程地址空間中准備好了參數(2)通過getprocaddress得到loadlibrary的地址。可以利用通常所有進程中kernel32.dll的基地址相同這個實時,把本進程的loadlibrary地址當做目標進程中該函數的地址。(3)調用createremotethread,線程函數是loadlibrary。(4)waitforsingleobject來等待遠程線程結束,表示加載完畢,然后virtualfreeex釋放裝有模塊名的地址。(5)最后以任何方式在目標進程中釋放注入的dll。比如以 freelibrary為線程函數調用createremotethread。
4. 利用轉發器替換dll:要用自己的a.dll替換合法的b.dll,先用轉發器在a.dll中轉發所有的函數到b.dll,再實現自己的功能,最后將a.dll改名為b.dll,原本的b.dll改成其他名。也可以在a.dll中轉發后,修改依賴b.dll的模塊的引入表,將它依賴的dll名改為a.dll,這種方式避免了改名。
5. 利用createprocess注入dll:父進程用createprocess創建子進程時暫停子進程的主線程,然后查詢子進程的入口函數(main),將入口函數的頭幾個字節改為跳到注入代碼,而注入代碼的末尾會跳轉回入口函數開頭。
6. api鈎子的兩種實現方式:(1)將原函數入口處的代碼改為“跳轉到掛鈎函數;原代碼b”,這樣原函數的調用都會跳轉到掛鈎函數;為了能夠訪問原函數的功能,另外准備一個可執行緩沖區,內容為“原代碼a;跳轉到原代碼b”。(2)修改進程中所有模塊的引入表,將所有引入表中指定dll的指定函數項地址改為掛鈎函數地址。注意進程調用loadlibrary后會引入新的dll及其依賴dll,因此需要再遍歷一次所有dll修改引入表。有必要的話也掛鈎getprocaddress返回偽造地址。用到的函數有imagedirectoryentrytodata,可以查詢指定dll的引入表地址。
第23章 終止處理程序
1. __finally塊可能由於三種原因被執行:(1)正常執行完__try塊。(2)控制流中斷__try塊(這種情形叫局部展開,local unwind)。包括continue、break、return、goto、longjump等。(3)發生異常中斷__try塊,系統開發正在進行全局展開。包括seh中的其他硬件 異常(除0、寫非法內存)和軟件開發異常(raiseexception)。在__finally塊中要區分是(1)或者(2)(3),可以用abnormaltermination,該函數返回true表示是(2)或(3)。(1)沒有額外開銷;(2)的開銷較大,建議用__leave替代;(3)是正常用途。
2. 當 __finally塊是由於__try塊中的return被執行時,如果再在__finally塊中調用return,最終函數會返回后一個return的值。
3. __finally中的return可以中斷全局棧展開(global unwind)。即線程不會從__except的處理代碼塊繼續執行(盡管exception filter返回的是exception_execute_handler),而是從return的__finally的上一級函數繼續執行。
4. 不建議在__try或__finally塊中使用return、goto等控制流語句。
第24章 異常處理程序與軟件開發異常
1. __try、__except組合主要有filter塊(__except后括號中的語句)和handler塊(__except后的{}塊),它們只在發生結構化異常的時候有可能被執行。發生結構化異常后,系統開發首先將異常信息(getexceptioninformation返回值)壓入棧頂,然后調用veh注冊函數(見25章),再執行最近的filter塊,如果filter塊返回exception_continue_search,則繼續查找下一個filter,這個開發過程 中包括異常發生點和異常信息的整個棧一直完好(即getexceptioninformation返回值有效),直到某個filter返回非exception_continue_search。如果filter塊返回exception_continue_execution則從異常點繼續執行,如果返回exception_execute_handler則先進行全局展開再執行handler塊。全局展開會造成異常信息失效和從內到外的__finally塊逐個執行,即handler中getexceptioninformation返回值會失效,且如果某個__finally通過return中斷全局展開,handler塊將不執行。如果用戶編寫的所有__except都返回exception_continue_search,最終系統開發將執行ms編寫在系統開發線程函數中的最頂層__except的filter塊,即unhandledexceptionfilter,這個過濾函數25章講。
2. 關於exception_continue_execution:對於其他硬件 異常(除0、內存非法訪問等cpu異常),會從觸發異常的那句匯編語句開始執行。對於軟件開發異常,會從raiseexception的下一句匯編開始執行。即filter返回該值可能讓其他硬件 異常循環觸發,而軟件開發異常只會觸發一次。
3. getexceptioncode只能出現在__except后的filter塊或者handler塊中,而不能出現在filter函數中,這由編譯器保證。getexceptioninformation同樣不能出現在filter函數中,但也不能出現在handler塊中,根據條款1中的描述,當系統開發執行handler塊時,全局展開已經結束,異常觸發點到handler點之間的棧幀已經失效,當然異常信息也已經失效。
第25章 未處理異常、向量化異常處理與c++異常
1. c++異常機制是由seh實現的,c++的所有異常都是以exception_noncontinuable為參數調用raiseexception拋出的軟件開發結構化異常。由於exception_noncontinuable只限制__except的filter塊,所以veh函數返回exception_continue_execution來忽略c++異常是合法的。
2. 無論是c/c++線程(_beginthreadex)還是windows線程(createthread),它們的內部線程函數都將用戶線程函數放在一組__try、__except中,當異常發生后所有的用戶filter都返回exception_continue_search時,系統開發將執行最外層的filter即unhandledexceptionfilter(一個系統開發api),如果該函數發現當前進程正在被調試則將控制權交給調試器而后者會中斷進程;非調試狀態下它會嘗試取出用戶通過setunhandledexceptionfilter注冊的頂層過濾函數,如果用戶頂層函數返回exception_execute_handler或exception_continue_execution,則unhandledexceptionfilter不再進一步處理。顯然用戶可以通過返回前者來記錄日志並無聲退出,而返回后者可以實現類似棧內存通過guard page自動commit的功能。如果用戶頂層過濾函數也返回exception_continue_search,則進程再嘗試調用通過addvectoredcontinuehandler注冊的veh函數,所有veh函數都返回exception_continue_search的話,系統開發就創建一個子進程並等待,子進程顯示對話框詢問用戶要結束進程還是附加調試器,等待結束后異常的進程要么退出要么已經被調試器附加。
3. seh實現的理解:有一個叫seh棧的容器被用來維護相關數據,棧的每一項可能是一個__try/__except或__try/__fianlly組合;線程進入一個__try塊就往seh棧中壓項,退出__try塊就從seh棧彈出一項。如果發生異常,系統開發判斷離棧頂最近的__except項的filter返回值,如果其返回值為exception_continue_search,則系統開發繼續從棧頂往棧底查找__except項並執行其filter。如果找到一個filter返回exception_continue_execution則流程結束,同時seh棧保持不變;如果某個filter返回exception_execute_handler,則系統開發將seh棧棧頂到該__except項的每一項都出棧,彈出的開發過程 中如果發現__finally項則執行其中的代碼塊。
4. veh(vectored exception handler,向量化異常處理),作為seh的補充,可以通過addvectoredexceptionhandler、removevectoredexceptionhandler管理一組異常過濾函數,這組函數將在異常發生之后到用戶filter被執行之前的這段時間被調用,它可以返回exception_continue_search讓系統開發執行下一個veh函數或者用戶filter;也可以返回exception_continue_execution起到忽略異常的效果。這組回調的特殊調用時機可以用於實現異常hook等。另外還可以通過addvectoredcontinuehandler、removevectoredcontinuehandler管理一組過濾函數,由unhandledexceptionfilter在用戶頂層過濾函數(setunhandledexceptionfilter)之后調用。
5. 兩種情況下調試器會通知用戶發生異常:(1)打開ide相應開關后,一拋出異常就觸發斷點。另外,無論是否打開開關,調試器都在輸出窗口打印異常相關信息。調試器顯然通過addvectoredexceptionhandler注冊了veh函數。可以模仿調試器來記錄異常。(2)對於用戶沒有處理的異常,被調試狀態下的unhandledexceptionfilter內部會通知調試器。
6. 對異常發生時調試器彈出框的解釋:(1)中斷。保持中斷的狀態,便於調試。(2)繼續。如果對話框在veh函數中彈出,即異常剛拋出,這個選項會讓veh函數返回exception_continue_search,繼續查找下個處理函數。如果對話框是在unhandledexceptionfilter中彈出,繼續選項等價於忽略。(3)忽略。veh過濾函數或unhandledexceptionfilter中代碼返回exception_continue_execution,因此該選項用於忽略包括c++異常在內的軟件開發異常(raiseexception)。
第26章 錯誤報告與應用開發程序恢復
1. 本章介紹的wer(windows error reporting,windows錯誤報告)內容主要在vista以上可用。
2. %systemroot%\system32\wercon.exe可以顯示系統開發中出現過的錯誤。
3. wersetflags可以影響wer的行為,比如要求不dump堆、發送報告到ms網站等。weraddexcludedapplication可以指定一些程序崩潰后跳過wer機制,適合正在調試的程序等。
4. webregis地理信息系統 termemroyblock-指定wer的dump數據中要包括指定位置的內存。werregis地理信息系統 terfile-要求將指定文件加入報告中。
5. 定制wer報告:werreportcreate、werreportsetparameter、werreportadddump、。werreportaddfile、werreportsetuioption、werreportsubmit、werreportclosehandle。
6. regis地理信息系統 terapplicationrest art-可以指定在何種錯誤情況下wer以特定參數重啟程序。
7. regis地理信息系統 terapplicationrecoverycallback-注冊一個回調,進程將要非正常結束的時候被調用,以便用戶自由備份一些狀態等。用戶可以在回調中以applicationrecoveryinprogress、applicationrecoveryfinished來通知ui進度。
附錄
1. 要在輸出窗口中打印調試信息,區別#pragma message和outputdebugstring,前者是在編譯期打印,后者是運行時打印。
2. 由於ms提供的函數debugbreak會斷點在kernel32.dll中,需要兩次才能單步到下一行(第一次跳出kernel32.dll);而__asm int 3;是斷點在用戶代碼中,更容易使用。兩者都只適合調試器存在時,非調試狀態這個斷點異常無法捕獲會崩掉程序。
3. 自己編寫發布版本也有效的斷言:verify。
4. #pragma comment(linker, "/manifestdependency:\"type='win32' name='microsoft.windows.common-controls'…”) 使gui程序能夠自動查找正確版本的comctrl32.dll來自繪,達到自適應系統開發主題(theme)的效果。
5. windowsx.h中包含一些簡單函數便於操作窗口,分別是消息處理宏、子控件宏和api宏。
第26章 窗口消息(第4版)
1. 一個進程可以創建上萬個用戶對象(user object)。內核對象屬於內核,可以跨進程使用,不會隨任何進程自動刪除;圖符、光標、窗口類、菜單、加速鍵表等用戶對象屬於進程,允許跨線程訪問,進程結束后自動刪除;窗口、掛鈎兩種用戶對象屬於線程,擁有者線程結束后自動刪除。
2. 線程的內部數據結構threadinfo中至少包括以下內容:post來的消息隊列、send來的消息隊列、send的應答隊列、exitcode、激活標志、消息隊列狀態標志(queuestatus)、虛擬輸入隊列(viq)、局部輸入狀態(鼠標/鍵盤焦點窗口、光標外形和可見性等)。
3. postmessage、postthreadmessage、postquitmessage、getwindowthreadprocessid。
4. sendmessage發送消息時,如果目標窗口位於發送線程,則sendmessage內部直接調用窗口開發過程 並返回,如果目標線程不是當前線程甚至位於其他進程,sendmessage往目標線程的send消息隊列內添加項過后(並設置qs_sendmessage),用msgwaitformultipleobjects等待處理完成通知(同時還處理本線程消息)。而sendmessagetimeout則包含等待send處理完畢、處理本線程消息、檢測超時三項功能,傳入smto_block參數后只進行有超時的等待而不處理消息。
5. sendmessagecallback的目標線程是當前線程時,直接調用窗口開發過程 並用回調通知;如果目標線程是其他線程,send消息后直接返回,之后本線程應該用getmessage來響應其他線程post回來的send處理完畢的通知,該通知的處理函數會調用注冊的回調。sendnotifymessage相當於回調為空的sendmessagecallback,它不關心完成通知,相比postmessage它還是具有send消息的一些特點:比post消息優先處理、目標線程是當前線程時直接調用窗口開發過程 。一種獲取所有窗口句柄的開發方法 :以hwnd_broadcast 為參數調用sendmessagecallback,然后getmessage、dispatchmessage處理回調,在回調中搜集所有的窗口句柄。
6. sendmessage會在目標線程不是當前線程時阻塞等待,為避免不必要的阻塞發送線程,消息處理函數一旦確定處理結果就可以馬上調用replaymessage傳入結果值來激活send線程,處理函數后半段即使進行費時操作也不再干擾send線程(處理函數的返回值也被忽略)。
7. insendmessage可以在消息處理開發過程 中判斷當前線程是否是send線程(線程不同返回true)。insendmessageex還可以判斷拋出消息的具體函數,以及當前是否已經reply了結果。
8. getqueuestatus,檢測當前線程的消息隊列狀態,是否有post消息、send消息、虛擬輸入、以及qs_quit、qs_timer等特殊標志。
9. translatemessage在遇到wm_keydown/wm_syskeydown時,會post一個wm_char/wm_syschar。因此如果使用了translatemessage,消息的處理順序會變成wm_keydown->wm_char->wm_keyup。
10. getmessage/peekmessage內部算法:先判斷線程消息隊列狀態是否有qs_sendmessage標志,如果有則從send隊列取消息並處理但不返回(即getmessage內部檢測到send的消息后,會replaymessage(dispatchmessage(msg));而如果將send消息交給用戶代碼來dispatch,后者可能忘記需要答復發送線程);再判斷是否有qs_postmessage,如果有則從post隊列取消息填充msg結構然后返回(因此,用戶通過msg結構從getmessage處取得的消息只能是post的消息);判斷是否有qs_quit標記,如有則表示已經postquitmessage於是填充msg結構並返回(因此即使先postquitmessage再post用戶消息,也能保證退出前用戶消息被處理)。再判斷是否有qs_input標志,如果viq中有輸入則填充msg結構並返回(因此即使有輸入也可以退出且如果有輸入則不重繪)。判斷是否有qs_paint標志,有則表示窗口仍然有臟區域(直到beginpaint)於是填充msg產生一個wm_paint消息。最后判斷是否有qs_timer標志,如有則表示剛到時,於是移除標志並填充msg結構返回。可以看見有幾種消息被賦予了相當低的優先級,並不加入消息隊列:wm_quit是為了保證退出前處理完所有普通消息;wm_paint是因為開銷大,只在空閑時處理;wm_timer是為了避免處理慢觸發快而導致消息隊列溢出。
11. msgwaitformultipleobjects實現為,在事件對象數組后追加一項,如果要檢測的消息隊列標志被置位則觸發新追加的事件對象。關於輸入消息的監聽,由於ms設計為只在新增輸入消息時事件對象才觸發,因此需要以mwmo_inputavailable為參數來調用msgwaitformultipleobjectsex,達到一旦輸入隊列非空就觸發的效果。另外msgwaitformultipleobjectsex還支持waitall及apc等功能。
12. 對於跨進程用sendmessage發送wm_gettext、wm_settext等消息,系統開發會自動使用共享內存來轉換消息參數的地址值以跨越進程邊界。顯然用戶自定義消息需要自己來處理跨進程問題。wm_copydata可以用來跨進程發送數據,發送進程傳入一個有數據的緩沖,接受進程得到的緩沖地址轉而指向一塊相同內容的共享內存,系統開發在sendmessage返回時釋放共享內存(故這個消息只能send)。
13. 任意一個窗口都有編碼屬性,這個屬性在綁定消息處理函數時確定(即調用regis地理信息系統 terclassa或以gwlp_wndproc調用setwindowlongptra表示這是一個ansi窗口而不是unicode窗口),通過系統開發在不同窗口間轉發數據時,系統開發會自動進行編碼轉換。判斷窗口的編碼iswindowunicode。
14. 對getkeystate和getasynckeystate的理解:線程的局部輸入狀態中有一份鍵盤狀態表,在處理每個鍵盤消息的時候更新。getkeyboardstate獲取整個表,getkeystate獲取某個表項,由於鍵盤消息不一定能夠及時處理,因此內部表不一定夠新,要獲得實事狀態,用getasynckeystate,該api通過其他硬件 中斷獲得最新按鍵狀態。考慮一種getasynckeystate的實現:線程先設置中斷函數,再等待一個事件,中斷到來時發現該線程中斷函數指針非空於是執行函數,函數內部查詢最新按鍵狀態然后觸發事件,中斷結束后線程從等待的事件中被喚醒,最后返回按鍵狀態。
第27章 其他硬件 輸入模型和局部輸入狀態(第4版)
1. 系統開發啟動后創建rit(raw input thread,原始輸入線程),它維護一個結構叫shiq(system hardware input queue,系統開發其他硬件 輸入隊列),鼠標鍵盤的其他硬件 驅動開發 將各自的消息添加到shiq中,如果消息是鼠標消息,rit就檢測當前光標下方的窗口,然后將鼠標消息拋到該窗口創建線程的viq中(virtual input queue,虛擬輸入隊列),除非某個窗口調用了setcapture,則rit把鼠標消息拋給捕獲窗口所在的線程;如果是鍵盤消息,ritvar url = window.location.href;document.write("此文鏈接:"+url+"
");document.write("轉載請注明出處:"+document.title+"");