在本文中,我們將示範如何使用 GNU 的連結工具,這包含目的檔格式的觀察、函式庫的建立、程式的編譯與連結等。
首先,請讀者先撰寫下列四個程式,這四個檔案實作了堆疊的定義、資料結構、函數與測試程式。
範例 1 :實作堆疊的四個 C 語言程式 (分別是 Stack.h, StackType.c, StackFunc.c, StackMain.c)
// 檔案 Stack.h #ifndef _StackType_H_ #define _StackType_H_ #define STACK_SIZE 100 extern int stack[]; extern void push(int x); #endif |
// 檔案:StackType.c #include <Stack.h> int stack[STACK_SIZE]; |
// 檔案 StackFunc.c #include <assert.h> #include <Stack.h> void push(int x) { int pop() { |
// 檔案 StackMain.c #include <stdio.h> #include <Stack.h> int main() { |
接著,讓我們來練習編譯與連結動作,請讀者按照範例 1 的步驟進行操作。
範例 1 的編譯、連結與執行結果
$ gcc -I . -c StackType.c -o StackType.o // 編譯 StackType.c 為目的檔
$ gcc -I . -c StackFunc.c -o StackFunc.o // 編譯 StackFunc.c 為目的檔
$ gcc -I . -c StackMain.c -o StackMain.o // 編譯 StackMain.c為執行檔
$ gcc StackMain.o StackFunc.o StackType.o -o stack // 連結上述三個程式
$ ./stack // 執行 stack 檔
x=3 // 結果輸出 3。
那麼,到底這些具有外部引用的 C 語言程式,會被編譯器編譯程甚麼樣子呢?讓我們用 gcc 中的 –S 參數,將這些檔案編譯程組合語言看看。您可以使用
將範例 1 的 C語言程式編譯為 IA32 組合語言
$ gcc -S StackType.c -o StackType.s -I . // 編譯 StackType.c,輸出組合語言檔StackType.s
$ gcc -S StackFunc.c -o StackFunc.s -I . // 編譯 StackFunc.c,輸出組合語言檔StackFunc.s
$ gcc -S StackMain.c -o StackMain.s -I . // 編譯 StackMain.c,輸出組合語言檔StackFunc.s
範例 2 顯示了 StackType.s, StackFunc.s, StackMain.c 等三個組合語言檔的內容。
範例 2. 範例 1 所對應的 IA32 組合語言檔
// 檔案:StackType.s
.file "StackType.c"
.globl _top
.bss
.align 4
_top:
.space 4
.comm _stack, 400 # 400
// 檔案:StackFunc.s
.file "StackFunc.c"
.section .rdata,"dr"
LC0:
.ascii "top < STACK_SIZE\0"
LC1:
.ascii "StackFunc.c\0"
.text
.globl _push
.def _push;.scl 2;.type 32;.endef
_push:
pushl %ebp
movl %esp, %ebp
subl $24, %esp
cmpl $99, _top
jle L3
movl $LC0, 8(%esp)
movl $5, 4(%esp)
movl $LC1, (%esp)
call ___assert
L3:
movl _top, %eax
movl %eax, %edx
movl 8(%ebp), %eax
movl %eax, _stack(,%edx,4)
incl _top
leave
ret
.section .rdata,"dr"
LC2:
.ascii "top > 0\0"
.text
.globl _pop
.def _pop;.scl 2;.type 32;.endef
_pop:
pushl %ebp
movl %esp, %ebp
subl $24, %esp
cmpl $0, _top
jg L6
movl $LC2, 8(%esp)
movl $10, 4(%esp)
movl $LC1, (%esp)
call ___assert
L6:
decl _top
movl _top, %eax
movl _stack(,%eax,4), %eax
leave
ret
.def ___assert;.scl 3;.type 32;.endef
// 檔案:StackMain.s
.file "StackMain.c"
.def ___main;.scl 2;.type 32;.endef
.section .rdata,"dr"
LC0:
.ascii "x=%d\12\0"
.text
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
pushl %ebp
movl %esp, %ebp
subl $24, %esp
andl $-16, %esp
movl $0, %eax
addl $15, %eax
addl $15, %eax
shrl $4, %eax
sall $4, %eax
movl %eax, -8(%ebp)
movl -8(%ebp), %eax
call __alloca
call ___main
movl $3, (%esp)
call _push
call _pop
movl %eax, -4(%ebp)
movl -4(%ebp), %eax
movl %eax, 4(%esp)
movl $LC0, (%esp)
call _printf
movl $0, %eax
leave
ret
.def _printf;.scl 3;.type 32;.endef
.def _pop;.scl 3;.type 32;.endef
.def _push;.scl 3;.type 32;.endef
對於程式 StackType.c 而言,其中的指令 int top=0; 被編譯為組合語言 .bss 段中的 _top .space 4,而 int stack[STACK_SIZE] 被編譯為 .comm _stack, 400,而 _top 被宣告為 .globl,代表全域變數 (global)。
對於程式 StackFunc.c 而言,其中的 push, pop 函數分別被編譯為標記 _push: 與 _pop,並且用.globl _push與 .globl _pop 標記為全域變數。同樣的 StackMain.c 當中的主程式 main 也被編譯為標記 _main,並標上全域變數記號 .globl,放在程式段 .text 當中。
如果我們用 nm 指令,檢視目的檔 StackType.o 與 StackFunc.o 中的連結地圖 (符號表),那麼,我們可以看到其中的分段與全域變數,下列範例顯示了使用 nm StackType.o 與 nm StackFunc.o 兩個指令,所查看到的連結地圖。
$ nm StackType.o 顯示StackType.o的連結地圖(map)
00000000 b .bss bss 段 (b:未初始化變數)
00000000 d .data data 段 (d:已初始化資料)
00000000 t .text text 段 (t:程式段)
00000190 C _stack int stack[] 的定義 (C:Common)
00000000 B _top int top=0 的定義 (B:bss)
$ nm StackFunc.o 顯示StackType.o的連結地圖(map)
00000000 b .bss bss 段 (b:未初始化變數)
00000000 d .data data 段 (d:已初始化資料)
00000000 r .rdata rdata 段 (r:唯讀資料段)
00000000 t .text text 段 (t:程式段)
U ___assert 未定義 (U:___assert)
00000044 T _pop int stack[] 的定義 (C:Common)
00000000 T _push int top=0 的定義 (B:bss)
U _stack 未定義 (U:_stack)
U _top 未定義 (U:_top)
如果要檢視目的檔中的字串表,可使用 strings 指令,下列範例就顯示了以 strings 檢視 StackType.o 檔案中字串表的結果,其中的檔案名稱、分段名稱、變數名稱都被放入的字串表當中。
$ strings StackType.o
.text
0`.data
.bss
.file
StackType.c
.text
.data
.bss
_top
_stack
如果想知道每一個區段 (Section) 的大小,可以使用 size 指令,就顯示了以 size 檢視 stack.exe 檔案中的區段大小,其中 text 段為 1140 bytes, data 段為 416 bytes, bss 段為 468 bytes, 總長為 2024 bytes。下列範例顯示了 size 指令的使用結果
範例:用 size 指令檢視目的檔中分段大小
$ size stack.exe
text data bss dec hex filename
1140 416 468 2024 7e8 stack.exe
製作靜態函式庫
要製作靜態函式庫,可以透過 ar (archive) 指令加上 –r 參數,將一群目的檔包裝為函式庫,例如,在下一個範例中,我們就用了ar -r lib/libstack.a StackFunc.o StackType.o 這樣一個指令,將StackFunc.o StackType.o這兩個目的檔包裝成 libstack.a 函式庫,放在 lib 資料夾下。然後,在連結的時候,再利用 gcc -o stack.o StackMain.c -lstack -L lib -I inc 這樣的指令,直接連結函式庫。只要懂得利用函式庫,就不需要逐個檔案進行連結了。
您也可以用 ar 指令加上 –tv 參數,檢視函式庫中到底包含了哪些目的檔,甚至可以用 ar 指令加上 –x 參數,將目的檔從函式庫當中取出,以下是 ar 的使用範例。
範例:使用 ar指令建立函式庫
$ ar -r lib/libstack.a StackFunc.o StackType.o 建立 lib/libstack.a 函式庫
ar: creating lib/libstack.a
$ gcc -o stack.o StackMain.c -lstack -L lib -I inc 編譯 StackMain.c 並連結 libstack.a 函式庫,
輸出執行檔stack.o
$ ./stack.o 執行 stack.o
x=3 結果輸出 3。
$ ar -tv lib/libstack.a 顯示函式庫 libstack.a 的內容
rw-r--r-- …略… 30 12:59 2009 StackFunc.o 包含StackFunc.o
rw-r--r-- …略… 30 12:59 2009 StackType.o
包含StackType.o
$ ar -x lib/libstack.a StackFunc.o 從lib/libstack.a中取出StackFunc.o
製作動態函式庫
要製作動態函式庫,可以直接使用 gcc加上 –shared 參數達成,例如下列指令會將 a.o, b.o, c.o 包裝成動態函式庫 libabc.so。
:
gcc -shared a.o b.o c.o -o libabc.so
但是,要安裝動態函式庫,則只要將該函式庫的路徑加入搜尋路徑中,讓動態連結器找得到即可。在 Linux 中,您可以將該路徑加入到 /etc/ld.so.conf 檔案中,然後執行 ldconfig 指令,即可讓動態連結器找到該檔案。
接著,您就可以像連結靜態函式庫一樣,在 gcc 中使用 -l<libname> 的方式,連結動態函式庫了,例如,下列程式就會將 test.c 檔與 libabc.so 函式庫,連結成 test 執行檔。
gcc -labc test.o -o test
GNU 工具的指令不只這些,其參數的用法更是繁多,在本節中,我們介紹了有關連結 (ld)、連結關係 (ldd)、函式庫 (ar) 與目的檔格式 (nm, objdump, strings) 等相關的指令,這些指令的用法請參考附錄C。
目的檔觀察工具 - objdump
GNU 的目的檔工具可分為觀察工具 (像是 objdump、nm、strings) 與修改工具 (像是 objcopy、strip) 等兩類。系統程式設計師通常會用 objdump 觀察目的檔,然後利用 objcopy 轉換目的檔。
在觀察工具中,objdump 可以用來傾印目的檔,而 nm可以觀察符號表,strings 可以觀察字串表。nm 與 strings 的用法已在前一節中示範過,在此不再重覆。此處我們將示範objdump工具的用法。
Objdump 是GNU主要的目的檔觀察工具,附錄C中有其詳細的用法,請讀者先閱讀後再回到此處繼續閱讀。
Objdump除了觀察目的檔之外,還可以用來反組譯目的檔,並輸出組合語言,例如,若我們使用指令objdump -d StackFunc.o指令對目的檔 StackFunc.o反組譯動作,其結果將如下列範例所示。
範例:使用 objdump 反組譯目的檔
指令:objdump -d StackFunc.o
StackFunc.o: file format pe-i386
Disassembly of section .text:
00000000 <_push>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 ec 18 sub $0x18,%esp
6: 83 3d 00 00 00 00 63 cmpl $0x63,0x0
d: 7e 1c jle 2b <_push+0x2b>
f: c7 44 24 08 00 00 00 movl $0x0,0x8(%esp)
16: 00
17: c7 44 24 04 05 00 00 movl $0x5,0x4(%esp)
1e: 00
1f: c7 04 24 11 00 00 00 movl $0x11,(%esp)
26: e8 00 00 00 00 call 2b <_push+0x2b>
2b: a1 00 00 00 00 mov 0x0,%eax
30: 89 c2 mov %eax,%edx
32: 8b 45 08 mov 0x8(%ebp),%eax
35: 89 04 95 00 00 00 00 mov %eax,0x0(,%edx,4)
3c: ff 05 00 00 00 00 incl 0x0
42: c9 leave
43: c3 ret
00000044 <_pop>:
44: 55 push %ebp
45: 89 e5 mov %esp,%ebp
47: 83 ec 18 sub $0x18,%esp
4a: 83 3d 00 00 00 00 00 cmpl $0x0,0x0
51: 7f 1c jg 6f <_pop+0x2b>
53: c7 44 24 08 1d 00 00 movl $0x1d,0x8(%esp)
5a: 00
5b: c7 44 24 04 0a 00 00 movl $0xa,0x4(%esp)
62: 00
63: c7 04 24 11 00 00 00 movl $0x11,(%esp)
6a: e8 00 00 00 00 call 6f <_pop+0x2b>
6f: ff 0d 00 00 00 00 decl 0x0
75: a1 00 00 00 00 mov 0x0,%eax
7a: 8b 04 85 00 00 00 00 mov 0x0(,%eax,4),%eax
81: c9 leave
82: c3 ret
83: 90 nop
當然,您也可以用 objdump 觀察目的檔的表頭與內容,您可以用 –x 參數顯示詳細的目的檔資訊,以下範例顯示了 objdump –x StackFunc.o 的執行結果。
範例:使用 objdump 觀察目的檔
指令:objdump -x StackFunc.o
StackFunc.o: file format pe-i386
StackFunc.o
architecture: i386, flags 0x00000039:
HAS_RELOC, HAS_DEBUG, HAS_SYMS, HAS_LOCALS
start address 0x00000000
…
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000084 00000000 00000000 000000b4 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 00000000 00000000 00000000 2**2
ALLOC, LOAD, DATA
2 .bss 00000000 00000000 00000000 00000000 2**2
ALLOC
3 .rdata 00000028 00000000 00000000 00000138 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
SYMBOL TABLE:
[ 0](sec -2)(fl 0x00)(ty 0)(scl 103) (nx 1) 0x00000000 StackFunc.c
File
[ 2](sec 1)(fl 0x00)(ty 20)(scl 2) (nx 1) 0x00000000 _push
AUX tagndx 0 ttlsiz 0x0 lnnos 0 next 0
[ 4](sec 1)(fl 0x00)(ty 20)(scl 2) (nx 0) 0x00000044 _pop
[ 5](sec 1)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000 .text
AUX scnlen 0x83 nreloc 14 nlnno 0
[ 7](sec 2)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000 .data
AUX scnlen 0x0 nreloc 0 nlnno 0
[ 9](sec 3)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000 .bss
AUX scnlen 0x0 nreloc 0 nlnno 0
[ 11](sec 4)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000 .rdata
AUX scnlen 0x25 nreloc 0 nlnno 0
[ 13](sec 0)(fl 0x00)(ty 0)(scl 2) (nx 0) 0x00000000 _top
[ 14](sec 0)(fl 0x00)(ty 0)(scl 2) (nx 0) 0x00000000 _stack
[ 15](sec 0)(fl 0x00)(ty 20)(scl 2) (nx 0) 0x00000000 ___assert
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
00000008 dir32 _top
00000013 dir32 .rdata
00000022 dir32 .rdata
00000027 DISP32 ___assert
0000002c dir32 _top
00000038 dir32 _stack
0000003e dir32 _top
0000004c dir32 _top
00000057 dir32 .rdata
00000066 dir32 .rdata
0000006b DISP32 ___assert
00000071 dir32 _top
00000076 dir32 _top
0000007d dir32 _stack
從上述範例的傾印結果中,我們可以看到StackFunc.o 目的檔的『SYMBOL TABLE』 段落中中含有 _push, _pop, .text, .data, .bss, .rdata, _top, _stack, ___assert 等符號,而這些符號的修正記錄在 『RELOCATION RECORDS FOR [.text]』段落中,仔細閱讀就會知道哪些記憶體位址需要修正(也就是包含修改記錄M<位址><修改值>)。
目的檔修改工具 - objcopy
工具objcopy 的用法很多樣,舉例而言,假如我們想把目的檔 code.o 中的目的碼從 ELF 中抽取出來,並且去除註解欄位後存入 code.bin 中,那就可以使用下列指令。
$ objcopy -O binary -S -R .comment -R .note code.o code.bin
如果只想將 code.o 中的取程式段 (.text) 抽取出來,存入 code.bin 中,就可以使用下列指令。
$ objcopy -O binary -j .text code.o code.bin
有時,專業的系統程式人員在使用objcopy 時,會有一些令人意想不到的用法,舉例而言,如果要將一個 JPEG 圖片檔 image.jpg,轉換成 image.o 的目的檔,以便在另一個程式內以陣列的方式存取這個影像,就可以用下列指令進行轉換。
objcopy -I binary -O elf32-i386 -B i386 image.jpg image.o
範例:使用 objcopy 把影像檔轉換為目的檔
$ objcopy --readonly-text -I binary -O elf32-i386 -B i386 ccc.jpg ccc.o
$ ls -all
total 8
drwxrwxrwx+ 2 ccc None 0 May 15 13:11 .
drwxrwxrwx+ 8 ccc None 0 May 15 13:07 ..
-rwxrwxrwx 1 ccc None 3183 Jul 3 2008 ccc.jpg
-rw-r--r-- 1 ccc None 3612 May 15 13:11 ccc.o
$ objdump -x ccc.o | grep ccc
ccc.o: file format elf32-i386
ccc.o
00000000 g .data 00000000 _binary_ccc_jpg_start
00000c6f g .data 00000000 _binary_ccc_jpg_end
00000c6f g *ABS* 00000000 _binary_ccc_jpg_size
然後,就可以在程式中透過外部變數引用的方式,直接引用這些記憶體標記,如以下範例所示。
範例:使用外部變數引用目的檔中的影像區塊
extern char *_binary_ccc_jpg_start;
extern char *_binary_ccc_jpg_end;
extern char *_binary_ccc_jpg_size;
上述範例的作法,在嵌入式系統中相當常見,因為嵌入式系統當中很可能沒有檔案系統,因此無法使用像 fopen() 這種檔案函數。此時,如果要顯示影像,就可以直接將影像放入記憶體,然後取得其記憶體區塊,直接在程式中使用。
除了 objcopy 之外,strip 工具也可以用來去除符號資訊,例如: strip a.o 就可以將 a.o 檔案的符號資訊去除。關於 objcopy、strip 等指令的詳細用法,請參考 GNU Binary Utility 的說明文件。
專案建置工具
當程式越來越多時,編譯、連結與測試的動作會變得相當繁瑣,此時就必須使用專案建置工具。GNU 的 make 是專案編譯上相當著名的典型工具,在此,我們將用 make 來學習大型專案開發所需的專案管理技巧。並且透過make觀察大型專案的開發過程,讓讀者得以學習到專業的系統程式開發流程。
專案編譯工具make 是用來編譯專案的強有力工具,使用 make 工具可以有效的整合許多程式,並且進行快速的大型專案編譯動作。像是著名的 Linux 作業系統就是以 gcc 與 make 等 GNU 工具所建構出來的。因此,學習 GNU 工具更是進入 Linux 系統程式設計的捷徑。
對於初學者而言,可能會覺得make 的語法相當怪異,然而,對於有經驗的程式設計人員而言,卻會覺得 make 專案管理工具相當方便。
在此,我們將利用上述的範例程式 (StackType.c, StackFunc.c, StackMain.c, Stack.h),示範如何使用 make 工具。要用 make 建置專案前必須先撰寫一個名稱為 Makefile 的專案檔案,內容如下:
範例:專案編譯的 Makefile 檔案。
CC = gcc
AR = ar
OBJS = StackType.o StackFunc.o
BIN = stack
RM = rm -f
INCS = -I .
LIBS = -L lib
CFLAGS = $(INCS) $(LIBS)
all: $(BIN)
clean:
${RM} *.o *.exe lib/*.a
$(BIN): $(AR)
$(CC) StackMain.c -lstack -o $(BIN) $(CFLAGS)
$(AR) : $(OBJS)
$(AR) -r lib/libstack.a $(OBJS)
StackFunc.o : StackFunc.c
$(CC) -c StackFunc.c -o StackFunc.o $(CFLAGS)
StackType.o : StackType.c
$(CC) -c StackType.c -o StackType.o $(CFLAGS)
接著,我們在 Cygwin環境下執行專案編譯的動作,其過程如下列範例所示。您可以看到當專案編譯指令make執行時,一連串的動作被觸發。
範例:在 Cygwin 中使用 make 工具的過程
$ make clean 清除上一次產生的檔案
rm -f *.o *.exe lib/*.a 使用rm清除*.o,*.exe,*.a檔
$ make 進行專案編譯
gcc -c StackType.c -o StackType.o -I inc -L lib 編譯 StackType中…
gcc -c StackFunc.c -o StackFunc.o -I inc -L lib 編譯 StackFunc中…
ar -r lib/libstack.a StackType.o StackFunc.o 建立函式庫 lib/libstack.a 中…
ar: creating lib/libstack.a 函式庫建立完成
gcc StackMain.c -lstack -o stack -I inc -L lib 編譯主程式,並連結函式庫
輸出執行檔 stack
$ ./stack 啟動執行檔 stack
x=3 輸出結果 x=3
在make 的建置過程中,第一個目標 all首先會被觸發。接著,根據all : $(BIN) 規則,其中 $(BIN) 指的是前面所定義的 BIN = stack,因此,$(BIN) 會被觸發。接著,由於 $(BIN): $(AR) 規則,於是目標 $(AR) 被觸發。根據這樣的連鎖反應規則,您可以看到如下的觸發樹與執行過程,請讀者仔細追蹤,應可理解 make 檔案的執行原理。
範例:make指令的觸發樹與執行過程
+ all
+ $(BIN)
+ $(AR)
+ $(OBJS)
+ StackType.o
StackType.o : gcc -c StackType.c -o StackType.o -I inc -L lib
+ StackFunc.o
StackFunc.o : gcc -c StackFunc.c -o StackFunc.o -I inc -L lib
$(AR) : ar -r lib/libstack.a StackType.o StackFunc.o
$(AR) : ar: creating lib/libstack.a
$(BIN) : gcc StackMain.c -lstack -o stack -I inc -L lib
Post preview:
Close preview