直接通過程式來分析,理論不說,網上一大堆:
#include <stdio.h>void test(void){ printf("%s\n","Hello");}int main(int argc, char *argv[]){ test(); return 0;}
下面來看看地址的轉移過程 ,gcc編譯後對a.out 進行objdump 反組譯碼結果如下(刪除一部分彙編代碼):
00000000004004c4 <test>: 4004c4:55 push %rbp 4004c5:48 89 e5 mov %rsp,%rbp 4004c8:bf e8 05 40 00 mov $0x4005e8,%edi 4004cd:e8 e6 fe ff ff callq 4003b8 <puts@plt> 4004d2:c9 leaveq 4004d3:c3 retq 00000000004004d4 <main>: 4004d4:55 push %rbp 4004d5:48 89 e5 mov %rsp,%rbp 4004d8:48 83 ec 10 sub $0x10,%rsp 4004dc:89 7d fc mov %edi,-0x4(%rbp) 4004df:48 89 75 f0 mov %rsi,-0x10(%rbp) 4004e3:e8 dc ff ff ff callq 4004c4 <test> 4004e8:b8 00 00 00 00 mov $0x0,%eax 4004ed:c9 leaveq 4004ee:c3 retq 4004ef:90 nop
左邊的地址是虛擬位址,這邊要涉及到幾個概念:
1.實體記憶體空間:主板上的記憶體條所提供的記憶體空間就為物理地址空間
2.物理地址:每個記憶體單元的實際地址
3.虛擬位址空間:我們自己看到的記憶體空間(從組合語言來看)
4.CR1:未定的控制寄存器
5.CR2:頁故障線性地址寄存器
6:CR3:頁目錄基底位址暫存器,儲存頁目錄的物理地址
..
繼續上面的內容:
分配給test()這個函數的起始地址為
00000000004004c4
Linux中最常見的可執行檔的格式為ELF(Executable and Linkable Format) .在ELF格式的可執行代碼中,ld總是從地址0x8000000 開始安排程式的"程式碼片段" 對每個程式都這樣,至於程式執行時在實體記憶體中的實際地址,則在核心為其建立記憶體映射時臨時分配, 具體地址取決於當時所分配的實體記憶體頁面.
假設CPU開始執行main()函數中的
4004e3:e8 dc ff ff ff callq 4004c4 <test>
於是轉移到虛擬位址4004c4 ,Linux核心設計的段式映射機制把這個地址原封不動的映射為線性地址,接著就進入頁式映射過程.
每當發送器選擇一個進程來運行時,核心就要為即將啟動並執行進程設定好控制寄存器CR3,而MMU的硬體總是從CR3中取得指向當前頁目錄的指標.
當程式轉移到地址 4004c4 的時候,進程正在運行中,CR3指向進程的頁目錄.根據線性地址
4004c4 最高10位,就可以找到相應的目錄項,把4004c4按二進位展開:
0000 0000 00100 0000 0000 0100 1100 0100
最高10位0000 0000 00即10進位0 ,於是以0為下標在頁目錄中找到其目錄項,這個目錄項中的高20位指向一個頁表,CPU在這20位後填12個0後就得到該頁表的物理地址.
找到頁表後,CPU再來找線性地址的中間10位為100 0000 000,10進位為 512
從CPU以512為下標在頁表中找到相應的頁表項,取出其高20位,假如為0x356,然後與線性地址的最低12位0x4c4拼接起來
就得到test()函數的入口物理地址0x3564c4,test()的執行代碼就儲存在這裡.