鍍金池/ 教程/ C/ 第四部分: 工具
第四部分: 工具
第二部分: 高級
第三部分: 系統(tǒng)
第一部分: 語言

第四部分: 工具

1. GCC

1.1 預(yù)處理

輸出預(yù)處理結(jié)果到文件。

$ gcc -E main.c -o main.i

保留文件頭注釋。

$ gcc -C -E main.c -o main.i

參數(shù) -Dname 定義宏 (源文件中不能定義該宏),-Uname 取消 GCC 設(shè)置中定義的宏。

$ tail -n 10 main.c

int main(int argc, char* argv[])
{
    #if __MY__
    printf("a");
    #else
    printf("b");
    #endif
    return EXIT_SUCCESS;
}

$ gcc -E main.c -D__MY__ | tail -n 10

int main(int argc, char* argv[])
{
    printf("a");
    return 0;
}

-Idirectory 設(shè)置頭文件(.h)的搜索路徑。

$ gcc -g -I./lib -I/usr/local/include/cbase main.c mylib.c

查看依賴文件。

$ gcc -M -I./lib main.c
$ gcc -MM -I./lib main.c # 忽略標(biāo)準(zhǔn)庫

1.2 匯編

我們可以將 C 源代碼編譯成匯編語言 (.s)。

$ gcc -S main.c
$ head -n 20 main.s
    .file "main.c"
    .section .rodata
.LC0:
    .string "Hello, World!"
    .text
.globl main
    .type main, @function
main:
    pushl %ebp
    movl %esp, %ebp
    andl $-16, %esp
    subl $16, %esp
    movl $.LC0, (%esp)
    call test
    movl $0, %eax
    leave
    ret
    .size main, .-main
    .ident "GCC: (Ubuntu 4.4.1-4ubuntu9) 4.4.1"
    .section .note.GNU-stack,"",@progbits

使用 -fverbose-asm 參數(shù)可以獲取變量注釋。如果需要指定匯編格式,可以使用 "-masm=intel"參數(shù)。

1.3 鏈接

參數(shù) -c 僅生成目標(biāo)文件 (.o),然后需要調(diào)用鏈接器 (link) 將多個目標(biāo)文件鏈接成單一可執(zhí)行文件。

$ gcc -g -c main.c mylib.c

參數(shù) -l 鏈接其他庫,比如 -lpthread 鏈接 libpthread.so?;蛑付?-static 參數(shù)進(jìn)行靜態(tài)鏈接。我們還可以直接指定鏈接庫 (.so, .a) 完整路徑。

$ gcc -g -o test main.c ./libmy.so ./libtest.a

$ ldd ./test
    linux-gate.so.1 => (0xb7860000)
    ./libmy.so (0xb785b000)
    libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7710000)
    /lib/ld-linux.so.2 (0xb7861000)

另外一種做法就是用 -L 指定庫搜索路徑。

$ gcc -g -o test -L/usr/local/lib -lgdsl main.c

$ ldd ./test
    linux-gate.so.1 => (0xb77b6000)
    libgdsl.so.1 => /usr/local/lib/libgdsl.so.1 (0xb779b000)
    libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7656000)
    /lib/ld-linux.so.2 (0xb77b7000)

1.4 動態(tài)庫

使用 "-fPIC -shared" 參數(shù)生成動態(tài)庫。

$ gcc -fPIC -c -O2 mylib.c

$ gcc -shared -o libmy.so mylib.o

$ nm libmy.so
... ...
00000348 T _init
00002010 b completed.6990
00002014 b dtor_idx.6992
... ...
0000047c T test

靜態(tài)庫則需要借助 ar 工具將多個目標(biāo)文件 (.o) 打包。

c$ gcc -c mylib.c

$ ar rs libmy.a mylib.o
ar: creating libmy.a

1.5 優(yōu)化

參數(shù) -O0 關(guān)閉優(yōu)化 (默認(rèn));-O1 (或 -O) 讓可執(zhí)行文件更小,速度更快;-O2 采用幾乎所有的優(yōu)化手段。

$ gcc -O2 -o test main.c mylib.c

1.6 調(diào)試

參數(shù) -g 在對象文件 (.o) 和執(zhí)行文件中生成符號表和源代碼行號信息,以便使用 gdb 等工具進(jìn)行調(diào)試。

$ gcc -g -o test main.c mylib.c

$ readelf -S test
There are 38 section headers, starting at offset 0x18a8:

Section Headers:
    [Nr] Name Type Addr Off Size ES Flg Lk Inf Al
    ... ...
    [27] .debug_aranges PROGBITS 00000000 001060 000060 00 0 0 8
    [28] .debug_pubnames PROGBITS 00000000 0010c0 00005b 00 0 0 1
    [29] .debug_info PROGBITS 00000000 00111b 000272 00 0 0 1
    [30] .debug_abbrev PROGBITS 00000000 00138d 00014b 00 0 0 1
    [31] .debug_line PROGBITS 00000000 0014d8 0000f1 00 0 0 1
    [32] .debug_frame PROGBITS 00000000 0015cc 000058 00 0 0 4
    [33] .debug_str PROGBITS 00000000 001624 0000d5 01 MS 0 0 1
    [34] .debug_loc PROGBITS 00000000 0016f9 000058 00 0 0 1
    ... ...

參數(shù) -pg 會在程序中添加性能分析 (profiling) 函數(shù),用于統(tǒng)計程序中最耗費(fèi)時間的函數(shù)。程序執(zhí)行后,統(tǒng)計信息保存在 gmon.out 文件中,可以用 gprof 命令查看結(jié)果。

$ gcc -g -pg main.c mylib.c

2. GDB

作為內(nèi)置和最常用的調(diào)試器,GDB 顯然有著無可辯駁的地位。熟練使用 GDB,就好像所有 Linux 下的開發(fā)人員建議你用 VIM 一樣,是個很 "奇怪" 的情節(jié)。

測試用源代碼。

#include <stdio.h>
int test(int a, int b)
{
    int c = a + b;
    return c;
}

int main(int argc, char* argv[])
{
    int a = 0x1000;
    int b = 0x2000;
    int c = test(a, b);
    printf("%d\n", c);

    printf("Hello, World!\n");
    return 0;
}

