## 一:背景 ### 1. 講故事 前段時間訓練營里有朋友問 `記憶體映射文件` 是怎麼玩的?說實話這東西理論我相信很多朋友都知道,就是將文件映射到進程的虛擬地址,說起來很容易,那如何讓大家眼見為實呢?可能會難倒很多人,所以這篇我以自己的認知嘗試讓大家眼見為實。 ## 二:如何眼見為實 ### 1. ...
一:背景
1. 講故事
前段時間訓練營里有朋友問 記憶體映射文件
是怎麼玩的?說實話這東西理論我相信很多朋友都知道,就是將文件映射到進程的虛擬地址,說起來很容易,那如何讓大家眼見為實呢?可能會難倒很多人,所以這篇我以自己的認知嘗試讓大家眼見為實。
二:如何眼見為實
1. 我想象的文件映射
在任何討論之前,記憶體文件映射大概像下麵這樣,多個進程可以完全View一個文件,也可以 View 文件的一部分到進程的虛擬地址中,畫個圖大概像下麵這樣。
但仔細一想,這裡還有很多的小細節,比如:
疑問1:到底是映射文件還是映射磁碟的物理地址 ?
疑問2:既然是後備存儲,那是不是每次修改虛擬地址都要刷硬碟 ?
疑問3:記憶體頁是4k為一個單位,文件大小不是 4k 整數倍怎麼辦 ?
這三個疑問我相信很多朋友或多或少都會遇到,這裡我簡單解答一下,後面再用 windbg 驗證。
-
嚴格來說是
硬碟物理地址
。 -
文件所處的硬碟地址為後備存儲這個不假,但這裡有個小細節,對虛擬地址的讀寫涉及到
記憶體頁
概念,如果訪問的虛擬地址所在的物理地址不在物理記憶體
中,就會引發缺頁中斷,操作系統會將 磁碟上的 4k 頁粒度灌入到物理記憶體
中,同樣的道理,如果修改了虛擬地址,那麼物理記憶體頁就是臟數據,會在後續的某個時刻刷新到硬碟
上,產生磁碟 IO。
總的來說:從磁碟到物理記憶體(記憶體條) 之間的記憶體頁的換入換出都是一種按需的 懶載入懶寫入行為,稍後我們用 windbg 驗證下。
- 記憶體的管理採用的是記憶體頁的方式,如果 View 大於 文件Size,那麼文件會擴容到 4k 對齊,這樣方便對文件追加寫入。
綜合上面的三點信息,圖就可以畫的再詳細一點了,比如下麵這樣:
熟悉記憶體管理的朋友應該知道,我們程式的 exe 和 dll 就是用 記憶體映射文件
的方式載入到虛擬地址中的,所以就拿它開刀吧。
2. 一段測試代碼
為了方便演示,上一段簡單的的測試代碼,觀察 ConsoleApp1.exe
的映射方式。
static void Main(string[] args)
{
Console.WriteLine($"當前時間:{DateTime.Now}, 程式啟動!");
Console.ReadLine();
}
接下來用 windbg 啟動 ConsoleApp1.exe
兩次,結合詳細分解圖,我們觀察下這兩個進程的虛擬地址所映射的記憶體條物理地址是否一致?
- 實例1
ModLoad: 00007ff6`bfe00000 00007ff6`bfe2a000 apphost.exe
ModLoad: 00007ff9`b1450000 00007ff9`b1648000 ntdll.dll
...
0:008> lmvm apphost
Browse full module list
start end module name
00007ff6`bfe00000 00007ff6`bfe2a000 apphost C (private pdb symbols) c:\mysymbols\apphost.pdb\1643A9EB126F4FE184548E9CC1B740B71\apphost.pdb
Loaded symbol image file: D:\net7\ConsoleApplication1\ConsoleApp1\bin\Debug\net6.0\ConsoleApp1.exe
Image path: apphost.exe
Image name: apphost.exe
...
0:008> ~
0 Id: 232c.4abc Suspend: 1 Teb: 0000000e`7b1a5000 Unfrozen
- 實例2
ModLoad: 00007ff6`bfe00000 00007ff6`bfe2a000 apphost.exe
ModLoad: 00007ff9`b1450000 00007ff9`b1648000 ntdll.dll
...
0:008> ~
0 Id: 60e8.3e3c Suspend: 1 Teb: 000000da`ab498000 Unfrozen
1 Id: 60e8.53b0 Suspend: 1 Teb: 000000da`ab49a000 Unfrozen
這裡要提醒一下的是在 Windows 平臺上 ConsoleApp1.exe
已經成了一個引導程式,通過 lmvm 可以看到它其實是 apphost.exe
。
兩個實例都開起來後,可以看到 apphost.exe
在各自進程的虛擬地址都一樣,那他們的物理地址是否也一樣呢? 要尋找答案,接下來我們到 Windows 內核態去挖一挖。
lkd> !process 0 0 ConsoleApp1.exe
PROCESS ffff838bd84c9080
SessionId: 8 Cid: 232c Peb: e7b1a4000 ParentCid: 0b14
FreezeCount 2
DirBase: 3468cf000 ObjectTable: ffff938feae02900 HandleCount: 172.
Image: ConsoleApp1.exe
PROCESS ffff838bef157080
SessionId: 8 Cid: 60e8 Peb: daab497000 ParentCid: 4804
FreezeCount 2
DirBase: 3552f3000 ObjectTable: ffff938fe8f7ec40 HandleCount: 166.
Image: ConsoleApp1.exe
從卦中看,Cid: 232c
是我們的實例1, Cid: 60e8
是我們的實例2,接下來用 windbg 提供的 !vtop 命令觀察 apphost.exe 的首地址對應的物理地址。
// ---- 實例1 -----
lkd> !vtop 3468cf000 00007ff6bfe00000
Amd64VtoP: Virt 00007ff6bfe00000, pagedir 00000003468cf000
Amd64VtoP: PML4E 00000003468cf7f8
Amd64VtoP: PDPE 00000001138dbed0
Amd64VtoP: PDE 00000002153dcff8
Amd64VtoP: PTE 000000024dadd000
Amd64VtoP: Mapped phys 00000002271c2000
Virtual address 7ff6bfe00000 translates to physical address 2271c2000.
//---- 實例2 -----
lkd> !vtop 3552f3000 00007ff6bfe00000
Amd64VtoP: Virt 00007ff6bfe00000, pagedir 00000003552f3000
Amd64VtoP: PML4E 00000003552f37f8
Amd64VtoP: PDPE 00000002db7ffed0
Amd64VtoP: PDE 0000000208100ff8
Amd64VtoP: PTE 000000033de01000
Amd64VtoP: Mapped phys 00000002271c2000
Virtual address 7ff6bfe00000 translates to physical address 2271c2000.
從卦中看,實例1 和 實例2 的 虛擬地址
映射的 物理地址
是相同的 2271c2000
。這也很好的解釋了那張圖。
有朋友可能會有疑問,能否看下 2271c2000 這個 物理地址
的內容? 這當然是可以的,用 windbg 的 !da
就好了。
lkd> !db 2271c2000
#2271c2000 4d 5a 90 00 03 00 00 00-04 00 00 00 ff ff 00 00 MZ..............
#2271c2010 b8 00 00 00 00 00 00 00-40 00 00 00 00 00 00 00 ........@.......
#2271c2020 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
#2271c2030 00 00 00 00 00 00 00 00-00 00 00 00 e8 00 00 00 ................
#2271c2040 0e 1f ba 0e 00 b4 09 cd-21 b8 01 4c cd 21 54 68 ........!..L.!Th
#2271c2050 69 73 20 70 72 6f 67 72-61 6d 20 63 61 6e 6e 6f is program canno
#2271c2060 74 20 62 65 20 72 75 6e-20 69 6e 20 44 4f 53 20 t be run in DOS
#2271c2070 6d 6f 64 65 2e 0d 0d 0a-24 00 00 00 00 00 00 00 mode....$.......
從卦中看,物理地址上有一段 This program cannot be run in DOS mode
,這不就是經典的 PE 文件哈,如果不相信可以用 WinHex 打開 ConsoleApp1.exe
即可,截圖如下:
最後就是內核中的 記憶體管理器
會將 物理地址 與 磁碟地址 進行打通,實現懶載入和懶寫入。
3. 如何自定義實現
Image 雖然是一個快捷的觀察記憶體文件映射方式,那如果自己能實現一個就更有意思了,比如下麵對 1.txt
進行文件映射,在 C# 中有一個快捷類 MemoryMappedFile
實現了 win32api 的封裝,參考代碼如下:
internal class Program
{
static void Main(string[] args)
{
int capaticy = 1024; //1k
using (var mmf = MemoryMappedFile.CreateFromFile(@"C:\1.txt", FileMode.OpenOrCreate,
"testmapfile",
capaticy,
MemoryMappedFileAccess.ReadWrite))
{
var viewAccessor = mmf.CreateViewAccessor(0, capaticy);
while (true)
{
Console.WriteLine("請輸入你要寫入的內容: ");
string input = Console.ReadLine();
viewAccessor.WriteArray(0, input.ToArray(), 0, input.Length);
}
}
}
}
接下來用 windbg 附加一下,觀察 1.txt 是不是被 MappedFile 上了,同時做的修改有沒有更新到物理磁碟上。
0:006> !address
BaseAddr EndAddr+1 RgnSize Type State Protect Usage
-----------------------------------------------------------------------------------------------
...
+ 31a0000 31a1000 1000 MEM_MAPPED MEM_COMMIT PAGE_READWRITE MappedFile "\Device\HarddiskVolume3\1.txt"
...
0:006> du 31a0000
031a0000 "helloworld!"
從卦中可以看到,雖然 1.txt 最大的 View 區間是 1k,但提交的記憶體頁還是按照最小粒度 4k 給的。
三:總結
這篇我們就簡單的淺聊一下,如果這塊是知識盲區的朋友應該會有一點幫助,希望沒有帶偏大家,更多的細節期待大家挖掘!
