歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux綜合 >> Linux內核 >> Linux內核工程導論——進程:ELF文件執行原理

Linux內核工程導論——進程:ELF文件執行原理

日期:2017/3/1 12:16:37   编辑:Linux內核

1、 進程的執行

我們都知道一個現象,windows下的進程在linux下無法雙擊打開,反之也一樣。但是同樣是是用C或者golang寫的程序分別在linux下編譯和在windows下編譯都可以執行。當然,如果你調用了操作系統特有的系統調用也是不可以執行的。確切的說是編譯不通過的。我們這裡討論沒有調用操作系統相關的系統調用,都使用標准的C庫函數。標准C庫函數在後台也是調用的系統調用的,但是這個轉換工作是分別在不同操作系統的不同C庫實現中完成的。

為什麼沒有調用操作系統相關的系統調用還無法執行呢?有人會說因為在linux下編譯的是elf格式,在windows下編譯的是exe格式。這個二進制格式是在內核中支持的,因為當調用了加載可執行程序的系統調用了,內核必須要知道它加載的可執行文件的格式,以便從中識別信息(例如32位還是64位架構,數據是大端還是小端存儲的,符號表放在哪裡,程序的入口在哪裡)。這個對存儲格式的識別過程有點像文件系統,內核必須要清楚的知道不同文件系統的組織格式,才能正確的索引和修改裡面的數據。讓內核擁有特定格式識別的能力的機制就叫做驅動。同樣是網卡發送數據需要不同的驅動,同樣是文件系統,讀取數據需要不同的驅動,同樣是二進制文件,執行代碼也需要驅動。windows沒有elf驅動,linux內核裡也沒有exe驅動。由於linux的開源特性,你完全可以寫一個內核的exe驅動,讓exe程序可以直接在linux中執行。

但是,就這麼簡單嗎?非也。在windows中編譯代碼使用的基礎庫也是只能在windows上運行的。而這個基礎庫規定了進程做系統調用時函數參數該以何種順序壓入堆棧,該如何進行系統調用(linux和windows陷入系統調用的方式不一樣)。也就是說如果基礎庫設計的足夠好,能在兩個操作系統之間兼容(符號表是一樣的),處理不同讓基礎庫去處理也可以。還不能忘記一個程序還會依賴很多動態庫,這些動態庫也是系統相關的。有的代碼甚至會直接繞過基礎庫操作系統調用。還有進程執行需要加載器,加載器也得能夠識別其他平台的格式。這也是wine能夠工作的基礎。linux下的wine程序就是通過將底層的所有不同做轉換,讓exe二進制在linux上兼容。所以,可以看出,如果內核的系統調用足夠多的與windows一致,再實現一些兼容的基礎庫,linux也是可以高效的兼容exe程序的。不過目前wine大部分轉換在用戶空間完成,難免損失效率。就算是在內核態完成,由於不同的邏輯設計,轉換的代價也會不小的。

像Linux和windows的這種情況叫做二進制不兼容,也即ABI不同。ABI會規定底層的調用和參數傳遞,二進制文件布局的具體格式。如果一個內核支持一個ABI,那麼無論在什麼操作系統,一次編譯就可以處處執行了。

2、 elf文件格式

磁盤存儲結構一般都要有頭部,elf也一樣。頭部有3部分,elf頭部、segment頭部和section頭部。其中一個二進制文件只有一個elf頭部,多個segment頭部和多個section頭部。一個segment邏輯上包含多個section。

那麼segment和section又是什麼概念呢?segment常見的有PT_LOAD、PT_DYNAMIC、PT_INTERP、PT_NOTE、PT_PHDR等。我們來思考進程執行的必要條件。

二進制文件在磁盤中的布局並不是內存中的布局,所以需要一個映射和一個實現這個映射的程序,還有linux上的二進制文件一般需要加載共享庫(例如libc幾乎是必備的),這個工作並不是內核中完成的,因為內核不認識庫這種概念,內核看來,所有的程序都是可執行代碼段,有的代碼段是可以映射和重定位的。執行外部庫搜索和加載的程序稱為加載器,elf格式的是ld-linux.so,a.out格式是ld.so。而由於加載器可能有多種實現,也可能有多個版本,所以每個二進制文件中都需要指明使用哪個加載器,指明使用哪個加載器的功能就是用segment實現的,這種segment就是PT_INTERP類型。由於golang一般使用靜態鏈接,所以你會發現幾乎只有golang的elf格式中是沒有PT_INTERP類型的segment的。

一個程序一般會有.dynamic段,而這個段就是放在類型為PT_DYNAMIC的segment中的。為什麼要單獨一個呢?因為這個段包括這個segment也都是用來服務於動態加載的。我們使用ldd命令可以讀取到一個二進制依賴的庫,這個依賴關系就是寫在這裡的。也就是說這個地方記錄了當前elf執行需要的庫的名字。至於到哪裡去找這些庫,就是ld-linux.so的事情了。

PT_NOTE則是記錄程序的一些輔助信息。程序可能會有什麼輔助信息呢?比如程序的類型,程序的所有者,程序的描述。這些信息不參與程序的執行,只有描述作用。

PT_LOAD就是真正的程序存儲的地方。這個構成了程序的主體。

而section就是segment裡面具體組織數據的格式了。每個section都有名字,這個名字是編譯器給起的,你也可以自定義名字,都以小數點開頭。例如.text .data等。連接器和加載器共同識別一些段,所以可以進行商量好的操作。例如加載器看到.text段就知道是代碼段,而這個.text段的創作者則是鏈接器。

3、 進程加載器

前面說過elf文件的加載器是ld-linux.so,而a.out文件的加載器是ld.so。但是這兩個加載使用的配置路徑都是一樣的:/etc/ld.so.conf文件。這個文件裡一般是include ld.so.conf.d目錄下的所有文件,所以要想添加一個庫路徑在目錄下建立一個文件最好。因為文件名是對這個庫用途的良好說明。添加完了需要運行ldconfig,因為實際的ld-linux.so並不是一個個去搜索路徑,那樣會極慢。而是從緩存中直接查詢。這個緩存文件就是ld.so.cache,這個文件中有每個庫的路徑,是使用ldconfig程序使用ld.so.conf文件計算出來的。所以每次修改了庫配置都需要執行這個命令。

你也可以做個實驗,所有linux進程能夠有效運行的原因是因為ld-linux.so位於同樣的目錄/lib/下。如果這個文件被移動或者重命名,幾乎所有程序都不能執行(用golang編譯的不使用ld-linux.so加載的程序仍可以執行)。此時如果你想恢復執行,你得將ld-linux.so繼續拷貝到/lib/目錄下,然而你會發現mv命令也無法執行了。但是builtin的cd之類的命令卻是可以的。恢復的辦法是顯示的使用./ld-linux.so mv a b,當然還要加上必要的參數。這裡只是要論證一點:所有gcc編譯的進程如果要執行,其實本質上是加載器程序先執行,然後由加載器調用實際的進程執行。就好像python程序無法直接執行,但是經過shell的設置後就可以自動找到python程序來執行。

另外,你有可能同一個庫有多個版本,這是不沖突的,只要你將路徑都加入即可。

Copyright © Linux教程網 All Rights Reserved