編譯命令 (注意使用 -g 參數(shù)生成調(diào)試符號):

$ gcc -g -o hello hello.c

開始調(diào)試:

$ gdb hello

GNU gdb 6.8-debian
Copyright (C) 2008 Free Software Foundation, Inc.
This GDB was configured as "i486-linux-gnu"...

(gdb)

2.1 源碼

在調(diào)試過程中查看源代碼是必須的。list (縮寫 l) 支持多種方式查看源碼。

(gdb) l # 顯示源代碼

2
3 int test(int a, int b)
4 {
5       int c = a + b;
6       return c;
7 }
8
9 int main(int argc, char* argv[])
10 {
11      int a = 0x1000;

(gdb) l # 繼續(xù)顯示

12      int b = 0x2000;
13      int c = test(a, b);
14      printf("%d\n", c);
15
16      printf("Hello, World!\n");
17      return 0;
18 }

(gdb) l 3, 10 # 顯示特定范圍的源代碼

3 int test(int a, int b)
4 {
5       int c = a + b;
6       return c;
7 }
8
9 int main(int argc, char* argv[])
10 {

(gdb) l main # 顯示特定函數(shù)源代碼
5       int c = a + b;
6       return c;
7 }
8
9 int main(int argc, char* argv[])
10 {
11      int a = 0x1000;
12      int b = 0x2000;
13      int c = test(a, b);
14      printf("%d\n", c);

可以用如下命令修改源代碼顯示行數(shù)。

(gdb) set listsize 50

2.2 斷點(diǎn)

可以使用函數(shù)名或者源代碼行號設(shè)置斷點(diǎn)。

(gdb) b main # 設(shè)置函數(shù)斷點(diǎn)
Breakpoint 1 at 0x804841b: file hello.c, line 11.

(gdb) b 13 # 設(shè)置源代碼行斷點(diǎn)
Breakpoint 2 at 0x8048429: file hello.c, line 13.

(gdb) b # 將下一行設(shè)置為斷點(diǎn) (循環(huán)、遞歸等調(diào)試很有用)
Breakpoint 5 at 0x8048422: file hello.c, line 12.

(gdb) tbreak main # 設(shè)置臨時斷點(diǎn) (中斷后失效)
Breakpoint 1 at 0x804841b: file hello.c, line 11.

(gdb) info breakpoints # 查看所有斷點(diǎn)

Num Type Disp Enb Address What
2 breakpoint keep y 0x0804841b in main at hello.c:11
3 breakpoint keep y 0x080483fa in test at hello.c:5

(gdb) d 3 # delete: 刪除斷點(diǎn) (還可以用范圍 "d 1-3",無參數(shù)時刪除全部斷點(diǎn))
(gdb) disable 2 # 禁用斷點(diǎn) (還可以用范圍 "disable 1-3")
(gdb) enable 2 # 啟用斷點(diǎn) (還可以用范圍 "enable 1-3")
(gdb) ignore 2 1 # 忽略 2 號中斷 1 次

當(dāng)然少不了條件式中斷。

(gdb) b test if a == 10
Breakpoint 4 at 0x80483fa: file hello.c, line 5.

(gdb) info breakpoints

Num Type Disp Enb Address What
4   breakpoint keep y 0x080483fa in test at hello.c:5
    stop only if a == 10

可以用 condition 修改條件,注意表達(dá)式不包含 if。

(gdb) condition 4 a == 30

(gdb) info breakpoints

Num Type Disp Enb Address What
2   breakpoint keep y 0x0804841b in main at hello.c:11
    ignore next 1 hits
4   breakpoint keep y 0x080483fa in test at hello.c:5
    stop only if a == 30

2.3 執(zhí)行

通常情況下,我們會先設(shè)置 main 入口斷點(diǎn)。

(gdb) b main
Breakpoint 1 at 0x804841b: file hello.c, line 11.

(gdb) r # 開始執(zhí)行 (Run)
Starting program: /home/yuhen/Learn.c/hello
Breakpoint 1, main () at hello.c:11
11 int a = 0x1000;

(gdb) n # 單步執(zhí)行 (不跟蹤到函數(shù)內(nèi)部, Step Over)
12 int b = 0x2000;

(gdb) n
13 int c = test(a, b);

(gdb) s # 單步執(zhí)行 (跟蹤到函數(shù)內(nèi)部, Step In)
test (a=4096, b=8192) at hello.c:5
5 int c = a + b;

(gdb) finish # 繼續(xù)執(zhí)行直到當(dāng)前函數(shù)結(jié)束 (Step Out)

Run till exit from #0 test (a=4096, b=8192) at hello.c:5
0x0804843b in main () at hello.c:13
13 int c = test(a, b);
Value returned is $1 = 12288

(gdb) c # Continue: 繼續(xù)執(zhí)行,直到下一個斷點(diǎn)。

Continuing.
12288
Hello, World!

Program exited normally.

2.4 堆棧

查看調(diào)用堆棧無疑是調(diào)試過程中非常重要的事情。

(gdb) where # 查看調(diào)用堆棧 (相同作用的命令還有 info s 和 bt)

#0 test (a=4096, b=8192) at hello.c:5
#1 0x0804843b in main () at hello.c:13

(gdb) frame # 查看當(dāng)前堆棧幀,還可顯示當(dāng)前代碼

#0 test (a=4096, b=8192) at hello.c:5
5 int c = a + b;

(gdb) info frame # 獲取當(dāng)前堆棧幀更詳細(xì)的信息

Stack level 0, frame at 0xbfad3290:
    eip = 0x80483fa in test (hello.c:5); saved eip 0x804843b
    called by frame at 0xbfad32c0
    source language c.
    Arglist at 0xbfad3288, args: a=4096, b=8192
    Locals at 0xbfad3288, Previous frame's sp is 0xbfad3290
    Saved registers:
    ebp at 0xbfad3288, eip at 0xbfad328c

可以用 frame 修改當(dāng)前堆棧幀,然后查看其詳細(xì)信息。

(gdb) frame 1
#1 0x0804843b in main () at hello.c:13
13 int c = test(a, b);

(gdb) info frame

Stack level 1, frame at 0xbfad32c0:
    eip = 0x804843b in main (hello.c:13); saved eip 0xb7e59775
    caller of frame at 0xbfad3290
    source language c.
    Arglist at 0xbfad32b8, args:
    Locals at 0xbfad32b8, Previous frame's sp at 0xbfad32b4
    Saved registers:
        ebp at 0xbfad32b8, eip at 0xbfad32bc

2.5 變量和參數(shù)

(gdb) info locals # 顯示局部變量
c = 0

(gdb) info args # 顯示函數(shù)參數(shù)(自變量)
a = 4096
b = 8192

我們同樣可以切換 frame,然后查看不同堆棧幀的信息。

(gdb) p a # print 命令可顯示局部變量和參數(shù)值
$2 = 4096

(gdb) p/x a # 十六進(jìn)制輸出
$10 = 0x1000

(gdb) p a + b # 還可以進(jìn)行表達(dá)式計算
$5 = 12288

x 命令內(nèi)存輸出格式:

  • d: 十進(jìn)制
  • u: 十進(jìn)制無符號
  • x: 十六進(jìn)制
  • o: 八進(jìn)制
  • t: 二進(jìn)制
  • c: 字符
  • set variable 可用來修改變量值。

(gdb) set variable a=100

(gdb) info args
a = 100
b = 8192

2.6 內(nèi)存及寄存器

x 命令可以顯示指定地址的內(nèi)存數(shù)據(jù)。

格式: x/nfu [address]
  • n: 顯示內(nèi)存單位 (組或者行)。
  • f: 格式 (除了 print 格式外,還有 字符串 s 和 匯編 i)。
  • u: 內(nèi)存單位 (b: 1字節(jié); h: 2字節(jié); w: 4字節(jié); g: 8字節(jié))。
(gdb) x/8w 0x0804843b # 按四字節(jié)(w)顯示 8 組內(nèi)存數(shù)據(jù)

0x804843b <main+49>: 0x8bf04589 0x4489f045 0x04c70424 0x04853024
0x804844b <main+65>: 0xfecbe808 0x04c7ffff 0x04853424 0xfecfe808

(gdb) x/8i 0x0804843b # 顯示 8 行匯編指令

0x804843b <main+49>: mov DWORD PTR [ebp-0x10],eax
0x804843e <main+52>: mov eax,DWORD PTR [ebp-0x10]
0x8048441 <main+55>: mov DWORD PTR [esp+0x4],eax
0x8048445 <main+59>: mov DWORD PTR [esp],0x8048530
0x804844c <main+66>: call 0x804831c <printf@plt>
0x8048451 <main+71>: mov DWORD PTR [esp],0x8048534
0x8048458 <main+78>: call 0x804832c <puts@plt>
0x804845d <main+83>: mov eax,0x0

(gdb) x/s 0x08048530 # 顯示字符串

0x8048530: "%d\n"

除了通過 "info frame" 查看寄存器值外,還可以用如下指令。

(gdb) info registers # 顯示所有寄存器數(shù)據(jù)

eax 0x1000 4096
ecx 0xbfad32d0 -1079168304
edx 0x1 1
ebx 0xb7fa1ff4 -1208344588
esp 0xbfad3278 0xbfad3278
ebp 0xbfad3288 0xbfad3288
esi 0x8048480 134513792
edi 0x8048340 134513472
eip 0x80483fa 0x80483fa <test+6>
eflags 0x286 [ PF SF IF ]
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51

(gdb) p $eax # 顯示單個寄存器數(shù)據(jù)
$11 = 4096

2.7 反匯編

我對 AT&T 匯編不是很熟悉,還是設(shè)置成 intel 格式的好。

(gdb) set disassembly-flavor intel # 設(shè)置反匯編格式
(gdb) disass main # 反匯編函數(shù)

Dump of assembler code for function main:
0x0804840a <main+0>: lea ecx,[esp+0x4]
0x0804840e <main+4>: and esp,0xfffffff0
0x08048411 <main+7>: push DWORD PTR [ecx-0x4]
0x08048414 <main+10>: push ebp
0x08048415 <main+11>: mov ebp,esp
0x08048417 <main+13>: push ecx
0x08048418 <main+14>: sub esp,0x24
0x0804841b <main+17>: mov DWORD PTR [ebp-0x8],0x1000
0x08048422 <main+24>: mov DWORD PTR [ebp-0xc],0x2000
0x08048429 <main+31>: mov eax,DWORD PTR [ebp-0xc]
0x0804842c <main+34>: mov DWORD PTR [esp+0x4],eax
0x08048430 <main+38>: mov eax,DWORD PTR [ebp-0x8]
0x08048433 <main+41>: mov DWORD PTR [esp],eax
0x08048436 <main+44>: call 0x80483f4 <test>
0x0804843b <main+49>: mov DWORD PTR [ebp-0x10],eax
0x0804843e <main+52>: mov eax,DWORD PTR [ebp-0x10]
0x08048441 <main+55>: mov DWORD PTR [esp+0x4],eax
0x08048445 <main+59>: mov DWORD PTR [esp],0x8048530
0x0804844c <main+66>: call 0x804831c <printf@plt>
0x08048451 <main+71>: mov DWORD PTR [esp],0x8048534
0x08048458 <main+78>: call 0x804832c <puts@plt>
0x0804845d <main+83>: mov eax,0x0
0x08048462 <main+88>: add esp,0x24
0x08048465 <main+91>: pop ecx
0x08048466 <main+92>: pop ebp
0x08048467 <main+93>: lea esp,[ecx-0x4]
0x0804846a <main+96>: ret
End of assembler dump.

可以用 "b *address" 設(shè)置匯編斷點(diǎn),然后用 si 和 ni 進(jìn)行匯編級單步執(zhí)行,這對于分析指針和尋址非常有用。

2.8 進(jìn)程

查看進(jìn)程相關(guān)信息,尤其是 maps 內(nèi)存數(shù)據(jù)是非常有用的。

(gdb) help info proc stat

Show /proc process information about any running process.
Specify any process id, or use the program being debugged by default.
Specify any of the following keywords for detailed info:
    mappings -- list of mapped memory regions.
    stat -- list a bunch of random process info.
    status -- list a different bunch of random process info.
    all -- list all available /proc info.

(gdb) info proc mappings !# 相當(dāng)于 cat /proc/{pid}/maps

process 22561
cmdline = '/home/yuhen/Learn.c/hello'
cwd = '/home/yuhen/Learn.c'
exe = '/home/yuhen/Learn.c/hello'
Mapped address spaces:

    Start Addr End Addr Size Offset objfile
    0x8048000 0x8049000 0x1000 0 /home/yuhen/Learn.c/hello
    0x8049000 0x804a000 0x1000 0 /home/yuhen/Learn.c/hello
    0x804a000 0x804b000 0x1000 0x1000 /home/yuhen/Learn.c/hello
    0x8a33000 0x8a54000 0x21000 0x8a33000 [heap]
    0xb7565000 0xb7f67000 0xa02000 0xb7565000
    0xb7f67000 0xb80c3000 0x15c000 0 /lib/tls/i686/cmov/libc-2.9.so
    0xb80c3000 0xb80c4000 0x1000 0x15c000 /lib/tls/i686/cmov/libc-2.9.so
    0xb80c4000 0xb80c6000 0x2000 0x15c000 /lib/tls/i686/cmov/libc-2.9.so
    0xb80c6000 0xb80c7000 0x1000 0x15e000 /lib/tls/i686/cmov/libc-2.9.so
    0xb80c7000 0xb80ca000 0x3000 0xb80c7000
    0xb80d7000 0xb80d9000 0x2000 0xb80d7000
    0xb80d9000 0xb80da000 0x1000 0xb80d9000 [vdso]
    0xb80da000 0xb80f6000 0x1c000 0 /lib/ld-2.9.so
    0xb80f6000 0xb80f7000 0x1000 0x1b000 /lib/ld-2.9.so
    0xb80f7000 0xb80f8000 0x1000 0x1c000 /lib/ld-2.9.so
    0xbfee2000 0xbfef7000 0x15000 0xbffeb000 [stack]

2.9 線程

可以在 pthread_create 處設(shè)置斷點(diǎn),當(dāng)線程創(chuàng)建時會生成提示信息。

(gdb) c

Continuing.
[New Thread 0xb7e78b70 (LWP 2933)]

(gdb) info threads # 查看所有線程列表

* 2 Thread 0xb7e78b70 (LWP 2933) test (arg=0x804b008) at main.c:24
1 Thread 0xb7e796c0 (LWP 2932) 0xb7fe2430 in __kernel_vsyscall ()

(gdb) where # 顯示當(dāng)前線程調(diào)用堆棧

#0 test (arg=0x804b008) at main.c:24
#1 0xb7fc580e in start_thread (arg=0xb7e78b70) at pthread_create.c:300
#2 0xb7f478de in clone () at ../sysdeps/unix/sysv/linux/i386/clone.S:130

(gdb) thread 1 # 切換線程

[Switching to thread 1 (Thread 0xb7e796c0 (LWP 2932))]#0 0xb7fe2430 in __kernel_vsyscall ()

(gdb) where # 查看切換后線程調(diào)用堆棧

#0 0xb7fe2430 in __kernel_vsyscall ()
#1 0xb7fc694d in pthread_join (threadid=3085405040, thread_return=0xbffff744) at pthread_join.c:89
#2 0x08048828 in main (argc=1, argv=0xbffff804) at main.c:36

2.10 其他

調(diào)試子進(jìn)程。

(gdb) set follow-fork-mode child

臨時進(jìn)入 Shell 執(zhí)行命令,Exit 返回。

(gdb) shell

調(diào)試時直接調(diào)用函數(shù)。

(gdb) call test("abc")

使用 "--tui" 參數(shù),可以在終端窗口上部顯示一個源代碼查看窗。

$ gdb --tui hello

查看命令幫助。

(gdb) help b

最后就是退出命令。

(gdb) q

和 Linux Base Shell 習(xí)慣一樣,對于記不住的命令,可以在輸入前幾個字母后按 Tab 補(bǔ)全。

2.11 Core Dump

在 Windows 下我們已經(jīng)習(xí)慣了用 Windbg 之類的工具調(diào)試 dump 文件,從而分析并排除程序運(yùn)行時錯誤。在 Linux 下我們同樣可以完成類似的工作 —— Core Dump。

我們先看看相關(guān)的設(shè)置。

$ ulimit -a

core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 20
file size (blocks, -f) unlimited
pending signals (-i) 16382
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) unlimited
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited

