摘自《Linux GNU C 程序观察》 罗秋明 著, 清华大学出版社
1. 编译的各阶段
C 程序 从源代码到可执行文件的全过程包括: 预处理、编译、汇编、链接等步骤。GCC(GNU Compiler Collection)编译系统将先后调用预处理器 cpp、 编译器 cc、 汇编器 as 和链接器 ld 逐步处理,最终生成可执行文件。假设有 hello.c 源程序,相应过程如下图所示:
图 1 生成可执行文件的编译步骤
一个广义的编译过程包括以下几个步骤:
(1)预处理:展开头文件内容以及宏定义符号等。
(2)编译:狭义的编译阶段,将预处理后的源代码转换成汇编代码。
(3)汇编:将汇编代码转换成机器码(.o 目标文件)。此时的目标文件称为可重定位的目标文件,我们可以使用 objdump -d hello.o 得到反汇编代码。查看反汇编代码可以发现,此时各节的代码都是以 0 地址作为起始,但是当它们链接到一起时,才会放置到不重叠的地址空间。
(4)链接:将一个或多个机器码(.o 目标文件)生产可执行文件。
有时,GCC 也称编译驱动器,因为它根据命令行选项而调用其它工具来完成所指派的任务。例如,预处理实际上是通过 cpp 工具来完成的,编译实际上是通过 cc 工具来完成,汇编实际上通过 as 工具完成的,而链接是通过 ld 工具完成的。
2. GCC 的基本用法
前面已经尝试了用 GCC 编译工具以及 -E、-S、-c 选项进行预处理、编译生成汇编程序以及编译生成目标代码的操作,并使用 -o 指定输出文件名。
下面学习 GCC 的基本用法。gcc --help 的输出如下:
[root@localhost liyang]# gcc --help
Usage: gcc [options] file...
Options:
-pass-exit-codes Exit with highest error code from a phase
--help Display this information
--target-help Display target specific command line options
--help={common|optimizers|params|target|warnings|[^]{joined|separate|undocumented}}[,...]
Display specific types of command line options
(Use '-v --help' to display command line options of sub-processes)
--version Display compiler version information
-dumpspecs Display all of the built in spec strings
-dumpversion Display the version of the compiler
-dumpmachine Display the compiler's target processor
-print-search-dirs Display the directories in the compiler's search path
-print-libgcc-file-name Display the name of the compiler's companion library
-print-file-name= Display the full path to library
-print-prog-name= Display the full path to compiler component
-print-multiarch Display the target's normalized GNU triplet, used as
a component in the library path
-print-multi-directory Display the root directory for versions of libgcc
-print-multi-lib Display the mapping between command line options and
multiple library search directories
-print-multi-os-directory Display the relative path to OS libraries
-print-sysroot Display the target libraries directory
-print-sysroot-headers-suffix Display the sysroot suffix used to find headers
-Wa, Pass comma-separated on to the assembler
-Wp, Pass comma-separated on to the preprocessor
-Wl, Pass comma-separated on to the linker
-Xassembler Pass on to the assembler
-Xpreprocessor Pass on to the preprocessor
-Xlinker Pass on to the linker
-save-temps Do not delete intermediate files
-save-temps= Do not delete intermediate files
-no-canonical-prefixes Do not canonicalize paths when building relative
prefixes to other gcc components
-pipe Use pipes rather than intermediate files
-time Time the execution of each subprocess
-specs= Override built-in specs with the contents of
-std= Assume that the input sources are for
--sysroot= Use as the root directory for headers
and libraries
-B Add to the compiler's search paths
-v Display the programs invoked by the compiler
-### Like -v but options quoted and commands not executed
-E Preprocess only; do not compile, assemble or link
-S Compile only; do not assemble or link
-c Compile and assemble, but do not link
-o Place the output into
-pie Create a position independent executable
-shared Create a shared library
-x Specify the language of the following input files
Permissible languages include: c c++ assembler none
'none' means revert to the default behavior of
guessing the language based on the file's extension
Options starting with -g, -f, -m, -O, -W, or --param are automatically
passed on to the various sub-processes invoked by gcc. In order to pass
other options on to these processes the -W options must be used.
For bug reporting instructions, please see:
.
下面学习更多的选项和用法。
附: gcc -O1 -O2 -O3 -Os -Ofast -Og的作用
2.1 C 语言标准
默认情况下,GCC 编译程序用的是 C 语言的 GNU “方言” —— GNU 实现的 C 语言的超集, 称之为 GNU C。 GNU C 集成了 C 语言官方 ANSI/ISO 标准和 GNU 对 C 语言的一些扩展,比如内嵌函数和变长数组。
- -std:
如果需要控制使用标准 C 还是 GNU C,可使用 -std 选项:
(1) -std = c99 或 -std=iso9899: 1999 :使用 C99;
(2)-std = c11 或 -std=iso9899: 2011: 使用 C11;
(3)-std = gnu90、-std = gnu99 或 -std = gnu11 : 附带 GNU 扩展的 C 语言便准可用这三种选项来指定。如果不指定 C语言版本,那么默认使用 GNU C11.
- -ansi/ -pedantic:
-ansi 选项禁止哪些与 ANSI/ISO 标准冲突的 GNU 扩展特性。
同时使用 -ansi 和 -pedantic 会导致 GCC 拒绝所有的 GNU C 扩展,而不单单是那些不兼容与 ANSI/ISO 标准的。这有助于编写遵循 ANSI/ISO 标准的可移植的程序。
2.2 库的使用
前人的代码常以库的形式提供,使我们不用重复造轮子。
2.2.1 库的头文件
使用库函数,需要得到该函数的参数和返回值的正确类型声明,则必须包含相应的头文件。头文件的搜索路径在 2.3.1 节会介绍。
2.2.2 静态库
主要介绍如何使用 GCC 工具创建并使用静态库。假设有三个代码:一个是主程序 main-lib.c,另有两个辅助函数 ---- 计算向量加法的 addvec.c 和 计算向量点乘的 multvec.c 。代码略。其中 addvec.c 和 multvec.c 的代码将形成静态库,以供其它程序调用。所以还需一个库函数的头文件,如下 vector.h 所示:
// vector.h
void addvec(int * x, int * y, int * z, int n);
void multvec(int * x, int * y, int * z, int n);
为了将向量加法和点乘的代码变成库,首先使用 gcc -Og -c addvec.c multvec.c
生成 addvec.o
和 multvec.o
两个目标文件,使用 file 命令可以看出它们都是可重定位的目标文件。
[root@localhost liyang]# gcc -Og -c addvec.c multvec.c
[root@localhost liyang]# ls *.o
addvec.o multvec.o
[root@localhost liyang]# file multvec.o addvec.o
multvec.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
addvec.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
然后,通过 ar rcs libvector.a addvec.o multvec.o
将 addvec.o
和 multvec.o
两个目标文件归档到 libvector.a
这个静态库中(其中参数中的 c 表示 create, r 表示 replace)。使用 ar -t libvector.a
命令可以确认其中有两个文件 addvec.o
和 multvec.o
。
[root@localhost liyang]# ar rcs libvector.a addvec.o multvec.o
[root@localhost liyang]# ar -t libvector.a
addvec.o
multvec.o
创建了静态库 libvector.a
后,就可尝试在程序中使用了。执行 gcc -Og -c main-lib.c
编译主程序 main-lib.c
生成 main-lib.o
目标文件,然后使用 gcc main-lib.o libvector.a -o main-lib
生成可执行文件 main-lib
并运行, 执行输出结果是“z = [4 6]”。
[root@localhost liyang]# gcc -Og -c main-lib.c
[root@localhost liyang]# ls *.o
addvec.o main-lib.o multvec.o
[root@localhost liyang]# gcc main-lib.o libvector.a -o main-lib
[root@localhost liyang]# file main-lib
main-lib: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=9aac7ca82154e7f6f6f30cc56e5c8a99e5875f96, not stripped
[root@localhost liyang]# ./main-lib
z = [4 6]
此时执行 objdump -d main-lib
查看 main-lib
的反编译结果, 可见 addvec.c 的代码已进入到 000000000040056f 开始的地址空间了。使用 objdump -d addvec.o
查看 addvec.o 的反汇编代码,发现两者几乎一致。而 multvec.o 中的 multvec.c 的代码则没有进入到可执行文件中。
[root@localhost liyang]# objdump -d main-lib
main-lib: file format elf64-x86-64
..........
000000000040052d :
40052d: 48 83 ec 08 sub $0x8,%rsp
400531: b9 02 00 00 00 mov $0x2,%ecx
400536: ba 48 10 60 00 mov $0x601048,%edx
40053b: be 34 10 60 00 mov $0x601034,%esi
400540: bf 3c 10 60 00 mov $0x60103c,%edi
400545: e8 25 00 00 00 callq 40056f
40054a: 8b 15 fc 0a 20 00 mov 0x200afc(%rip),%edx # 60104c <__TMC_END__+0x4>
400550: 8b 35 f2 0a 20 00 mov 0x200af2(%rip),%esi # 601048 <__TMC_END__>
400556: bf 20 06 40 00 mov $0x400620,%edi
40055b: b8 00 00 00 00 mov $0x0,%eax
400560: e8 ab fe ff ff callq 400410
400565: b8 00 00 00 00 mov $0x0,%eax
40056a: 48 83 c4 08 add $0x8,%rsp
40056e: c3 retq
000000000040056f :
40056f: b8 00 00 00 00 mov $0x0,%eax
400574: eb 12 jmp 400588
400576: 4c 63 c0 movslq %eax,%r8
400579: 46 8b 0c 86 mov (%rsi,%r8,4),%r9d
40057d: 46 03 0c 87 add (%rdi,%r8,4),%r9d
400581: 46 89 0c 82 mov %r9d,(%rdx,%r8,4)
400585: 83 c0 01 add $0x1,%eax
400588: 39 c8 cmp %ecx,%eax
40058a: 7c ea jl 400576
40058c: f3 c3 repz retq
40058e: 66 90 xchg %ax,%ax
........
[root@localhost liyang]# objdump -d addvec.o
addvec.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 :
0: b8 00 00 00 00 mov $0x0,%eax
5: eb 12 jmp 19
7: 4c 63 c0 movslq %eax,%r8
a: 46 8b 0c 86 mov (%rsi,%r8,4),%r9d
e: 46 03 0c 87 add (%rdi,%r8,4),%r9d
12: 46 89 0c 82 mov %r9d,(%rdx,%r8,4)
16: 83 c0 01 add $0x1,%eax
19: 39 c8 cmp %ecx,%eax
1b: 7c ea jl 7
1d: f3 c3 repz retq
如果不指出所使用的静态库 libvector.a
而直接编译 main-lib.c
,将提示 addvec
函数无定义。
[root@localhost liyang]# gcc main-lib.o -o main-lib
main-lib.o: In function `main':
main-lib.c:(.text+0x19): undefined reference to `addvec'
collect2: error: ld returned 1 exit status
2.2.3 动态库
使用静态库时,所有被用到的目标文件中的全部函数代码都进入到可执行程序中。很多程序都共享的库函数,如果拷贝到各进程,无疑会造成大量空间浪费。动态链接库(又称动态库、共享库),在启动或运行时用到的动态库才映射到进程空间,而且和其他进程共享动态链接库的代码。
动态链接库后面章节会专门讨论。
使用gcc -shared -fPIC -o libvector.so addvec.c multvec.c
命令将两个源代码编译成动态库libvector.so。 其中参数-fPIC
就是指明生成位置无关代码,这是动态库的必要属性。-shared
指明生成动态库。
[root@localhost liyang]# gcc -shared -fPIC -o libvector.so addvec.c multvec.c
[root@localhost liyang]# file libvector.so
libvector.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=b7418d2b111fd671186c64a452, not stripped
紧接着,可与静态库一样,在 main-lib.c
程序中引用动态库,编译时使用gcc -o main-lib-shared main-lib.c libvector.so
命令,生成引用动态库的main-lib-shared
可执行文件。但是运行时,结果却显示无法找到对应的libvector.so
动态库:
[root@localhost liyang]# gcc -o main-lib-shared main-lib.c libvector.so
[root@localhost liyang]# file main-lib-shared
main-lib-shared: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=f22103b363b19e7bc4769c614d476355a70b4316, not stripped
[root@localhost liyang]# ./main-lib-shared
./main-lib-shared: error while loading shared libraries: libvector.so: cannot open shared object file: No such file or directory
这就涉及到 Linux 运行环境中关于库的搜索路径的问题。这在 2.3 节 搜索路径会讨论。
最后使用 ldd
工具可以查看可执行文件引用了哪些动态库。可以看到其中使用了 libvector.so
、标准C语言库 libc.so.6
以及关于动态链接的辅助库 /lib64/ld-linux-x86-64.so.2
,还有系统调用相关的库 linux-vdso.so.1
。
[root@localhost liyang]# ldd main-lib-shared
linux-vdso.so.1 => (0x00007ffd44f6b000)
libvector.so => not found
libc.so.6 => /lib64/libc.so.6 (0x00007f699c0d1000)
/lib64/ld-linux-x86-64.so.2 (0x000055f687c65000)
2.3 搜索路径
搜索路径包括头文件的搜索路径(又称为 include 路径)、库的搜索路径(又简称为搜索路径或链接路径)。
2.3.1 头文件搜索路径
GCC 默认对 #include
的头文件按照如下先后顺序在下面两个目录中搜索。
/usr/local/include
/usr/include
而对于 #include "XXX.h"
的头文件则会优先在当前目录寻找,找不到则继续在上述两个目录依次查找。
前面的例子,由于 vector.h 头文件就是放在 main-lib.c 目录下,并以 #include "vector.h"
的方式引用,所以它能被搜索到。如果将其放到 /tmp/my-lib 目录下,则必须是用完整的路径 #include "/tmp/my-lib/vector.h"
或者 使用 -I /tmp/my-lib
添加一个 include 搜索路径。
[root@localhost liyang]# mv vector.h /tmp/my-lib
[root@localhost liyang]# gcc -c main-lib.c
main-lib.c:2:20: fatal error: vector.h: No such file or directory
#include "vector.h"
^
compilation terminated.
[root@localhost liyang]# gcc -c main-lib.c -I /tmp/my-lib
头文件的搜索路径也可以添加到环境变量中,避免在命令行中出现较长的头文件路径。例如 C 程序使用 C_INCLUDE_PATH 环境变量指定头文件路径,C++ 使用 CPLUS_INCLUDE_PATH 来指定搜索路径。出于方便,可写入 /etc/profile 中。
[root@localhost liyang]# export C_INCLUDE_PATH=/tmp/my-lib
[root@localhost liyang]# echo $C_INCLUDE_PATH
/tmp/my-lib
[root@localhost liyang]# gcc -c main-lib.c
2.3.2 编译时的库搜索路径
之前的示例,直接将静态库或动态库作为链接文件名输入给 gcc 命令行。编译时如果按照标准的库文件使用方式,将使用 -lvector
参数来指定引用 vector 库(而不必写出库文件全名 libvector.a ),同时用 -L.
参数指出库所在目录是当前目录。但是执行 gcc -static -I/tmp/my-lib -L. -lvector main-lib.c
却报出了错误,说找不到 -lc,这是因为未安装 C 语言的静态库导致的。使用 yum install -y glibc-static
[root@localhost liyang]# gcc -static -I/tmp/my-lib -L. -lvector main-lib.c
/bin/ld: cannot find -lc
collect2: error: ld returned 1 exit status
[root@localhost liyang]# yum install -y glibc-static
[root@localhost liyang]# gcc -static -I/tmp/my-lib -L. -lvector main-lib.c
/tmp/ccv9KblI.o: In function `main':
main-lib.c:(.text+0x19): undefined reference to `addvec'
collect2: error: ld returned 1 exit status
[root@localhost liyang]# gcc -static -I/tmp/my-lib main-lib.c -L. -lvector
[root@localhost liyang]# ./a.out
z = [4 6]
假设使用 64 位系统,默认情况下连接时,将在以下目录搜索库:
/usr/local/lib64
/usr/lib64
如果你将静态库 libvector.a 移到 /tmp/my-lib 下,则需使用 -L/tmp/my-lib -lvector 指出搜索路径。
[root@localhost liyang]# mv libvector.a /tmp/my-lib/
[root@localhost liyang]# gcc -static -I/tmp/my-lib main-lib.c -L. -lvector
/bin/ld: cannot find -lvector
collect2: error: ld returned 1 exit status
[root@localhost liyang]# gcc -static -I/tmp/my-lib main-lib.c -L/tmp/my-lib -lvector
[root@localhost liyang]# ./a.out
z = [4 6]
除了直接在命令行加入库的搜索路径,也可像头文件那样使用环境变量 LIBRARY_PATH 将目录加入到库文件的搜索路径,使用 gcc -print-search-dirs
可以打印 gcc 的搜索路径,这很有用。
[root@localhost liyang]# export LIBRARY_PATH=/tmp/my-lib
[root@localhost liyang]# gcc -static -I/tmp/my-lib main-lib.c -lvector
[root@localhost liyang]# gcc -print-search-dirs
install: /usr/lib/gcc/x86_64-redhat-linux/4.8.5/
programs: =/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/:/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/:/usr/libexec/gcc/x86_64-redhat-linux/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/:/usr/lib/gcc/x86_64-redhat-linux/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../x86_64-redhat-linux/bin/x86_64-redhat-linux/4.8.5/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../x86_64-redhat-linux/bin/
libraries: =/tmp/my-lib/x86_64-redhat-linux/4.8.5/:/tmp/my-lib/../lib64/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../x86_64-redhat-linux/lib/x86_64-redhat-linux/4.8.5/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../x86_64-redhat-linux/lib/../lib64/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../x86_64-redhat-linux/4.8.5/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/:/lib/x86_64-redhat-linux/4.8.5/:/lib/../lib64/:/usr/lib/x86_64-redhat-linux/4.8.5/:/usr/lib/../lib64/:/tmp/my-lib/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../x86_64-redhat-linux/lib/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../:/lib/:/usr/lib/
2.3.3 (运行时)动态库的载入路径
2.2.3 节指出,在 main-lib.c
程序中引用动态库,编译时使用gcc -o main-lib-shared main-lib.c libvector.so
命令,成功生成引用动态库的main-lib-shared
可执行文件。但是实际运行时,结果却显示无法找到对应的libvector.so
动态库:
[root@localhost liyang]# gcc -o main-lib-shared main-lib.c libvector.so
[root@localhost liyang]# file main-lib-shared
main-lib-shared: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=f22103b363b19e7bc4769c614d476355a70b4316, not stripped
[root@localhost liyang]# ./main-lib-shared
./main-lib-shared: error while loading shared libraries: libvector.so: cannot open shared object file: No such file or directory
这是因为编译机制和运行机制中路径搜索使用不同的信息。
-
LD_LIBRARY_PATH 环境变量
即使 libvector.so 动态库就在 main-lib-shared 同级目录,也需告知运行环境从哪里载入动态库。最简单的方式是使用 LD_LIBRARY_PATH 环境变量,也可写入 /etc/profile。
[root@localhost liyang]# gcc -o main-lib-shared -I/tmp/my-lib main-lib.c libvector.so [root@localhost liyang]# export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:. [root@localhost liyang]# echo $LD_LIBRARY_PATH :. [root@localhost liyang]# ./main-lib-shared z = [4 6]
-
ldconfig 管理工具
ldconfig 工具主要用途是建立动态库的搜索路径的缓存 —— 在默认搜索目录 /lib 和 /usr/lib 以及动态库配置文件
/etc/ld.so.conf
内所列的目录下,搜索除可共享的动态链接库(格式如lib*.so*
) , 进而创建出动态装入程序(ld.so)所需的链接缓存文件。缓存文件默认存于 /etc/ld.so.cache, 此文件保存已排好序的动态链接库的名字列表。为了让动态链接库为系统所共享,可运行动态链接库的管理命令 /usr/bin/ldconfig。查看一下 /etc/ld.so.conf 文件的内容,它只是将 /etc/ld.so.conf.d/ 目录之下的所有 *.conf 文件包含进来。而且每个 *.conf 文件中只有相应目录的字符串。
[root@localhost liyang]# cat /etc/ld.so.conf include ld.so.conf.d/*.conf [root@localhost liyang]# ls /etc/ld.so.conf.d/ dyninst-x86_64.conf kernel-3.10.0-693.25.4.el7.x86_64.conf mariadb-x86_64.conf [root@localhost liyang]# cat /etc/ld.so.conf.d/mariadb-x86_64.conf /usr/lib64/mysql
由于 动态库 libvector.so 并没有拷贝至系统默认目录中,也没在 /etc/ld.so.conf 中添加信息,因此用ldconfig -p 显示已建立缓存的库,是找不到 libvector.so 的。 使用 echo /tmp/my-lib > /etc/ld.so.conf.d/my-lib.conf 创建一个 my-lib.conf, 接着运行 ldconfig, 将 /tmp/my-lib 路径加入到库文件的路径缓存中,再次执行 ldconfig -p 就可以看到了。这种方式不需要设置 LD_LIBRARY_PATH 环境变量。
[root@localhost liyang]# mv libvector.so /tmp/my-lib [root@localhost liyang]# ldconfig -p | grep vector [root@localhost liyang]# ll /tmp/my-lib/* -rw-r--r-- 1 root root 2712 Sep 9 23:52 /tmp/my-lib/libvector.a -rwxr-xr-x 1 root root 7928 Sep 10 00:08 /tmp/my-lib/libvector.so -rw-r--r-- 1 root root 90 Sep 9 23:49 /tmp/my-lib/vector.h [root@localhost liyang]# cat /etc/ld.so.conf include ld.so.conf.d/*.conf [root@localhost liyang]# ll /etc/ld.so.conf.d/my-lib.conf -rw-r--r-- 1 root root 12 Sep 10 01:21 /etc/ld.so.conf.d/my-lib.conf [root@localhost liyang]# ll /etc/ld.so.conf.d/*.conf -rw-r--r--. 1 root root 19 Mar 6 2015 /etc/ld.so.conf.d/dyninst-x86_64.conf -r--r--r--. 1 root root 63 Apr 29 2018 /etc/ld.so.conf.d/kernel-3.10.0-693.25.4.el7.x86_64.conf -rw-r--r--. 1 root root 17 Nov 15 2016 /etc/ld.so.conf.d/mariadb-x86_64.conf -rw-r--r-- 1 root root 12 Sep 10 01:21 /etc/ld.so.conf.d/my-lib.conf [root@localhost liyang]# cat /etc/ld.so.conf.d/my-lib.conf /tmp/my-lib [root@localhost liyang]# ldconfig [root@localhost liyang]# ldconfig -p | grep vector libvector.so (libc6,x86-64) => /tmp/my-lib/libvector.so
2.4 编译警告
2.4.1 -Wall
GCC 可使用 -Wall 参数提示所有发现的警告。-Wall 选项打开所有最常用的编译警告,建议在编译生产环境的程序时都使用该选项。如果不想对所有警告都提示,可用具体的选项在编译时发出警告。 -Wformat 选项提示 printf() 或 scanf() 函数中的格式化字符串的误用; -Wunused 选项,会对未被使用的变量发出警告;-Wimplicit 选项对未申明类型的函数调用发出警告;-Wreturn-type 对函数返回值类型不对的情况发出警告。
2.4.2 -Wall 范围外的警告
-W
: 通常和 -Wall
同时使用,对一些常用编程错误发出警告,例如需要返回值但无返回值的函数,或将有符号数和无符号数进行比较。比如判断无符号数 是否 <0, -Wall 通常不会发出警告,但 -W 则会。
-Wconversion
: 警告可能引起意外结果的隐式转换,比如 将 -1 赋给 一个无符号数。
-Wshadow
: 用来警告同名变量覆盖的情形。
-Wcast-qual
: 用来警告对指针的转换操作移除了某种类型修饰符,比如 const。
-Wwrite-strings
: 用来警告对字面量字符串的覆写。
2.4.3 -Werror
将警告转变为错误,一遇到警告就停止编译。
3. GDB 调试
为了让 GDB 调试,可执行文件需要附加额外的调试信息,因此在编译的时候需要给 GCC 传递 -g
参数。例如 gcc -g demo-gdb.c -o demo-gdb
编译代码生成带有调试信息的可执行文件 demo-gdb。
gdb 有 3 种启动方式:
-
gdb
: 使用 gdb 启动程序
。gdb 将装入系统形成子进程。 -
gdb
: 指定正在运行的进程的 PID,GDB 会自动 attach 上去,并调试该进程。亦可用gdb -p
调试正在运行的进程。这不会创建新进程。 -
gdb
: 用 GDB 同时调试一个可执行程序和 core 文件。这会根据可执行程序和 core dump 文件创建新进程。core
直接运行代码:
(gdb) r
Starting program: /home/liyang/demo-gdb/demo-gdb
result[1-100] = 4950
result[1-200] = 19900
[Inferior 1 (process 16149) exited with code 026]
Missing separate debuginfos, use: debuginfo-install glibc-2.17-157.el7_3.5.x86_64
单步调试:
首先用 start 启动调试,然后用 n(next) 或 s (step) 逐条指令执行。其中 n 不会进入 C 函数内部, 而 s 则会。另外 , si 和 ni 是汇编级别的断定定位, si 会进入汇编和 C 函数的内部,ni 不会进入函数内部。
(gdb) start
Temporary breakpoint 2 at 0x400563: file demo-gdb.c, line 16.
Starting program: /home/liyang/demo-gdb/demo-gdb
Temporary breakpoint 2, main () at demo-gdb.c:16
16 long result =0;
(gdb) s
18 for (i=0; i< 100; i++)
(gdb) s
20 result += i;
断点:
下面使用 b 21 和 b func 命令分别在程序第 21 行和 func() 函数入口处设置断点,然后用 r 开始执行程序到第 1 个断电,再用 c(完整命令是 continue ) 继续运行到第 2 个断点, 最后使用 c 运行到结尾。使用 i b
可以查看所有断点及其编号。 使用 d (delete) 断点编号,可删除对应的断点。
(gdb) b 21
Breakpoint 1 at 0x400587: file demo-gdb.c, line 21.
(gdb) b func
Breakpoint 2 at 0x400534: file demo-gdb.c, line 5.
(gdb) i b
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000000400587 in main at demo-gdb.c:21
2 breakpoint keep y 0x0000000000400534 in func at demo-gdb.c:5
(gdb) r
Starting program: /home/liyang/demo-gdb/demo-gdb
Breakpoint 1, main () at demo-gdb.c:23
23 printf("result[1-100] = %ld\n", result);
Missing separate debuginfos, use: debuginfo-install glibc-2.17-157.el7_3.5.x86_64
(gdb) l
18 for (i=0; i< 100; i++)
19 {
20 result += i;
21 }
22
23 printf("result[1-100] = %ld\n", result);
24 printf("result[1-200] = %d\n", func(200));
25 }
26
(gdb) c
Continuing.
result[1-100] = 4950
Breakpoint 2, func (n=200) at demo-gdb.c:5
5 int sum=0,i;
(gdb) c
Continuing.
result[1-200] = 19900
[Inferior 1 (process 29446) exited with code 026]
(gdb) i b
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000000400587 in main at demo-gdb.c:21
breakpoint already hit 1 time
2 breakpoint keep y 0x0000000000400534 in func at demo-gdb.c:5
breakpoint already hit 1 time
条件断点:
如果需要再特定条件下让程序停下来检查,则需使用条件断点(catchpoint)。例如在程序第 19 行只有当循环到 i == 20 的时候才停下来检查,可以使用 b 19 if i==20
创建条件断点。
(gdb) i b
No breakpoints or watchpoints.
(gdb) b 19 if i==20
Breakpoint 1 at 0x400574: file demo-gdb.c, line 19.
(gdb) b 21
Breakpoint 2 at 0x400587: file demo-gdb.c, line 21.
(gdb) b func
Breakpoint 3 at 0x400534: file demo-gdb.c, line 5.
(gdb) i b
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000000400574 in main at demo-gdb.c:19
stop only if i==20
2 breakpoint keep y 0x0000000000400587 in main at demo-gdb.c:21
3 breakpoint keep y 0x0000000000400534 in func at demo-gdb.c:5
(gdb) r
Starting program: /home/liyang/demo-gdb/demo-gdb
Breakpoint 1, main () at demo-gdb.c:20
20 result += i;
Missing separate debuginfos, use: debuginfo-install glibc-2.17-157.el7_3.5.x86_64
(gdb) p i
$1 = 20
(gdb) p result
$2 = 190
可用 condition 命令设置断点的条件,为普通无条件断点增加条件,例如, condition 3 a > 5
为 3 号断点设置条件 a > 5。 如果将条件设置为空, 如 condition 3 可以删除 3 号条件断点的条件而变成普通断点。
ignore
指令会忽略若干次断点,例如 ignore 3 10
会忽略前 10 次执行到 3 号断点。ignore 不仅对 breakpoint, 也对 catchpoint 和后面的 watchpoint 有效。
查看变量和内存:
寄存器: 使用
i r
查看寄存器, 使用 info all-registers 可显示更多寄存器内容。如果只想查看一个寄存器的内容,使用p $REG
-
变量与内存单元:
p/? 变量名
可以按照指定的格式输出变量的值:x 按十六进制显示变量 t 按二进制显示变量 d 按十进制显示变量 a 按十六进制显示变量 u 按十六进制显示无符号整数 c 按字符格式显示变量 o 按八进制显示变量 f 按浮点数格式显示变量 如需查看内存单元,可以使用
p address
或 使用 x 命令(对应 examine),具体格式为x/nfu address
- n 表示显示数据的个数,是个正整数
- f 表示显示的格式,如上表所示,如果是指令地址,格式为 i ;
- u 表示从当前地址往后请求的字节数,默认是 4 个字节。如果指定的话,b 表示单字节,h 表示双字节, w 表示四个字节,g 表示八个字节。
例如,
x/4xh 0x400574
表示从 0x400574 开始按十六进制的形式,打印 4 个 双字节数据。监控点(watchpoint):
使用 watch 命令对变量进行监控,仅在其值发生变化时才将程序中断。