1 可執行文件的格式
在UNIX傳統的操作系統中,所有編譯生成的輸出文件都缺省地使用同一個名字a.out,在現代操作系統中,a.out格式的可執行文件是鏈接器的輸出,而不是匯編程序的輸出(在計算機的遠古時代,a.out是匯編器的輸出,那個時候還沒有鏈接器)。
目標文件和可執行文件有幾種不同的格式,大多數都采用了一種ELF的格式,更多的格式可以使用下面的命令來查看(有的系統中可能找不到手冊):
$ man a.out
你可以在linux系統中,對編譯鏈接生成的可執行文件使用:
$ file 可執行文件名
可以看到他的輸出,說明這是一個ELF格式的可執行文件:
yuanlu@bear-labpc:~/workspace/wifi/wifi_hello$ file hello.ko
hello.ko: ELF 32-bit LSB relocatable, ARM, version 1 (SYSV), not stripped
2 UNIX中段的概念
在NUIX系統中,有很多的不同格式,但他們都有一個共同的概念—段。
所謂的段是目標文件的概念,一個目標文件有多個段,他們是二進制文件中簡單的的區域,裡面保存了和某種特定類型相關的所有信息,如:符號表條目。這裡不要把UNIX和Intel X86中的段概念混淆,後者中的段表示一種內存模型的設計結果,在這種設計中,地址空寂並非一個整體,而是分成一些固定大小的區域,稱之為段。
對於一個目標文件,運行size命令可以告訴你這個文件的三個段的大小。這三個段分別是:代碼段(文本段),數據段和bss段:
yuanlu@bear-labpc:~/workspace/wifi/wifi_hello$ size hello.ko
text data bss dec hex filename
224 300 0 524 20c hello.ko
這裡對三個段中存放的內容做一個說明:
(1) 文本段:即代碼段,存放要執行的指令代碼
(2) 數據段:存放全局或靜態的已經初始化的數據變量
(3) bss段:存放全局或靜態的尚未初始化的數據變量。由於BSS段只保存沒有值得變量,所以事實上他並不需要保存這些變量的映像,而只是將BSS段在運行時需要的大小記錄在目標文件中,因此BSS段並不占據目標文件的任何空間。
需要注意的是一個a.out可執行文件的組成是下面這個樣子的:
a. a.out神奇數字
b. a.out的其他內容
c. BSS數據段所需大小
d. 數據段(初始化後的全局和靜態變量)
e. 文本段(可執行文件的指令)
其中文件的頭部有一個a.out的神奇數字,它是一種能夠確認一組隨機的二進制位集合的什麼數字,我們暫且不用理會;對於局部變量而言,它不存在a.out中而是在運行時創建。下面我們借用號稱最簡單的hello world驅動來證明上面的內容的正確性。
1 #include <linux/init.h>
2 #include <linux/module.h>
3
4 MODULE_LICENSE("GPL");
5
6 /* the init function*/
7 static __init int hello_init(void)
8
9 {
10
11 printk(KERN_WARNING "Hello world !/n");
12
13 return 0;
14 }
15
16 /* the distory function*/
17 static __exit void hello_exit(void)
18 {
19 printk(KERN_WARNING "Goodbye!/n");
20 }
21
22 module_init(hello_init);
23 module_exit(hello_exit);
編譯生成hello.ko可執行文件後,使用size hello.ko命令查看每個段的使用情況:
yuanlu@bear-labpc:~/workspace/wifi/wifi_hello$ size hello.ko
text data bss dec hex filename
224 300 0 524 20c hello.ko
現在的bss段的內容為空,這裡的0表示bss段的大小,data段的大小為300(單位都是字節)。我們增加分別兩個全局和靜態已經初始化的變量和未初始化的變量,在函數中也增加一個大數組的聲明:
1 #include <linux/init.h>
2 #include <linux/module.h>
3
4 MODULE_LICENSE("GPL");
5
6 int i;
7 int m = 2;
8
9 static int j;
10 static int n = 1;
11
12 /* the init function*/
13 static __init int hello_init(void)
14
15 {
16 printk(KERN_WARNING "Hello world !/n");
17
18 return 0;
19 }
20
21 /* the distory function*/
22 static __exit void hello_exit(void)
23 {
24 printk(KERN_WARNING "Goodbye!/n");
25 }
26
27 module_init(hello_init);
28 module_exit(hello_exit);
編譯後使用size工具:
yuanlu@bear-labpc:~/workspace/wifi/wifi_hello$ size hello.ko
text data bss dec hex filename
224 300 8 532 214 hello.ko
這裡發現bss段的大小變成了8,data段卻沒有增加,通過後面的繼續求證發現這樣一個事實:
(1)對於函數內部的局部變量沒有存放在可執行文件裡
(2)對於全局變量而言,未初始化的變量存放在bss段,初始化的變量分兩種:第一,如果被初始化為0,仍然被放在bss段,否則放在數據段(這一點有點不同哦)
(3)對於靜態全局變量而言,不管有沒有初始化,都不存放在可執行文件中
如果說前面兩點很好理解的話,俺麼對於第三點的實際表現與理論上的比較大的出入,有待後面繼續求證,這裡只能猜測是編譯器的差異(我的環境是交叉編譯環境)。
3 a.out的內存布局
段可以被方便地映射到連接器在運行時可以直接載入的對象中,載入器只是去可執行文件的每一個段的一個映像,本質上段就是正在執行的程序中的一塊內存區域。連接器把每個段從文件拷貝到內存中,一般使用mmap()系統調用。
下面是可執行文件中的段在內存中的布局圖:
堆棧段
空洞
a. a.out神奇數字
b. a.out的其他內容
c. BSS數據段所需大小 ----------------------------------------> BSS段
d. 數據段(初始化後的全局和靜態變量)--------------------> 數據段
e. 文本段(可執行文件的指令) --------------------> 文本段
代碼段包含要執行的指令,數據段包含經過初始化的全局和靜態變量以及他們的值(可能不同的編譯器的某些特性稍有差異),BSS段的大小從可執行文件的bss段得到,緊跟在數據段之後,當bss內采取進入程序的地址空間後全部清0,數據段和BSS段統稱數據區。
這些區域還不足以滿足一個程序的所有需求,因為一個進程還需要保存局部變量、臨時變量、傳遞到函數中的參數以及返回值等,堆棧段就是用於這個目的。當然也少不了堆空間,用於滿足進程的動態分配內存的需求。
要注意的是一個用戶進程的虛擬地址空間最低部分並未被映射,就是說在進程的最低虛擬地址的幾K字節沒有做頁表映射(賦予物理地址),這樣它可以用於捕捉空指針和小整形值得指針引用內存情況。