Linux動態函式庫解析

Linux動態函式庫解析


前言
MS Windows一段時間的讀者,應該都聽過動態函式庫這個名詞。在Windows 9X/ME或是Windows NT/2000中,常見到的動態函式庫為副檔名 “DLL” (Dynamic Loading Library)的檔案。
而在Linux中,當然也有動態函式庫的機制存在。如此一來,所撰寫的程式便無需透過靜態連結(Static Link),而可以在編譯時透過動態連結(Dynamic Link)產生我們所要的執行檔。
使用動態函式庫的好處有許多。首先,就是由於執行檔主要呼叫的函式都包含於動態函式庫中,所以檔案所佔的空間可以因而縮小。其次,當動態函式庫的函式內容有所改變時,呼叫該動態函式庫的程式,可以在最小修正甚至是不需重新編譯的情況下,就可以叫用到新版本的函式庫服務。
對於發展Embedded Linux的業者來說,能夠儘可能減少應用程式執行環境所需空間的大小,便可以把日後成品所需的Flash容量降到最低,在整體成本以及所耗用的記憶體空間來說,都可以得到許多的好處,而在動態函式庫來著手所得到的效益也是相當可觀的,儘可能的刪去不必要的動態函式庫,以及針對動態函式庫改寫來縮小或是透過工具刪去用不到的函式,都可以帶來許多的助益。
當然囉,動態函式庫的好處還不只這些,相信讀者們在文章中可以發現其它的妙用的。
檔案格式(ELF<Executable and Linking Format> VS A.out)
首先,我們必須先確定目前所執行的Linux Kernel版本有開啟 ELF 與 Aout執行檔案格式的支援(通常都會有)
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.outELF兩種檔案的格式,其中目前我們最常見的便是ELF檔案格式。在Linux的函式庫目錄中,我們常常可以見到 “*.so” 的檔案,例如: “/lib/libc.so.6” 或是 “/lib/ld-linux.so.2”。這些便是在Linux中所常見到的動態函式庫檔案。由下圖我們可以看到動態函式庫 libc.so.6 ELF Header:

 
libc.so.6ELF 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_DYNe_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 <stdio.h>
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目錄底下找到許多系統上所安裝的動態函式庫,在文章的這個部分,筆者將把整個函式庫大略的架構作一個說明。
其實LinuxWindows一樣,提供了一組很基本的動態函式庫,在Windows上面我們知道kernel32.dll提供了其它動態函式庫基本的函式呼叫,而在Linux上面則透過ld-linux.so.2提供了其它動態函式庫基本的函式,在筆者電腦上的RedHat6.1 ld-linux.so.2是透過linkld-2.1.2.so(這部分需視各人所使用的glibc版本不同而定)