"core file size (blocks, -c) 0" 意味著在程序崩潰時不會生成 core dump 文件,我們需要修改一下設(shè)置。如果你和我一樣懶得修改配置文件,那么就輸入下面這樣命令吧。

$ sudo sh -c "ulimit -c unlimited; ./test" # test 是可執(zhí)行文件名。

等等…… 我們還是先準(zhǔn)備個測試目標(biāo)。

#include <stdio.h>
#include <stdlib.h>
void test()
{
    char* s = "abc";
    *s = 'x';
}

int main(int argc, char** argv)
{
    test();
    return (EXIT_SUCCESS);
}

很顯然,我們在 test 里面寫了一個不該寫的東東,這無疑會很嚴(yán)重。生成可執(zhí)行文件后,執(zhí)行上面的命令。

$ sudo sh -c "ulimit -c unlimited; ./test"

Segmentation fault (core dumped)

$ ls -l

total 96
-rw------- 1 root root 167936 2010-01-06 13:30 core
-rwxr-xr-x 1 yuhen yuhen 9166 2010-01-06 13:16 test

這個 core 文件就是被系統(tǒng) dump 出來的,我們分析目標(biāo)就是它了。

$ sudo gdb test core

GNU gdb (GDB) 7.0-ubuntu
Copyright (C) 2009 Free Software Foundation, Inc.

