目錄
1.溫故知新
2.ELF文件介紹
3.ELF文件組成
4.ELF文件形成到加載
5.連接過程
1.溫故知新
? ? ? ? 上一篇博客,我們介紹了我們的動靜態,知道了我們的庫其實也是文件,如果我們想寫一個庫也是可以的,我們的把我們的庫文件編譯成.o文件,然后把它們進行打包.a文件,形成靜態庫,然后我們把我們的程序和庫進行打包編譯,就把我們的靜態庫和我們的代碼進行合并,我們就可以使用庫的函數了。動態庫需要我們把它的庫文件放在我們的默認查找路徑下/lib/64下,讓我們可以找到我們的庫文件就可以了,而至于我們的頭文件,我們只需要讓我們的文件找到它就可以了。
2.ELF文件介紹
首先我們知道ELF文件是什么?是文件啊。是文件就有內容和屬性,了解它無非就是內容上了解,屬性上了解。
? ? ? ?我們的.o文件,可執行程序,.so文件,core dump都是我們的ELF文件,所以我們想要進一步了解編譯鏈接的細節就需要來了解我們的ELF文件。
3.ELF文件組成
? ? ? ? 我們的ELF文件由四部分組成:ELF頭,程序頭表,節頭表,節組成。
我們的ELF header里面定位文件的其他部分。ELF文件內容對我來講,就是一個數組。
因為我只要知道他這個文件的起始位置和它們的大小,我們就可以找到這個文件的任意位置了。
ELF文件內容里面的四個部分。
????????? ? ? ??
我們內核里面ELF的代碼是有這個程序的入口的,它位于我們文件的開始位置,可以定位文件的其他部分,它也有我們程序的入口,讓我們編譯器知道從程序的哪個位置開始執行。
4.ELF文件形成到加載
我們進行動靜態庫鏈接需要先把.c文件編譯成.o文件,我們還需要對多個.o文件進行合并。
但是這個合并是在我們鏈接的時候進行的。我們做的是把我們.o庫文件進行打包。
我們的ELF文件有多種不同的Section,在加載到內存的時候,也會進行Section的合并
我們的節頭表在鏈接時起作用,節頭表里面有對每個節的描述,鏈接的時候可以根據對節的描述把節進行合并成段,提高空間利用效率,而我們的程序頭表里面列舉了每個段的描述和屬性,知道每個段在哪里分開,把每個段進行分開,這樣可以幫助我們加載的時候告訴我們OS怎樣加載可執行文件完成進程內存的初始化。
說白了就是節是具體的,告訴怎么合并,程序頭表是告訴段的劃分是怎么樣的,讓OS進行內存初始化。節頭表是我們鏈接的時候,程序頭表告訴我們怎么把我們鏈接好的文件放到內存里進行執行。
上面我們說過,我們ELF頭可以幫助我們定位ELF文件每個部分,所以我們編譯器只需要知道我們ELF節頭表,就可以對節進行合并了,合并好了就鏈接好了啊。
當我們想要執行的時候我們的OS可以找到這個文件ELF頭表,再根據頭表找到程序頭表,知道哪些模塊要加載進內存。知道我們程序的加載。
所以我們上面說我們OS和編譯器都要知道ELF頭表因為我們鏈接時要用,加載時也要用。不知道
ELF怎么完成這個工作呢?
實踐中,當我們將一個.c文件編譯成我們的.o文件我們去查看我們的文件,發現我們文件中調庫函數地方的地址是沒有的。
這是因為我們只有函數的聲明,我們還沒有對我們的,o文件進行鏈接,找不到具體的實現方法,所以這里學習的是不是和我們以前介紹的對上了呢?因為沒有鏈接,所以找不到具體的實現,但是頭文件里面有我們函數的聲明,所以并沒有報錯。
我們靜態鏈接就是把我們的.o進行合并,鏈接合并的時候,鏈接器會根據我們的重定位表對函數的地址進行重定位從而形成我們的可執行文件。這就是靜態鏈接的過程。
我們鏈接之后我們發現我們的函數地址就被填充上了,我們就可以找到對應的實現方法了。
所以鏈接在干什么?就是把我們不知道函數具體實現的地方找到了函數實現的地址,對地址實現了重定位。
這里我們終于知道了為什么.o叫做可重定位文件了,因為它需要重定位才可以執行,不然我們只有函數聲明,沒有函數實現,執行個毛!
兩個.o代碼合并到一起,進行統一的編制,鏈接的時候,修改.o中沒有確定的函數地址,進行相關call地址,完成代碼調用。
5.連接過程
事實上,我們對ELF進行編制,采用平坦模式進行編制,說白了就是從000-fff進行線性統一編制。
磁盤可執行文件,就是起始地址+偏移量這種地址,只不過我們平坦模式的起始地址是0.
我們給磁盤中這種文件編制的地址叫邏輯地址。它和我們加載到內存的虛擬地址是一樣的。
如同一個硬幣的兩面,硬幣翻面了還是那個硬幣啊!
? ? ? ??
在我們的磁盤中,我們的文件就有了邏輯地址了,所以我們可以看到我們的文件還沒有被加載到內存的時候,就已經被編制了,并不是加載到內存才有得地址。
而且我們的虛擬地址初始化的時候還需要依賴ELF文件,每個segment有自己的起始地址和結束地址,就可以用來初始化虛擬地址了。
根據下面這張圖,我們來做一個總結,我們的ELF文件在我們的磁盤中的時候就有了虛擬地址了,然后當我們的可執行程序被加載到內存的時候,我們會拿到它的虛擬地址,在頁表中為它建立映射關系,根據ELF合并之后的段對我們的虛擬地址空間進行初始化,也就是虛擬地址對物理內存映射的建立過程,然后我們就根據ELF文件提供的內容完成了我們可執行程序從硬盤加載到內存調用的過程了。
給到我們的虛擬地址,當我們調用我們的程序的時候,根據我們的MMU,我們就找到了物理內存的實際位置進行調用。
這里要說明的是,當我們建立虛擬和物理的映射關系時,不會立即在內存分配空間,而是當你調用的時候再為你分配空間。這個是我們缺頁異常導致的,就好比我們fork一個子進程,只有當我們修改的時候我們才會寫實拷貝,這是因為我們進行權限檢查發現越界了,OS檢查后對它進行了修改。類似!
我們下期再見!