ld-linux.文字方塊: -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     
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.6|more
========================================================
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,在筆者的電腦中是linklibreadline.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 Fileaout  ELF),不過Linux所採用的方式是透過各個檔案格式的處理程序來決定目前的執行檔所屬的處理程序。
如下圖,會先去檢驗檔案是否為 Script檔,若是直進入Script檔的處理程序。若不是,則再進入Aout檔案格式的處理程序,若該執行檔為Aout的檔案格式便交由Aout檔案格式的處理程序來執行。如果仍然不是的話,便再進入ELF檔案格式的處理程序,如果都找不到的話,則傳回錯誤訊息。
由這種執行的流程來看的話,如果Linux Kernel想要加入其他的執行檔格式的話,就要在search_binary_handler() 加入新的執行檔的處理程序,這樣一旦新的執行檔格式產生後,在Linux下要執行時,因為在do_load_scriptdo_load_aout_binarydo_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 <stdio.h>
int main()
{
printf(“\ntest\n”);
}
然後,透過如下的編譯過程
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”], []) ;的執行流程有大略的了解。在這裡,我們會把整個執行流程更完整的來檢視一遍。
(1) 首先,我們所在的執行環境會透過 execve(“./test”, [“./test”], []) ; 的函式呼叫來啟動 test 執行檔。
(2) 呼叫open(“/etc/ld.so.cache”, O_RDONLY); ,以唯讀模式開啟 ld.so.cache,這個檔案的功能是作為動態函式庫的快取,它會記錄了目前系統中所存在的動態函式庫的資訊以及這些函式庫所存在的位置。所以說,如果我們在系統中安裝了新的函式庫時,我們便需要去更新這個檔案的內容,以使新的函式庫可以在我們的Linux環境中發生作用,我們可以透過ldconfig 這個指令來更新ld.so.cache的內容。
(3) 呼叫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 bytesPROT_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的其它應用,可參考本文最後的註一)
(4) 呼叫open(“/lib/libc.so.6″, O_RDONLY);,開啟 libc.so.6
(5) 呼叫read(3, “\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\250\202″…, 4096); 讀取libc.so.6的檔頭。
(6) 呼叫mmap(0, 993500, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0),把libc.so.6映射到記憶體中,由檔頭開始映射 993500 bytes,若是使用RedHat 6.1(或其它版本的RedHat)的讀者或許會好奇libc.so.6link到的檔案 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
(7) 呼叫mprotect(0x40101000, 30940, PROT_NONE);,用來設定記憶體的使用權限,而PROT_NONE屬性是代表這塊記憶體區間(0x40101000—0x401088DC)是不能讀取、寫入與執行的。
(8) 呼叫mmap(0x40101000, 16384, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 3, 0xea000);,映射libc.so.6由起始位置0xea000映射16384bytes到記憶體位置0x40101000
(9) 呼叫mmap(0x40105000, 14556, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0);MAP_ANONYMOUS表示沒有檔案被映射,且產生一個初始值全為0的記憶體區塊。
(10)呼叫munmap(0x40013000, 9937);,把原本映射到ld.so.cache的記憶體解除映射(此時已把執行檔所需的動態函式庫都映射到記憶體中了)
(11)呼叫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 */



















(12)呼叫getpid(),取得目前ProcessProcess ID
(13)呼叫mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)傳回值為 0x400130MAP_ANONYMOUS表示沒有檔案被映射,且產生一個初始值全為0的記憶體區塊。
(14)呼叫write(1, “\ntest\n”, 6),顯示字串在畫面上。
(15)呼叫munmap(0x40013000, 4096);,解除記憶體位置0x40013000的記憶體映射。
(16)呼叫_exit(6),結束程式執行。
在這段所舉的例子,只用到了一個函式庫 libc.so.6,我們可以舉像是RedHatTelnet指令為例,首先檢視他的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_IFREG|0755, st_size=274985, …}) ;
read(3, “\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\340\335″…, 4096) ;
mmap(0, 254540, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0);
mprotect(0x40048000, 49740, PROT_NONE);
mmap(0x40048000, 36864, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 3, 0x31000);
mmap(0x40051000, 12876, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_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 KernelImage以及我們之後所動態載入的模組。

文章到此正式結束了,讀者若有任何的問題或是這篇文章有任何疏漏的部份,歡迎各位可以來信指教,謝謝各位。。。^_^
My E-Mail: hlchou@gmail.com
註一:(http://www.cuspy.com/~mcculley/mapself/)
筆者在寫這篇文章時,在一個網頁上看到一個很有意思的記憶體區塊拷貝效率比較,我們知道在Linux下面如果要把記憶體區塊由 拷貝到 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次到目的記憶體區塊中),藉此來比較memcpymmap在執行記憶體區塊拷貝時的效率。
如下表
筆者電腦配備: PII 35064MB 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



我們不難發現當記憶體區塊為512102420484096時,memcpy都勝過mmap。不過當拷貝的記憶體區塊越來越大時,mmap明顯表現的相當有效率,像最後測試的記憶體區塊大小為65536 bytesmmap相較於memcpy所花的時間少了約6秒鐘。
由此我們可以了解到,如果在Linux上我們所撰寫的系統需要使用較大的記憶體區塊拷貝時,透過mmap來作或許是一個不錯的選擇。