一個地址的旅程
The Trip of An Address -- An OutlineJason Lee
[Scene 1. Code -> VA]
本文將以如下代碼(ttoaa.c)為例,觀察函數f入口地址的行程。
整篇文章的內容會涉及Linux和Windows兩種不同係統的場景。
#include <stdio.h> void f() { printf("0x%08x\n", f); } int main() { f(); printf("0x%08x\n", main); return 0; }將以上代碼在Windows上進行編譯、鏈接,得到ttoaa.exe,運行輸出:
0x00401000 0x00401020
由此可知道函數f的VA為0x00401000。那麼,這個VA是怎麼來的?
通過執行命令dumpbin /ALL ttoaa.exe > ttoaa.exe.dump,可以得到PE文件格式內容。
OPTIONAL HEADER VALUES 10B magic # (PE32) 8.00 linker version 9000 size of code 5000 size of initialized data 0 size of uninitialized data 131E entry point (0040131E) 1000 base of code A000 base of data 400000 image base (00400000 to 0040EFFF) 1000 section alignment 1000 file alignment 4.00 operating system version 0.00 image version
在Optional Header Values中可以看到image base為0x00400000,而base of code為1000,
所以可以知道代碼段的起始位置為0x00401000,而函數f正好位於代碼段起始位置。
參考ttoaa.exe.dump文件的內容,可以想象這個程序在內存中的布局可能如下圖:

[ 圖 - 1 ]
[Scene 2. TLB]
關於TLB的位置,我有點小疑惑:是在Segmentation之前還是之後呢?
後來我想,不論是從TLB的實際作用(VA -> PA),還是從目的(加快地址映射)來看,都應該在Segmentation前麵。
現在獲得了函數f的VA,CPU要執行相應代碼,首先要獲取第一條指令。
怎麼獲取這一條指令呢?首先看main函數:

前麵就知道main函數的入口地址為0x00401020,從反匯編結果可以看出一進入main函數先保存棧頂指針,繼而調用函數f。
現在CPU需要做的就是獲取VA為0x00401000的指令,然後執行。

[ 圖 - 2 ]
如上圖,CPU的取指(Fetch)階段會先訪問TLB,看是否存在VA到PA的映射。
並且由於現在要獲取的是指令,所以對應的是i-TLB,即Instruction TLB。
TLB全稱為Translation Lookaside Buffer,存儲著key-value映射,其中key為VA,value為PA。
由於是第一次訪問0x00401000,所以i-TLB中還沒有相關映射,需要繼續進行地址映射,並把映射結果回填到i-TLB,以加速下次訪問。
[Scene 3. Segmentation]
按理說,現在位於代碼段中,VA需要加上CS指定的偏移量才能獲得線性地址,不過Windows和Linux都采用Flat Model屏蔽了段機製,畢竟32位地址線足夠指向整個4G空間。
所以,邏輯地址(或者說虛擬地址)即是線性地址。
為了實現Flat Model(地址平麵化),隻需要設置CS、DS兩個段寄存器(指向GDT中的段描述符)相應的段描述符,將其基址設為0,段範圍限製設為4G。
[Scene 4. Paging]
這裏考慮的是頁麵大小為4KB的情況。
如果沒有開啟PAE,頁式映射過程如下(參考Intel Software Developer Manual):

[ 圖 - 3 ]
如果開啟了PAE,頁式映射過程如下:

[ 圖 - 4 ]
[Scene 5. Cache]
昂貴的Cache本身就已經是為了盡量提高CPU的執行速度,消除CPU和內存之間的瓶頸,除此之外,芯片設計者為了提高查找緩存的速度,還將訪問Cache和頁式映射兩件事設計成同時進行的——這利用了低位/頁內地址在影射過程的不變形。
先了解下Cache的工作原理。
首先,Cache對於CPU給的地址有兩種觀察方式:Look-aside和Look-through。在前者模式下,Cache就是作為旁觀者,類似抓了一個包過來觀察的同時不影響原有的電路邏輯,直到HIT了才終止到內存尋找的邏輯;在後者模式下,Cache就作為必經之路,一定要先Cache判斷為MISS後,才允許電路繼續執行。
其次,Cache是由若幹個Cache-line組成的,每一個Cache-line又分為Tag和Data,一列Cache-line稱為一路(1-way)。
用Everest查看本機數據,得知L1-iCache為32KB:

這裏引用下Gustavo
Duarte博客關於Cache的用圖,剛好和我機子上的芯片是一樣的:

從上圖可以了解到L1 Cache有8路,每一路有64個Cache-line,每一個Cache-line分為24位的tag和64字節的數據部分。
這裏可以發現每一路都是4K,對應著一個內存頁麵,於是可以產生一種良好(當然還有混亂的、隻適合小緩存使用的全相聯等方式,這裏不討論)的關聯方式,每一個內存頁相應的頁內地址直接對應一個Cache-line。
根據頁式映射[ 圖 - 3 ]可以知道低12位是不變的,隻在最後定位頁內地址時才使用到。所以,在進行頁式映射的同時,就可以利用地址的低位信息先確定緩存索引,等物理映射完成後,就不必再匹配索引了,直接利用高24位匹配Cache-line的tag。
如果上述tag和索引都匹配到了,那麼地址的低6位就可以用來確定Cache-line中64字節的位置,進而獲取數據。
[Scene 6. Memory]
如果Cache那邊MISS了,就進一步來到了內存。
這時候由控製總線確定操作類型(比如讀或寫),然後由地址總線確定內存中的位置,再由數據總線來傳輸要讀或者寫的數據。
這些電平的傳輸就要靠我們拆開機箱、卸下風扇才能看到的芯片引腳了。

如果缺頁了,就要進行換頁了,由OS來控製。
[End]
結尾有點倉促,不過今天確實有點累(昨天去上海聽演唱會,回來又熬夜寫了下記錄),也想這周一定要結束這一個係列的回顧學習,並以此篇作為一個學習小結,畢竟拖了有段時間了。
[參考資料]
1. Wikipedia
2. Gustavo Duarte
3. CSAPP
4. Linux內源源代碼情景分析
5. Windows內核原理與實現
6. Intel Software Developer Manual
最後更新:2017-04-02 06:52:09