標籤:

程序如何根據變數名在內存中找到存放這個變數的地址?

比如在c中,int i = 10; 我們通過i可以獲得i的地址,這個地址根據i的類型可能在stack或heap中。但是對於程序而言,它是如何從i這個名字找到其所在的內存地址並獲得值?是不是有一個表來記錄所有變數名、函數名及其對應的地址?如果有,是如何實現的?


給 @Hooao的答案補充幾句。這裡我們不考慮專門為調試而插入的調試信息。

一般來說,C/C++寫的程序引用變數時不會再需要查表。但這並不是絕對的,而且這個表是確實存在的。表裡面不僅有變數,還有函數,但都只限全局對象。它一般被稱為「全局符號表」,在編譯之後就存在於目標碼中。

這個表裡包含了所有沒定義成static的全局函數和全局變數的名字和地址。它的作用是用來給「鏈接」過程指路。這些全局的對象如果需要在其他的文件中引用,那麼在鏈接生成exe文件(靜態鏈接)的時候,就需要連接器去查表確定其地址。如果你載入了一個動態模塊(windows下的dll或者*ninx下的so),需要引用dll/so導出的全局變數和函數(動態鏈接),那麼,你的程序就需要在運行時查表了。所以動態載入的模塊,性能會稍微低一點。但只有一點點,因為不會每次訪問都要查表。


如果你說的是debugger怎麼找的,答案就是全部都寫在了pdb文件里。程序運行到不同的地方的時候,獲取同一個變數的地址的方法可能不一樣,這都記錄了。


i 的地址確定是在鏈接的過程中就完成了。編譯器在編譯和鏈接的過程中,詞法分析的結束會為每一個單詞(Token)輸出一個二元組。例如下面的代碼片段

if( i == NULL)
return;

輸出的二元組形式為

  1. &

  2. &<, 運算符&>

  3. &

  4. &<==, 運算符&>

  5. &

  6. &

  7. &<;, 結束符&>

符號表項存儲了該變數行號、變數的類型和作用域、函數名字、函數參數和函數的作用域特性。

最後在鏈接的時候,鏈接程序根據詞法分析和語法分析的結果,程序中有引用i均由鏈接時分配的地址代替。

所以,簡而言之,函數在執行的過程中,實際上程序並不通過變數的名字來確定內存位置,在使用變數的時候,實際上已經是直接對地址進行操作了


大哉問!需要理解編譯、目標文件格式、LinkerLoader。

簡單點說,標準C在程序編譯鏈接完成之後,這些名字根本就不存在,有的只是地址。為了調試方便,編譯器可以額外存一份符號名與地址的對照信息,可能嵌在目標文件里,也可以存成獨立的文件,看具體實現。


先說局部變數吧,這個地址在編譯階段是確定的,也就是相對於棧指針的偏移量

全局變數和靜態變數的地址在編譯階段是未知的,一種常見的做法是 預留一個坑,這個坑的地址編譯器是知道的,將來鏈接器會在坑裡填入變數的地址,運行的時候有點間接定址的意思。

那麼鏈接器怎麼知道哪裡有坑要填呢?ELF里有專門的數據,稱為重定位項。

對於動態庫,可重定位可執行程序(比如支持地址隨機裝載),這個要更複雜些。具體的做法也和操作系統相關。


這個問題涉及的領域是Linker Loader,所以我一直覺得知乎可能滿足碎片化的閱讀是夠的,但是專業化的知識還是得系統閱讀書籍.不多說開始正題.

一個源代碼的文件在成為可執行文件一共會經歷很多步,簡單來說有:預處理,編譯(包括詞法分析,語法分析,語意分析等階段),彙編,鏈接.所以高級語言的一個變數,在底層分別被彙編指令,符號層層傳遞下去.

所以訪問一個變數地址,其實就是求一條彙編指令的數據地址.如果寫過彙編,大概還記得有那麼些不同的段(segment),最重要的幾個包括數據段,指令段和bss段.對於一個初始化了的全局變數或者靜態變數,它存儲於數據段,未初始化的全局變數和局部靜態變數存儲於bss段,這裡要知道的是,數據位於一個段中.我們只要有段的基址和符號對應的偏移就能知道符號地址了.求各個偏移就是鏈接器一個非常重要的工作--重定位.當然這裡其實還涉及到其他工作,因為源代碼一般都引用了很多其他源文件,包含了很多全局變數,第三方動態/靜態庫等等,這些符號都需要分配地址,所以在重定位之前還有兩個比較主要的階段--地址和空間分配和符號決議.簡單地想就是空間是鏈接器分配的,他有一個符號表,經過重定位之後記錄了各個符號在段中的偏移.

這裡需要知道的是,在完成地址空間分配後,其實各個段在鏈接之後的虛擬地址就已經確定了,至於這個地址怎麼映射到物理內存,這是另一個話題了.簡單地說就是現在處理器通過分頁等虛擬化手段讓每個虛擬地址段都能夠映射為物理地址,至於一個變數的物理地址位於哪裡,其實就是簡單的虛擬地址和物理地址的轉換,由頁表和TLB配合進行,各個操作系統以及體系結構書籍都會有所介紹,可以詳細找本書了解下.


i 是編譯鏈接後的地址,而且是固定的,

實際運行時,i可以在物理內存的用戶區的任意地方。

如果樓主你是說這個變數所在內存的物理地址么?

這需要操作系統層面來看,


只會c語言,我們把c語言的變數分成2類:

1:全局變數

2:局部變數

2比較簡單,在定義變數以後,變數會放到棧中,取變數地址時,直接通過棧指針加減運算就可以得到(這個是gcc編譯時就確定好的)

int main(){
int i = 10;
...
int *p = i; //比如p在寄存器rax中
...
}

就會變成

mov 10,8(rsp)
...
leal 8(rsp),rax //把i的地址賦值給p

1比較麻煩,在編譯彙編(還沒有鏈接)以後,全局變數的值會放在.data段,有一個符號表負責記錄變數對應在data中的位置.同時在代碼中每一次對於全局變數(包括對地址)的引用都會被記錄在rel.data段.在鏈接的時候可以確定每段的位置,鏈接器遍歷rel.data表,重新計算代碼中全局變數的地址.

(感覺說的不明白啊..上圖吧)

求大神指正。


作過彙編逆向工作的我來斗膽怒答。

不管你是用c語言還是delphi還是c++,這些高級語言寫出來代碼主要是給人看的,通過編譯器編譯以後就變成了只有機器能懂的二進位指令。幫你定義一個變數的時候他就是數據和你的代碼指令混著一塊全部都放到內存裡面。

一般來說,在內存裡面是沒有你所謂的符號的。

在內存裡面只有內存地址。像你說的這個變數他在內存裡面就是一個四個位元組大小的空間而已。

內存的存放數據的地方有兩大類型,一種是全局的堆還有一種就是函數內部的棧,如果你這個變數定義的是全局變數,那麼他就會放在堆裡面,如果你定的這個函數是在函數裡面定義的局部變數,他就會放在贊裡面不管是放在什麼地方代表他的就只能是一個內存地址。


全局變數和靜態變數才有名字,這些變數在鏈接的時候確定絕對地址,分配在data段,bss段。函數內部的變數,這個是在棧上的,執行完畢就不能訪問了。看csapp就知道了。


推薦閱讀:

如何減小GacLib生成的可執行文件大小?
OCaml pattern有哪些葵花寶典?
C語言if與else if寫成的這樣一段代碼效率上或者編譯完成後的結構上是否有區別(主要看補充內容中的詳細)?
如何實現 C 語言編譯器?

TAG:編程 | 編譯器 |