Reading symbols from .../dist/Debug/test...done.

warning: Can't read pathname for load map: Input/output error.
Reading symbols from /lib/tls/i686/cmov/libpthread.so.0... ...done.
(no debugging symbols found)...done.
Loaded symbols for /lib/tls/i686/cmov/libpthread.so.0
Reading symbols from /lib/tls/i686/cmov/libc.so.6... ...done.
(no debugging symbols found)...done.
Loaded symbols for /lib/tls/i686/cmov/libc.so.6
Reading symbols from /lib/ld-linux.so.2... ...done.
(no debugging symbols found)...done.
Loaded symbols for /lib/ld-linux.so.2

Core was generated by `./test'.
Program terminated with signal 11, Segmentation fault.
#0 0x080483f4 in test () at main.c:16

warning: Source file is more recent than executable.
16 *s = 'x';

最后這幾行提示已經(jīng)告訴我們錯誤的原因和代碼位置,接下來如何調(diào)試就是 gdb 的技巧了,可以先輸入 where 看看調(diào)用堆棧。

(gdb) where

#0 0x080483f4 in test () at main.c:16
#1 0x08048401 in main (argc=1, argv=0xbfd53e44) at main.c:22

(gdb) p s
$1 = 0x80484d0 "abc"

(gdb) info files

Symbols from ".../dist/Debug/test".
Local core dump file:

Local exec file:
    `.../dist/Debug/test', file type elf32-i386.
    Entry point: 0x8048330
    0x08048134 - 0x08048147 is .interp
    ... ...
    0x08048330 - 0x080484ac is .text
    0x080484ac - 0x080484c8 is .fini
    0x080484c8 - 0x080484d4 is .rodata

