Linux C 程序的编译与运行

摘自《Linux GNU C 程序观察》 罗秋明 著, 清华大学出版社

1. 编译的各阶段

C 程序 从源代码到可执行文件的全过程包括: 预处理、编译、汇编、链接等步骤。GCC(GNU Compiler Collection)编译系统将先后调用预处理器 cpp、 编译器 cc、 汇编器 as 和链接器 ld 逐步处理,最终生成可执行文件。假设有 hello.c 源程序,相应过程如下图所示:

生产可执行文件的编译步骤.jpg

​ 图 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 语言的一些扩展,比如内嵌函数和变长数组。

  1. -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.

  1. -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.omultvec.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.oaddvec.omultvec.o 两个目标文件归档到 libvector.a 这个静态库中(其中参数中的 c 表示 create, r 表示 replace)。使用 ar -t libvector.a命令可以确认其中有两个文件 addvec.omultvec.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 种启动方式:

  1. gdb : 使用 gdb 启动程序 。gdb 将 装入系统形成子进程。
  2. gdb : 指定正在运行的进程的 PID,GDB 会自动 attach 上去,并调试该进程。亦可用 gdb -p 调试正在运行的进程。这不会创建新进程。
  3. gdb core : 用 GDB 同时调试一个可执行程序和 core 文件。这会根据可执行程序和 core dump 文件创建新进程。

直接运行代码:

(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 有效。

查看变量和内存:

  1. 寄存器: 使用 i r 查看寄存器, 使用 info all-registers 可显示更多寄存器内容。如果只想查看一个寄存器的内容,使用 p $REG

  2. 变量与内存单元:

    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 命令对变量进行监控,仅在其值发生变化时才将程序中断。

你可能感兴趣的:(Linux C 程序的编译与运行)