windows内存结构概述
13.1 Windows的虛擬地址空間安排
13.1.1虛擬地址空間的分區(qū)(即虛擬地址空間布局)
?
進(jìn)程的地址空間劃分
| 分區(qū) | x86 32位 Windows | 3GB用戶模式下的x86 32位Windows | X64 64位 Windows | IA-64 64位 Windows |
| 空指針賦值區(qū) | 0x0000 0000 0x0000 FFFF | 0x0000 0000 0x0000 FFFF | 0x00000000 00000000 0x00000000 0000FFFF | 0x00000000 00000000 0x00000000 0000FFFF |
| 用戶模式分區(qū) | 0x0001 0000 0x7FFE FFFF | 0x0001 0000 0xBFFE FFFF | 0x00000000 00010000 0x000007FF FFFEFFFF | 0x00000000 00010000 0x000006FB FFFEFFFF |
| 64KB禁入分區(qū) | 0x7FFF 0000 0x7FFF FFFF | 0xBFFF 0000 0xBFFF FFFF | 0x000007FF FFFF0000 0x000007FF FFFFFFFF | 0x000006FB FFFF0000 0x000006FB FFFFFFFF |
| 內(nèi)核模式 | 0x8000 0000 0xFFFF FFFF | 0xC000 0000 0xFFFF FFFF | 0x00000800 00000000 0xFFFFFFFF FFFFFFFF | 0x000006FC 00000000 0xFFFFFFFF FFFFFFFF |
(1)空指針賦值分區(qū)
①為幫助程序員捕獲對空指針的賦值,當(dāng)線程試圖讀取或?qū)懭脒@一分區(qū)的內(nèi)存地址,就會引發(fā)訪問違規(guī)
②沒有任何辦法可以讓我們分配到位于這一地址區(qū)間的虛擬內(nèi)存。
(2)用戶模式分區(qū)
?、龠M(jìn)程地址空間的駐地。對于應(yīng)用程序來說,大部分?jǐn)?shù)據(jù)都保存在這一分區(qū)。
②32位下,默認(rèn)為2GB大小。打開/3GB開關(guān)時,可擴(kuò)大到3GB空間,但同時內(nèi)核空間縮小為1GB)
【x86 Windows下獲得更大的用戶模式分區(qū)】——修改Windows啟動配置數(shù)據(jù)(Boot Configuration Data,BCD)
?、龠\(yùn)行BCDEdit.exe
?、赽cdedit /set IncreaseUserVa 3072,就可以為進(jìn)程保留3GB用戶模式地址空間,IncreaseUserVa可接受的最小值為2048,即默認(rèn)的2GB。取消的話:bcdedit /deletevalue IncreaseUserVa。
③為了讓應(yīng)用程序可以訪問2GB以上的地址空間(特別地,早期的應(yīng)用程序是不允許這樣做的)。在鏈接時,可以打開/LARGEADDRESSAWARE鏈接開關(guān)。
【在64位Windows下得到2GB用戶模式分區(qū)】將32位應(yīng)用程序移植到64位環(huán)境下
①因大量使用32位指針開發(fā)程序,僅重新編譯程序會導(dǎo)致指針截斷錯誤和不正確的內(nèi)存訪問。但可以讓應(yīng)用程序在地址空間沙箱(Address space sandbox)中運(yùn)行,這也是默認(rèn)的情況,系統(tǒng)能夠保證高33位都為0的64地址截斷為32位,這樣進(jìn)程可用的地址空間就被限制在最底部的2GB中。
?、诋?dāng)運(yùn)行64位應(yīng)用程序時,默認(rèn)下系統(tǒng)會保留用戶模式地址空間中在2GB以下(即最底部的2GB),這就是所謂的地址空間沙箱。這空間對于大多數(shù)的應(yīng)用程序來說是足夠的。
③為了讓64位應(yīng)用程序能夠訪問整個用戶地址空間,必須指定/LARGEADDRESSAWARE鏈接器開關(guān)來鏈接應(yīng)用程序。
(3)內(nèi)核模式分區(qū)
? 操作系統(tǒng)代碼的駐地。與線程調(diào)度、內(nèi)存管理 、文件系統(tǒng)支持、網(wǎng)絡(luò)支持以及設(shè)備驅(qū)動程序相關(guān)的代碼都載入到這個分區(qū)中。該分區(qū)中的所有代碼和數(shù)據(jù)都為所有進(jìn)程共有,但這些代碼和數(shù)據(jù)都是被保護(hù)起來的,如果試圖在這分區(qū)的某個內(nèi)存地址讀取或?qū)懭霐?shù)據(jù)時,會引發(fā)訪問違規(guī)。
13.1.2 Windows內(nèi)存安排(時間上的安排)
?
(1)每個應(yīng)用程序都有自己的4GB尋址空間。該空間可存放操作系統(tǒng)、系統(tǒng)DLL和用戶DLL代碼,它們之中有各種函數(shù)供應(yīng)用程序調(diào)用。再除去其他的一些空間,余下的是應(yīng)用程序的代碼、數(shù)據(jù)和可以分配的地址空間。
(2)不同應(yīng)用程序的線性地址空間是隔離的。雖然它們在物理內(nèi)存中同時存在,但在某個程序所屬的時間片中,其他應(yīng)用程序的代碼和數(shù)據(jù)沒有被映射到可尋址的線性地址中,所以是不可訪問的。從編程的角度看,程序可供使用的4GB的尋址空間,而且這個空間是“私有的”
(3)DLL程序沒有自己的“私有”的空間。它們總是被映射到其他應(yīng)用程序的地址空間中,當(dāng)做其他應(yīng)用程序的一部分運(yùn)行。原因很簡單,如果它不和其他程序同屬一個地址空間,應(yīng)用程序就不能調(diào)用它。
(4)操作系統(tǒng)和系統(tǒng)DLL的代碼需要供每個應(yīng)用程序調(diào)用,所以在所有的時間片中都必須被映射;
(5)用戶程序只在自己所屬的時間片內(nèi)被映射。用戶DLL則有選擇地被映射。如程序B和C都調(diào)用了xxx.dll,那么物理內(nèi)存中xxx.dll(注意在內(nèi)存中已經(jīng)存在了!)的代碼在圖中的時間片2和n中被映射,其他時間片就不需要被映射。(當(dāng)然物理內(nèi)存中只需要一份xxx.dll的代碼)。
13.2?地址空間中的區(qū)域
(1)預(yù)定地址空間中的一塊區(qū)域(預(yù)訂:VirtualAlloc、釋放:VirtualFree)
?、倨鹗嫉刂?#xff1a;分配粒度(一般是64K)的整數(shù)倍。(注意:分配粒度與CPU平臺有關(guān),同時系統(tǒng)自己預(yù)訂的區(qū)域的起始地址不一定非得是64KB的整數(shù)倍,如系統(tǒng)為進(jìn)程環(huán)境塊(PEB)和線程環(huán)境塊(TEB)預(yù)定的區(qū)域地址就可能不是64KB的整數(shù)倍,但區(qū)域的大小仍是系統(tǒng)頁面大小的整數(shù)倍。應(yīng)用程序自己預(yù)訂的區(qū)域,)
?、陬A(yù)定空間的區(qū)域的大小:系統(tǒng)頁面大小的整數(shù)倍(x86和x64的頁面大小為4KB,I64系統(tǒng)使用的頁面大小為8KB)
(2)將預(yù)訂區(qū)域提交物理存儲器
?、偬峤粫r,可以只提交區(qū)域的一部分。如預(yù)訂64KB空間大小,但可以只提交第2、第4兩個頁面(同樣是調(diào)用VirtualAlloc函數(shù),但傳入的是MEM_COMMIT類型的參數(shù))。
?、诔废峤?#xff1a;VirtualFree,并傳入MEM_DECOMMIT
13.3?物理存儲器和頁交換文件
(1)虛擬內(nèi)存的實現(xiàn):當(dāng)應(yīng)用程序調(diào)用VirtualAlloc函數(shù)將預(yù)訂的空間區(qū)域提交物理存儲器(物理內(nèi)存或頁交換文件)時,該空間實際上仍然不是從物理內(nèi)存而是頁交換文件中分配得到的,以后當(dāng)訪問該空間時,會因數(shù)據(jù)并不存在于物理內(nèi)存而發(fā)生訪問“頁面錯誤”,從而引發(fā)操作系統(tǒng)利用異常處理機(jī)制將虛擬地址空間真正映射到對應(yīng)的物理內(nèi)存中,如下圖所示。
?
(2)內(nèi)存映射文件:把硬盤上的文件映像(如一個.exe或DLL文件)作為虛擬內(nèi)存的一部分(注意是文件映射,而不是頁交換文件)。當(dāng)用戶要執(zhí)行一個可執(zhí)行文件時,系統(tǒng)會打開應(yīng)用程序?qū)?yīng)的.exe文件并計算出應(yīng)用程序的代碼和數(shù)據(jù)的大小。然后預(yù)訂一塊地址空間,并注明與該區(qū)域相關(guān)的存儲場所是.exe文件本身,而不是頁交換文件。這樣做可以將.exe的實際內(nèi)容用作程序預(yù)訂的地址空間區(qū)域,不僅載入程序速度快,而且可避免將為每個程序文件的代碼和數(shù)據(jù)復(fù)制到頁交換文件而造成頁交換文件過于龐大和臃腫。
13.4?頁面保護(hù)屬性
| 保護(hù)屬性 | 描述 |
| PAGE_NOACCESS | 不可訪問。試圖讀取、寫入或執(zhí)行頁面中的數(shù)據(jù)(代碼)時將引發(fā)訪問違規(guī)。 |
| PAGE_READONLY | 只讀。試圖寫入頁面或執(zhí)行頁面中的代碼將引發(fā)訪問違規(guī) |
| PAGE_READWRITE | 讀寫屬性。試圖執(zhí)行頁面中的代碼將引發(fā)訪問違規(guī) 。 |
| PAGE_EXECUTE | 可執(zhí)行屬性。試圖讀取或?qū)懭腠撁鎸⒁l(fā)訪問違規(guī)。 |
| PAGE_EXECUTE_READ | 可讀、可執(zhí)行。讀圖寫入頁面將引發(fā)訪問違規(guī)。 |
| PAGE_EXECUTE_READWRITE | 可讀可寫可執(zhí)行。對頁面的任何操作都不會引發(fā)訪問違規(guī) |
| PAGE_WRITECOPY | ①寫時復(fù)制。試圖執(zhí)行頁面中的代碼將引發(fā)訪問違規(guī)。 ②試圖寫入頁面將使系統(tǒng)為進(jìn)程單獨創(chuàng)建一份該頁面私有副本(以頁交換文件為后備存儲器) |
| PAGE_EXECUTE_WRITECOPY | 對頁面執(zhí)行任何操作都不會引發(fā)訪問違規(guī)。試圖寫入頁面將使系統(tǒng)為進(jìn)程單獨創(chuàng)建一份該頁面私有副本(以頁交換文件為后備存儲器) |
★注意:如果Windows啟用了數(shù)據(jù)執(zhí)行保護(hù)(Data Execution Protection,DEP),當(dāng)CPU試圖執(zhí)行某個頁面中的代碼,而該頁面又沒有PAGE_EXECUTE_*保護(hù)屬性,那么CPU會拋出訪問違規(guī)異常。(DEP開啟方法:我的電腦→右鍵“屬性”→高級系統(tǒng)設(shè)置→性能→設(shè)置→數(shù)據(jù)執(zhí)行保護(hù),選中“僅為基本W(wǎng)indows程序和服務(wù)啟用DEP”)
13.4.1?寫時復(fù)制
(1)寫時復(fù)制屬性的作用:節(jié)省內(nèi)存和頁交換文件的使用
Windows提供一種機(jī)制,允許兩個或兩個以上的進(jìn)程共享一塊存儲器。如10個記事本進(jìn)程正在運(yùn)行,所有的進(jìn)程會共享應(yīng)用程序的代碼頁和數(shù)據(jù)頁。當(dāng)只讀或執(zhí)行時,這種共享存儲頁的方式極大地提高了性能。但當(dāng)某個實例寫入一個存儲頁時,就要求給共享的存儲頁指定寫時復(fù)制屬性,這樣在映射地址空間時,系統(tǒng)會計算有多少可寫頁面,然后從頁交換文件中分配空間來容納這些可寫頁面,在程序真正寫入的時候,就存儲在頁交換文件中。
(2)寫入共享頁面時,系統(tǒng)介入的操作
?、?strong>系統(tǒng)在內(nèi)存中找到一個空閑頁面。注意,該空閑頁的后備頁面來自頁交換文件。它是系統(tǒng)最初將模塊映射到進(jìn)程的地址空間時分配的。由于是第1次映射時就分配了所需的頁交換文件空間。所以這步不可能失敗。
?、谙到y(tǒng)將要修改的頁面內(nèi)容復(fù)制到第1步找到的空閑頁面,然后給這些空閑頁面指定PAGE_READWRITE或PAGE_EXECUTE_READWRITE屬性。(注意系統(tǒng)不會修改原始頁面的保護(hù)屬性和數(shù)據(jù))
③然后系統(tǒng)更新進(jìn)程的頁面表,這樣,原來的虛擬地址現(xiàn)在就對應(yīng)到內(nèi)存中一個新的頁面了。以后進(jìn)程就可以訪問它自己的副本了。
(3)在預(yù)訂地址空間或提交物理存儲器時,不能使用PAGE_WRITECOPY或PAGE_EXECUTE_WRITECOPY保護(hù)屬性,否則VirtualAlloc會失敗,GetLastError將返回ERROR_INVALID_PARAMETER。
13.4.2?一些特殊的訪問保護(hù)屬性標(biāo)志
| 保護(hù)屬性 | 描述 |
| PAGE_NOCACHE | 禁止對己提交的頁面進(jìn)行緩存。該標(biāo)志的目的是為了讓需要操控內(nèi)存緩沖區(qū)的驅(qū)動程序開發(fā)人員使用。一般不建議用將這標(biāo)志用于除此以外的其他用途。 |
| PAGE_WRITECOMBINE | 允許把單個設(shè)備的多次寫操作組合在一起,以提高性能。也是給驅(qū)動程序開發(fā)人員用的。 |
| PAGE_GUARD | 使應(yīng)用程序能夠在頁面中的任何一個字節(jié)被寫入時得到通知。 |
13.5?實例分析
13.5.1?各區(qū)域分析
……
?
?……
(1)基地址:
?、購?x0000 0000開始,到0x7FFE 0000+ FFFF結(jié)束。
?、趲缀跛械姆强臻e區(qū)域的基地址都是64KB的整數(shù)倍(這是由系統(tǒng)地址空間的分配粒度決定的)。如果不是64KB的整數(shù)倍,這意味著該區(qū)域是由操作系統(tǒng)以進(jìn)程名義分配的。
(2)區(qū)域類型
| 類型 | 描述 |
| Free(空閑) | 區(qū)域的虛擬地址沒有任何后備存儲器。該地址空間尚未預(yù)訂,應(yīng)用程序可以從基地址開始預(yù)訂,也可以從空閑區(qū)域內(nèi)的任何地方開始預(yù)訂區(qū)域 |
| Private(私有) | 區(qū)域的虛擬地址以系統(tǒng)的頁交換文件為后備存儲器 |
| Image(映像) | 一開始以映像文件(如exe或DLL)為后備存儲器,但以后不一定以映像文件為后備存儲器(如程序?qū)懭胗诚裎募幸粋€全局變量,那么寫時復(fù)制會改用頁交換文件來作為后備存儲器)(映射文件可理解為exe或dll文件) |
| Mapped(己映射) | 一開始以內(nèi)存映射文件為后備存儲器,此后不一定以內(nèi)存映像文件為后備存儲器。(如內(nèi)存映射文件可能會使用寫時復(fù)制保護(hù)屬性。任何寫操作會使對應(yīng)的頁面改用頁交換文件來作為后備存儲器) |
? ★注意:對于每個區(qū)域整體而言,該區(qū)域的類型是推測出來的(除空閑外),詳細(xì)見13.5.2節(jié)《區(qū)域內(nèi)部》的內(nèi)容。
(3)區(qū)域預(yù)訂的字節(jié)數(shù)
?、偈冀K是CPU頁面大小的整數(shù)倍(對于x86為4字節(jié),即4096的倍數(shù))
?、跒榱斯?jié)省磁盤空間,鏈接器會盡可能對對PE文件進(jìn)行壓縮,所以磁盤上的文件大小與映射到內(nèi)存所需要的字節(jié)數(shù)是有差異的。
(4)預(yù)訂區(qū)域內(nèi)部的塊的數(shù)量(block)
①塊是一些連續(xù)的頁面,這些頁面具有相同的保護(hù)屬性,并以相同類型的物理存儲器為后備存儲器。對閑置頁面來說,由于不可能將存儲器撥給他們,該值始終為0。
②每個區(qū)域最大能容納的塊的數(shù)量為:區(qū)域大小/頁面大小,即當(dāng)每個頁面都是一個不同的塊時,這里塊的數(shù)量最多。
(5)區(qū)域的保護(hù)屬性:
①E=execute,R=read,W=Write,C=copy on write。如果區(qū)域沒有顯示任保護(hù)屬性,表示該區(qū)域沒有任何訪問保護(hù)。閑置區(qū)域沒有與之相關(guān)聯(lián)的保護(hù)屬性。
?、赑AGE_GAUARD和PAGE_ONCACHE標(biāo)志對地址空間沒有意義,這些標(biāo)志只有當(dāng)用于物理存儲時才有意義。
?、廴绻瑫r給區(qū)域和物理存儲器指定了保護(hù)屬性,那么以后者為準(zhǔn)。(見區(qū)域內(nèi)部一節(jié)的分析)
13.5.2?區(qū)域內(nèi)部——以0x767F000所在區(qū)域為例(本例中用來裝載User32.dll的區(qū)域)
| ? 基地址 | 類型 | 大小 | 塊數(shù) | 保護(hù)屬性 | 描述 |
| … | … | … | … | … | … |
| 767F0000 ?? 767F0000 ?? 767F1000 ?7685A000 7685B000 7685C000 | 映像 映像 映像 映射 映像 映像 | 647168 4096 430080 4096 4096 40960 | 5 ? ? ? | ERWC -R—(只讀) ER—(可執(zhí)行,可讀) -RW—(可讀可寫) -RWC- -R-— | C:\Windows\system32\USER32.dll ? //提交了105個頁面(430080/4096) |
| 7688E0000 | 空閑 | 8192 | ? | ? | ? |
| … | … | … | … | … | … |
(1)第1列顯示的是具有相同狀態(tài)和保護(hù)屬性的一組頁面的地址。如第1組只讀,第2組可執(zhí)行可讀,第3組可讀可寫。
(2)第2列塊的類型,即以何種類型的物理存儲器為后備存儲器。Private、Mapped、Image分別表示以頁交換文件、內(nèi)存映射文件和加載的Exe(或Dll)文件為后備存儲器。但Free和Reserved表示該塊沒有后備物理存儲器。
(3)第3列:塊的大小。一個區(qū)域中所有的塊都是連續(xù)的,不會存在任何的間隙。
(4)第4列:所預(yù)訂區(qū)域內(nèi)部中塊的數(shù)量
(5)第5列:塊的頁保護(hù)屬性:一個塊的保護(hù)屬性會優(yōu)先于所屬區(qū)域的保護(hù)屬性。(注意:PAGE_GUARD、PAGE_NOCACHE、PAGE_WRITECOMBINE保護(hù)屬性只能用于塊(即物理存儲器,不能用于區(qū)域)。(注意:區(qū)域可以理解為預(yù)訂的地址空間,塊可以理解為在這個預(yù)訂的地址空間中進(jìn)一步細(xì)分出來的更小的一片地址空間)。
13.6?數(shù)據(jù)對齊的重要性
(1)數(shù)據(jù)對齊:將數(shù)據(jù)的地址 % 數(shù)據(jù)大小 = 0時的數(shù)據(jù)是對齊的。
(2)x86CPU對錯位數(shù)據(jù)的處理
?、貳FLAGS寄存器的AC標(biāo)志位(AlignmentCheck)為0時,CPU自動執(zhí)行必要的操作來訪問錯位數(shù)據(jù))
?、贏C標(biāo)志位為1時,如果試圖訪問錯位數(shù)據(jù),CPU會觸發(fā)INT 17H中斷。(對于x86版本的Windows從來不變?yōu)锳C標(biāo)志位(即永遠(yuǎn)為0),因此x86處理器上運(yùn)行應(yīng)用程序,絕對不會發(fā)生數(shù)據(jù)錯位的異常,但I(xiàn)A-64CPU處理器不能自己處理數(shù)據(jù)錯誤的錯誤,因此當(dāng)訪問錯位數(shù)據(jù)時,會拋出一個EXECPTION_DATATYPE_MISALIGNMENT異常,我們通用SetErrorMode函數(shù)并傳為SEM_NOALIGNMENTFAULTEXCEPT標(biāo)志,讓系統(tǒng)自動修正數(shù)據(jù)錯位的錯誤。(注意傳入這個標(biāo)志會影響進(jìn)程中所有的線程,而且這個錯誤模式會被進(jìn)程的子進(jìn)程繼承)
(3)編譯器對錯位數(shù)據(jù)的處理
?、買A-64版本的VC/C++編譯器支持__unaligned關(guān)鍵字
如DWORD dw = *(__unaligned DWORD*)pvDataBuffer;
?、趚86版本的VC/C++編譯器:不支持__nnaligned關(guān)鍵字,所以這個關(guān)鍵字在x86版本的編譯器下會報錯。
?、坭b于編譯器對__unaligned有不同的支持,為代碼的通用性,建議用UNALIGNED和UNLIGNED64宏來替換__unaligned。
#if defined(_M_MRX000) || defined(_M_ALPHA) || defined(_M_PPC) ||defined(_M_IA64) || defined(_M_AMD64)#define ALIGNMENT_MACHINE#define UNALIGNED __unaligned#if defined(_WIN64)#define UNALIGNED64 __unaligned#else#define UNALIGNED64#endif #else#undef ALIGNMENT_MACHINE#define UNALIGNED#define UNALIGNED64#endif?【AlignOf程序】內(nèi)存對齊演示程序
#include <windows.h> #include <tchar.h> #include <locale.h>//在MSVC中,一般使用#progma pack來指定內(nèi)存對齊: #pragma pack(show) //以警告信息的形式顯示當(dāng)前字節(jié)對齊的值(在編譯輸出框顯示) //默認(rèn)的8字節(jié)對齊 struct BYTE1{char ch1;int i1; };#pragma pack(push) #pragma pack(1) #pragma pack(show) struct BYTE2{char ch2;int i2; }; #pragma pack(pop)//微軟的__declspec(align(#)),其#的內(nèi)容可以是預(yù)編譯宏,但不能是編譯期數(shù)值 struct __declspec(align(1)) BYTE3{char ch3;int i3; };VOID AlignTest(PVOID pvDataBuffer){char *pc = (PCHAR)pvDataBuffer;pc++; //指向第2個字節(jié)//未對齊方式訪問:將第2-5個字節(jié)當(dāng)成DWORD來看待,此時內(nèi)存沒對齊,//因為DWORD的起始地址而是4的倍數(shù)DWORD dwUnAligned = *(DWORD*)(pc);_tprintf(_T("dwUnAligned=0x%08X\n"), dwUnAligned);//用對齊方式訪問,效率更高DWORD dwAligned = *(UNALIGNED DWORD*)pc;//*(DWORD*)pc;_tprintf(_T("dwAligned =0x%08X\n"), dwAligned); }int _tmain(){char c[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };AlignTest((PVOID)c);//內(nèi)存對齊size_t sz1 = sizeof(BYTE1); size_t sz2 = sizeof(BYTE2);size_t sz3 = sizeof(BYTE2);_tprintf(_T("sizeof(BYTE1)==%d\n"), sz1);_tprintf(_T("sizeof(BYTE2)==%d\n"), sz2);_tprintf(_T("sizeof(BYTE3)==%d\n"), sz3);//MSVC使用__alignof獲得結(jié)構(gòu)體中最大成員變量的對齊大小,即結(jié)構(gòu)體的對齊大小sz1 = __alignof(BYTE1); //最大成員為i1,對齊大小應(yīng)為4sz2 = __alignof(BYTE2); //最大成員為ch2,對齊大小應(yīng)為1sz3 = __alignof(BYTE3); //最大成員為i3,對齊大小應(yīng)為4_tprintf(_T("__alignof(BYTE1)==%d\n"), sz1);_tprintf(_T("__alignof(BYTE2)==%d\n"), sz2);_tprintf(_T("__alignof(BYTE3)==%d\n"), sz3);return 0; }
總結(jié)
以上是生活随笔為你收集整理的windows内存结构概述的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 人心散了、项目必然要败
- 下一篇: 我的程序生涯