很顯然 abc 屬于 .rodata,嚴(yán)禁調(diào)戲。

附:如果你調(diào)試的是 Release (-O2) 版本,而且刪除(strip)了符號表,那還是老老實(shí)實(shí)數(shù)匯編代碼吧。可見用 Debug 版本試運(yùn)行是很重要滴?。?!

3. VIM

Unix-like 環(huán)境下最常用的編輯器,應(yīng)該掌握最基本的快捷鍵操作。

在 OSX 下可以用 macvim 代替,畢竟圖形化界面要更方便一點(diǎn)。

全局配置文件:/etc/vim/vimrc 用戶配置文件:~/.vimrc

" 顯示行號
set nu

" 高亮當(dāng)前行
set cursorline

" 用空格代替Tab
set expandtab

" 自動縮進(jìn)
set autoindent
set smartindent
set smarttab
set cindent

" 縮進(jìn)寬度
set tabstop=4
set shiftwidth=4

" 語法高亮
syntax on

" 禁止在 Makefile 中將 Tab 轉(zhuǎn)換成空格
autocmd FileType make set noexpandtab

http://wiki.jikexueyuan.com/project/c-study-notes/images/9.png" alt="" /> http://wiki.jikexueyuan.com/project/c-study-notes/images/10.png" alt="" /> http://wiki.jikexueyuan.com/project/c-study-notes/images/11.png" alt="" /> http://wiki.jikexueyuan.com/project/c-study-notes/images/12.png" alt="" /> http://wiki.jikexueyuan.com/project/c-study-notes/images/13.png" alt="" />

4. Make

一個完整的 Makefile 通常由 "顯式規(guī)則"、"隱式規(guī)則"、"變量定義"、"指示符"、"注釋" 五部分組成。

  • 顯式規(guī)則: 描述了在何種情況下如何更新一個或多個目標(biāo)文件。
  • 隱式規(guī)則: make 默認(rèn)創(chuàng)建目標(biāo)文件的規(guī)則。(可重寫)
  • 變量定義: 類似 shell 變量或 C 宏,用一個簡短名稱代表一段文本。
  • 指示符: 包括包含(include)、條件執(zhí)行、宏定義(多行變量)等內(nèi)容。
  • 注釋: 字符 "#" 后的內(nèi)容被當(dāng)作注釋。

(1) 在工作目錄按 "GNUmakefile、makefile、Makefile (推薦)" 順序查找執(zhí)行,或 -f 指定。 (2) 如果不在 make 命令行顯式指定目標(biāo)規(guī)則名,則默認(rèn)使用第一個有效規(guī)則。 (3) Makefile 中 $、# 有特殊含義,可以進(jìn)行轉(zhuǎn)義 "#"、"$$"。 (4) 可以使用 \ 換行 (注釋行也可以使用),但其后不能有空格,新行同樣必須以 Tab 開頭和縮進(jìn)。

注意: 本文中提到的目標(biāo)文件通常是 ".o",類似的還有源文件 (.c)、頭文件 (.h) 等。

4.1 規(guī)則

規(guī)則組成方式:

target...: prerequisites...
    command
    ...
  • target: 目標(biāo)。
  • prerequisites: 依賴列表。文件名列表 (空格分隔,通常是 ".o, .c, .h",可使用通配符)。
  • command: 命令行。shell 命令或程序,且必須以 TAB 開頭 (最容易犯的錯誤)。

沒有命令行的規(guī)則只能指示依賴關(guān)系,沒有依賴項(xiàng)的規(guī)則指示 "如何" 構(gòu)建目標(biāo),而非 "何時" 構(gòu)建。

目標(biāo)的依賴列表可以通過 GCC -MM 參數(shù)獲得。

規(guī)則處理方式:

  • 目標(biāo)文件不存在,使用其規(guī)則 (顯式或隱式規(guī)則) 創(chuàng)建。
  • 目標(biāo)文件存在,但如果任何一個依賴文件比目標(biāo)文件修改時間 "新",則重新創(chuàng)建目標(biāo)文件。
  • 目標(biāo)文件存在,且比所有依賴文件 "新",則什么都不做。

4.1.1 隱式規(guī)則

當(dāng)我們不編寫顯式規(guī)則時,隱式規(guī)則就會生效。當(dāng)然我們可以修改隱式規(guī)則的命令。

%.o: %.c
    $(CC) $(CFLAGS) -o $@ -c $<

未定義規(guī)則或者不包含命令的規(guī)則都會使用隱式規(guī)則。

# 隱式規(guī)則
%.o: %.c
    @echo $<
    @echo $^
    $(CC) $(CFLAGS) -o $@ -c $<

all: test.o main.o
    $(CC) $(CFLAGS) $(LDFLAGS) -o $(OUT) $^

main.o: test.o test.h

輸出:

$ make

./lib/test.c
./lib/test.c
gcc -Wall -g -std=c99 -I./lib -I./src -o test.o -c ./lib/test.c

./src/main.c
./src/main.c test.o ./lib/test.h
gcc -Wall -g -std=c99 -I./lib -I./src -o main.o -c ./src/main.c

gcc -Wall -g -std=c99 -I./lib -I./src -lpthread -o test test.o main.o

