linux 可執行文件的分析(gcc GUN BUILEIN)

1、GCC

The History of GCC


?

1984年,Richard Stallman發起了自由軟件運動,GNU (Gnu's Not Unix)項目應運而生,3年后,最初版的GCC橫空出世,成為第一款可移植、可優化、支持ANSI C的開源C編譯器。
GCC最初的全名是GNU C Compiler,之后,隨著GCC支持的語言越來越多,它的名稱變成了GNU Compiler Collection。
這里介紹的gcc是GCC的前端,C編譯器.


警告信息


-Wall : 顯示所有常用的編譯警告信息。
?? ?-W??? : 顯示更多的常用編譯警告,如:變量未使用、一些邏輯錯誤。
?? ?-Wconversion : 警告隱式類型轉換。
?? ?-Wshadow : 警告影子變量(在代碼塊中再次聲明已聲明的變量)
?? ?-Wcast-qual :警告指針修改了變量的修飾符。如:指針修改const變量。
?? ?-Wwrite-strings : 警告修改const字符串。
?? ?-Wtraditional : 警告ANSI編譯器與傳統C編譯器有不同的解釋。
?? ?-Werror : 即使只有警告信息,也不編譯。(gcc默認:若只有警告信息,則進行編譯,若有錯誤信息,則不編譯)


C語言標準


你可以在gcc的命令行中通過指定選項來選擇相應的C語言標準: 從傳統c到最新的GNU擴展C. 默認情況下, gcc使用最新的GNU C擴展.

?? ?-ansi : 關閉GNU擴展中與ANSI C相抵觸的部分。
?? ?-pedantic????????? : 關閉所有的GNU擴展。
?? ?-std=c89?????????? : 遵循C89標準
?? ?-std=c99?????????? : 遵循C99標準
??? -std=traditional : 使用原始C
注意:后4個選項可以與-ansi結合使用,也可以單獨使用。

可在gcc中使用大量GNU C擴展.

生成特定格式的文件


以hello.c為例子,可以設置選項生成hello.i, hello.s, hello.o以及最終的hello文件:

?? ?hello.c : 最初的源代碼文件;
?? ?hello.i : 經過編譯預處理的源代碼;
?? ?hello.s : 匯編處理后的匯編代碼;
?? ?hello.o : 編譯后的目標文件,即含有最終編譯出的機器碼,但它里面所引用的其他文件中函數的內存位置尚未定義。
??? hello / a.out : 最終的可執行文件
??? (還有.a(靜態庫文件), .so(動態庫文件), .s(匯編源文件)留待以后討論)

如果你不通過-o指定生成可執行文件名,那么會默認生成a.out. 不指定生成文件名肯能覆蓋你上次生成的a.out.

e.g.
$ gcc hello.c
在不給gcc傳遞任何參數的情況下, gcc執行默認的操作: 將源文件編譯為目標文件--> 將目標文件連接為可執行文件(名為a.out) --> 刪除目標文件.

-c生成.o文件時,默認生成與源代碼的主干同名的.o文件。比如對應hello.c生成hello.o. 但也可在生成目標文件時指定目標文件名(注意同時要給出.o后綴): $gcc -c -o demo.o demo.c

??? $ gcc -Wall -c hello.c????????????? : 生成hello.o
?? ?$ gcc -Wall -c -save-temps hello.c? : 生成hello.i, hello.s, hello.o
?? ?注意-Wall 選項的使用場合:僅在涉及到編譯(即會生成.o文件時,用-Wall)??

多文件編譯、連接


如果原文件分布于多個文件中:file1.c, file2,c
?? ?$ gcc -Wall file1.c file2.c -o name

若對其中一個文件作了修改,則可只重新編譯該文件,再連接所有文件:
?? ?$ gcc -Wall -c file2.c
?? ?$ gcc file1.c file2.o -c name

注意:若編譯器在命令行中從左向右順序讀取.o文件,則它們的出現順序有限制:含有某函數定義的文件必須出現在含有調用該函數的文件之后。好在GCC無此限制。

編譯預處理


以上述的hello.c為例, 要對它進行編譯預備處理, 有兩種方法: 在gcc中指定-E選項, 或直接調用cpp.gcc的編譯預處理命令程序為cpp,比較新版本的gcc已經將cpp集成了,但仍提供了cpp命令. 可以直接調用cpp命令, 也可以在gcc中指定-E選項指定它只進行編譯預處理.

$ gcc -E hello.c??????????????????????????? ==? $ cpp hello.c
上述命令馬上將預處理結果顯示出來. 不利于觀看. 可采用-c將預處理結果保存:
$ gcc -E -c hello.i hello.c????????????? ==? $ cpp -o hello.i hello.c
注意, -c指定名稱要給出".i"后綴.

另外, gcc針對編譯預處理提供了一些選項:
(1) 除了直接在源代碼中用 #define NAME來定義宏外,gcc可在命令行中定義宏:-DNAME(其中NAME為宏名),? 也可對宏賦值: -DNAME=value 注意等號兩邊不能有空格! 由于宏擴展只是一個替換過程,也可以將value換成表達式,但要在兩邊加上雙括號: -DNAME="statement"
e.g. $ gcc -Wall -DVALUE="2+2" tmp.c -o tmp
如果不顯示地賦值,如上例子,只給出:-DVALUE,gcc將使用默認值:1.

(2) 除了用戶定義的宏外, 有一些宏是編譯器自動定義的,它們以__開頭,運行: $ cpp -dM /dev/null, 可以看到這些宏. 注意, 其中含有不以__開頭的非ANSI宏,它們可以通過-ansi選項被禁止。
?
查看宏擴展

1, 運行 $ gcc -E test.c ,gcc對test.c進行編譯預處理,并立馬顯示結果. (不執行編譯) 2, 運行 $gcc -c -save-temps test.c ,不光產生test.o,還產生test.i, test.s,前者是編譯預處理結果, 后者是匯編結果.
???
利用Emacs查看編譯預處理結果

針對含有編譯預處理命令的代碼,可以利用emacs方便地查看預處理結果,而不需執行編譯,更為方便的是,可以只選取一段代碼,而非整個文件:
1,選擇想要查看的代碼
2,C-c C-e (M-x c-macro-expand)
這樣,就自動在一個名為"Macroexpansion"的buffer中顯示pre-processed結果.

生成匯編代碼

使用"-S"選項指定gcc生成以".s"為后綴的匯編代碼:
$ gcc -S hello.c
$ gcc -S -o hello.s hello.c

生成匯編語言的格式取決于目標平臺. 另外, 如果是多個.c文件, 那么針對每一個.c文件生成一個.s文件.

包含頭文件在程序中包含與連接庫對應的頭文件是很重要的方面,要使用庫,就一定要能正確地引用頭文件。一般在代碼中通過#include引入頭文件, 如果頭文件位于系統默認的包含路徑(/usr/includes), 則只需在#include中給出頭文件的名字, 不需指定完整路徑.? 但若要包含的頭文件位于系統默認包含路徑之外, 則有其它的工作要做: 可以(在源文件中)同時指定頭文件的全路徑. 但考慮到可移植性,最好通過-I在調用gcc的編譯命令中指定。

?

?

下面看這個求立方的小程序(陰影語句表示剛開始不存在):

#include <stdio.h>
#include <math.h>
int main(int argc, char *argv[])
{
? double x = pow (2.0, 3.0);
? printf("The cube of 2.0 is %f/n", x);
? return 0;
}

使用gcc-2.95來編譯它(-lm選項在后面的連接選項中有介紹, 這里只討論頭文件的包含問題):
$ gcc-2.95 -Wall pow.c -lm -o pow_2.95
pow.c: In function `main':
pow.c:5: warning: implicit declaration of function `pow'

程序編譯成功,但gcc給出警告: pow函數隱式聲明。
$ ./pow_2.95
The cube of 2.0 is 1.000000

明顯執行結果是錯誤的,在源程序中引入頭文件(#include <math.h>),消除了錯誤。

不要忽略Warning信息!它可能預示著,程序雖然編譯成功,但運行結果可能有錯。故,起碼加上"-Wall"編譯選項!并盡量修正Warning警告。

搜索路徑

首先要理解 #include<file.h>和#include"file.h"的區別:
#include<file.h>只在默認的系統包含路徑搜索頭文件
#include"file.h"首先在當前目錄搜索頭文件, 若頭文件不位于當前目錄, 則到系統默認的包含路徑搜索頭文件.

UNIX類系統默認的系統路徑為:

頭文件,包含路徑: /usr/local/include/? or? /usr/include/
庫文件,連接路徑: /usr/local/lib/????????? or? /usr/lib/???

對于標準c庫(glibc或其它c庫)的頭文件, 我們可以直接在源文件中使用#include <file.h>來引入頭文件.

如果要在源文件中引入自己的頭文件, 就需要考慮下面的問題:

1, 如果使用非系統頭文件, 頭文件和源文件位于同一個目錄, 如何引用頭文件呢?
——我們可以簡單地在源文件中使用 #include "file.h", gcc將當前目錄的file.h引入到源文件. 如果你很固執, 仍想使用#include <file.h>語句, 可以在調用gcc時添加"-I."來將當前目錄添加到系統包含路徑. 細心的朋友可能會想到: 這樣對引用其它頭文件會不會有影響? 比如, #include<file.h>之后緊接著一個#include<math.h>, 它能正確引入math.h嗎? 答案是: 沒有影響. 仍然能正確引用math.h. 我的理解是: "-I."將當前目錄作為包含路徑的第一選擇, 若在當前目錄找不到頭文件, 則在默認路徑搜索頭文件. 這實際上和#include"file.h"是一個意思.

2, 對于比較大型的工程, 會有許多用戶自定義的頭文件, 并且頭文件和.c文件會位于不同的目錄. 又該如何在.c文件中引用頭文件呢?
—— 可以直接在.c文件中利用#include“/path/file.h", 通過指定頭文件的路徑(可以是絕對路徑, 也可以是相對路徑)來包含頭文件. 但這明顯降低了程序的可移植性. 在別的系統環境下編譯可能會出現問題. 所以還是利用"-I"選項指定頭文件完整的包含路徑.

針對頭文件比較多的情況, 最好把它們統一放在一個目錄中, 比如~/project/include. 這樣就不需為不同的頭文件指定不同的路徑. 如果你嫌每次輸入這么多選項太麻煩, 你可以通過設置環境變量來添加路徑:
$ C_INCLUDE_PATH=/opt/gdbm-1.8.3/include
$ export C_INCLUDE_PATH
$ LIBRART_PATH=/opt/gdbm-1.8.3/lib
$ export LIBRART_PATH

可一次指定多個搜索路徑,":"用于分隔它們,"."表示當前路徑,如:
$ C_INCLUDE_PATH=.:/opt/gdbm-1.8.3/include:/net/include
$ LIBRARY_PATH=.:/opt/gdbm-1.8.3/lib:/net/lib
(可以添加多個路徑,路徑之間用:相隔,.代表當前目錄,若.在最前頭,也可省略)

當然,若想永久地添加這些路徑,可以在.bash_profile中添加上述語句.

3, 還有一個比較猥瑣的辦法: 系統默認的包含路徑不是/usr/include或/usr/local/include么? 我把自己的頭文件拷貝到其中的一個目錄, 不就可以了么? 的確可以這樣, 如果你只想在你自己的機器上編譯運行這個程序的話.

前面介紹了三種添加搜索路徑的方法,如果這三種方法一起使用,優先級如何呢?
命令行設置 > 環境變量設置 > 系統默認

與外部庫連接


前面介紹了如何包含頭文件. 而頭文件和庫是息息相關的, 使用庫時, 要在源代碼中包含適當的頭文件,這樣才能聲明庫中函數的原型(發布庫時, 就需要給出相應的頭文件).

和包含路徑一樣, 系統也有默認的連接路徑:
頭文件,包含路徑: /usr/local/include/? or? /usr/include/
庫文件,連接路徑: /usr/local/lib/????????? or? /usr/lib/??

同樣地, 我們想要使用某個庫里的函數, 必須將這個庫連接到使用那些函數的程序中.

有一個例外: libc.a或libc.so (C標準庫,它包含了ANSI C所定義的C函數)是不需要你顯式連接的, 所有的C程序在運行時都會自動加載c標準庫.

除了C標準庫之外的庫稱之為"外部庫", 它可能是別人提供給你的, 也可能是你自己創建的(后面有介紹如何創建庫的內容).

外部庫有兩種:(1)靜態連接庫lib.a
???????????????????? (2)共享連接庫lib.so

兩者的共同點:
??? .a, .so都是.o目標文件的集合,這些目標文件中含有一些函數的定義(機器碼),而這些函數將在連接時會被最終的可執行文件用到。

兩者的區別:
?? ?靜態庫.a? : 當程序與靜態庫連接時,庫中目標文件所含的所有將被程序使用的函數的機器碼被copy到最終的可執行文件中. 靜態庫有個缺點: 占用磁盤和內存空間. 靜態庫會被添加到和它連接的每個程序中, 而且這些程序運行時, 都會被加載到內存中. 無形中又多消耗了更多的內存空間.

??? 共享庫.so : 與共享庫連接的可執行文件只包含它需要的函數的引用表,而不是所有的函數代碼,只有在程序執行時, 那些需要的函數代碼才被拷貝到內存中, 這樣就使可執行文件比較小, 節省磁盤空間(更進一步,操作系統使用虛擬內存,使得一份共享庫駐留在內存中被多個程序使用).共享庫還有個優點: 若庫本身被更新, 不需要重新編譯與它連接的源程序。

靜態庫

下面我們來看一個簡單的例子,計算2.0的平方根(假設文件名為sqrt.c):

#include <math.h>
#include <stdio.h>
int
main (void)
{
double x = sqrt (2.0);
printf ("The square root of 2.0 is %f/n", x);
return 0;
}

用gcc將它編譯為可執行文件:
$ gcc -Wall sqrt.c -o sqrt
編譯成功,沒有任何警告或錯誤信息。執行結果也正確。
$ ./sqrt
The square root of 2.0 is 1.414214
????
下面我們來看看剛才使用的gcc版本:
$ gcc --version
??gcc (GCC) 4.0.2 20050808 (prerelease) (Ubuntu 4.0.1-4ubuntu9)

現在我用2.95版的gcc把sqrt.c再編譯一次:
$ gcc-2.95 -Wall sqrt.c -o sqrt_2.95
??/tmp/ccVBJd2H.o: In function `main':
??sqrt.c:(.text+0x16): undefined reference to `sqrt'
???? collect2: ld returned 1 exit status
????
編譯器會給出上述錯誤信息,這是因為sqrt函數不能與外部數學庫"libm.a"相連。sqrt函數沒有在程序中定義,也不存在于默認C庫 "libc.a"中,如果用gcc-2.95,應該顯式地選擇連接庫。上述出錯信息中的"/tmp/ccVBJd2H.o"是gcc創造的臨時目標文件,用作連接時用。

使用下列的命令可以成功編譯:
$ gcc-2.95 -Wall sqrt.c /usr/lib/libm.a -o sqrt_2.95
它告知gcc:在編譯sqrt.c時,加入位于/usr/lib中的libm.a庫(C數學庫)。

C庫文件默認位于/usr/lib, /usr/local/lib系統目錄中; gcc默認地從/usr/local/lib, /usr/lib中搜索庫文件。(在我的Ubuntu系統中,C庫文件位于/urs/lib中。

這里還要注意連接順序的問題,比如上述命令,如果我改成:
$ gcc-2.95 -Wall /usr/lib/libm.asqrt.c -o sqrt_2.95
gcc會給出出錯信息:
?/tmp/cc6b3bIa.o: In function `main':
?sqrt.c:(.text+0x16): undefined reference to `sqrt'
?collect2: ld returned 1 exit status

正如讀取目標文件的順序,gcc也在命令行中從左向右讀取庫文件——任何包含某函數定義的庫文件必須位于調用該函數的目標文件之后!

指定庫文件的絕對路徑比較繁瑣,有一種簡化方法,相對于上述命令,可以用下面的命令來替代:
$ gcc-2.95 -Wall sqrt.c -lm -o sqrt_2.95
其中的"-l"表示與庫文件連接,"m"代表"libm.a"中的m。一般而言,"-lNAME"選項會使gcc將目標文件與名為"libNAME.a"的庫文件相連。(這里假設使用默認目錄中的庫,對于其他目錄中的庫文件,參考后面的“搜索路徑”。)

上面所提到的"libm.a"就是靜態庫文件,所有靜態庫文件的擴展名都是.a!
$ whereis libm.a
??libm: /usr/lib/libm.a /usr/lib/libm.so

正如前面所說,默認的庫文件位于/usr/lib/或/usr/local/lib/目錄中。其中,libm.a是靜態庫文件,libm.so是后面會介紹的動態共享庫文件。

如果調用的函數都包含在libc.a中(C標準庫被包含在/usr/lib/libc.a中,它包含了ANSI C所定義的C函數)。那么沒有必要顯式指定libc.a:所有的C程序運行時都自動包含了C標準庫!(試試 $ gcc-2.95 -Wall hello.c -o hello)。

共享庫

正因為共享庫的優點,如果系統中存在.so庫,gcc默認使用共享庫(在/usr/lib/目錄中,庫文件以共享和靜態兩種版本存在)。?

運行:$ gcc -Wall -L. hello.c -lNAME -o hello
gcc先檢查是否有替代的libNAME.so庫可用。???

正如前面所說,共享庫以.so為擴展名(so == shared object)。

那么,如果不想用共享庫,而只用靜態庫呢?可以加上 -static選項
$ gcc -Wall -static hello.c -lNAME -o hello
它等價于:
$ gcc -Wall hello.c libNAME.a -o hello

$ gcc-2.95 -Wall sqrt.c -static -lm -o sqrt_2.95_static
$ gcc-2.95 -Wall sqrt.c -lm -o sqrt_2.95_default
$ gcc-2.95 -Wall sqrt.c /usr/lib/libm.a -o sqrt_2.95_a
$ gcc-2.95 -Wall sqrt.c /usr/lib/libm.so -o sqrt_2.95_so

$ ls -l sqrt*
-rwxr-xr-x? 1 zp zp? 21076 2006-04-25 14:52 sqrt_2.95_a
-rwxr-xr-x? 1 zp zp?? 7604 2006-04-25 14:52 sqrt_2.95_default
-rwxr-xr-x? 1 zp zp?? 7604 2006-04-25 14:52 sqrt_2.95_so
-rwxr-xr-x? 1 zp zp 487393 2006-04-25 14:52 sqrt_2.95_static

上述用四種方式編譯sqrt.c,并比較了可執行文件的大小。奇怪的是,-static -lm 和 /lib/libm.a為什么有區別?有知其原因著,懇請指明,在此謝謝了! :)

如果libNAME.a在當前目錄,應執行下面的命令:
$ gcc -Wall -L. hello.c -lNAME -o hello
-L.表示將當前目錄加到連接路徑。

利用GNU archiver創建庫

$ ar cr libhello.a hello_fn.o by_fn.o
從hello_fn.o和by_fn.o創建libihello.a,其中cr表示:creat & replace
$ ar t libhello.a
列出libhello.a中的內容,t == table
(也可創建libhello.so)

關于創建庫的詳細介紹,可參考本blog的GNU binutils筆記


調試

?


一般地,可執行文件中是不包含任何對源代碼的參考的,而debugger要工作,就要知道目標文件/可執行文件中的機器碼對應的源代碼的信息(如:哪條語句、函數名、變量名...). debugger工作原理:將函數名、變量名,對它們的引用,將所有這些對象對應的代碼行號儲存到目標文件或可執行文件的符號表中。

?

GCC提供-g選項,將調試信息加入到目標文件或可執行文件中。
$ gcc -Wall -g hello.c -o hello

注意:若發生了段錯誤,但沒有core dump,是由于系統禁止core文件的生成!
$ ulimit -c  ,若顯示為0,則系統禁止了core dump

解決方法:
$ ulimit -c unlimited  (只對當前shell進程有效)
或在~/.bashrc 的最后加入: ulimit -c unlimited (一勞永逸)

優化


GCC具有優化代碼的功能,代碼的優化是一項比較復雜的工作,它可歸為:源代碼級優化、速度與空間的權衡、執行代碼的調度。

?

GCC提供了下列優化選項:
??? -O0? : 默認不優化(若要生成調試信息,最好不優化)
??? -O1? : 簡單優化,不進行速度與空間的權衡優化;???
??? -O2? : 進一步的優化,包括了調度。(若要優化,該選項最適合,它是GNU發布軟件的默認優化級別;
??? -O3? : 雞肋,興許使程序速度更慢;
??? -funroll-loops? : 展開循環,會使可執行文件增大,而速度是否增加取決于特定環境;
??? -Os? : 生成最小執行文件;

一般來說,調試時不優化,一般的優化選項用-O2(gcc允許-g與-O2聯用,這也是GNU軟件包發布的默認選項),embedded可以考慮-Os。

注意:此處為O!(非0或小寫的o,-o是指定可執行文件名)。

檢驗優化結果的方法:$ time ./prog

time測量指定程序的執行時間,結果由三部分組成:
??? real : 進程總的執行時間, 它和系統負載有關(包括了進程調度,切換的時間)
??? user: 被測量進程中用戶指令的執行時間
??? sys? : 被測量進程中內核代用戶指令執行的時間

user和sys的和被稱為CPU時間.

注意:對代碼的優化可能會引發警告信息,移出警告的辦法不是關閉優化,而是調整代碼。


2、ar


??? ar用于建立、修改、提取檔案文件(archive)。archive是一個包含多個被包含文件的單一文件(也稱之為庫文件),其結構保證了可以從中檢索并得到原始的被包含文件(稱之為archive中的member)。member的原始文件內容、模式(權限)、時間戳、所有著和組等屬性都被保存在 archive中。member被提取后,他們的屬性被恢復到初始狀態。
???
??? ar主要用于創建C庫文件

?

創建靜態庫?
??? (1) 生成目標文件:??

$ gcc -Wall -c file1.c file2.c file3.c
???
??? 不用指定生成.o文件名(默認生成file1.o, file2.o, file3.o)。

??? (2) 從.o目標文件創建靜態連接庫:
???
$ ar rv libNAME.a file1.o file2.o file3.o
???
??? ar生成了libNAME.a庫,并列出庫中的文件。
??? r : 將flie1.o, file2,o, file3.o插入archive,如故原先archive中已經存在某文件,則先將該文件刪除。
??? v : 顯示ar操作的附加信息(如被處理的member文件名)

注: 對于BSD系統, 還需要在創建靜態庫之后創建索引: $ ranlib libNAME.a Linux中不需要這一步(運行它也是無害的).

創建動態庫(利用gcc,未用ar)

(1) 生成目標文件

$ gcc -Wall -c -fpic file1.c file2.c file3.c

-fpic: 指定生成的.o目標文件可被重定址. pic是position idependent code的縮寫: 位置無關代碼.

(2)生成動態庫文件

$ gcc -shared -o libNAME.so file1.o file2.o file3.o

一般地, 連接器使用main()函數作為程序入口. 但在動態共享庫中沒有這樣的入口. 所以就要指定-shared選項來避免編譯器顯示出錯信息.

實際上, 上述的兩條命令可以合并為下面這條:

$ gcc -Wall -shared -fpic -o libNAME.so file1.c file2.c file3.c


此后,將main函數所在的程序與libNAME.so連接(注意庫連接路徑和頭文件包含路徑,以及連接順序!參考 gcc筆記)
???
至此,與動態庫連接的函數編譯成了一個可執行文件。貌似成功了,但還差最后一步。如果直接運行該程序,會給出這樣的錯誤信息:

error while loading shared libraries: libhello.so:
cannot open shared object file: No such file or directory

這是因為與動態庫連接的程序在運行時,首先將該動態庫加載到內存中,而gcc默認加載動態庫文件所在目錄為/usr/local/lib, /usr/lib。剛才的程序雖然能編譯成功,但如果我們自己建立的動態庫沒有位于默認目錄中,則執行時會應為無法找到它而失敗。
??
解決辦法:改變加載路徑對應的環境變量,然后再執行。
???
export LD_LIBRARY_PATH=動態庫所在目錄:$LD_LIBRARY_PATH

查看archive內容

$ ar tv archiveNAME

t : 顯示archive中member的內容,若不指定member,則列出所有。
v : 與t結合使用時,顯示member的詳細信息。

要想進了解ar的詳細選項,參考ar的 on-line manual


nm

??? nm用來列出目標文件中的符號,可以幫助程序員定位和分析執行程序和目標文件中的符號信息和它的屬性。
??? 如果沒有目標文件作為參數傳遞給nm, nm假定目標文件為a.out.
??? 這里用一個簡單的示例程序來介紹nm的用法:

main.c:

int main(int argc, char *argv[])
{
? hello();
? bye();
? return 0;
}

hello.c:??
void hello(void)
{
? printf("hello!/n");
}

bye.c:
???
void bye(void)
{
? printf("good bye!/n");
}

??? 運行下列命令:
??? $ gcc -Wall -c main.c hello.c bye.c
??? gcc生成main.o, hello.o, bye.o三個目標文件(這里沒有聲明函數原型,加了-Wall,gcc會給出警告)
??? $ nm main.o hello.o bye.o

結果顯示如下:??
main.o:
???????????????? U bye
???????????????? U hello
00000000 T main

hello.o:
00000000 T hello
??????? ? ? ? ?? U puts

bye.o:
00000000 T bye
???????????????? U puts

??? 結合這些輸出結果,以及程序代碼,可以知道:
??? 對于main.o, bye和hello未被定義, main被定義了
??? 對于hello.o, hello被定義了, puts未被定義
??? 對于bye.o, bye被定義了,puts未被定義

幾個值得注意的問題:
??? (1)"目標文件"指.o文件, 庫文件, 最終的可執行文件
??? .o? : 編譯后的目標文件,即含有最終編譯出的機器碼,但它里面所引用的其他文件中函數的內存位置尚未定義.
??? (2)如果用nm查看可執行文件, 輸出會比較多, 仔細研究輸出, 可以對nm用法有更清醒的認識.
??? (3)在上述hello.c, bye.c中, 調用的是printf(), 而nm輸出中顯示調用的是puts(), 說明最終程序實際調用的puts(), 如果令hello.c或bye.c中的printf()使用格式化輸出,則nm顯示調用printf(). ( 如: printf("%d", 1); )
???
??? 關于nm的參數選項,參考 on-line manual


objcopy

??? objcopy可以將一種格式的目標文件轉化為另外一種格式的目標文件. 它使用GNU BFD庫進行讀/寫目標文件.使用BFD, objcopy就能將原格式的目標文件轉化為不同格式的目標文件.
??? 以我們在nm中使用的hello.o目標文件和hello可執行為例:

$ file hello.o hello
??
??? file命令用來判別文件類型, 輸出如下:
???
hello.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
hello:? ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux 2.2.0, dynamically linked (uses shared libs), not stripped
???
??? 現在運行objcopy來改變hello的文件類型: 原先它是ELF格式的可執行程序, 現將它轉換為srec格式. srec格式文件是Motolora S-Record格式的文件, 主要用來在主機和目標機之間傳輸數據.
???
$ objcopy -O srec hello hello_srec
$ file hello.o hello

??? file命令結果: hello_srec: Motorola S-Record; binary data in text format

??? 注意objcopy的格式, "-O"指定輸出文件類型; 輸入文件名和輸出文件名位于命令末尾. 關于objcopy命令的詳細選項, 參考 on-line manual


objdump

??? objdump用來顯示目標文件的信息. 可以通過選項控制顯示那些特定信息. objdump一個最大的用處恐怕就是將C代碼反匯編了. 在嵌入式軟件開發過程中, 也可以用它查看執行文件或庫文件的信息.
??? 下面我們用上文提到的hello可執行文件和hello_srec可執行文件為例, 介紹objdump的簡單用法:
???

$ objdump -f hello hello_srec

輸出如下:
hello:???? file format elf32-i386
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x080482c0

hello_srec:???? file format srec
architecture: UNKNOWN!, flags 0x00000000:
start address 0x00000000080482c0
???
-f : 顯示目標文件的頭文件概要信息.

生成反匯編代碼:
???

$ objdump -d hello.o

顯示如下:
hello.o:???? file format elf32-i386

Disassembly of section .text:

00000000 <hello>:
?? 0:?? 55????????????????????? push?? %ebp
?? 1:?? 89 e5?????????????????? mov??? %esp,%ebp
?? 3:?? 83 ec 08??????????????? sub??? $0x8,%esp
?? 6:?? 83 ec 0c??????????????? sub??? $0xc,%esp
?? 9:?? 68 00 00 00 00????????? push?? $0x0
?? e:?? e8 fc ff ff ff????????? call?? f <hello+0xf>
? 13:?? 83 c4 10??????????????? add??? $0x10,%esp
? 16:?? c9????????????????????? leave
? 17:?? c3????????????????????? ret

??? -d : 顯示目標文件中機器指令使用的匯編語言. 只反匯編那些應該含有指令機器碼的節(顯示.text段); 如果用-D, 則反匯編所有節的內容.
??? 關于objcopy命令的詳細選項, 參考 on-line manual


readelf

??? readelf用來顯示ELF格式目標文件的信息.可通過參數選項來控制顯示哪些特定信息.(注意: readelf不支持顯示archive文檔, 也不支持64位的ELF文件).
??? 下面利用先前的hello可執行文件演示readelf的簡單用法:
???

$ readelf -h hello

ELF Header:
? Magic:?? 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
? Class:??????????????????????????? ? ? ? ? ? ? ?? ELF32
? Data:??????????????????????????????????????????? 2's complement, little endian
? Version:??????????????????????????????????????? 1 (current)
? OS/ABI:????????????????????????? ? ? ? ? ? ? ? ? UNIX - System V
? ABI Version:???????????????????? ? ? ? ? ? ? ? 0
? Type:????????????????????????????????????????????? EXEC (Executable file)
? Machine:??????????????????????????????????????? Intel 80386
? Version:????????????????????????????????????????? 0x1
? Entry point address:?????????????????????? 0x80482c0
? Start of program headers:???????? ? ?? 52 (bytes into file)
? Start of section headers:???????? ? ? ? ? 3848 (bytes into file)
? Flags:?????????????????????????? ? ? ? ? ? ? ? ? ? ? 0x0
? Size of this header:????????????????????????? 52 (bytes)
? Size of program headers:???????? ? ? ?? 32 (bytes)
? Number of program headers:?????? ?? 7
? Size of section headers:????????? ? ? ? ? 40 (bytes)
? Number of section headers:??????????? 34
? Section header string table index:?? 31

注意: readelf只能用于ELF格式目標文件, 且選項中至少要指定一個(除V, H外)的選項!


gprof

??? gprof被用來測量程序的性能. 它記錄每個函數被調用的次數以及相應的執行時間. 這樣就能鎖定程序執行時花費時間最多的部分, 對程序的優化就可集中于對它們的優化.
???
??? 用一個簡單的數值計算程序來掩飾gprof的用法:

collatz.c:

#include <stdio.h>
/* Computes the length of Collatz sequences */
unsigned int step (unsigned int x)
{
???? if (x % 2 == 0)
???? {
??? ? return (x / 2);
???? }
???? else
???? {
??? ? return (3 * x + 1);
???? }
}

unsigned int nseq (unsigned int x0)
{
???? unsigned int i = 1, x;
???? if (x0 == 1 || x0 == 0)
??? ? return i;
???? x = step (x0);
???? while (x != 1 && x != 0)
???? {
??? ? x = step (x);
??? ? i++;
???? }
???? return i;
}

int main (void)
{
???? unsigned int i, m = 0, im = 0;
???? for (i = 1; i < 500000; i++)
???? {
??? ? unsigned int k = nseq (i);
??? ? if (k > m)
??? ? {
??? ?????? m = k;
??? ?????? im = i;
??? ?????? printf ("sequence length = %u for %u/n", m, im);
??? ? }
???? }
???? return 0;
}

??? 先將collatz.c編譯成目標文件collatz.o, gcc通過 -pg選項來打開gprof支持:
???
$ gcc -Wall -c -pg collatz.c

$ gcc -Wall -pg -o collatz collatz.o

??? 注意:兩條命令都要加 "-pg"選項。前一條命令生成collatz.o目標文件。后一條命令生成可執行文件,該可執行文件中包含了記錄函數執行時間的指令。
??? 生成collatz可執行文件后,現執行它,結果與一般程序的執行無疑。但此時在PWD目錄生成一個名為"gmon.out"的文件,gprof通過它來分析程序的執行。
??? 如果不現執行程序,而直接用gprof來分析它,會提示“gmon.out: No such file or directory”。
??? gprof用法:
???
$ gprof ./collatz

關于gprof更多的描述,參考gprof的 on-line manual

3、可執行文件格式

目標文件格式與類型

GNU C compiler根據源文件的后綴名來對文件進行預處理、匯編或編譯操作。在編譯鏈接時,生成的目標文件都是ELF格式的(可執行鏈接格式,Executable and Linking Format)。Object文件格式有三種類型:

(1)可重定位(relocatable)文件:用來和其他的object文件一起鏈接為一個可執行文件(executable)或一個共享文件(.so文件,shared object)。

(2)可執行(executable)文件;

(3)共享目標文件(shared object file):用于被下面的兩個鏈接器鏈接。一是鏈接編輯器(ld),可以和其他的relocatable或shared object file來創建其他的目標文件,例如.so共享庫(可用file命令查看其屬性);二是動態鏈接器,聯合一個可執行文件和其他的shared object file來創建一個進程映像

?

首先看看ELF文件的總體布局:

ELF header(ELF頭部)
Program header table(程序頭表)
Segment1(段1)
Segment2(段2)
………
Sengmentn(段n)
Setion header table(節頭表,可選)

段由若干個節(Section)構成,節頭表對每一個節的信息有相關描述。對可執行程序而言,節頭表是可選的。 參考資料 1中作者談到把節頭表的所有數據全部設置為0,程序也能正確運行!ELF頭部是一個關于本文件的路線圖(road map),從總體上描述文件的結構。下面是ELF頭部的數據結構:

typedef struct
{unsigned char e_ident[EI_NIDENT];     /* 魔數和相關信息 */Elf32_Half    e_type;                 /* 目標文件類型 */Elf32_Half    e_machine;              /* 硬件體系 */Elf32_Word    e_version;              /* 目標文件版本 */Elf32_Addr    e_entry;                /* 程序進入點 */Elf32_Off     e_phoff;                /* 程序頭部偏移量 */Elf32_Off     e_shoff;                /* 節頭部偏移量 */Elf32_Word    e_flags;                /* 處理器特定標志 */Elf32_Half    e_ehsize;               /* ELF頭部長度 */Elf32_Half    e_phentsize;            /* 程序頭部中一個條目的長度 */Elf32_Half    e_phnum;                /* 程序頭部條目個數  */Elf32_Half    e_shentsize;            /* 節頭部中一個條目的長度 */Elf32_Half    e_shnum;                /* 節頭部條目個數 */Elf32_Half    e_shstrndx;             /* 節頭部字符表索引 */
} Elf32_Ehdr;

下面我們對ELF頭表中一些重要的字段作出相關說明,完整的ELF定義請參閱 參考資料6參考資料 7

e_ident[0]-e_ident[3]包含了ELF文件的魔數,依次是0x7f、'E'、'L'、'F'。注意,任何一個ELF文件必須包含此魔數。 參考資料 3中討論了利用程序、工具、/Proc文件系統等多種查看ELF魔數的方法。e_ident[4]表示硬件系統的位數,1代表32位,2代表64位。e_ident[5]表示數據編碼方式,1代表小印第安排序(最大有意義的字節占有最低的地址),2代表大印第安排序(最大有意義的字節占有最高的地址)。e_ident[6]指定ELF頭部的版本,當前必須為1。e_ident[7]到e_ident[14]是填充符,通常是0。ELF格式規范中定義這幾個字節是被忽略的,但實際上是這幾個字節完全可以可被利用。如病毒Lin/Glaurung.676/666(參考資料 1)設置e_ident[7]為0x21,表示本文件已被感染;或者存放可執行代碼(參考資料 2)。ELF頭部中大多數字段都是對子頭部數據的描述,其意義相對比較簡單。值得注意的是某些病毒可能修改字段e_entry(程序進入點)的值,以指向病毒代碼,例如上面提到的病毒Lin/Glaurung.676/666。

一個實際可執行文件的文件頭部形式如下:(利用命令readelf)

  	ELF Header:Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class:                             ELF32Data:                              2's complement, little endianVersion:                           1 (current)OS/ABI:                            UNIX - System VABI Version:                       0Type:                              EXEC (Executable file)Machine:                           Intel 80386Version:                           0x1Entry point address:               0x80483ccStart of program headers:          52 (bytes into file)Start of section headers:          14936 (bytes into file)Flags:                             0x0Size of this header:               52 (bytes)Size of program headers:           32 (bytes)Number of program headers:         6Size of section headers:           40 (bytes)Number of section headers:         34Section header string table index: 31

緊接ELF頭部的是程序頭表,它是一個結構數組,包含了ELF頭表中字段e_phnum定義的條目,結構描述一個段或其他系統準備執行該程序所需要的信息。

typedef struct {Elf32_Word  p_type;				/* 段類型 */Elf32_Off   p_offset;     	 	/* 段位置相對于文件開始處的偏移量 */Elf32_Addr  p_vaddr;   			/* 段在內存中的地址 */Elf32_Addr  p_paddr;   			/* 段的物理地址 */Elf32_Word  p_filesz;				/* 段在文件中的長度 */Elf32_Word  p_memsz;				/* 段在內存中的長度 */Elf32_Word  p_flags;				/* 段的標記 */Elf32_Word  p_align;				/* 段在內存中對齊標記 */} Elf32_Phdr;

在詳細討論可執行文件程序頭表之前,首先查看一個實際文件的輸出:

  Program Headers:
Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
PHDR           0x000034 0x08048034 0x08048034 0x000c0 0x000c0 R E 0x4
INTERP         0x0000f4 0x080480f4 0x080480f4 0x00013 0x00013 R   0x1[Requesting program interpreter: /lib/ld-linux.so.2]LOAD           0x000000 0x08048000 0x08048000 0x00684 0x00684 R E 0x1000LOAD           0x000684 0x08049684 0x08049684 0x00118 0x00130 RW  0x1000DYNAMIC        0x000690 0x08049690 0x08049690 0x000c8 0x000c8 RW  0x4NOTE           0x000108 0x08048108 0x08048108 0x00020 0x00020 R   0x4Section to Segment mapping:Segment Sections...00     01     .interp 02     .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame 03     .data .dynamic .ctors .dtors .jcr .got .bss 04     .dynamic 
05     .note.ABI-tag
Section Headers:[Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al[ 0]                   NULL            00000000 000000 000000 00      0   0  0[ 1] .interp           PROGBITS        080480f4 0000f4 000013 00   A  0   0  1[ 2] .note.ABI-tag     NOTE            08048108 000108 000020 00   A  0   0  4[ 3] .hash             HASH            08048128 000128 000040 04   A  4   0  4[ 4] .dynsym           DYNSYM          08048168 000168 0000b0 10   A  5   1  4[ 5] .dynstr           STRTAB          08048218 000218 00007b 00   A  0   0  1[ 6] .gnu.version      VERSYM          08048294 000294 000016 02   A  4   0  2[ 7] .gnu.version_r    VERNEED         080482ac 0002ac 000030 00   A  5   1  4[ 8] .rel.dyn          REL             080482dc 0002dc 000008 08   A  4   0  4[ 9] .rel.plt          REL             080482e4 0002e4 000040 08   A  4   b  4[10] .init             PROGBITS        08048324 000324 000017 00  AX  0   0  4[11] .plt              PROGBITS        0804833c 00033c 000090 04  AX  0   0  4[12] .text             PROGBITS        080483cc 0003cc 0001f8 00  AX  0   0  4[13] .fini             PROGBITS        080485c4 0005c4 00001b 00  AX  0   0  4[14] .rodata           PROGBITS        080485e0 0005e0 00009f 00   A  0   0 32[15] .eh_frame         PROGBITS        08048680 000680 000004 00   A  0   0  4[16] .data             PROGBITS        08049684 000684 00000c 00  WA  0   0  4[17] .dynamic          DYNAMIC         08049690 000690 0000c8 08  WA  5   0  4[18] .ctors            PROGBITS        08049758 000758 000008 00  WA  0   0  4[19] .dtors            PROGBITS        08049760 000760 000008 00  WA  0   0  4[20] .jcr              PROGBITS        08049768 000768 000004 00  WA  0   0  4[21] .got              PROGBITS        0804976c 00076c 000030 04  WA  0   0  4[22] .bss              NOBITS          0804979c 00079c 000018 00  WA  0   0  4[23] .comment          PROGBITS        00000000 00079c 000132 00      0   0  1[24] .debug_aranges    PROGBITS        00000000 0008d0 000098 00      0   0  8[25] .debug_pubnames   PROGBITS        00000000 000968 000040 00      0   0  1[26] .debug_info       PROGBITS        00000000 0009a8 001cc6 00      0   0  1[27] .debug_abbrev     PROGBITS        00000000 00266e 0002cc 00      0   0  1[28] .debug_line       PROGBITS        00000000 00293a 0003dc 00      0   0  1[29] .debug_frame      PROGBITS        00000000 002d18 000048 00      0   0  4[30] .debug_str        PROGBITS        00000000 002d60 000bcd 01  MS  0   0  1[31] .shstrtab         STRTAB          00000000 00392d 00012b 00      0   0  1[32] .symtab           SYMTAB          00000000 003fa8 000740 10     33  56  4[33] .strtab           STRTAB          00000000 0046e8 000467 00      0   0  1

對一個ELF可執行程序而言,一個基本的段是標記p_type為PT_INTERP的段,它表明了運行此程序所需要的程序解釋器(/lib/ld-linux.so.2),實際上也就是動態連接器(dynamic linker)。最重要的段是標記p_type為PT_LOAD的段,它表明了為運行程序而需要加載到內存的數據。查看上面實際輸入,可以看見有兩個可LOAD段,第一個為只讀可執行(FLg為R E),第二個為可讀可寫(Flg為RW)。段1包含了文本節.text,注意到ELF文件頭部中程序進入點的值為0x80483cc,正好是指向節.text在內存中的地址。段二包含了數據節.data,此數據節中數據是可讀可寫的,相對的只讀數據節.rodata包含在段1中。ELF格式可以比COFF格式包含更多的調試信息,如上面所列出的形式為.debug_xxx的節。在I386平臺LINUX系統下,用命令file查看一個ELF可執行程序的可能輸出是:a.out: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux 2.2.5, dynamically linked (uses shared libs), not stripped。

ELF文件中包含了動態連接器的全路徑,內核定位"正確"的動態連接器在內存中的地址是"正確"運行可執行文件的保證, 參考資料 13討論了如何通過查找動態連接器在內存中的地址以達到顛覆(Subversiver)動態連接機制的方法。

最后我們討論ELF文件的動態連接機制。每一個外部定義的符號在全局偏移表(Global Offset Table GOT)中有相應的條目,如果符號是函數則在過程連接表(Procedure Linkage Table PLT)中也有相應的條目,且一個PLT條目對應一個GOT條目。對外部定義函數解析可能是整個ELF文件規范中最復雜的,下面是函數符號解析過程的一個描述。

1:代碼中調用外部函數func,語句形式為call 0xaabbccdd,地址0xaabbccdd實際上就是符號func在PLT表中對應的條目地址(假設地址為標號.PLT2)。

2:PLT表的形式如下

          .PLT0: pushl   4(%ebx)    /* GOT表的地址保存在寄存器ebx中 */
jmp     *8(%ebx)nop; nopnop; nop.PLT1:	jmp     *name1@GOT(%ebx)pushl   $offsetjmp     .PLT0@PC.PLT2:	jmp     *func@GOT(%ebx)pushl   $offsetjmp     .PLT0@PC

3:查看標號.PLT2的語句,實際上是跳轉到符號func在GOT表中對應的條目。

4:在符號沒有重定位前,GOT表中此符號對應的地址為標號.PLT2的下一條語句,即是pushl $offset,其中$offset是符號func的重定位偏移量。注意到這是一個二次跳轉。

5:在符號func的重定位偏移量壓棧后,控制跳到PLT表的第一條目,把GOT[1]的內容壓棧,并跳轉到GOT[2]對應的地址。

6:GOT[2]對應的實際上是動態符號解析函數的代碼,在對符號func的地址解析后,會把func在內存中的地址設置到GOT表中此符號對應的條目中。

7:當第二次調用此符號時,GOT表中對應的條目已經包含了此符號的地址,就可直接調用而不需要利用PLT表進行跳轉。

動態連接是比較復雜的,但為了獲得靈活性的代價通常就是復雜性。其最終目的是把GOT表中條目的值修改為符號的真實地址,這也可解釋節.got包含在可讀可寫段中。

動態連接是一個非常重要的進步,這意味著庫文件可以被升級、移動到其他目錄等等而不需要重新編譯程序(當然,這不意味庫可以任意修改,如函數入參的個數、數據類型應保持兼容性)。從很大程度上說,動態連接機制是ELF格式代替a.out格式的決定性原因。如果說面對對象的編程本質是面對接口(interface)的編程,那么動態連接機制則是這種思想的地一個非常典型的應用,具體的講,動態連接機制與設計模式中的橋接(BRIDGE)方法比較類似,而它的LAZY特性則與代理(PROXY)方法非常相似。動態連接操作的細節描述請參閱參考資料 8,9,10,11。通過閱讀命令readelf、objdump 的源代碼以及參考資料 14中所提及的相關軟件源代碼,可以對ELF文件的格式有更徹底的了解。

?

4. 鏈接與鏈接腳本

鏈接器ld把object文件中的每個section都作為一個整體,為其分配運行的地址(memory layout),這個過程就是重定位(relocation);最后把所有目標文件合并為一個目標文件。

?


鏈接通過一個linker script來控制,這個腳本描述了輸入文件的sections到輸出文件的映射,以及輸出文件的memory layout。

因此,linker總會使用一個linker script,如果不特別指定,則使用默認的script;可以使用‘-T’命令行選項來指定一個linker script。

?

*映像文件的輸入段與輸出段

linker把多個輸入文件合并為一個輸出文件。輸出文件和輸入文件都是目標文件(object file),輸出文件通常被稱為可執行文件(executable)。

每個目標文件都有一系列section,輸入文件的section稱為input section,輸出文件的section則稱為output section。

一個section可以是loadable的,即輸出文件運行時需要將這樣的section加載到memory(類似于RO&RW段);也可以是allocatable的,這樣的section沒有任何內容,某些時候用0對相應的memory區域進行初始化(類似于ZI段);如果一個section既非loadable也非allocatable,則它通常包含的是調試信息。

每個loadable或allocatable的output section都有兩個地址,一是VMA(virtual memory address),是該section的運行時域地址;二是LMA(load memory address),是該section的加載時域地址。

可以通過objdump工具附加'-h'選項來查看目標文件中的sections

*簡單的Linker script

(1) SECTIONS命令:

The SECTIONS command tells the linker how to map input sections into output sections, and how to place the output sections in memory.

命令格式如下:

SECTIONS

{

sections-command

sections-command

......

}

其中sections-command可以是ENTRY命令,符號賦值,輸出段描述,也可以是overlay描述。

?

(2) 地址計數器‘.’(location counter):

該符號只能用于SECTIONS命令內部,初始值為‘0’,可以對該符號進行賦值,也可以使用該符號進行計算或賦值給其他符號。它會自動根據SECTIONS命令內部所描述的輸出段的大小來計算當前的地址。

(3) 輸出段描述(output section description):

前面提到在SECTIONS命令中可以作輸出段描述,描述的格式如下:

section [address] [(type)] : [AT(lma)]

{

output-section-command

output-section-command

...

} [>region] [AT>lma_region] [:phdr :phdr ...] [=fillexp]

很多附加選項是用不到的。其中的output-section-command又可以是符號賦值,輸入段描述,要直接包含的數據值,或者某一特定的輸出段關鍵字

?

*linker script 實例

==============================

OUTPUT_ARCH(arm)

ENTRY(_start)

SECTIONS {

??? . = 0xa3f00000;

??? __boot_start = .;

??? .start ALIGN(4) : {

??????? *(.text.start)

??? }


???

setup ALIGN(4) : {

??????? setup_block = .;

??????? *(.setup)

??????? setup_block_end = .;

??? }


??? .text ALIGN(4) : {

??????? *(.text)

??? }


??? .rodata ALIGN(4) : {

??????? *(.rodata)

??? }

??? .data ALIGN(4) : {

??????? *(.data)

??? }


??? .got ALIGN(4) : {

??????? *(.got)

??? }

??? __boot_end = .;


??? .bss ALIGN(16) : {

??????? bss_start = .;

??????? *(.bss)

??????? *(COMMON)

??????? bss_end = .;

??? }


??? .comment ALIGN(16) : {

??????? *(.comment)

??? }

??? stack_point = __boot_start + 0x00100000;

??? loader_size = __boot_end - __boot_start;

??? setup_size = setup_block_end - setup_block;

}

?

=============================??

在SECTIONS命令中的類似于下面的描述結構就是輸出段描述:

.start ALIGN(4) : {

??? *(.text.start)

}

.start為output section name,ALIGN(4)返回一個基于location counter(.)的4字節對齊的地址值。*(.text.start)是輸入段描述,*為通配符,意思是把所有被鏈接的object文件中的.text.start段都鏈接進這個名為.start的輸出段。

源文件中所標識的section及其屬性實際上就是對輸入段的描述,例如.text.start輸入段在源文件start.S中的代碼如下:

.section .text.start

.global _start

_start :

??? b start


--------------------------------------------------------------------------------
[推薦閱讀]
1.ARM學習報告002--GNU tool開發ARM程序及生成映象文件機理
2.Using ld, the GNU Linker

?

5、再議論 鏈接

?

1. 概論


每一個鏈接過程都由鏈接腳本(linker script, 一般以lds作為文件的后綴名)控制. 鏈接腳本主要用于規定如何把輸入文件內的section放入輸出文件內, 并控制輸出文件內各部分在程序地址空間內的布局. 但你也可以用連接命令做一些其他事情.

連接器有個默認的內置連接腳本, 可用ld --verbose查看. 連接選項-r和-N可以影響默認的連接腳本(如何影響?).

-T選項用以指定自己的鏈接腳本, 它將代替默認的連接腳本。你也可以使用<暗含的連接腳本>以增加自定義的鏈接命令.

以下沒有特殊說明,連接器指的是靜態連接器.


2. 基本概念


鏈接器把一個或多個輸入文件合成一個輸出文件.

輸入文件: 目標文件或鏈接腳本文件.
輸出文件: 目標文件或可執行文件.

目標文件(包括可執行文件)具有固定的格式, 在UNIX或GNU/Linux平臺下, 一般為ELF格式. 若想了解更多, 可參考 UNIX/Linux平臺可執行文件格式分析

有時把輸入文件內的section稱為輸入section(input section), 把輸出文件內的section稱為輸出section(output sectin).

目標文件的每個section至少包含兩個信息: 名字和大小. 大部分section還包含與它相關聯的一塊數據, 稱為section contents(section內容). 一個section可被標記為“loadable(可加載的)”或“allocatable(可分配的)”.

loadable section: 在輸出文件運行時, 相應的section內容將被載入進程地址空間中.

allocatable section: 內容為空的section可被標記為“可分配的”. 在輸出文件運行時, 在進程地址空間中空出大小同section指定大小的部分. 某些情況下, 這塊內存必須被置零.

如果一個section不是“可加載的”或“可分配的”, 那么該section通常包含了調試信息. 可用objdump -h命令查看相關信息.

每個“可加載的”或“可分配的”輸出section通常包含兩個地址: VMA(virtual memory address虛擬內存地址或程序地址空間地址)和LMA(load memory address加載內存地址或進程地址空間地址). 通常VMA和LMA是相同的.

在目標文件中, loadable或allocatable的輸出section有兩種地址: VMA(virtual Memory Address)和LMA(Load Memory Address). VMA是執行輸出文件時section所在的地址, 而LMA是加載輸出文件時section所在的地址. 一般而言, 某section的VMA == LMA. 但在嵌入式系統中, 經常存在加載地址和執行地址不同的情況: 比如將輸出文件加載到開發板的flash中(由LMA指定), 而在運行時將位于flash中的輸出文件復制到SDRAM中(由VMA指定).

可這樣來理解VMA和LMA, 假設:
(1) .data section對應的VMA地址是0x08050000, 該section內包含了3個32位全局變量, i、j和k, 分別為1,2,3.
(2) .text section內包含由"printf( "j=%d ", j );"程序片段產生的代碼.

連接時指定.data section的VMA為0x08050000, 產生的printf指令是將地址為0x08050004處的4字節內容作為一個整數打印出來。

如果.data section的LMA為0x08050000,顯然結果是j=2
如果.data section的LMA為0x08050004,顯然結果是j=1

還可這樣理解LMA:
.text section內容的開始處包含如下兩條指令(intel i386指令是10字節,每行對應5字節):

jmp 0x08048285
movl $0x1,%eax

如果.text section的LMA為0x08048280, 那么在進程地址空間內0x08048280處為“jmp 0x08048285”指令, 0x08048285處為movl $0x1,%eax指令. 假設某指令跳轉到地址0x08048280, 顯然它的執行將導致%eax寄存器被賦值為1.

如果.text section的LMA為0x08048285, 那么在進程地址空間內0x08048285處為“jmp 0x08048285”指令, 0x0804828a處為movl $0x1,%eax指令. 假設某指令跳轉到地址0x08048285, 顯然它的執行又跳轉到進程地址空間內0x08048285處, 造成死循環.

符號(symbol): 每個目標文件都有符號表(SYMBOL TABLE), 包含已定義的符號(對應全局變量和static變量和定義的函數的名字)和未定義符號(未定義的函數的名字和引用但沒定義的符號)信息.

符號值: 每個符號對應一個地址, 即符號值(這與c程序內變量的值不一樣, 某種情況下可以把它看成變量的地址). 可用nm命令查看它們. (nm的使用方法可參考本blog的 GNU binutils筆記)


3. 腳本格式

鏈接腳本由一系列命令組成, 每個命令由一個關鍵字(一般在其后緊跟相關參數)或一條對符號的賦值語句組成. 命令由分號‘;’分隔開.

文件名或格式名內如果包含分號';'或其他分隔符, 則要用引號‘"’將名字全稱引用起來. 無法處理含引號的文件名.
/* */之間的是注釋。


4. 簡單例子


在介紹鏈接描述文件的命令之前, 先看看下述的簡單例子:

以下腳本將輸出文件的text section定位在0x10000, data section定位在0x8000000:

SECTIONS
{
. = 0x10000;
.text : { *(.text) }
. = 0x8000000;
.data : { *(.data) }
.bss : { *(.bss) }
}

解釋一下上述的例子:
. = 0x10000 : 把定位器符號置為0x10000 (若不指定, 則該符號的初始值為0).

.text : { *(.text) } : 將所有(*符號代表任意輸入文件)輸入文件的.text section合并成一個.text section, 該section的地址由定位器符號的值指定, 即0x10000.

. = 0x8000000 :把定位器符號置為0x8000000
.data : { *(.data) } : 將所有輸入文件的.text section合并成一個.data section, 該section的地址被置為0x8000000.

.bss : { *(.bss) } : 將所有輸入文件的.bss section合并成一個.bss section,該section的地址被置為0x8000000+.data section的大小.

連接器每讀完一個section描述后, 將定位器符號的值*增加*該section的大小. 注意: 此處沒有考慮對齊約束.


5. 簡單腳本命令


- 1 -

ENTRY(SYMBOL): 將符號SYMBOL的值設置成入口地址。

入口地址(entry point): 進程執行的第一條用戶空間的指令在進程地址空間的地址)

ld有多種方法設置進程入口地址, 按一下順序: (編號越前, 優先級越高)
1, ld命令行的-e選項
2, 連接腳本的ENTRY(SYMBOL)命令
3, 如果定義了start符號, 使用start符號值
4, 如果存在.text section, 使用.text section的第一字節的位置值
5, 使用值0

- 2 -
INCLUDE filename : 包含其他名為filename的鏈接腳本

相當于c程序內的的#include指令, 用以包含另一個鏈接腳本.

腳本搜索路徑由-L選項指定. INCLUDE指令可以嵌套使用, 最大深度為10. 即: 文件1內INCLUDE文件2, 文件2內INCLUDE文件3... , 文件10內INCLUDE文件11. 那么文件11內不能再出現 INCLUDE指令了.

- 3 -
INPUT(files): 將括號內的文件做為鏈接過程的輸入文件

ld首先在當前目錄下尋找該文件, 如果沒找到, 則在由-L指定的搜索路徑下搜索. file可以為 -lfile形式,就象命令行的-l選項一樣. 如果該命令出現在暗含的腳本內, 則該命令內的file在鏈接過程中的順序由該暗含的腳本在命令行內的順序決定.

- 4 -
GROUP(files) : 指定需要重復搜索符號定義的多個輸入文件

file必須是庫文件, 且file文件作為一組被ld重復掃描,直到不在有新的未定義的引用出現。

- 5 -
OUTPUT(FILENAME) : 定義輸出文件的名字

同ld的-o選項, 不過-o選項的優先級更高. 所以它可以用來定義默認的輸出文件名. 如a.out

- 6 -
SEARCH_DIR(PATH) :定義搜索路徑,

同ld的-L選項, 不過由-L指定的路徑要比它定義的優先被搜索。

- 7 -
STARTUP(filename) : 指定filename為第一個輸入文件

在鏈接過程中, 每個輸入文件是有順序的. 此命令設置文件filename為第一個輸入文件。

- 8 -
OUTPUT_FORMAT(BFDNAME) : 設置輸出文件使用的BFD格式

同ld選項-o format BFDNAME, 不過ld選項優先級更高.

- 9 -
OUTPUT_FORMAT(DEFAULT,BIG,LITTLE) : 定義三種輸出文件的格式(大小端)

若有命令行選項-EB, 則使用第2個BFD格式; 若有命令行選項-EL,則使用第3個BFD格式.否則默認選第一個BFD格式.

TARGET(BFDNAME):設置輸入文件的BFD格式

同ld選項-b BFDNAME. 若使用了TARGET命令, 但未使用OUTPUT_FORMAT命令, 則最用一個TARGET命令設置的BFD格式將被作為輸出文件的BFD格式.

另外還有一些:
ASSERT(EXP, MESSAGE):如果EXP不為真,終止連接過程

EXTERN(SYMBOL SYMBOL ...):在輸出文件中增加未定義的符號,如同連接器選項-u

FORCE_COMMON_ALLOCATION:為common symbol(通用符號)分配空間,即使用了-r連接選項也為其分配

NOCROSSREFS(SECTION SECTION ...):檢查列出的輸出section,如果發現他們之間有相互引用,則報錯。對于某些系統,特別是內存較緊張的嵌入式系統,某些section是不能同時存在內存中的,所以他們之間不能相互引用。

OUTPUT_ARCH(BFDARCH):設置輸出文件的machine architecture(體系結構),BFDARCH為被BFD庫使用的名字之一。可以用命令objdump -f查看。

可通過 man -S 1 ld查看ld的聯機幫助, 里面也包括了對這些命令的介紹.


6. 對符號的賦值

在目標文件內定義的符號可以在鏈接腳本內被賦值. (注意和C語言中賦值的不同!) 此時該符號被定義為全局的. 每個符號都對應了一個地址, 此處的賦值是更改這個符號對應的地址.

e.g. 通過下面的程序查看變量a的地址:
/* a.c */
#include <stdio.h>
int a = 100;
int main(void)
{
??? printf( "&a=0x%p ", &a );
??? return 0;
}

/* a.lds */
a = 3;

$ gcc -Wall -o a-without-lds a.c
&a = 0x8049598

$ gcc -Wall -o a-with-lds a.c a.lds
&a = 0x3

注意: 對符號的賦值只對全局變量起作用!

一些簡單的賦值語句
能使用任何c語言內的賦值操作:

SYMBOL = EXPRESSION ;
SYMBOL += EXPRESSION ;
SYMBOL -= EXPRESSION ;
SYMBOL *= EXPRESSION ;
SYMBOL /= EXPRESSION ;
SYMBOL <<= EXPRESSION ;
SYMBOL >>= EXPRESSION ;
SYMBOL &= EXPRESSION ;
SYMBOL |= EXPRESSION ;

除了第一類表達式外, 使用其他表達式需要SYMBOL被定義于某目標文件。
. 是一個特殊的符號,它是定位器,一個位置指針,指向程序地址空間內的某位置(或某section內的偏移,如果它在SECTIONS命令內的某section描述內),該符號只能在SECTIONS命令內使用。
注意:賦值語句包含4個語法元素:符號名、操作符、表達式、分號;一個也不能少。
被賦值后,符號所屬的section被設值為表達式EXPRESSION所屬的SECTION(參看11. 腳本內的表達式)
賦值語句可以出現在連接腳本的三處地方:SECTIONS命令內,SECTIONS命令內的section描述內和全局位置;如下,
floating_point = 0; /* 全局位置 */
SECTIONS
{
.text :
{
*(.text)
_etext = .; /* section描述內 */
}
_bdata = (. + 3) & ~ 4; /* SECTIONS命令內 */
.data : { *(.data) }
}

PROVIDE關鍵字
該關鍵字用于定義這類符號:在目標文件內被引用,但沒有在任何目標文件內被定義的符號。
例子:
SECTIONS
{
.text :
{
*(.text)
_etext = .;
PROVIDE(etext = .);
}
}
當目標文件內引用了etext符號,確沒有定義它時,etext符號對應的地址被定義為.text section之后的第一個字節的地址。


7. SECTIONS命令

SECTIONS命令告訴ld如何把輸入文件的sections映射到輸出文件的各個section: 如何將輸入section合為輸出section; 如何把輸出section放入程序地址空間(VMA)和進程地址空間(LMA).該命令格式如下:

SECTIONS
{
SECTIONS-COMMAND
SECTIONS-COMMAND
...
}

SECTION-COMMAND有四種:
(1) ENTRY命令
(2) 符號賦值語句
(3) 一個輸出section的描述(output section description)
(4) 一個section疊加描述(overlay description)

如果整個連接腳本內沒有SECTIONS命令, 那么ld將所有同名輸入section合成為一個輸出section內, 各輸入section的順序為它們被連接器發現的順序.

如果某輸入section沒有在SECTIONS命令中提到, 那么該section將被直接拷貝成輸出section。

輸出section描述
輸出section描述具有如下格式:

SECTION [ADDRESS] [(TYPE)] : [AT(LMA)]
{
OUTPUT-SECTION-COMMAND
OUTPUT-SECTION-COMMAND
...
} [>REGION] [AT>LMA_REGION] [:PHDR :PHDR ...] [=FILLEXP]

[ ]內的內容為可選選項, 一般不需要.
SECTION:section名字
SECTION左右的空白、圓括號、冒號是必須的,換行符和其他空格是可選的。
每個OUTPUT-SECTION-COMMAND為以下四種之一,
符號賦值語句
一個輸入section描述
直接包含的數據值
一個特殊的輸出section關鍵字

輸出section名字(SECTION):
輸出section名字必須符合輸出文件格式要求,比如:a.out格式的文件只允許存在.text、.data和.bss section名。而有的格式只允許存在數字名字,那么此時應該用引號將所有名字內的數字組合在一起;另外,還有一些格式允許任何序列的字符存在于 section名字內,此時如果名字內包含特殊字符(比如空格、逗號等),那么需要用引號將其組合在一起。

輸出section地址(ADDRESS):
ADDRESS是一個表達式,它的值用于設置VMA。如果沒有該選項且有REGION選項,那么連接器將根據REGION設置VMA;如果也沒有 REGION選項,那么連接器將根據定位符號‘.’的值設置該section的VMA,將定位符號的值調整到滿足輸出section對齊要求后的值,輸出 section的對齊要求為:該輸出section描述內用到的所有輸入section的對齊要求中最嚴格的。
例子:
.text . : { *(.text) }

.text : { *(.text) }
這兩個描述是截然不同的,第一個將.text section的VMA設置為定位符號的值,而第二個則是設置成定位符號的修調值,滿足對齊要求后的。
ADDRESS可以是一個任意表達式,比如ALIGN(0x10)這將把該section的VMA設置成定位符號的修調值,滿足16字節對齊后的。
注意:設置ADDRESS值,將更改定位符號的值。

輸入section描述:
最常見的輸出section描述命令是輸入section描述。
輸入section描述是最基本的連接腳本描述。
輸入section描述基礎:
基本語法:FILENAME([EXCLUDE_FILE (FILENAME1 FILENAME2 ...) SECTION1 SECTION2 ...)
FILENAME文件名,可以是一個特定的文件的名字,也可以是一個字符串模式。
SECTION名字,可以是一個特定的section名字,也可以是一個字符串模式
例子是最能說明問題的,
*(.text) :表示所有輸入文件的.text section
(*(EXCLUDE_FILE (*crtend.o *otherfile.o) .ctors)) :表示除crtend.o、otherfile.o文件外的所有輸入文件的.ctors section。
data.o(.data) :表示data.o文件的.data section
data.o :表示data.o文件的所有section
*(.text .data) :表示所有文件的.text section和.data section,順序是:第一個文件的.text section,第一個文件的.data section,第二個文件的.text section,第二個文件的.data section,...
*(.text) *(.data) :表示所有文件的.text section和.data section,順序是:第一個文件的.text section,第二個文件的.text section,...,最后一個文件的.text section,第一個文件的.data section,第二個文件的.data section,...,最后一個文件的.data section
下面看連接器是如何找到對應的文件的。
當FILENAME是一個特定的文件名時,連接器會查看它是否在連接命令行內出現或在INPUT命令中出現。
當FILENAME是一個字符串模式時,連接器僅僅只查看它是否在連接命令行內出現。
注意:如果連接器發現某文件在INPUT命令內出現,那么它會在-L指定的路徑內搜尋該文件。

字符串模式內可存在以下通配符:
* :表示任意多個字符
? :表示任意一個字符
[CHARS] :表示任意一個CHARS內的字符,可用-號表示范圍,如:a-z
:表示引用下一個緊跟的字符

在文件名內,通配符不匹配文件夾分隔符/,但當字符串模式僅包含通配符*時除外。
任何一個文件的任意section只能在SECTIONS命令內出現一次。看如下例子,
SECTIONS {
.data : { *(.data) }
.data1 : { data.o(.data) }
}
data.o文件的.data section在第一個OUTPUT-SECTION-COMMAND命令內被使用了,那么在第二個OUTPUT-SECTION-COMMAND命令內將不會再被使用,也就是說即使連接器不報錯,輸出文件的.data1 section的內容也是空的。
再次強調:連接器依次掃描每個OUTPUT-SECTION-COMMAND命令內的文件名,任何一個文件的任何一個section都只能使用一次。
讀者可以用-M連接命令選項來產生一個map文件,它包含了所有輸入section到輸出section的組合信息。
再看個例子,
SECTIONS {
.text : { *(.text) }
.DATA : { [A-Z]*(.data) }
.data : { *(.data) }
.bss : { *(.bss) }
}
這個例子中說明,所有文件的輸入.text section組成輸出.text section;所有以大寫字母開頭的文件的.data section組成輸出.DATA section,其他文件的.data section組成輸出.data section;所有文件的輸入.bss section組成輸出.bss section。
可以用SORT()關鍵字對滿足字符串模式的所有名字進行遞增排序,如SORT(.text*)。
通用符號(common symbol)的輸入section:
在許多目標文件格式中,通用符號并沒有占用一個section。連接器認為:輸入文件的所有通用符號在名為COMMON的section內。
例子,
.bss { *(.bss) *(COMMON) }
這個例子中將所有輸入文件的所有通用符號放入輸出.bss section內。可以看到COMMOM section的使用方法跟其他section的使用方法是一樣的。
有些目標文件格式把通用符號分成幾類。例如,在MIPS elf目標文件格式中,把通用符號分成standard common symbols(標準通用符號)和small common symbols(微通用符號,不知道這么譯對不對?),此時連接器認為所有standard common symbols在COMMON section內,而small common symbols在.scommon section內。
在一些以前的連接腳本內可以看見[COMMON],相當于*(COMMON),不建議繼續使用這種陳舊的方式。
輸入section和垃圾回收:
在連接命令行內使用了選項--gc-sections后,連接器可能將某些它認為沒用的section過濾掉,此時就有必要強制連接器保留一些特定的 section,可用KEEP()關鍵字達此目的。如KEEP(*(.text))或KEEP(SORT(*)(.text))
最后看個簡單的輸入section相關例子:
SECTIONS {
outputa 0x10000 :
{
all.o
foo.o (.input1)
}
outputb :
{
foo.o (.input2)
foo1.o (.input1)
}
outputc :
{
*(.input1)
*(.input2)
}
}
本例中,將all.o文件的所有section和foo.o文件的所有(一個文件內可以有多個同名section).input1 section依次放入輸出outputa section內,該section的VMA是0x10000;將foo.o文件的所有.input2 section和foo1.o文件的所有.input1 section依次放入輸出outputb section內,該section的VMA是當前定位器符號的修調值(對齊后);將其他文件(非all.o、foo.o、foo1.o)文件的. input1 section和.input2 section放入輸出outputc section內。

在輸出section存放數據命令:
能夠顯示地在輸出section內填入你想要填入的信息(這樣是不是可以自己通過連接腳本寫程序?當然是簡單的程序)。
BYTE(EXPRESSION) 1 字節
SHORT(EXPRESSION) 2 字節
LOGN(EXPRESSION) 4 字節
QUAD(EXPRESSION) 8 字節
SQUAD(EXPRESSION) 64位處理器的代碼時,8 字節
輸出文件的字節順序big endianness 或little endianness,可以由輸出目標文件的格式決定;如果輸出目標文件的格式不能決定字節順序,那么字節順序與第一個輸入文件的字節順序相同。
如:BYTE(1)、LANG(addr)。
注意,這些命令只能放在輸出section描述內,其他地方不行。
錯誤:SECTIONS { .text : { *(.text) } LONG(1) .data : { *(.data) } }
正確:SECTIONS { .text : { *(.text) LONG(1) } .data : { *(.data) } }
在當前輸出section內可能存在未描述的存儲區域(比如由于對齊造成的空隙),可以用FILL(EXPRESSION)命令決定這些存儲區域的內容, EXPRESSION的前兩字節有效,這兩字節在必要時可以重復被使用以填充這類存儲區域。如FILE(0x9090)。在輸出section描述中可以有=FILEEXP屬性,它的作用如同FILE()命令,但是FILE命令只作用于該FILE指令之后的section區域,而=FILEEXP屬性作用于整個輸出section區域,且FILE命令的優先級更高!!!

輸出section內命令的關鍵字:
CREATE_OBJECT_SYMBOLS :為每個輸入文件建立一個符號,符號名為輸入文件的名字。每個符號所在的section是出現該關鍵字的section。
CONSTRUCTORS :與c++內的(全局對象的)構造函數和(全局對像的)析構函數相關,下面將它們簡稱為全局構造和全局析構。
對于a.out目標文件格式,連接器用一些不尋常的方法實現c++的全局構造和全局析構。當連接器生成的目標文件格式不支持任意section名字時,比如說ECOFF、XCOFF格式,連接器將通過名字來識別全局構造和全局析構,對于這些文件格式,連接器把與全局構造和全局析構的相關信息放入出現 CONSTRUCTORS關鍵字的輸出section內。
符號__CTORS_LIST__表示全局構造信息的的開始處,__CTORS_END__表示全局構造信息的結束處。
符號__DTORS_LIST__表示全局構造信息的的開始處,__DTORS_END__表示全局構造信息的結束處。
這兩塊信息的開始處是一字長的信息,表示該塊信息有多少項數據,然后以值為零的一字長數據結束。
一般來說,GNU C++在函數__main內安排全局構造代碼的運行,而__main函數被初始化代碼(在main函數調用之前執行)調用。是不是對于某些目標文件格式才這樣???
對于支持任意section名的目標文件格式,比如COFF、ELF格式,GNU C++將全局構造和全局析構信息分別放入.ctors section和.dtors section內,然后在連接腳本內加入如下,
__CTOR_LIST__ = .;
LONG((__CTOR_END__ - __CTOR_LIST__) / 4 - 2)
*(.ctors)
LONG(0)
__CTOR_END__ = .;
__DTOR_LIST__ = .;
LONG((__DTOR_END__ - __DTOR_LIST__) / 4 - 2)
*(.dtors)
LONG(0)
__DTOR_END__ = .;
如果使用GNU C++提供的初始化優先級支持(它能控制每個全局構造函數調用的先后順序),那么請在連接腳本內把CONSTRUCTORS替換成SORT (CONSTRUCTS),把*(.ctors)換成*(SORT(.ctors)),把*(.dtors)換成*(SORT(.dtors))。一般來說,默認的連接腳本已作好的這些工作。

輸出section的丟棄:
例子,.foo { *(.foo) },如果沒有任何一個輸入文件包含.foo section,那么連接器將不會創建.foo輸出section。但是如果在這些輸出section描述內包含了非輸入section描述命令(如符號賦值語句),那么連接器將總是創建該輸出section。
有一個特殊的輸出section,名為/DISCARD/,被該section引用的任何輸入section將不會出現在輸出文件內,這就是DISCARD的意思吧。如果/DISCARD/ section被它自己引用呢?想想看。

輸出section屬性:
終于講到這里了,呵呵。
我們再回顧以下輸出section描述的文法:
SECTION [ADDRESS] [(TYPE)] : [AT(LMA)]
{
OUTPUT-SECTION-COMMAND
OUTPUT-SECTION-COMMAND
...
} [>REGION] [AT>LMA_REGION] [:PHDR :PHDR ...] [=FILLEXP]
前面我們瀏覽了SECTION、ADDRESS、OUTPUT-SECTION-COMMAND相關信息,下面我們將瀏覽其他屬性。

TYPE :每個輸出section都有一個類型,如果沒有指定TYPE類型,那么連接器根據輸出section引用的輸入section的類型設置該輸出section的類型。它可以為以下五種值,
NOLOAD :該section在程序運行時,不被載入內存。
DSECT,COPY,INFO,OVERLAY :這些類型很少被使用,為了向后兼容才被保留下來。這種類型的section必須被標記為“不可加載的”,以便在程序運行不為它們分配內存。

輸出section的LMA :默認情況下,LMA等于VMA,但可以通過關鍵字AT()指定LMA。
用關鍵字AT()指定,括號內包含表達式,表達式的值用于設置LMA。如果不用AT()關鍵字,那么可用AT>LMA_REGION表達式設置指定該section加載地址的范圍。
這個屬性主要用于構件ROM境象。
例子,
SECTIONS
{
.text 0x1000 : { *(.text) _etext = . ; }
.mdata 0x2000 :
AT ( ADDR (.text) + SIZEOF (.text) )
{ _data = . ; *(.data); _edata = . ; }
.bss 0x3000 :
{ _bstart = . ; *(.bss) *(COMMON) ; _bend = . ;}
}
程序如下,
extern char _etext, _data, _edata, _bstart, _bend;
char *src = &_etext;
char *dst = &_data;

/* ROM has data at end of text; copy it. */
while (dst < &_edata) {
*dst++ = *src++;
}

/* Zero bss */
for (dst = &_bstart; dst< &_bend; dst++)
*dst = 0;

此程序將處于ROM內的已初始化數據拷貝到該數據應在的位置(VMA地址),并將為初始化數據置零。
讀者應該認真的自己分析以上連接腳本和程序的作用。

輸出section區域:可以將輸出section放入預先定義的內存區域內,例子,
MEMORY { rom : ORIGIN = 0x1000, LENGTH = 0x1000 }
SECTIONS { ROM : { *(.text) } >rom }

輸出section所在的程序段:可以將輸出section放入預先定義的程序段(program segment)內。如果某個輸出section設置了它所在的一個或多個程序段,那么接下來定義的輸出section的默認程序段與該輸出 section的相同。除非再次顯示地指定。例子,
PHDRS { text PT_LOAD ; }
SECTIONS { .text : { *(.text) } :text }
可以通過:NONE指定連接器不把該section放入任何程序段內。詳情請查看PHDRS命令

輸出section的填充模版:這個在前面提到過,任何輸出section描述內的未指定的內存區域,連接器用該模版填充該區域。用法:=FILEEXP,前兩字節有效,當區域大于兩字節時,重復使用這兩字節以將其填滿。例子,
SECTIONS { .text : { *(.text) } =0x9090 }

覆蓋圖(overlay)描述:
覆蓋圖描述使兩個或多個不同的section占用同一塊程序地址空間。覆蓋圖管理代碼負責將section的拷入和拷出。考慮這種情況,當某存儲塊的訪問速度比其他存儲塊要快時,那么如果將section拷到該存儲塊來執行或訪問,那么速度將會有所提高,覆蓋圖描述就很適合這種情形。文法如下,
SECTIONS {
...

OVERLAY [START] : [NOCROSSREFS] [AT ( LDADDR )]
{
SECNAME1
{
OUTPUT-SECTION-COMMAND
OUTPUT-SECTION-COMMAND
...
} [:PHDR...] [=FILL]
SECNAME2
{
OUTPUT-SECTION-COMMAND
OUTPUT-SECTION-COMMAND
...
} [:PHDR...] [=FILL]
...
} [>REGION] [:PHDR...] [=FILL]

...
}
由以上文法可以看出,同一覆蓋圖內的section具有相同的VMA。SECNAME2的LMA為SECTNAME1的LMA加上SECNAME1的大小,同理計算SECNAME2,3,4...的LMA。SECNAME1的LMA由LDADDR決定,如果它沒有被指定,那么由START決定,如果它也沒有被指定,那么由當前定位符號的值決定。
NOCROSSREFS關鍵字指定各section之間不能交叉引用,否則報錯。
對于OVERLAY描述的每個section,連接器將定義兩個符號__load_start_SECNAME和__load_stop_SECNAME,這兩個符號的值分別代表SECNAME section的LMA地址的開始和結束。
連接器處理完OVERLAY描述語句后,將定位符號的值加上所有覆蓋圖內section大小的最大值。
看個例子吧,
SECTIONS{
...

OVERLAY 0x1000 : AT (0x4000)
{
.text0 { o1/*.o(.text) }
.text1 { o2/*.o(.text) }
}
...
}
.text0 section和.text1 section的VMA地址是0x1000,.text0 section加載于地址0x4000,.text1 section緊跟在其后。
程序代碼,拷貝.text1 section代碼,
extern char __load_start_text1, __load_stop_text1;
memcpy ((char *) 0x1000, &__load_start_text1,
&__load_stop_text1 - &__load_start_text1);


8. 內存區域命令
---------------

注意:以下存儲區域指的是在程序地址空間內的。
在默認情形下,連接器可以為section分配任意位置的存儲區域。你也可以用MEMORY命令定義存儲區域,并通過輸出section描述的> REGION屬性顯示地將該輸出section限定于某塊存儲區域,當存儲區域大小不能滿足要求時,連接器會報告該錯誤。
MEMORY命令的文法如下,
MEMORY {
NAME1 [(ATTR)] : ORIGIN = ORIGIN1, LENGTH = LEN2
NAME2 [(ATTR)] : ORIGIN = ORIGIN2, LENGTH = LEN2
...
}
NAME :存儲區域的名字,這個名字可以與符號名、文件名、section名重復,因為它處于一個獨立的名字空間。
ATTR :定義該存儲區域的屬性,在講述SECTIONS命令時提到,當某輸入section沒有在SECTIONS命令內引用時,連接器會把該輸入 section直接拷貝成輸出section,然后將該輸出section放入內存區域內。如果設置了內存區域設置了ATTR屬性,那么該區域只接受滿足該屬性的section(怎么判斷該section是否滿足?輸出section描述內好象沒有記錄該section的讀寫執行屬性)。ATTR屬性內可以出現以下7個字符,
R 只讀section
W 讀/寫section
X 可執行section
A ‘可分配的’section
I 初始化了的section
L 同I
! 不滿足該字符之后的任何一個屬性的section
ORIGIN :關鍵字,區域的開始地址,可簡寫成org或o
LENGTH :關鍵字,區域的大小,可簡寫成len或l

例子,
MEMORY
{
rom (rx) : ORIGIN = 0, LENGTH = 256K
ram (!rx) : org = 0x40000000, l = 4M
}
此例中,把在SECTIONS命令內*未*引用的且具有讀屬性或寫屬性的輸入section放入rom區域內,把其他未引用的輸入section放入 ram。如果某輸出section要被放入某內存區域內,而該輸出section又沒有指明ADDRESS屬性,那么連接器將該輸出section放在該區域內下一個能使用位置。


9. PHDRS命令
------------

該命令僅在產生ELF目標文件時有效。
ELF目標文件格式用program headers程序頭(程序頭內包含一個或多個segment程序段描述)來描述程序如何被載入內存。可以用objdump -p命令查看。
當在本地ELF系統運行ELF目標文件格式的程序時,系統加載器通過讀取程序頭信息以知道如何將程序加載到內存。要了解系統加載器如何解析程序頭,請參考ELF ABI文檔。
在連接腳本內不指定PHDRS命令時,連接器能夠很好的創建程序頭,但是有時需要更精確的描述程序頭,那么PAHDRS命令就派上用場了。
注意:一旦在連接腳本內使用了PHDRS命令,那么連接器**僅會**創建PHDRS命令指定的信息,所以使用時須謹慎。
PHDRS命令文法如下,
PHDRS
{
NAME TYPE [ FILEHDR ] [ PHDRS ] [ AT ( ADDRESS ) ]
[ FLAGS ( FLAGS ) ] ;
}
其中FILEHDR、PHDRS、AT、FLAGS為關鍵字。
NAME :為程序段名,此名字可以與符號名、section名、文件名重復,因為它在一個獨立的名字空間內。此名字只能在SECTIONS命令內使用。
一個程序段可以由多個‘可加載’的section組成。通過輸出section描述的屬性:PHDRS可以將輸出section加入一個程序段,: PHDRS中的PHDRS為程序段名。在一個輸出section描述內可以多次使用:PHDRS命令,也即可以將一個section加入多個程序段。
如果在一個輸出section描述內指定了:PHDRS屬性,那么其后的輸出section描述將默認使用該屬性,除非它也定義了:PHDRS屬性。顯然當多個輸出section屬于同一程序段時可簡化書寫。
在TYPE屬性后存在FILEHDR關鍵字,表示該段包含ELF文件頭信息;存在PHDRS關鍵字,表示該段包含ELF程序頭信息。
TYPE可以是以下八種形式,
PT_NULL 0
表示未被使用的程序段
PT_LOAD 1
表示該程序段在程序運行時應該被加載
PT_DYNAMIC 2
表示該程序段包含動態連接信息
PT_INTERP 3
表示該程序段內包含程序加載器的名字,在linux下常見的程序加載器是ld-linux.so.2
PT_NOTE 4
表示該程序段內包含程序的說明信息
PT_SHLIB 5
一個保留的程序頭類型,沒有在ELF ABI文檔內定義
PT_PHDR 6
表示該程序段包含程序頭信息。
EXPRESSION 表達式值
以上每個類型都對應一個數字,該表達式定義一個用戶自定的程序頭。
AT(ADDRESS)屬性定義該程序段的加載位置(LMA),該屬性將**覆蓋**該程序段內的section的AT()屬性。
默認情況下,連接器會根據該程序段包含的section的屬性(什么屬性?好象在輸出section描述內沒有看到)設置FLAGS標志,該標志用于設置程序段描述的p_flags域。
下面看一個典型的PHDRS設置,
PHDRS
{
headers PT_PHDR PHDRS ;
interp PT_INTERP ;
text PT_LOAD FILEHDR PHDRS ;
data PT_LOAD ;
dynamic PT_DYNAMIC ;
}
SECTIONS
{
. = SIZEOF_HEADERS;
.interp : { *(.interp) } :text :interp
.text : { *(.text) } :text
.rodata : { *(.rodata) } /* defaults to :text */
...
. = . + 0x1000; /* move to a new page in memory */
.data : { *(.data) } :data
.dynamic : { *(.dynamic) } :data :dynamic
...
}


10. 版本號命令
--------------

當使用ELF目標文件格式時,連接器支持帶版本號的符號。
讀者可以發現僅僅在共享庫中,符號的版本號屬性才有意義。
動態加載器使用符號的版本號為應用程序選擇共享庫內的一個函數的特定實現版本。
可以在連接腳本內直接使用版本號命令,也可以將版本號命令實現于一個特定版本號描述文件(用連接選項--version-script指定該文件)。
該命令的文法如下,
VERSION { version-script-commands }
以下內容直接拷貝于以前的文檔,
===================== 開始 ==================================
內容簡介
---------
0 前提
1 帶版本號的符號的定義
2 連接到帶版本的符號
3 GNU擴充
4 我的疑問
5 英文搜索關鍵字
6 我的參考


0. 前提

-- 只限于ELF文件格式
-- 以下討論用gcc

1. 帶版本號的符號的定義(共享庫內)

文件b.c內容如下,
int old_true()
{
return 1;
}

int new_true()
{
return 2;
}

寫連接器的版本控制腳本,本例中為b.lds,內容如下
VER1.0{
new_true;
};
VER2.0{
};

$gcc -c b.c
$gcc -shared -Wl,--version-script=b.lds -o libb.so b.o

可以在{}內填入要綁定的符號,本例中new_true符號就與VER1.0綁定了。
那么如果有一個應用程序連接到該庫的new_true符號,那么它連接的就是VER1.0版本的new_true符號

如果把b.lds更改為,
VER1.0{
};
VER2.0{
new_true;
};

然后在生成libb.so文件,在運行那個連接到VER1.0版本的new_true符號的應用程序,可以發現該應用程序不能運行了,
因為庫內沒有VER1.0版本的new_true,只有VER2.0版本的new_true。


2. 連接到帶版本的符號
寫一個簡單的應用(名為app)連接到libb.so,應用符號new_true
假設libb.so的版本控制文件為,
VER1.0{
};
VER2.0{
new_true;
};

$ nm app | grep new_true
U new_true@@VER1.0
$
用nm命令發現app連接到VER1.0版本的new_true

3. GNU的擴充
它允許在程序文件內綁定 *符號* 到 *帶版本號的別名符號*

文件b.c內容如下,
int old_true()
{
return 1;
}

int new_true()
{
return 2;
}
__asm__( ".symver old_true,true@VER1.0" );
__asm__( ".symver new_true,true@@VER2.0" );


其中,帶版本號的別名符號是true,其默認的版本號為VER2.0

供連接器用的版本控制腳本b.lds內容如下,
VER1.0{
};
VER2.0{
};

版本控制文件內必須包含版本VER1.0和版本VER2.0的定義,因為在b.c文件內有對他們的引用

****** 假定libb.so與app.c在同一目錄下 ********

以下應用程序app.c連接到該庫,
int true();
int main()
{
printf( "%d ", true );
}

$ gcc app.c libb.so
$ LD_LIBRARY_PATH=. ./app
2
$ nm app | grep true
U true@@VER2.0
$

很明顯,程序app使用的是VER2.0版本的別名符號true,如果在b.c內沒有指明別名符號true的默認版本,
那么gcc app.c libb.so將出現連接錯誤,提示true沒有定義。

也可以在程序內指定特定版本的別名符號true,程序如下,
__asm__( ".symver true,true@VER1.0" );
int true();
int main()
{
printf( "%d ", true );
}

$ gcc app.c libb.so
$ LD_LIBRARY_PATH=. ./app
1
$ nm app | grep true
U true@VER1.0
$

顯然,連接到了版本號為VER1.0的別名符號true。其中只有一個@表示,該版本不是默認的版本




我的疑問:
版本控制腳本文件中,各版本號節點之間的依賴關系


英文搜索關鍵字:
.symver
versioned symbol
version a shared library

參考:
info ld, Scripts node
===================== 結束 ==================================


11. 表達式
----------

表達式的文法與C語言的表達式文法一致,表達式的值都是整型,如果ld的運行主機和生成文件的目標機都是32位,則表達式是32位數據,否則是64位數據。
能夠在表達式內使用符號的值,設置符號的值。
下面看六項表達式相關內容,

常表達式:
_fourk_1 = 4K; /* K、M單位 */
_fourk_2 = 4096; /* 整數 */
_fourk_3 = 0x1000; /* 16 進位 */
_fourk_4 = 01000; /* 8 進位 */
1K=1024 1M=1024*1024
符號名:
沒有被引號""包圍的符號,以字母、下劃線或'.'開頭,可包含字母、下劃線、'.'和'-'。當符號名被引號包圍時,符號名可以與關鍵字相同。如,
"SECTION"=9
"with a space" = "also with a space" + 10;
定位符號'.':
只在SECTIONS命令內有效,代表一個程序地址空間內的地址。
注意:當定位符用在SECTIONS命令的輸出section描述內時,它代表的是該section的當前**偏移**,而不是程序地址空間的絕對地址。
先看個例子,
SECTIONS
{
output :
{
file1(.text)
. = . + 1000;
file2(.text)
. += 1000;
file3(.text)
} = 0x1234;
}
其中由于對定位符的賦值而產生的空隙由0x1234填充。其他的內容應該容易理解吧。
再看個例子,
SECTIONS
{
. = 0x100
.text: {
*(.text)
. = 0x200
}
. = 0x500
.data: {
*(.data)
. += 0x600
}
} .text section在程序地址空間的開始位置是0x
表達式的操作符:
與C語言一致。
優先級 結合順序 操作符
1 left ! - ~ (1)
2 left * / %
3 left + -
4 left >> <<
5 left == != > < <= >=
6 left &
7 left |
8 left &&
9 left ||
10 right ? :
11 right &= += -= *= /= (2)
(1)表示前綴符,(2)表示賦值符。
表達式的計算:
連接器延遲計算大部分表達式的值。
但是,對待與連接過程緊密相關的表達式,連接器會立即計算表達式,如果不能計算則報錯。比如,對于section的VMA地址、內存區域塊的開始地址和大小,與其相關的表達式應該立即被計算。
例子,
SECTIONS
{
.text 9+this_isnt_constant :
{ *(.text) }
}
這個例子中,9+this_isnt_constant表達式的值用于設置.text section的VMA地址,因此需要立即運算,但是由于this_isnt_constant變量的值不確定,所以此時連接器無法確立表達式的值,此時連接器會報錯。
相對值與絕對值:
在輸出section描述內的表達式,連接器取其相對值,相對與該section的開始位置的偏移
在SECTIONS命令內且非輸出section描述內的表達式,連接器取其絕對值
通過ABSOLUTE關鍵字可以將相對值轉化成絕對值,即在原來值的基礎上加上表達式所在section的VMA值。
例子,
SECTIONS
{
.data : { *(.data) _edata = ABSOLUTE(.); }
}
該例子中,_edata符號的值是.data section的末尾位置(絕對值,在程序地址空間內)。
內建函數:
ABSOLUTE(EXP) :轉換成絕對值
ADDR(SECTION) :返回某section的VMA值。
ALIGN(EXP) :返回定位符'.'的修調值,對齊后的值,(. + EXP - 1) & ~(EXP - 1)
BLOCK(EXP) :如同ALIGN(EXP),為了向前兼容。
DEFINED(SYMBOL) :如果符號SYMBOL在全局符號表內,且被定義了,那么返回1,否則返回0。例子,
SECTIONS { ...
.text : {
begin = DEFINED(begin) ? begin : . ;
...
}
...
}
LOADADDR(SECTION) :返回三SECTION的LMA
MAX(EXP1,EXP2) :返回大者
MIN(EXP1,EXP2) :返回小者
NEXT(EXP) :返回下一個能被使用的地址,該地址是EXP的倍數,類似于ALIGN(EXP)。除非使用了MEMORY命令定義了一些非連續的內存塊,否則NEXT(EXP)與ALIGH(EXP)一定相同。
SIZEOF(SECTION) :返回SECTION的大小。當SECTION沒有被分配時,即此時SECTION的大小還不能確定時,連接器會報錯。
SIZEOF_HEADERS :
sizeof_headers :返回輸出文件的文件頭大小(還是程序頭大小),用以確定第一個section的開始地址(在文件內)。???


12. 暗含的連接腳本


輸入文件可以是目標文件,也可以是連接腳本,此時的連接腳本被稱為 暗含的連接腳本
如果連接器不認識某個輸入文件,那么該文件被當作連接腳本被解析。更進一步,如果發現它的格式又不是連接腳本的格式,那么連接器報錯。
一個暗含的連接腳本不會替換默認的連接腳本,僅僅是增加新的連接而已。
一般來說,暗含的連接腳本符號分配命令,或INPUT、GROUP、VERSION命令。
在連接命令行中,每個輸入文件的順序都被固定好了,暗含的連接腳本在連接命令行內占住一個位置,這個位置決定了由該連接腳本指定的輸入文件在連接過程中的順序。
典型的暗含的連接腳本是libc.so文件,在GNU/linux內一般存在/usr/lib目錄下。


References


1, gnu ld在線手冊

2, 程序的鏈接和裝入及Linux下動態鏈接的實現

3, UNIX/Linux平臺可執行文件格式分析

4, John R. Levine.《Linkers & Loaders》

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/454154.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/454154.shtml
英文地址,請注明出處:http://en.pswp.cn/news/454154.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Cassandra 的數據存儲結構——本質是SortedMapRowKey, SortedMapColumnKey, ColumnValue

Cassandra 的數據存儲結構 Cassandra 的數據模型是基于列族&#xff08;Column Family&#xff09;的四維或五維模型。它借鑒了 Amazon 的 Dynamo 和 Googles BigTable 的數據結構和功能特點&#xff0c;采用 Memtable 和 SSTable 的方式進行存儲。在 Cassandra 寫入數據之前&a…

清華大學《操作系統》(八):置換算法

功能&#xff1a;置換算法是指當出現缺頁異常時&#xff0c;需要調入新頁面而內存已滿時&#xff0c;置換算法選擇被置換的物理頁面。 設計目標&#xff1a; 盡可能減少頁面的調入調出次數&#xff1b;把未來不再訪問或短期內不訪問的頁面調出。 頁面鎖定&#xff1a; 了解具…

python email模塊詳解_python模塊之email: 電子郵件編碼解碼 (一、解碼郵件)

python自帶的email模塊是個很有意思的東西&#xff0c;它可以對郵件編碼解碼&#xff0c;用來處理郵件非常好用。處理郵件是一個很細致的工作&#xff0c;尤其是解碼郵件&#xff0c;因為它的格式變化太多了&#xff0c;下面先看看一個郵件的源文件&#xff1a;Received: from …

爛泥:通過vsphere給esxi添加本地硬盤

公司ESXi服務器的硬盤空間不夠使用&#xff0c;現在新加了一塊硬盤在ESxi服務器上。在服務器上添加完硬盤后&#xff0c;在Vsphere上是看不到新加硬盤的。 下面我們來通過虛擬機模擬該情況&#xff0c;先添加一塊硬盤。如下圖&#xff1a; 在Esxi添加完硬盤后&#xff0c;現在通…

清華大學《操作系統》(九):進程和線程

進程 定義&#xff1a; 進程是指一個具有一定獨立功能的程序在一個數據集合上的一次動態執行的過程。 組成&#xff1a; 代碼數據狀態寄存器&#xff08;正在運行的一個程序的所有狀態信息&#xff09;&#xff1a;CPU狀態CP0、指令指針IP通用寄存器&#xff1a;AX、BX、CX…

開始Flask項目

1.新建Flask項目。2.設置調試模式。3.理解Flask項目主程序。4.使用裝飾器&#xff0c;設置路徑與函數之間的關系。5.使用Flask中render_template&#xff0c;用不同的路徑&#xff0c;返回首頁、登錄員、注冊頁。6.用視圖函數反轉得到URL&#xff0c;{{url_for(‘login’)}}&am…

gcc交叉編譯的實現

gcc支持多種不同的語言&#xff0c;也支持多種不同的CPU架構。在它的實現上&#xff0c;不同語言編譯的實現是通過conststruct lang_hooks lang_hooks LANG_HOOKS_INITIALIZER;這個結構體的不同定義來實現的。比如c語言的編譯器就通過gcc/c-lang.c指定了lang_hooks這個結構體的…

爛泥:mysql數據庫使用的基本命令

1、連接數據庫的格式 mysql -h IP -u用戶名 -p密碼; 1.1連接遠程數據庫 mysql -h 192.168.1.214 -uroot -p123456 也可寫成&#xff1a; mysql -h 192.168.1.214 -u root -p 123456 1.2連接本地數據庫 mysql -uroot -p123456 也可寫成&#xff1a; mysql -u root -p 123456 2、…

mse均方誤差計算公式_PCA的兩種解讀:方差最大與均方誤差最小的推導

這張圖片很關鍵&#xff0c;來自統計學習方法的PCA插圖又要考試了&#xff0c;推導一下方差最大化與均方差最小化&#xff0c;老師上課講了一些均方差最小化&#xff0c;推導的過程很詳細不過自己沒有記下來&#xff0c;復習的時候再推一遍加深印象。感謝 耳東陳 老師的精彩課件…

《操作系統》OS學習(十):進程控制

進程切換&#xff08;上下文切換&#xff09;&#xff1a; 定義&#xff1a;暫停當前運行進程&#xff0c;從運行狀態變成其他狀態&#xff0c;調度另一個進程從就緒狀態變成運行狀態要求&#xff1a;切換前&#xff0c;保存進程上下文&#xff1b;切換后&#xff0c;恢復進程…

日志管理

1、錯誤日志配置 錯誤日志屬于核心功能模塊的參數 worker_processes 1; error_log /data/logs/nginx/error.log error; #一般配置這一行即可 events {worker_connections 1024; }語法規則&#xff1a;error_log file level 錯誤的日志級別有[debug|info|notice|warn|err…

GCC 命令選項使用詳解

GCC 命令行詳解[轉帖] 1、gcc包含的c/c編譯器 gcc、cc、c、g gcc和cc是一樣的&#xff0c;c和g是一樣的&#xff0c;一般c程序就用gcc編譯&#xff0c;c程序就用g編譯 2、gcc的基本用法 gcc test.c這樣將編譯出一個名為a.out的程序 gcc test.c -o test這樣將編譯出一個名為t…

mvn 打包_Spark源碼打包編譯的過程

前言上篇文章介紹了下 安裝sbt環境 啟動scala項目安裝SBT環境運行Scala項目為什么要弄這個 因為我本來是想對spark源碼編譯部署spark是用scala語言編譯的spark源碼https://gitee.com/pingfanrenbiji/sparkspark提供的編譯方式編譯的前提是將所有的依賴包都下載下來而資源包管理…

審計日志功能監控

背景&#xff1a;公司的審計日志經常出現不記錄命令的情況&#xff0c;但是又無法監控到審計功能是否正常。所以我們思路是&#xff0c;每天從CMDB服務器 ssh登錄到每一臺主機。如果審計功能正常&#xff0c;則一定會在auditlog.info文件中有登錄的記錄。如果24小時內這個文件沒…

清華大學《操作系統》(十一):處理機調度

一、處理機調度概念 進程切換&#xff08;上下文切換&#xff09;&#xff1a;切換CPU的當前任務&#xff0c;從一個進程/線程到另一個&#xff0c;保存當前在PCB/TCB中的執行上下文&#xff0c;讀取下一個的上下文 CPU調度&#xff1a;從就緒隊列中挑選一個進程/線程作為CPU…

通過純css實現圖片居中的多種實現方式

html結構&#xff1a; 1 <div class"demo" style"width: 800px;height: 600px; border:1px solid #ddd"> 2 <img src"default.jpg" width"400" height"300"/> 3 </div> 實現img位于外層div的居中顯示…

GCC 命令行詳解

作者&#xff1a; www.linuxfans.org mozilla 1。gcc包含的c/c編譯器 gcc,cc,c,g,gcc和cc是一樣的&#xff0c;c和g是一樣的&#xff0c;(沒有看太明白前面這半句是什 么意思:))一般c程序就用gcc編譯&#xff0c;c程序就用g編譯 2。gcc的基本用法 gcc test.c這樣將編譯出一個…

Java網絡編程從入門到精通(5):使用InetAddress類的getHostName方法獲得域名

該方法可以得到遠程主機的域名&#xff0c;也可以得到本機名。getHostName方法的定義如下&#xff1a; publicString getHostName() 下面是三種創建InetAddress對象的方式&#xff0c;在這三種方式中&#xff0c;getHostName返回的值是不同的。 1&#xff0e;使用getLocalHost方…

猿輔導python面試_猿輔導面試經歷—個人感受

今天參加了猿輔導的二面&#xff0c;無數槽點&#xff0c;不知道是不是很多公司都是這樣&#xff0c;但是我還是忍不住要逼逼叨。6月10號&#xff0c;我向猿輔導投了簡歷&#xff0c;想做招聘邀約專員這個崗位&#xff0c;然后hr加了我的微信&#xff0c;要了一份簡歷之后通知我…

對稱加密與非對稱加密

&#xff08;一&#xff09;對稱加密&#xff08;Symmetric Cryptography&#xff09; 對稱加密是最快速、最簡單的一種加密方式&#xff0c;加密&#xff08;encryption&#xff09;與解密&#xff08;decryption&#xff09;用的是同樣的密鑰&#xff08;secret key&#xff…