前言 用 MS Windows 一段時間的讀者,應該都聽過動態函式庫這個名詞。在 Windows 9X/ME 或是 Windows NT/2000 中,常見到的動態函式庫為副檔名 “DLL” (Dynamic Loading Library)的檔案。 而在 Linux 中,當然也有動態函式庫的機制存在。如此一來,所撰寫的程序便無需透過靜態連結(Static Link),而可以在編程時透過動態連結(Dynamic Link)產生我們所要的執行檔。 使用動態函式庫的好處有許多。首先,就是由於執行檔主要呼叫的函式都包含於動態函式庫中,所以檔案所占的空間可以因而縮小。其次,當動態函式庫的函式內容有所改變時,呼叫該動態函式庫的程序,可以在最小修正甚至是不需重新編程的情況下,就可以叫用到新版本的函式庫服務。 對於發展 Embedded Linux 的業者來說,能夠盡可能減少應用程序執行環境所需空間的大小,便可以把日後成品所需的 Flash 容量降到最低,在整體成本以及所耗用的記憶體空間來說,都可以得到許多的好處,而在動態函式庫來著手所得到的效益也是相當可觀的,盡可能的刪去不必要的動態函式庫,以及針對動態函式庫改寫來縮小或是透過工具刪去用不到的函式,都可以帶來許多的助益。 當然棉,動態函式庫的好處還不只這些,相信讀者們在文章中可以發現其它的妙用的。 檔案格式(ELF VS A.out) 首先,我們必須先確定目前所執行的 Linux Kernel 版本有開啟 ELF 與 A.out 執行檔案格式的支援(通常都會有) Kernel support for a.out binaries (CONFIG_BINFMT_AOUT) [M/n/y/?] Kernel support for ELF binaries (CONFIG_BINFMT_ELF) [Y/m/n/?] 舉個例子來說,若要執行 a.out 格式的執行檔時,我們必須確認 CONFIG_BINFMT_AOUT 為 Y,也就是由 Kernel 直接支援 a.out 檔案格式,或者 CONFIG_BINFMT_AOUT 為 M,也就是不把 a.out 的檔案格式支援編入 Kernel 中,改以 Module 的形式存在,一旦 Kernel 需要執行 a.out 格式的程序時,在動態的載入該 Module,來啟動具備執行 a.out 執行檔的能力。不過 a.out 執行檔的格式,是 Unix 上使用了相當久的的檔案格式,ELF 是目前較新的的檔案格式。a.out 檔案格式共有三個 Section,分別為.text, .data, 及 .bss,並還包括了一個文字表(String Table)與符號表(Symbol Table)。與ELF 檔案格式比較起來,a.out 相形之下顯得較為缺乏彈性,ELF檔案格式允許多個節區的存在,執行檔可以根據需求提供應用程序執行環境的節區,並且 ELF 檔支援了 32-bit 與 64-bit 的執行環境。其實,兩者之間還有其它規格上的不同,有興趣的讀者也可以自行找一些相關的資料來比較即可了解。 再來呢,我們就來討論動態函式庫的檔案格式。我們都知道在 Linux中有 a.out 與 ELF 兩種檔案的格式,其中目前我們最常見的便是 ELF 檔案格式。在 Linux 的函式庫目錄中,我們常常可以見到 “*.so” 的檔案,例如:“/lib/libc.so.6” 或是 “/lib/ld-linux.so.2”。這些便是在 Linux中所常見到的動態函式庫檔案。由下圖我們可以看到動態函式庫 libc.so.6 的 ELF Header: libc.so.6 的 ELF Header e_ident ->EI_MAG0:7fh ->EI_MAG1:E ->EI_MAG2:L ->EI_MAG3:F ->EI_CLASS:32-bit objects ->EI_DATA:ELFDATA2LSB ->EI_VERSION:1h ->EI_PAD:0h ->EI_NIDENT:3h e_type: ET_DYN (Shared Obj File) e_machine:Intel 80386 e_version:Current version e_entry:182a8h e_phoff:34h e_shoff:3bbf8ch e_flags:0h e_ehsize:34h e_phentsize:20h e_phnum:5h e_shentsize:28h e_shnum:40h e_shstrndx:3dh 由圖中,我們可以注意到 e_type: ET_DYN,e_type 是在ELF 檔案的格式中,用來描述目前該檔的檔案型態,我們所舉的例子為 libc.so.6 這個動態函式庫的檔案,所以 e_type 的屬性為 Shared Obj File。 當然棉,我們若再拿一個ELF執行檔來比較也是不錯的,所以如下圖 ls 的 ELF Header e_ident ->EI_MAG0:7fh ->EI_MAG1:E ->EI_MAG2:L ->EI_MAG3:F ->EI_CLASS:32-bit objects ->EI_DATA:ELFDATA2LSB ->EI_VERSION:1h ->EI_PAD:0h ->EI_NIDENT:2h e_type: ET_EXEC (Executable file) e_machine:Intel 80386 e_version:Current version e_entry:8049130h e_phoff:34h e_shoff:bea4h e_flags:0h e_ehsize:34h e_phentsize:20h e_phnum:6h e_shentsize:28h e_shnum:1ah e_shstrndx:19h 我們可以注意到 e_type: ET_EXEC,這就是 ELF 檔中對於執行檔所定義的檔案屬性。 動態連結 VS 靜態聯結 在 Linux 中,執行檔我們可以編程成靜態聯結以及動態連結,以下我們舉一個簡短的程序作為例子: #include int main() { printf("ntest"); }
若我們執行 : [root@hlchou /root]# gcc test.c -o test 所產生出來的執行檔 test,預設為使用動態函式庫,所以我們可以用以下的指令 : [root@hlchou /root]# ldd test libc.so.6 => /lib/libc.so.6 (0x40016000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) 來得知目前該執行檔共用了哪些動態函式庫,以我們所舉的 test 執行檔來說,共用了兩個動態函式庫,分別為 libc.so.6 與 ld-linux.so.2。我們還可以透過下面的 file 指令,來得知該執行檔的相關屬性,如下 [root@hlchou /root]# file test test: ELF 32-bit LSB executable, Intel 80386, version 1, dynamically linked (use s shared libs), not stripped not stripped 表示這個執行檔還沒有透過 strip 指令來把執行時用不到的符號、以及相關除錯的資訊刪除,舉個例子來說,目前這個test 執行檔大小約為 11694 bytes [root@hlchou /root]# ls -l test -rwxr-xr-x 1 root root 11694 Oct 24 02:31 test 經過strip後,則變為 3004 bytes [root@hlchou /root]# strip test [root@hlchou /root]# ls -l test -rwxr-xr-x 1 root root 3004 Oct 24 02:48 test 不過讀者必須注意到一點,經過 strip 過的執行檔,就無法透過其它的除錯軟件從裡面取得函式在編程時所附的相關資訊,這些資訊對我們在除錯軟件時,可以提供不少的幫助,各位在應用上請自行注意。 相對於編程出來使用動態函式庫的執行檔 test,我們也可以做出靜態聯結的執行檔 test [root@hlchou /root]# gcc -static test.c -o test 透過指令 ldd,我們可以確定執行檔 test 並沒有使用到動態函式庫 [root@hlchou /root]# ldd test not a dynamic executable 再透過指令 file,可以注意到 test 目前為 statically linked,且亦尚未經過 strip [root@hlchou /root]# file test test: ELF 32-bit LSB executable, Intel 80386, version 1, statically linked, not stripped 相信大夥都會好奇,使用靜態聯結,且又沒有經過 strip 刪去不必要的符號的執行檔的大小會是多少,透過 ls -l來看,我們發現大小變成 932358 bytes 比起靜態聯結的執行檔大了相當多 [root@hlchou /root]# ls -l test -rwxr-xr-x 1 root root 932258 Oct 24 02:51 test 若再經過 strip,則檔案大小變為 215364 bytes [root@hlchou /root]# strip test [root@hlchou /root]# ls -l test -rwxr-xr-x 1 root root 215364 Oct 24 02:55 test 與使用動態函式庫的執行檔 test 比較起來,大了約 70倍 (215364/3004)。因此,整體來說,在使用的環境中使用動態函式庫並且經過 strip 處理的話,可以讓整體的空間較為精簡。許多執行檔都會用到同一組的函式庫,像 libc 中的函式是每個執行檔都會使用到的,若是使用動態函式庫,則可以盡量減少同樣的函式庫內容重復存在系統中,進而達到節省空間的目的。 筆者一年前曾寫過一個可以用來刪去動態函式庫中不必要函式的工具,針對這個只用到了 printf 的程序來產生新的 libc.so 的話,我們可以得到一個精簡過的 libc.so 大小約為 219068 bytes [root@hlchoua lib]# ls -l libc.so* -rwxr-xr-x 1 root root 219068 Nov 2 04:47 libc.so lrwxrwxrwx 1 root root 7 Nov 1 03:40 libc.so.6 -> libc.so 與靜態聯結的執行檔大小 215364 bytes 比較起來,若是在這個環境中使用了動態函式庫的話成本約為 3004 + 219068 =222072 bytes,不過這是只有一個執行檔的情況下,使用動態函式庫的環境會小輸給使用靜態聯結的環境,在一個基本的 Linux 環境中,如果大量的使用動態函式庫的話,像是有 2 個以上的執行檔的話,那用動態函式庫的成本就大大的降低了,像如果兩個執行檔都只用到了 printf,那靜態聯結的成本為 215364 *2 =430728 bytes,而使用動態函式庫的成本為3004 *2 + 219068=225076 bytes,兩者相差約一倍。
很明顯的,我們可以看到動態函式庫在 Linux 環境中所發揮的妙用,它大幅的降低了整體環境的持有成本,提高了環境空間的利用率。 ld-linux.so.2 在 RedHat 6.1 中,我們可以在 /lib 或是 /usr/lib 目錄底下找到許多系統上所安裝的動態函式庫,在文章的這個部分,筆者將把整個函式庫大略的架構作一個說明。 其實 Linux 跟 Windows 一樣,提供了一組很基本的動態函式庫,在 Windows 上面我們知道 kernel32.dll 提供了其它動態函式庫基本的函式呼叫,而在 Linux 上面則透過 ld-linux.so.2 提供了其它動態函式庫基本的函式,在筆者電腦的 RedHat6.1 上,ld-linux.so.2 是透過 link 到 ld-2.1.2.so(這部分需視各人所使用的 glibc 版本不同而定) -rwxr-xr-x 1 root root 368878 Jan 20 14:28 ld-2.1.2.so lrwxrwxrwx 1 root root 11 Jan 20 14:28 ld-linux.so.2 -> ld-2.1.2.so ld-linux.so 是屬於 Glibc (GNU C Library) 套件的一部分,只要是使用 Glibc 動態函式庫的環境,就可以見到 ld-linux.so 的蹤影。 接下來,我們透過指令 ldd 來驗證出各個函式庫間的階層關系,首先如下圖我們執行了 ”ldd ls”、”ldd pwd” 與 “ldd vi”,可以看出各個執行檔呼叫了哪些動態函式庫,像執行檔 ls 呼叫了 /lib/libc.so.6 (0x40016000) 與 /lib/ld-linux.so.2 (0x40000000),而括號內的數字為該函式庫載入記憶體的位置,在本文的稍後,會介紹到函式庫載入時的細節,到時讀者會有更深入的了解。 其實我們不難發現,在 Linux 上使用動態函式庫的執行檔,幾乎都會去呼叫 libc.so.6 與 ld-linux.so.2 這兩個動態函式庫,筆者過去修改 Glibc 的套件時,也了解到在 Linux 中函式庫的關系,ld-linux.so.2 算是最底層的動態函式庫,它本身為靜態聯結,主要的工作是提供基本的函式給其他的函式庫,而我們最常會呼叫的 libc.so.6 則是以 ld-linux.so.2 為基礎的一個架構完成的動態函式庫,它幾乎負責了所有我們常用的標准 C 函式庫,像是我們在 Linux 下寫的 Socket 程序,其中的connect()、bind()、send() .....之類的函式,都是由 libc.so.6 所提供的。 也因此,libc.so.6 的大小也是相當可觀的,在 RedHat 6.1 中經過 strip 後,大小約為 1052428 bytes。 [root@hlchoua /root]# ldd /bin/ls libc.so.6 => /lib/libc.so.6 (0x40016000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) [root@hlchoua /root]# ldd /bin/pwd libc.so.6 => /lib/libc.so.6 (0x40016000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) [root@hlchoua /root]# ldd /bin/vi liBTermcap.so.2 => /lib/libtermcap.so.2 (0x40016000) libc.so.6 => /lib/libc.so.6 (0x4001b000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) 如下,我們透過 ldd 驗證 vi 所用到的動態函式庫 /lib/libtermcap.so.2,它本身是呼叫了 libc.so.6 的函式所組成的。 [root@hlchoua /root]# ldd /lib/libtermcap.so.2 libc.so.6 => /lib/libc.so.6 (0x40007000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x80000000) 接下來,我們依序測試了 /lib/libc.so.6 與 /lib/ld-linux.so.2 [root@hlchoua /root]# ldd /lib/libc.so.6 /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) [root@hlchoua /root]# ldd /lib/ld-linux.so.2 statically linked 我們可以整理以上的結論,畫成如下的一個架構圖 在這個圖中,我們可以清楚的明白 ld-linux.so.2 負責了最基礎的函式,而 libc.so.6 再根據這些基本的函式架構了完整的 C 函式庫,供其它的動態函式庫或是應用程序來呼叫。 透過筆者所寫的一個 ELF 工具程序(注二),我們也可以清楚的看到 libc.so.6呼叫了 ld-linux.so.2 哪些函式 [root@hlchoua /root]# /I-elf /lib/libc.so.6more ======================================================== open_target_file:/lib/libc.so.6 ==>ld-linux.so.2 __register_frame_table cfsetispeed xdr_int32_t utmpname _dl_global_scope_alloc __strcasestr hdestroy_r rename __iswctype_l __sigaddset xdr_callmsg pthread_setcancelstate xdr_union __wcstoul_internal setttyent strrchr __sysv_signal ...┅(more) 其實,ldd 指令為一個 shell script 的檔案,它主要是透過呼叫 ”run-time dynamic linker” 的命令,並以 LD_TRACE_LOADED_OBJECTS 為參數來秀出這些結果的。 如下,就是我們不透過 ldd 指令直接以 eval 搭配 LD_TRACE_LOADED_OBJECTS參數來檢視 libcrypt.So.1 與 libm.so.6這兩個動態函式庫的結果。 [root@hlchoua /root]# eval LD_TRACE_LOADED_OBJECTS=1 '/lib/libcrypt-2.1.2.so' libc.so.6 => /lib/libc.so.6 (0x40016000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) [root@hlchoua /root]# eval LD_TRACE_LOADED_OBJECTS=1 '/lib/libm.so.6' libc.so.6 => /lib/libc.so.6 (0x40016000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
如何得知動態函式庫的位置? 提到 Linux 的動態函式庫,讀者首先會面對到的問題應該是,當我們執行程序時,系統會到哪些目錄去搜尋執行檔所用到的函式庫呢? 其實如果我們去檢視 ”/etc/ld.so.conf” 檔案中的內容如下: /usr/X11R6/lib /usr/i486-linux-libc5/lib 這裡面所存放的是在 Linux 中搜尋動態函式庫時的路徑資訊,不過這並不是系統所會搜尋的所有路徑,以筆者的 RedHat 6.1 來說,我的程序用到了 libreadline.so.3 這個動態函式庫,可是筆者把這個函式庫移除了,所以實際上,它並不存在這台電腦中,當我啟動有用到 libreadline.so.3 的執行檔時,系統會先去檢視這個函式庫是否在動態函式庫的快取(檔名為 ld.so.cache,在本文稍後會提到)中存在,如果不存在的話,系統仍會試著去找尋這個動態函式庫的檔案,它所搜尋的路徑如下順序 /lib/i686/mmx/libreadline.so.3 /lib/i686/libreadline.so.3 /lib/mmx/libreadline.so.3 /lib/libreadline.so.3 /usr/lib/i686/mmx/libreadline.so.3 /usr/lib/i686/libreadline.so.3 /usr/lib/mmx/libreadline.so.3 /usr/lib/libreadline.so.3 如果還是找不到的話,就會顯示如下的錯誤訊息 [root@hlchoua bin]#./test test: error in loading shared libraries: libreadline.so.3: cannot open shared object file: No sUCh file or Directory 如果先不透過 ldconfig 把函式庫路徑設定檔 ld.so.conf 的內容處理過,直接把 libreadline.so.3 放到系統內定會去搜尋的目錄中的其中一個,例如/usr/lib,然後再追蹤一次系統搜尋函式庫的過程,系統還是會依循 /lib/i686/mmx/libreadline.so.3 /lib/i686/libreadline.so.3 /lib/mmx/libreadline.so.3 /lib/libreadline.so.3 /usr/lib/i686/mmx/libreadline.so.3 /usr/lib/i686/libreadline.so.3 /usr/lib/mmx/libreadline.so.3 /usr/lib/libreadline.so.3 的順序來尋找 libreadline.so.3 這個動態函式庫,不過,在搜尋到最後一個目錄後,終於找到了 libreadline.so.3,也使得筆者用來測試的這蘋用到動態函式庫 libreadline.so.3 的執行檔可以順利的執行。 其實,這種逐一目錄尋找的方式很缺乏效率,因此 Linux 提供了一個動態函式庫快取的機制,它所存在的檔案位置為 /etc/ld.so.cache,舉我們之前的例子來說,在ld.so.conf 裡面紀錄了系統搜尋動態函式庫時所會依序去尋找的路徑,如果把我們所要加入的動態函式庫檔案所存在的路徑加入此處,或是以下路徑的其中之一,這樣我們執行程序時,便可以縮短函式庫搜尋所花的時間 /lib/ /usr/lib/ 其實筆者原本是把 libreadline.so.3 放到路徑 /usr/lib/mmx,可是我發現在執行 ldconfig 時,它預設並不會主動到 /usr/lib/mmx目錄中去取得其中動態函式庫檔案的資訊,每當我在執行有用到 libreadline.so.3的程序時,它仍然無法透過動態函式庫快取取得 libreadline.so.3的路徑資訊,而是用一個一個目錄嘗試開啟的方法,直到在 /usr/lib/mmx目錄中找到了 libreadline.so.3,因此筆者比較建議如果要新增動態函式庫到Linux 中最好是直接新增到 /lib 或是 /usr/lib 目錄下,不然就是把函式庫所在的目錄放到ld.so.conf 裡面,再透過 ldconfig 建立動態函式庫的快取資料檔,這樣 Linux 在執行時會更加的便利。 最後,筆者自己新增一個函式庫的目錄,把 libreadline.so.3 放到 /root/lib 中,並且修改 /etc/ld.so.conf 檔案的內容如下 /usr/X11R6/lib /usr/i486-linux-libc5/lib /root/lib 接著筆者把動態函式庫檔案 libreadline.so.3 移到 /root/lib 目錄下,執行ldconfig ˉD,讀者們可以看到它會依序到以下目錄去建立動態函式庫的快取 /usr/X11R6/lib /usr/i486-linux-libc5/lib /root/lib /usr/lib /lib 當我們再次執行有用到 libreadline.so.3 的執行檔時,它便會直接去 /root/lib開啟 libreadline.so.3,而不會再一個個目錄的搜尋了,最後,讀者請注意 libreadline.so.3 必須是一個 link,在筆者的電腦中是 link 到 libreadline.so.3.0,所以請執行 ln -s libreadline.so.3.0 libreadline.so.3 後再執行 ldconfig,不然會產生以下的錯誤訊息 ldconfig: warning: /root/lib/libreadline.so.3 is not a symlink 程序啟動的流程 在 linux 的環境中最常見的可執行檔的種類包括了 Script 檔、Aout 格式的執行檔、ELF 格式的執行檔。在本文的這個部分,我會針對 Linux 系統是如何來辨別這些不同的可執行檔,以及整體的執行流程來作一個說明。 我在此大略說明一下程序啟動的流程,當我們在 shell 中輸入指令時,會先去系統的路徑中來尋找是否有該可執行檔存在,如果找不到的話,就會顯示出找不到該可執行檔的訊息。如果找到的話,就會去呼叫 execve()來執行該檔案,接下來 execve() 會呼叫 System Call sys_execv(),這是在Linux 中 User Mode 透過 80 號中斷(int 80 ah=11)進入 Kernel Mode 所執行的第一個指令,之後在 Kernel 中陸續執行 do_exec()、 prepare_binprm()、read_exec()、 search_binary_handler(),而在 search_binary_handler() 函式中,會逐一的去檢查目前所執行檔案的型態(看看是否為Script File、aout 或 ELF 檔),不過 Linux 所采用的方式是透過各個檔案格式的處理程序來決定目前的執行檔所屬的處理程序。 如下圖,會先去檢驗檔案是否為 Script 檔,若是直進入 Script 檔的處理程序。若不是,則再進入 Aout 檔案格式的處理程序,若該執行檔為 Aout 的檔案格式便交由 Aout檔案格式的處理程序來執行。如果仍然不是的話,便再進入 ELF 檔案格式的處理程序,如果都找不到的話,則傳回錯誤訊息。 由這種執行的流程來看的話,如果 Linux Kernel 想要加入其他的執行檔格式的話,就要在 search_binary_handler() 加入新的執行檔的處理程序,這樣一旦新的執行檔格式產生後,在 Linux 下要執行時,因為在do_load_script、do_load_aout_binary、do_load_elf_binary都會傳回錯誤,因此只有我們自己的 do_load_xxxx_binary 函式可以正確的接手整個執行檔的處理流程,因此便可以達成新的檔案格式置入的動作哩。
在函式 do_load_elf_binary () 執行時,首先會去檢視目前的檔案是否為 ELF 格式,如下程序碼 if (elf_ex.e_ident[0] != 0x7f strncmp(&elf_ex.e_ident[1], "ELF", 3) != 0) goto out; 便是去檢查該檔的前四個 bytes 是否為 0x7f 加上 “ELF” (0x 45 0x4c 0x46),若非,則結束 do_load_elf_binary 的執行。之後,便是去檢視我們之前提過的 e_type 屬性,來得知是否為 ET_EXEC(Executable File) 或是 ET_DYN(Shared Object File) 這兩個值的其中之一 if (elf_ex.e_type != ET_EXEC && elf_ex.e_type != ET_DYN) goto out; 如果都不是這兩個值之一,便結束 do_load_elf_binary 的執行 之後便是一連串讀取 ELF 檔表格的動作,在此就不多說,有興趣的讀者可以自行參閱/usr/src/linux/fs/binfmt_elf.c 的內容即可。 在此我們檢視一個執行檔由啟動到結束的完整流程,首先這個執行檔具有如下的程序碼 #include int main() { printf("ntestn"); } 然後,透過如下的編程過程 gcc test.c ˉo test 我們如果檢視執行檔的 ELF Header 可以得知它主要呼叫了 /lib/libc.so.6函式庫中以下的函式 printf __deregister_frame_info __libc_start_main __register_frame_info 接下來,我們便把程序的執行流程大略整理如下,而 execve("./test", ["./test"], []) 執行的流程,就是剛剛我們所提到的內容,若不熟悉的讀者,可以再回頭看看剛剛的內容,即可對 execve("./test", ["./test"], []) 的執行流程有大略的了解。在這裡,我們會把整個執行流程更完整的來檢視一遍。 首先,我們所在的執行環境會透過 execve("./test", ["./test"], []) 的函式呼叫來啟動 test 執行檔。 呼叫 open("/etc/ld.so.cache", O_RDONLY),以唯讀模式開啟 ld.so.cache,這個檔案的功能是作為動態函式庫的快取,它會記錄了目前系統中所存在的動態函式庫的資訊以及這些函式庫所存在的位置。所以說,如果我們在系統中安裝了新的函式庫時,我們便需要去更新這個檔案的內容,以使新的函式庫可以在我們的 Linux 環境中發生作用,我們可以透過 ldconfig 這個指令來更新 ld.so.cache 的內容。 呼叫 mmap(0, 9937, PROT_READ, MAP_PRIVATE, 3, 0),把 ld.so.cache 檔案映射到記憶體中,mmap 函式的宣告為 mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset),在筆者的電腦上 ld.so.cache 的檔案大小為 9937 bytes,PROT_READ代表這塊記憶體位置是可讀取的,MAP_PRIVATE 則表示產生一個行程私有的 copy-on-write 映射,因此這個呼叫會把整個 ld.so.cache 檔案映射到記憶體中,在筆者電腦上所傳回的映射記憶體起始位置為 0x40013000。 注: mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset)代表我們要求在檔案 fd中,起始位置為offset去映射 length 長度的資料,到記憶體位置 start ,而 prot 是用來描述該記憶體位置的保護權限(例如:讀、寫、執行),flags用來定義所映射物件的型態,例如這塊記憶體是否允許多個 Process 同時映射到,也就是說一旦有一個 Process 更改了這個記憶體空間,那所有映射到這塊記憶體的Process 都會受到影響,或是 flag 設定為 Process 私有的記憶體映射,這樣就會透過 copy-on-write 的機制,當這塊記憶體被別的 Process 修改後,會自動配置實體的記憶體位置,讓其他的 Process 所映射到的記憶體內容與原本的相同。(有關mmap的其它應用,可參考本文最後的注一) 呼叫 open("/lib/libc.so.6", O_RDONLY),開啟 libc.so.6。 呼叫 read(3, "177ELF111331250202"..., 4096) 讀取libc.so.6的檔頭。 呼叫 mmap(0, 993500, PROT_READPROT_EXEC, MAP_PRIVATE, 3, 0),把 libc.so.6 映射到記憶體中,由檔頭開始映射 993500 bytes,若是使用 RedHat 6.1(或其它版本的 RedHat)的讀者或許會好奇 libc.so.6 所 link 到的檔案 libc-2.1.2.so 大小不是 4118715 bytes 嗎? 其實原本 RedHat 所附的 libc.so.6 動態函式庫是沒有經過 strip 過的,如果經過 strip 後,大小會變為 1052428 bytes,而 libc.so.6 由檔頭開始在 993500 bytes 之後都是一些版本的資訊,筆者猜想應該是這樣的原因,所以在映射檔時,並沒有把整個 libc.so.6 檔案映射到記憶體中,只映射前面有意義的部分。與映射 ld.so.cache 不同的是,除了 PROT_READ 屬性之外,libc.so.6 的屬性還多了 PROT_EXEC,這代表了所映射的這塊記憶體是可讀可執行的。在筆者的電腦中,libc.so.6 所映射到的記憶體起始位置為 0x40016000。 呼叫 mprotect(0x40101000, 30940, PROT_NONE),用來設定記憶體的使用權限,而 PROT_NONE 屬性是代表這塊記憶體區間(0x40101000—0x401088DC)是不能讀取、寫入與執行的。 呼叫 mmap(0x40101000, 16384, PROT_READPROT_WRITE, MAP_PRIVATEMAP_FIXED, 3, 0xea000),映射 libc.so.6 由起始位置 0xea000 映射 16384bytes 到記憶體位置 0x40101000。 呼叫 mmap(0x40105000, 14556, PROT_READPROT_WRITE, MAP_PRIVATEMAP_FIXEDMAP_ANONYMOUS, -1, 0),MAP_ANONYMOUS 表示沒有檔案被映射,且產生一個初始值全為 0 的記憶體區塊。 呼叫 munmap(0x40013000, 9937),把原本映射到 ld.so.cache 的記憶體解除映射(此時已把執行檔所需的動態函式庫都映射到記憶體中了)。 呼叫 personality(0),可以設定目前 Process 的執行區間(execution domain),換個說法就是 Linux 支援了多個執行區間,而我們所設定的執行區間會告訴 Linux 如何去映射我們的訊息號碼(signal numbers)到各個不同的訊息動作(signal actions)中。這執行區間的功能,允許 Linux 對其它 Unix-Like 的操作系統,提供有限度的二進位檔支援。如這個例子中,personality(0) 的參數為 0,就是指定為 PER_LINUX 的執行區間(execution domain)。 #define PER_MASK (0x00ff) #define PER_LINUX (0x0000) #define PER_LINUX_32BIT (0x0000 ADDR_LIMIT_32BIT) #define PER_SVR4 (0x0001 STICKY_TIMEOUTS) #define PER_SVR3 (0x0002 STICKY_TIMEOUTS) #define PER_SCOSVR3 (0x0003 STICKY_TIMEOUTS WHOLE_SECONDS) #define PER_WYSEV386 (0x0004 STICKY_TIMEOUTS) #define PER_ISCR4 (0x0005 STICKY_TIMEOUTS) #define PER_BSD (0x0006) #define PER_XENIX (0x0007 STICKY_TIMEOUTS) #define PER_LINUX32 (0x0008) #define PER_IRIX32 (0x0009 STICKY_TIMEOUTS) /* IRIX5 32-bit */ #define PER_IRIXN32 (0x000a STICKY_TIMEOUTS) /* IRIX6 new 32-bit */ #define PER_IRIX64 (0x000b STICKY_TIMEOUTS) /* IRIX6 64-bit */ 呼叫 getpid(),取得目前 Process 的 Process ID。 呼叫 mmap(0, 4096, PROT_READPROT_WRITE, MAP_PRIVATEMAP_ANONYMOUS, -1, 0),傳回值為 0x400130,MAP_ANONYMOUS 表示沒有檔案被映射,且產生一個初始值全為 0 的記憶體區塊。 呼叫 write(1, "ntestn", 6),顯示字串在畫面上。 呼叫 munmap(0x40013000, 4096),解除記憶體位置0x40013000的記憶體映射。 呼叫 _exit(6),結束程序執行。 在這段所舉的例子,只用到了一個函式庫 libc.so.6,我們可以舉像是 RedHat 中 Telnet 指令為例,首先檢視他的 ELF Header
==>libncurses.so.4 tgetent ==>libc.so.6 strcpy ioctl printf cfgetospeed recv connect ............┅ sigsetmask __register_frame_info close free 它主要呼叫了函式庫 libncurses.so.4 的函式 tgetent,以及函式庫 libc.so.6 中為數不少的函式,當然我們也可以去檢視它執行的流程,與之前只呼叫了 libc.so.6 的printf 函式來比較,我們可以發現它主要的不同就是去載入了 libncurses.so.4 open("/usr/lib/libncurses.so.4", O_RDONLY) ; fstat(3, {st_mode=S_IFREG0755, st_size=274985, ...}) ; read(3, "177ELF111331340335"..., 4096) ; mmap(0, 254540, PROT_READPROT_EXEC, MAP_PRIVATE, 3, 0); mprotect(0x40048000, 49740, PROT_NONE); mmap(0x40048000, 36864, PROT_READPROT_WRITE, MAP_PRIVATEMAP_FIXED, 3, 0x31000); mmap(0x40051000, 12876, PROT_READPROT_WRITE, MAP_PRIVATEMAP_FIXEDMAP_ANONYMOUS, -1, 0) ; close(3); 結語 最後,我想各位讀者應該對於 Linux 上的動態函式庫的架構有了進一步的了解,筆者根據自己電腦 Linux 的記憶體配置畫了下面的架構圖,相信會讓有心了解整個運作的人,有了更清楚的一個印象。 在這張圖中,我們所執行的程序是由記憶體 0x08048000 開始載入的,而所用到的動態函式庫則是在記憶體位置 0x40000000 開始載入,以筆者的電腦為例,動態函式庫載入的記憶體映射情況大略為 40000000-40001000 /usr/share/locale/en_US/LC_MESSAGES/SYS_LC_MESSAGES 40001000-40002000 /usr/share/locale/en_US/LC_MONETARY 40002000-40003000 /usr/share/locale/en_US/LC_TIME 40003000-4000b000 /lib/libnss_files-2.1.2.so 4000b000-4000c000 /lib/libnss_files-2.1.2.so 4000c000-400f7000 /lib/libc-2.1.2.so 400f7000-400fb000 /lib/libc-2.1.2.so 400fb000-400ff000 0 400ff000-40111000 /lib/ld-2.1.2.so 40111000-40112000 /lib/ld-2.1.2.so 40112000-4011b000 /lib/libnss_nisplus-2.1.2.so ......┅(more) 若我們程序透過 malloc 配置動態的記憶體,則會配置在標示為 “Free Space”的記憶體空間中,程序所用到的堆疊(Stack) 是由 0xbfffffff 開始,往下延伸。 而在記憶體位置 0xc0000000 以上,則是屬於 Kernel Mode 的部分,這部份包含了Linux Kernel 的 Image 以及我們之後所動態載入的模組。 文章到此正式結束了,讀者若有任何的問題或是這篇文章有任何疏漏的部份,歡迎各位可以來信指教,謝謝各位。。。^_^My E-Mail: [email protected] 注一:(http://www.cuspy.com/~mcculley/mapself/)筆者在寫這篇文章時,在一個網頁上看到一個很有意思的記憶體區塊拷貝效率比較,我們知道在Linux下面如果要把記憶體區塊由 A 拷貝到 B,我們除了可以使用memcpy來完成以外,還可以透過mmap來開啟檔案/proc/self/mem,來完成拷貝記憶體區塊的目的。舉個例子來說,如果我們要把記憶體區塊由A拷貝到B共 chunksize 個bytes,可以透過如下的寫法 memcpy(B, A, chunksize); 透過 mmap 來做的話,可以藉由以下的寫法 int self; self = open("/proc/self/mem", O_RDONLY); B = mmap(B, chunksize, PROT_READ PROT_WRITE, MAP_PRIVATE MAP_FIXED, self, (off_t)A); 也就是透過 Linux 提供給每個 Process 的記憶體裝置檔案 mem,來完成記憶體的拷貝動作。 不過,雖然我們可以有這兩種方法可以選擇,可是遇到要拷貝記憶體時,卻不免會遇到要選擇何種方式來實做的問題,因此該網頁的作者寫了一個小程序來測試這兩種方式的優缺點,首先在 Linux 上每個記憶體的 Page 大小為 512bytes,因此測試時就是利用 512 bytes為單位來逐漸增加測試的記憶體區塊大小。每個階段都有一個固定的記憶體區塊大小,與兩個內容不同的記憶體區塊作為拷貝時的來源端,每一個循環都會先拷貝一個來源端到目的的記憶體區塊中,再比較內容,若相同,則拷貝另一個來源端的資料到目的的記憶體區塊中,再比較內容,如此重復10000 次(表示共拷貝了 20000 次到目的記憶體區塊中),藉此來比較 memcpy 與 mmap 在執行記憶體區塊拷貝時的效率。 如下表(筆者電腦配備: PII 350,64MB RAM) memcpy mmap 512 0.14 0.23 1024 0.26 0.35 2048 0.51 0.59 4096 1.00 1.06 8192 2.56 2.10 16384 5.67 4.55 32768 11.71 8.96 65536 23.63 17.75 我們不難發現當記憶體區塊為 512、1024、2048、4096 時,memcpy 都勝過 mmap。不過當拷貝的記憶體區塊越來越大時,mmap 明顯表現的相當有效率,像最後測試的記憶體區塊大小為 65536 bytes,mmap 相較於 memcpy所花的時間少了約 6 秒鐘。 由此我們可以了解到,如果在 Linux 上我們所撰寫的系統需要使用較大的記憶體區塊拷貝時,透過 mmap 來作或許是一個不錯的選擇。