test.o 規(guī)則不存在,使用隱式規(guī)則。main.o 沒有命令,使用隱式規(guī)則的同時,還會合并依賴列表。

可以有多個隱式規(guī)則,比如:

%.o: %.c
    ...
%o: %c %h
    ...

4.1.2 模式規(guī)則

在隱式規(guī)則前添加特定的目標(biāo),就形成了模式規(guī)則。

test.o main.o: %.o: %.c
    $(CC) $(CFLAGS) -o $@ -c $<

4.1.3 搜索路徑

在實(shí)際項(xiàng)目中我們通常將源碼文件分散在多個目錄中,將這些路徑寫入 Makefile 會很麻煩,此時可以考慮用 VPATH 變量指定搜索路徑。

all: lib/test.o src/main.o
    $(CC) $(CFLAGS) $(LDFLAGS) -o $(OUT) $^

改寫成 VPATH 方式后,要調(diào)整項(xiàng)目目錄就簡單多了。

# 依賴目標(biāo)搜索路徑
VPATH = ./src:./lib

# 隱式規(guī)則
%.o:%.c
    -@echo "source file: $<"
    $(CC) $(CFLAGS) -o $@ -c $<

all:test.o main.o
    $(CC) $(CFLAGS) $(LDFLAGS) -o $(OUT) $^

執(zhí)行:

$ make

source file: ./lib/test.c
gcc -Wall -g -std=c99 -I./lib -I./src -o test.o -c ./lib/test.c

source file: ./src/main.c
gcc -Wall -g -std=c99 -I./lib -I./src -o main.o -c ./src/main.c

gcc -Wall -g -std=c99 -I./lib -I./src -lpthread -o test test.o main.o

還可使用 make 關(guān)鍵字 vpath。比 VPATH 變量更靈活,甚至可以單獨(dú)為某個文件定義路徑。

vpath %.c ./src:./lib # 定義匹配模式(%匹配任意個字符)和搜索路徑。
vpath %.c # 取消該模式
vpath # 取消所有模式

相同的匹配模式可以定義多次,make 會按照定義順序搜索這多個定義的路徑。

vpath %.c ./src
vpath %.c ./lib
vpath %.h ./lib

VPATH 和 vpath 定義的搜索路徑僅對 makefile 規(guī)則有效,對 gcc/g++ 命令行無效,比如不能用它定義命令行頭文件搜索路徑參數(shù)。

4.1.4 偽目標(biāo)

當(dāng)我們?yōu)榱藞?zhí)行命令而非創(chuàng)建目標(biāo)文件時,就會使用偽目標(biāo)了,比如 clean。偽目標(biāo)總是被執(zhí)行。

clean:
    -rm *.o
.PHONY: clean

使用 "-" 前綴可以忽略命令錯誤,".PHONY" 的作用是避免和當(dāng)前目錄下的文件名沖突 (可能引發(fā)隱式規(guī)則)。

4.2 命令

每條命令都在一個獨(dú)立 shell 環(huán)境中執(zhí)行,如希望在同一 shell 執(zhí)行,可以用 ";" 將命令寫在一行。

test:
    cd test; cp test test.bak

提示: 可以用 "\" 換行,如此更美觀一些。

默認(rèn)情況下,多行命令會順序執(zhí)行。但如果命令出錯,默認(rèn)會終止后續(xù)執(zhí)行。可以添加 "-" 前綴來忽略命令錯誤。另外還可以添加 "@" 來避免顯示命令行本身。

all: test.o main.o
    @echo "build ..."
    @$(CC) $(CFLAGS) $(LDFLAGS) -o $(OUT) $^

執(zhí)行其他規(guī)則:

all: test.o main.o
    $(MAKE) info
    @$(CC) $(CFLAGS) $(LDFLAGS) -o $(OUT) $^

info:
    @echo "build..."

4.3 變量

Makefile 支持類似 shell 的變量功能,相當(dāng)于 C 宏,本質(zhì)上就是文本替換。 變量名區(qū)分大小寫。變量名建議使用字母、數(shù)字和下劃線組成。引用方式 $(var) 或 ${var}。引用未定義變量時,輸出空。

4.3.1 變量定義

首先注意的是 "=" 和 ":=" 的區(qū)別。

  • = : 遞歸展開變量,僅在目標(biāo)展開時才會替換,也就是說它可以引用在后面定義的變量。
  • := : 直接展開變量,在定義時就直接展開,它無法后置引用。
A = "a: $(C)"
B := "b: $(C)"
C = "haha..."

all:
    @echo $A
    @echo $B

輸出:

$ make

a: haha...
b:

由于 B 定義時 C 尚未定義,所以直接展開的結(jié)果就是空。修改一下,再看。

C = "none..."
A = "a: $(C)"
B := "b: $(C)"
C = "haha..."

all:
    @echo $A
    @echo $B

輸出:

$ make

a: haha...
b: none...

可見 A 和 B 的展開時機(jī)的區(qū)別。

除了使用 "="、":=" 外,還可以用 "define ... endef" 定義多行變量 (宏,遞歸展開,只需在調(diào)用時添加 @ 即可)。

define help
    echo ""
    echo " make release : Build release version."
    echo " make clean : Clean templ files."
    echo ""
endef

debug:
    @echo "Build debug version..."
    @$(help)
    @$(MAKE) $(OUT) DEBUG=1

release:
    @echo "Build release version..."
    @$(help)
    @$(MAKE) clean $(OUT)

4.3.2 操作符

"?=" 表示變量為空或未定義時才進(jìn)行賦值操作。

A ?= "a"

A ?= "A"
B = "B"

all:
    @echo $A
    @echo $B

輸出:

$ make

a
B

"+=" 追加變量值。注意變量展開時機(jī)。

A = "$B"
A += "..."
B = "haha"

all:
    @echo $A

輸出:

$ make
haha ...

4.3.3 替換引用

使用 "$(VAR:A=B)" 可以將變量 VAR 中所有以 A 結(jié)尾的單詞替換成以 B 結(jié)尾。

A = "a.o b.o c.o"

all:
    @echo $(A:o=c)

輸出:

$ make
a.c b.c c.o

4.3.4 命令行變量

命令行變量會替換 Makefile 中定義的變量值,除非使用 override。

