GNU 的連結工具 (ld, nm, objdump, ar)

在本文中,我們將示範如何使用 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 int top;

extern void push(int x);
extern int pop();

#endif

// 檔案:StackType.c
#include <Stack.h>

int stack[STACK_SIZE];
int top = 0;

// 檔案 StackFunc.c
#include <assert.h>
#include <Stack.h>

void push(int x) {
assert(top < STACK_SIZE);
stack[top++] = x;
}

int pop() {
assert(top > 0);
return stack[—top];
}

// 檔案 StackMain.c
#include <stdio.h>
#include <Stack.h>

int main() {
int x;
push(3);
x= pop();
printf("x=%d\n",x);
return 0;
}

接著,讓我們來練習編譯與連結動作,請讀者按照範例 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
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License