之前的章節(jié)中了解了邏輯地址通過分段到線性地址的轉(zhuǎn)換,線性地址通過分頁到物理地址的轉(zhuǎn)換,在這個章節(jié)講以linux的一個內(nèi)存尋址將之前的知識寫成一個完整的例子。在學(xué)習(xí)的過程中,學(xué)習(xí)的知識不是很重要,我了解一個知識,我喜歡先學(xué)習(xí)使用方式,然后學(xué)習(xí)理論知識,最后我會完整的將學(xué)習(xí)的理論知識將知識應(yīng)用到實(shí)際中,驗(yàn)證理論知識的正確性。 這個章節(jié)很長,首先提供一個邏輯地址(虛擬地址)如何從分段機(jī)制中得到線性地址,然后線性地址如何映射到真正的物理地址,最后處理器通過這個物理地址進(jìn)行尋址。而且我們會區(qū)分內(nèi)核空間和用戶空間的尋址。我們提供一個虛擬地址 0xffffffff81bd6b60。那么,處理器是如何找到對應(yīng)在物理內(nèi)存中的地址呢?下面就讓我們慢慢揭開這個面紗。
邏輯地址轉(zhuǎn)換為線性地址其實(shí),在之前的知識中我們知道在x86_64架構(gòu),處理器已經(jīng)弱化了段寄存器的地位,通常我們訪問的虛擬地址都被處理器轉(zhuǎn)換為相同的物理地址,但在操作系統(tǒng)的實(shí)現(xiàn)中,仍然需要提供段描述符。如果需要尋址一個代碼數(shù)據(jù),那么在處理器默認(rèn)使用的是 CS 段寄存器,我們看看 `CS`的內(nèi)容 (gdb) info registers cscs 0x10 16 我們將這個數(shù)據(jù)解析成處理器理解的數(shù)據(jù)結(jié)構(gòu):
`Table indicator(TI)`說明代碼段寄存器使用的是GDT描述符表,RPL為0,查找在GDT描述表的索引位置為2,實(shí)際上,處理器將這個索引乘以8,得到的是16,處理器將從這個位置獲取8個字節(jié)的代碼段描述符。 下面我們詳細(xì)查看一下Linux系統(tǒng)中段描述符表的數(shù)據(jù)內(nèi)容。這里我們使用的qemu模擬處理器的運(yùn)行,得到gdt地址,將這個地址的128字符輸出成二進(jìn)制 這一串慕名奇妙的二進(jìn)制數(shù)據(jù)表示什么意思吶(在計(jì)算機(jī)中本質(zhì)上就是二進(jìn)制,但上下文賦予了這些數(shù)據(jù)不同的含義)。按照Intel文檔中對段描述符表進(jìn)行解析后 NULL [0]: {limit = 0x00000000, base = 0x00000000, type = 0x00, s = 0x0, dpl = 0x0, p = 0x0, avl = 0x0, l = 0x0, d = 0x0, g = 0x0}GDT_ENTRY_KERNEL32_CS [1]: {limit = 0xffffffff, base = 0x00000000, type = 0x0b, s = 0x1, dpl = 0x0, p = 0x1, avl = 0x0, l = 0x0, d = 0x1, g = 0x1}GDT_ENTRY_KERNEL_CS [2]: {limit = 0xffffffff, base = 0x00000000, type = 0x0b, s = 0x1, dpl = 0x0, p = 0x1, avl = 0x0, l = 0x1, d = 0x0, g = 0x1}GDT_ENTRY_KERNEL_DS [3]: {limit = 0xffffffff, base = 0x00000000, type = 0x03, s = 0x1, dpl = 0x0, p = 0x1, avl = 0x0, l = 0x0, d = 0x1, g = 0x1}GDT_ENTRY_DEFAULT_USER32_CS [4]: {limit = 0xffffffff, base = 0x00000000, type = 0x0b, s = 0x1, dpl = 0x3, p = 0x1, avl = 0x0, l = 0x0, d = 0x1, g = 0x1}GDT_ENTRY_DEFAULT_USER_DS [5]: {limit = 0xffffffff, base = 0x00000000, type = 0x03, s = 0x1, dpl = 0x3, p = 0x1, avl = 0x0, l = 0x0, d = 0x1, g = 0x1}GDT_ENTRY_DEFAULT_USER_CS [6]: {limit = 0xffffffff, base = 0x00000000, type = 0x0b, s = 0x1, dpl = 0x3, p = 0x1, avl = 0x0, l = 0x1, d = 0x0, g = 0x1}Unused [7]: {limit = 0x00000000, base = 0x00000000, type = 0x00, s = 0x0, dpl = 0x0, p = 0x0, avl = 0x0, l = 0x0, d = 0x0, g = 0x0}GDT_ENTRY_TSS [8]: {limit = 0x00004087, base = 0xfffffe0000003000, type = 0x0b, dpl = 0x0, p = 0x1, zero0 = 0x0, g = 0x0, zero1 = 0x0}GDT_ENTRY_LDT [10]: {limit = 0x00000000, base = 0x0000000000000000, type = 0x00, dpl = 0x0, p = 0x0, zero0 = 0x0, g = 0x0, zero1 = 0x0}GDT_ENTRY_TLS_MIN [12]: {limit = 0x00000000, base = 0x00000000, type = 0x00, s = 0x0, dpl = 0x0, p = 0x0, avl = 0x0, l = 0x0, d = 0x0, g = 0x0}GDT_ENTRY_TLS [13]: {limit = 0x00000000, base = 0x00000000, type = 0x00, s = 0x0, dpl = 0x0, p = 0x0, avl = 0x0, l = 0x0, d = 0x0, g = 0x0}GDT_ENTRY_TLS_MAX [14]: {limit = 0x00000000, base = 0x00000000, type = 0x00, s = 0x0, dpl = 0x0, p = 0x0, avl = 0x0, l = 0x0, d = 0x0, g = 0x0}GDT_ENTRY_CPUNODE [15]: {limit = 0x00000000, base = 0x00000000, type = 0x05, s = 0x1, dpl = 0x3, p = 0x1, avl = 0x0, l = 0x0, d = 0x1, g = 0x0} 在`x86_64` 架構(gòu)體系中,Linux使用了16個段描述符,第一個位空描述符,這個是Intel要求的,后面的描述符就是Linux系統(tǒng)本身的設(shè)計(jì)需要了,我們在上文中知道代碼段寄存器(CS)選擇子的索引值為2,索引處理器在此應(yīng)用是是 GDT_ENTRY_KERNEL_CS 段描述符
由于這個段描述符中S被置位,所以這個段是代碼段或數(shù)據(jù)段,根據(jù)代碼段和數(shù)據(jù)段類型表 在這里Type的二進(jìn)制為 1011,可以知道 CS 段選擇子訪問的是一個代碼段,可執(zhí)行可讀可訪問的段,也就是我們所說的代碼。有于段選擇子的RPL為0,而段描述符中的DPL也是0,也就是說有權(quán)限訪問。由于標(biāo)志L被置位了,說明這個段執(zhí)行的是64位模式代碼。同時P位也被置位了,說明訪問的數(shù)據(jù)在內(nèi)存中。在這里由于G被置位,說明段大小為4G,基址為0,說明Linux采用的是平坦模式,邏輯地址(虛擬地址)等于線性地址,雖然這里提供了limit,但在64位模式下的CPU通常會忽略段大小校驗(yàn)。 所以經(jīng)過分段地址轉(zhuǎn)換,我們計(jì)算出了訪問內(nèi)存的線性地址,在開始計(jì)算分頁轉(zhuǎn)換前,我們先將線性地址轉(zhuǎn)換為各個分頁的分級索引。假設(shè)將虛擬地址 0xffffffff81bd6b60,在開始這個分割之前,我們先了解一下Linux的虛擬內(nèi)存地址布局 ======================================================================================================================== Start addr | Offset | End addr | Size | VM area description ======================================================================================================================== | | | | 0000000000000000 | 0 | 00007fffffffffff | 128 TB | user-space virtual memory, different per mm __________________|____________|__________________|_________|___________________________________________________________ | | | | 0000800000000000 | 128 TB | ffff7fffffffffff | ~16M TB | ... huge, almost 64 bits wide hole of non-canonical | | | | virtual memory addresses up to the -128 TB | | | | starting offset of kernel mappings. __________________|____________|__________________|_________|___________________________________________________________ | | Kernel-space virtual memory, shared between all processes: ____________________________________________________________|___________________________________________________________ | | | | ffff800000000000 | -128 TB | ffff87ffffffffff | 8 TB | ... guard hole, also reserved for hypervisor ffff880000000000 | -120 TB | ffff887fffffffff | 0.5 TB | LDT remap for PTI ffff888000000000 | -119.5 TB | ffffc87fffffffff | 64 TB | direct mapping of all physical memory (page_offset_base) ffffc88000000000 | -55.5 TB | ffffc8ffffffffff | 0.5 TB | ... unused hole ffffc90000000000 | -55 TB | ffffe8ffffffffff | 32 TB | vmalloc/ioremap space (vmalloc_base) ffffe90000000000 | -23 TB | ffffe9ffffffffff | 1 TB | ... unused hole ffffea0000000000 | -22 TB | ffffeaffffffffff | 1 TB | virtual memory map (vmemmap_base) ffffeb0000000000 | -21 TB | ffffebffffffffff | 1 TB | ... unused hole ffffec0000000000 | -20 TB | fffffbffffffffff | 16 TB | KASAN shadow memory __________________|____________|__________________|_________|____________________________________________________________ | | Identical layout to the 56-bit one from here on: ____________________________________________________________|____________________________________________________________ | | | | fffffc0000000000 | -4 TB | fffffdffffffffff | 2 TB | ... unused hole | | | | vaddr_end for KASLR fffffe0000000000 | -2 TB | fffffe7fffffffff | 0.5 TB | cpu_entry_area mapping fffffe8000000000 | -1.5 TB | fffffeffffffffff | 0.5 TB | ... unused hole ffffff0000000000 | -1 TB | ffffff7fffffffff | 0.5 TB | %esp fixup stacks ffffff8000000000 | -512 GB | ffffffeeffffffff | 444 GB | ... unused hole ffffffef00000000 | -68 GB | fffffffeffffffff | 64 GB | EFI region mapping space ffffffff00000000 | -4 GB | ffffffff7fffffff | 2 GB | ... unused hole ffffffff80000000 | -2 GB | ffffffff9fffffff | 512 MB | kernel text mapping, mapped to physical address 0 ffffffff80000000 |-2048 MB | | | ffffffffa0000000 |-1536 MB | fffffffffeffffff | 1520 MB | module mapping space ffffffffff000000 | -16 MB | | | FIXADDR_START | ~-11 MB | ffffffffff5fffff | ~0.5 MB | kernel-internal fixmap range, variable size and offset ffffffffff600000 | -10 MB | ffffffffff600fff | 4 kB | legacy vsyscall ABI ffffffffffe00000 | -2 MB | ffffffffffffffff | 2 MB | ... unused hole __________________|____________|__________________|_________|___________________________________________________________ 從這個虛擬地址,從內(nèi)存布局中知道這個地址是一個存放內(nèi)核代碼的地址,內(nèi)核將內(nèi)核代碼映射到虛擬內(nèi)存 0xffffffff80000000 開始的2G空間。另外我們還需注意 0xffff888000000000 開始的虛擬地址空間,這個虛擬地址空間大小為119.5G,用于直連映射到物理地址,這個機(jī)制為我們后續(xù)的分析提供了一些便利設(shè)施。比如我們想訪問物理地址為 0x1000000,那么我們直接通過虛擬地址 0xffff888001000000 訪問內(nèi)存即可。 另外還需要注意的是,Intel的處理器已經(jīng)支持5級分頁,支持更大的內(nèi)存空間,但在這里我們的模擬環(huán)境只是一個4級分頁的模擬處理器而已。 那么,進(jìn)入正題,我們?nèi)绾螌⑻摂M地址 0xffffffff81bd6b60 劃分為各級分頁的索引呢,64位系統(tǒng)支持4K,2M,1G的內(nèi)存分頁,這個地址是處在分頁大小是多少的內(nèi)存空間呢,我們現(xiàn)在并不知道這個地址是處在分頁大小為多少的空間,我們先按照4K的分頁換分分頁索引。將之前64位4K的索引處理重新看一下,在Linux的劃分如下
這是一個支持4級映射的劃分,在Linux頁支持5級分頁,第五級分頁叫做p4d,這里我們忽略。我么按照這個規(guī)則處理后如下, pgd_index = 511, pud_index = 510, pmd_index= 013, pte_index = 470, offset = 001d6b60 通過使用GDB查找cr3寄存器的物理地址,我們知道 CR3 寄存器中存放了第一個PGD的物理地址
我們需要將 CR3`的低12位屏蔽,得到了地址 `0x2610000,這個地址就是PDG表的存放位置,PGD表是存放PDG項(xiàng)的內(nèi)存空間,是一個每項(xiàng)為8個字節(jié)的數(shù)組,如果想訪問這個地址在保護(hù)模式下,我們是無法直接使用物理地址訪問內(nèi)存數(shù)據(jù)的,但前面說到`0xffff888000000000` 開始的虛擬地址空間映射到物理空間,我們可以根據(jù)這個規(guī)則訪問對應(yīng)的物理內(nèi)存,查看物理內(nèi)存中的內(nèi)容。這樣訪問虛擬地址 0xffff888002610000 實(shí)際上訪問的就是物理內(nèi)存`0x2610000`的數(shù)據(jù)。 (gdb) x/8bx ((pgd_t *)0xffff888002610000 511 )0xffff888002610ff8: 0x67 0x50 0x61 0x02 0x00 0x00 0x00 0x00 在這里,我們使用pdg_index加到物理地址上,(pgd_t *) 是一個8字節(jié)的指針,加上511實(shí)際地址為 0xffff888002610000 511 * 8。由于Intel處理器是小端序,所以這8個字節(jié)的實(shí)際數(shù)據(jù)為 0x0000000002615067。這個數(shù)據(jù)將低12位屏蔽,得到地址為 0x2615000。這個物理地址就是PUD表的基址。同樣的我們訪問對應(yīng)的數(shù)據(jù),根據(jù)索引pud_index獲得PUD項(xiàng)。
這個表項(xiàng)中 PS 沒有被置位,所以這個表項(xiàng)指向PMD表,這個地址的數(shù)據(jù)獲得對應(yīng)的PMD表的基址 0x2616000,根據(jù)pmd_index獲得PMD項(xiàng) (gdb) x/8bx ((pgd_t *)0xffff888002616000 13)0xffff888002616068: 0xe3 0x01 0xa0 0x01 0x00 0x00 0x00 0x00 我們根據(jù)這個表項(xiàng)值 0x0000000001a001e3,由于 PS 被置位,說明這個表指向的是頁大小的位2M的頁,那么剩下的線性地址的21位作為偏移量,處理器將根據(jù)頁表中的項(xiàng)決定是否有權(quán)限訪問等額外的工作。
同時這個表項(xiàng)的基址為 0x1a00000,將基址加上偏移量就是最終的物理地址 0x1a00000 0x1d6b60 = 0x1bd6b60 最終的結(jié)果 0x1bd6b60 就是虛擬地址 0xffffffff81bd6b60 訪問的物理地址為 0x1bd6b60。 寫個一個小工具,解釋這個過程,這個工具根據(jù)內(nèi)核地址翻譯成中間的頁表轉(zhuǎn)換過程
(gdb) hack-tool print-x86_64-gpt 0xffffffff81bd6b60pdg: 0x2615000cr3(pgt) => pud: [cr3: 0x4a9e000, index: 511] => [pud : 0x2615000]pud => pmd: [pud: 0x2615000, index: 510] => [pmd : 0x2616000],? page_size=2G : Falsepud => pmd: [pmd: 0x2616000, index: 13] => [pa : 0x1bd6b60] | ? page_size=2G : Trueva -> pa: ffffffff81bd6b60 => 1bd6b60 這兩個例子,第一個是系統(tǒng)啟動過程中進(jìn)行斷點(diǎn)調(diào)試的打印結(jié)果,第二個是系統(tǒng)已經(jīng)啟動進(jìn)行斷點(diǎn)調(diào)試的打印結(jié)果,可以看到雖然CR3寄存器不一樣,但相同的內(nèi)核地址映射到相同的物理地址。CR3寄存器之所以不一樣是因?yàn)長inux系統(tǒng)的復(fù)制機(jī)制,CR3寄存器通常和進(jìn)程綁定,一個進(jìn)程會有自己的頁表,但系統(tǒng)統(tǒng)一為進(jìn)程提供了內(nèi)核相同的布局,我們可以理解為進(jìn)程雖然有自己的用戶內(nèi)存空間,但使用相同視圖的內(nèi)核空間。 總結(jié)上文就是一個虛擬地址轉(zhuǎn)換為物理地址的過程,在這個過程我們應(yīng)該意識到分段轉(zhuǎn)換為線性地址,在64位中,雖然處理器弱化了段的作用,但仍提供了使用的方式。處理器也強(qiáng)化了分頁的機(jī)制,分頁為現(xiàn)代操作系統(tǒng)支持虛擬內(nèi)存提供了堅(jiān)實(shí)的基礎(chǔ)。 |
|