A = "aaa"
override B = "bbb"
C += "ccc"
override D += "ddd"

all:
    @echo $A
    @echo $B
    @echo $C
    @echo $D

執(zhí)行:

$ make A="111" B="222" C="333" D="444"

111
bbb
333
444 ddd

我們注意到追加方式在使用 override 后才和命令行變量合并。

4.3.5 目標(biāo)變量

僅在某個特定目標(biāo)中生效,相當(dāng)于局部變量。

test1: A = "abc"

test1:
    @echo "test1" $A

test2:
    @echo "test2" $A

輸出:

$ make test1 test2

test1 abc
test2

還可以定義模式變量。

test%: A = "abc"

test1:
    @echo "test1" $A

test2:
    @echo "test2" $A

輸出:

$ make test1 test2

test1 abc
test2 abc

4.3.6 自動化變量

  • $? : 比目標(biāo)新的依賴項(xiàng)。
  • $@ : 目標(biāo)名稱。
  • $< : 第一個依賴項(xiàng)名稱 (搜索后路徑)。
  • $^ : 所有依賴項(xiàng) (搜索后路徑,排除重復(fù)項(xiàng))。

4.3.7 通配符

在變量定義中使用通配符則需要借助 wildcard。

FILES = $(wildcard *.o)

all:
    @echo $(FILES)

4.3.8 環(huán)境變量

和 shell 一樣,可以使用 "export VAR" 將變量設(shè)定為環(huán)境變量,以便讓命令和遞歸調(diào)用的 make 命令能接收到參數(shù)。

例如: 使用 GCC C_INCLUDE_PATH 環(huán)境變量來代替 -I 參數(shù)。

C_INCLUDE_PATH := ./lib:/usr/include:/usr/local/include
export C_INCLUDE_PATH

4.4 條件

沒有條件判斷是不行滴。

CFLAGS = -Wall -std=c99 $(INC_PATHS)
ifdef DEBUG
    CFLAGS += -g
else
    CFLAGS += -O3
endif

類似的還有: ifeq、ifneq、ifndef 格式: ifeq (ARG1, ARG2) 或 ifeq "ARG1" "ARG2"

# DEBUG == 1
ifeq "$(DEBUG)" "1"
    ...
else
    ...
endif

# DEBUG 不為空
ifneq ($(DEBUG), )
    ...
else
    ...
endif

實(shí)際上,我們可以用 if 函數(shù)來代替。相當(dāng)于編程語言中的三元表達(dá)式 "?:"。

CFLAGS = -Wall $(if $(DEBUG), -g, -O3) -std=c99 $(INC_PATHS)

4.5 函數(shù)

*nix 下的 "配置" 都有點(diǎn) "腳本語言" 的感覺。

make 支持函數(shù)的使用,調(diào)用方法 "$(function args)" 或 "${function args}"。多個參數(shù)之間用"," (多余的空格可能會成為參數(shù)的一部分)。

例如: 將 "Hello, World!" 替換成 "Hello, GNU Make!"。

A = Hello, World!
all:
    @echo $(subst World, GUN Make, $(A))

注意: 字符串沒有用引號包含起來,如果字符串中有引號字符,使用 "\" 轉(zhuǎn)義。

4.5.1 foreach

這個 foreach 很好,執(zhí)行結(jié)果輸出 "[1] [2] [3]"。

A = 1 2 3
all:
    @echo $(foreach x,$(A),[$(x)])

4.5.2 call

我們還可以自定義一個函數(shù),其實(shí)就是用一個變量來代替復(fù)雜的表達(dá)式,比如對上面例子的改寫。

A = x y z
func = $(foreach x, $(1), [$(x)])
all:
    @echo $(call func, $(A))
    @echo $(call func, 1 2 3)

傳遞的參數(shù)分別是 "$(1), $(2) ..."。

用 define 可以定義一個更復(fù)雜一點(diǎn)的多行函數(shù)。

A = x y z
define func
    echo "$(2): $(1) -> $(foreach x, $(1), [$(x)])"
endef

all:
    @$(call func, $(A), char)
    @$(call func, 1 2 3, num)

輸出:

$ make

char: x y z -> [x] [y] [z]
num: 1 2 3 -> [1] [2] [3]

4.5.3 eval

eval 函數(shù)的作用是動態(tài)生成 Makefile 內(nèi)容。

define func
    $(1) = $(1)...
endef

$(eval $(call func, A))
$(eval $(call func, B))

all:
    @echo $(A) $(B)

上面例子的執(zhí)行結(jié)果實(shí)際上是 "動態(tài)" 定義了兩個變量而已。當(dāng)然,借用 foreach 可以更緊湊一些。

$(foreach x, A B, $(eval $(call func, $(x))))

4.5.4 shell

執(zhí)行 shell 命令,這個非常實(shí)用。

A = $(shell uname)
all:
    @echo $(A)

更多的函數(shù)列表和詳細(xì)信息請參考相關(guān)文檔。

4.6 包含

include 指令會讀取其他的 Makefile 文件內(nèi)容,并在當(dāng)前位置展開。通常使用 ".mk" 作為擴(kuò)展名,支持文件名通配符,支持相對和絕對路徑。

4.7 執(zhí)行

Makefile 常用目標(biāo)名:

  • all: 默認(rèn)目標(biāo)。
  • clean: 清理項(xiàng)目文件的偽目標(biāo)。
  • install: 安裝(拷貝)編譯成功的項(xiàng)目文件。
  • tar: 創(chuàng)建源碼壓縮包。
  • dist: 創(chuàng)建待發(fā)布的源碼壓縮包。
  • tags: 創(chuàng)建 VIM 使用的 CTAGS 文件。
  • make 常用命令參數(shù):

  • -n: 顯示待執(zhí)行的命令,但不執(zhí)行。
  • -t: 更新目標(biāo)文件時間戳,也就是說就算依賴項(xiàng)被修改,也不更新目標(biāo)文件。
  • -k: 出錯時,繼續(xù)執(zhí)行。
  • -B: 不檢查依賴列表,強(qiáng)制更新目標(biāo)。
  • -C: 執(zhí)行 make 前,進(jìn)入特定目錄。讓我們可以在非 Makefile 目錄下執(zhí)行 make 命令。
  • -e: 使用系統(tǒng)環(huán)境變量覆蓋同名變量。
  • -i: 忽略命令錯誤。相當(dāng)于 "-" 前綴。
  • -I: 指定 include 包含文件搜索目錄。
  • -p: 顯示所有 Makefile 和 make 的相關(guān)參數(shù)信息。
  • -s: 不顯示執(zhí)行的命令行。相當(dāng)于 "@" 前綴。

順序執(zhí)行多個目標(biāo):

$ make clean debug

5. Scons

Scons 采用 Python 編寫,用來替換 GNU Make 的自動化編譯構(gòu)建工具。相比 Makefile 和類似的老古董,scons 更智能,更簡單。

5.1 腳本

在項(xiàng)目目錄下創(chuàng)建名為 SConstruct (或 Sconstruct、 sconstruct) 的文件,作用類似 Makefile。實(shí)質(zhì)上就是 py 源文件。

簡單樣本:

Program("test", ["main.c"])

常用命令:

$ scons!! ! ! ! ! # 構(gòu)建,輸出詳細(xì)信息。
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
gcc -o main.o -c main.c
gcc -o test main.o
scons: done building targets.

$ scons -c! ! ! ! ! # 清理,類似 make clean。
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Cleaning targets ...
Removed main.o
Removed test
scons: done cleaning targets.

$ scons -Q! ! ! ! ! # 構(gòu)建,簡化信息輸出。
gcc -o main.o -c main.c
gcc -o test main.o

$ scons -i! ! ! ! ! # 忽略錯誤,繼續(xù)執(zhí)行。
$ scons -n! ! ! ! ! # 輸出要執(zhí)行的命令,但并不真的執(zhí)行。
$ scons -s! ! ! ! ! # 安靜執(zhí)行,不輸出任何非錯誤信息。
$ scons -j 2! ! ! ! ! # 并行構(gòu)建。

如需調(diào)試,建議插入 "import pdb; pdb.set_trace()",命令行參數(shù) "--debug=pdb" 并不好用??捎?SConscript(path/filename) 包含其他設(shè)置文件 (或列表),按慣例命名為 SConscript。

5.2 環(huán)境

影響 scons 執(zhí)行的環(huán)境 (Environment ) 因素包括:

  • External:外部環(huán)境。執(zhí)行 scons 時的操作系統(tǒng)環(huán)境變量,可以用 os.environ 訪問。
  • Construction: 構(gòu)建環(huán)境,用來控制實(shí)際的編譯行為。
  • Execution: 執(zhí)行環(huán)境,用于設(shè)置相關(guān)工具所需設(shè)置。比如 PATH 可執(zhí)行搜索路徑。

簡單程序,可直接使用默認(rèn)構(gòu)建環(huán)境實(shí)例。

env = DefaultEnvironment(CCFLAGS = "-g")! ! # 返回默認(rèn)構(gòu)建環(huán)境實(shí)例,并設(shè)置參數(shù)。
Program("test", ["main.c"])! ! ! ! # 相當(dāng)于 env.Program()

輸出:

gcc -o main.o -c -g main.c
gcc -o test main.o

如需多個構(gòu)建環(huán)境,可用 Environment 函數(shù)創(chuàng)建。同一環(huán)境可編譯多個目標(biāo),比如用相同設(shè)置編譯靜態(tài)庫和目標(biāo)執(zhí)行程序。

env = Environment(CCFLAGS = "-O3")
env.Library("my", ["test.c"], srcdir = "lib")
env.Program("test", ["main.c"], LIBS = ["my"], LIBPATH = ["."])

輸出:

gcc -o lib/test.o -c -O3 lib/test.c
ar rc libmy.a lib/test.o
ranlib libmy.a

gcc -o main.o -c -O3 main.c
gcc -o test main.o -L. -lmy

常用環(huán)境參數(shù):

  • CC: 編譯器,默認(rèn) "gcc"。
  • CCFLAGS: 編譯參數(shù)。
  • CPPDEFINES: 宏定義。
  • CPPPATH:頭文件搜索路徑。
  • LIBPATH:庫文件搜索路徑。
  • LIBS: 需要鏈接的庫名稱。
  • 除直接提供鍵值參數(shù)外,還可用名為 parse_flags 的特殊參數(shù)一次性提供,它會被 ParseFlags 方法自動分解。

env = Environment(parse_flags = "-Ilib -L.")
print env["CPPPATH"], env["LIBPATH"]

輸出:

['lib'] ['.']

調(diào)用 Dictionary 方法返回環(huán)境參數(shù)字典,或直接用 Dump 方法返回 Pretty-Print 字符串。

print env.Dictionary(); ! print env.Dictionary("LIBS", "CPPPATH")
print env.Dump(); ! ! print env.Dump("LIBS")

用 "ENV" 鍵訪問執(zhí)行環(huán)境字典。系統(tǒng)不會自動拷貝外部環(huán)境變量,需自行設(shè)置。

import os
env = DefaultEnvironment(ENV = os.environ)
print env["ENV"]["PATH"]

5.3 方法

5.3.1 編譯

同一構(gòu)建環(huán)境,可用相關(guān)方法編譯多個目標(biāo)。無需關(guān)心這些方法調(diào)用順序,系統(tǒng)會自動處理依賴關(guān)系,安排構(gòu)建順序。

  • Program: 創(chuàng)建可執(zhí)行程序 (ELF、.exe)。
  • Library, StaticLibrary: 創(chuàng)建靜態(tài)庫 (.a, .lib)。
  • SharedLibrary: 創(chuàng)建動態(tài)庫 (.so, .dylib, .dll)。
  • Object: 創(chuàng)建目標(biāo)文件 (.o)。
  • 如果沒有構(gòu)建環(huán)境實(shí)例,那么這些函數(shù)將使用默認(rèn)環(huán)境實(shí)例。

用首個位置參數(shù)指定目標(biāo)文件名 (不包括擴(kuò)展名),或用 target、source 指定命名參數(shù)。source 是單個源文件名 (包含擴(kuò)展名) 或列表。

Program("test1", "main.c")
Program("test2", ["main.c", "lib/test.c"])! ! # 列表
Program("test3", Split("main.c lib/test.c"))!! # 分解成列表
Program("test4"