一、Linux 下的 GCC 编译器工具集
在 Linux 环境下的编译器之 Vim 安装与基本操作使用 中介绍了如何使用 Linux 环境下的编译器编写程序,并编写了一个 hello.c 的程序。要使编写的程序能够运行,需要进行程序的编译。 Linux 环境下采用的编译器 GCC 来进行对程序的编译。
1. GCC 简介
GCC 是 Linux 下的编译工具集,是 GNU Compiler Collection 的缩写,包含 gcc、g++ 等编译器。这个工具集不仅包含编译器,还包含其他工具集,例如 ar、nm 等。
gcc -o(output) -o 是指定输出的文件叫什么
gcc -v / --v / --version 查看 gcc 版本号
gcc -I<dir> 指定头文件目录,注意 -I 和指定目录之间没有空格 例:gcc -I./dir/dir test.c dict.c -o app (test.c 中的头文件 <dict.c> 的在 ./dir/dir 目录下,可以理解为间接使 dict.c 成为了系统目录下的文件)
gcc -c 只编译,生成 .o 文件,不进行链接
gcc -g 包含调试信息,生成的程序里面包含调试信息,可以用 Debug 工具进行调试。例:gcc test.c -g -o app
gcc -On n=0〜3 编译优化,n 越大优化得越多
gcc -Wall 提示更多警告信息,更加严格去编译你的程序,但凡有一点地方不符合语法的地方就给你对应的警告
gcc -D<DEF> 编译时定义宏,注意 -D 和指定的宏之间没有空格 例:gcc test.c -DDEBUG -o app (DEBUG 定义的宏)
gcc -E 生成预处理文件 例:gcc test.c -E -o test.c
gcc -M 生成 .c 文件与头文件依赖关系以用于 Makefile,包括系统库的头文件
gcc -MM 生成 .c 文件与头文件依赖关系以用于 Makefile,不包括系统库的头文件
GCC 工具集不仅能编译 C/C++ 语言,其他例如 Objective-C、Pascal、Fortran、Java、Ada 等语言均能进行编译。
GCC 在可以根据不同的硬件平台进行编译,即能进行交叉编译, 在 A 平台上编译 B 平台的程序,支持常见的 X86 、ARM、PowerPC、mips等,以及 Linux、Windows 等软件平台。
GCC 在各种平台下都被广泛地采用,特别是嵌入式平台下,这得益于它的目标机定义规则。当对目标机的硬件进行了合适的定义后,可以生成目标机能够正确解析的文件格式。 另外,GCC 的强大前端是定义新语言的好方法,例如可以重新定义语法解析规则,定义自己使用的专有编程语言。査看 www.gnu.org 上关于 GCC 介绍的 gcc internel 可以得到更多更详细的信息。
GCC 的 C 编译器是 gcc ,其命令格式为:
Usage: gcc [options] file...
GCC 支持默认扩展名策略,下表是 GCC 下默认文件扩展名的含义。
文件扩展名 | GCC 所理解的含义 |
---|---|
*.c | 该类文件为 C 语言的源文件 |
*.h | 该类文件为 C 语言的头文件 |
*.i | 该类文件为预处理后的 C 文件 |
*.C | 该类文件为 C++ 语言的源文件 |
*.cc | 该类文件为 C++ 语言的源文件 |
*.cxx | 该类文件为 C++ 语言的源文件 |
*.m | 该类文件为 Objective-C 语言的源文件 |
*.s | 该类文件为汇编语言的源文件 |
*.o | 该类文件为汇编后的目标文件 |
*.a | 该类文件为静态库 |
*.so | 该类文件为共享库 |
a.out | 该类文件为链接后的输出文件 |
GCC 下有很多编译器,可以支持 C 语言、C++ 语言等多种语言,下表是常用的几个编译器。
GCC 编译器命令 | 含 义 |
---|---|
cc | 指的是 C 语言编译器 |
gcc | 指的是 C 语言编译器 |
cpp | 指的是预处理编译器 |
g++ | 指的是 C++ 语言编译器 |
进行程序编译的时候,头文件路径和库文件路径是编译器默认查找的地方,参见下表(默认路径):
类型 | 存放路径 |
---|---|
头文件 | 按照先后顺序査找如下目录: /usr/local/include /usr/lib/gcc/i686-Linux-gnu/4.6.3/include /usr/include |
库文件 | 按照先后顺序查找如下路径: /usr/lib/gcc/i686-Linux-gnu/4.6.3/ /usr/lib/gcc/i686-Linux-gnu/4.6.3/ /usr/lib/gcc/i686-Linux-gnu/4.6.3/…/…/…/i686-Linux-gnu/lib/i686-Linux-gnu/4.6.3/ /usr/lib/gcc/i686-Linux-gnu/4.6.3/…/…/…/i686-Linux-gnu/lib/ /usr/lib/gcc/i686-Linux-gnu/4.6.3/…/…/…/i686-Linux-gnu/4.6.3/ /usr/lib/gcc/i686-Linux-gnu/4.6.3/…/…/…/ /lib/i686-Linux-gnu/4.6.3/ /lib/ /usr/lib/i686-Linux-gnu/4.6.3/ /usr/lib/ |
㊨ 注意:
gcc 和 GCC 是两个不同的东西。
GCC:GNU Compiler Collection(GUN 编译器集合),它可以编译 C、C++、JAV、Fortran、Pascal、Object-C、Ada 等语言。
gcc 是 GCC 中的 GUN C Compiler(C 编译器)。
g++ 是 GCC 中的 GUN C++ Compiler(C++ 编译器)。
一个有趣的事实就是,就本质而言,gcc 和g ++ 并不是编译器,也不是编译器的集合,它们只是一种驱动器,根据参数中要编译的文件的类型,调用对应的GUN编译器而已。
由于编译器是可以更换的,所以 gcc 不仅仅可以编译 C 文件。所以,更准确的说法是:gcc 调用了 C compiler,而 g++ 调用了 C++ compiler。
gcc 和 g++ 的主要区别
-
对于 *.c 和 *.cpp 文件,gcc 分别当做 c 和 cpp 文件编译(c 和 cpp 的语法强度是不一样的)
-
对于 *.c 和 *.cpp 文件,g++ 则统一当做 cpp 文件编译
-
使用 g++ 编译文件时,g++ 会自动链接标准库 STL,而 gcc 不会自动链接 STL
-
gcc 在编译 C 文件时,可使用的预定义宏是比较少的
-
gcc 在编译 cpp 文件时 / g++ 在编译 c 文件和 cpp 文件时(这时候 gcc 和 g++ 调用的都是 cpp 文件的编译器),会加入一些额外的宏。
-
在用 gcc 编译 c++ 文件时,为了能够使用 STL,需要加参数 -lstdc++ ,但这并不代表 gcc -lstdc++ 和 g++ 等价,它们的区别不仅仅是这个。
2. 编译程序的基本知识
GCC 编译器对程序的编译过程如下图所示,分为 4 个阶段:预编译、编译和优化、汇编、链接。
源文件、目标文件和可执行文件是编译过程中经常用到的名词。
源文件通常指存放可编辑代码的文件,如存放 C、C++ 和汇编语言的文件。
目标文件是指经过编译器的编译生成的 CPU 可识别的二进制代码,但是目标文件一般不能执行,因为其中的一些函数过程没有相关的指示和说明。
可执行文件就是目标文件与相关的库链接后的文件,它是可以执行的。
预编译过程将程序中引用的头文件包含进源代码中,并对一些宏进行替换。
编译过程将用户可识别的语言翻译成一组处理器可识别的操作码,生成目标文件,通常翻译成汇编语言,而汇编语言通常和机器操作码之间是一种一对一的关系。GNU 中有 C/C++ 编译器 GCC 和汇编器 as 。
所有的目标文件必须用某种方式组合起来才能运行,这就是链接的作用。目标文件中通常仅解析了文件内部的变量和函数,对于引用的函数和变量还没有解析,这需要将其他已经编写好的目标文件引用进来,将没有解析的变量和函数进行解析,通常引用的目标是库。链接完成后会生成可执行文件。
3. 单个文件编译成执行文件
在 Linux 下使用 GCC 编译器编译单个文件十分简单,直接使用 gcc 命令后面加上要编译的 C 语言的源文件,GCC 会自动生成文件名为 a.out 的可执行文件。
自动编译的过程包括头文件扩展、目标文件编译,以及链接默认的系统库生成可执行文件,最后生成系统默认的可执行程序 a.out。
下面是一个程序的源代码,代码的作用是在控制台输出“ Hello World! ”字符串。
/* hello.c */
#include <stdio.h> /* 头文件包含 */
int main(void)
{
printf ("Hello World! \n"); /* 打印"Hello World! " */
return 0;
}
将代码存入 hello.c 文件中,运行如下命令将代码直接编译成可以执行文件:
$ gcc hello.c
在前面中列出了 GCC 编译器可以识别的默认文件扩展名,通过检査 hello.c 文件的扩展名,GCC 知道这是一个 C 文件。
使用上面的编译命令进行编译的时候,GCC 先进行扩展名判断,选择编译器。由于 hello.c 的扩展名为 .c ,GCC 认为这是一个 C 文件,会选择 gcc 编译器来编译 hello.c 文件。
GCC 将采取默认步骤,先将 C 文件编译成目标文件,然后将目标文件链接成可执行文件,最后删除目标文件。上述命令没有指定生成执行文件的名称,GCC 将生成默认的文件名 a.out 。运行结果如下:
$ ./a.out (执行 a.out 可执行文件)
Hello World!
如果希望生成指定的可执行文件名,选项 -o 可以使编译程序生成指定文件名,例如将上述程序编译输出一个名称为 test 的执行程序:
$ gcc -o test hello.c
上述命令把 hello.c 源文件编译成可执行文件 test 。运行可执行文件 test,向终端输出“ Hello World! ”字符串。运行结果如下:
$ ./test
Hello World!
4. 编译生成目标文件
目标文件是指经过编译器的编译生成的 CPU 可识别的二进制代码,因为其中一些函数过程没有相关的指示和说明,目标文件不能执行。
在前面介绍了直接生成可执行文件的编译方法,在这种编译方法中,中间文件作为临时文件存在,在可执行文件生成后,会删除中间文件。在很多情况下需要生成中间的目标文件,用于不同的编译目标。
GCC 的 -c 选项用于生成目标文件,这一选项将源文件生成目标文件,而不是生成可执行文件。默认情况下生成的目标文件的文件名和源文件的名称一样,只是扩展名为 .o 。例如,下面的命令会生成一个名字为 hello.o 的目标文件:
$ gcc -c hello.c
如果需要生成指定的文件名,可以使用 -o 选项。下面的命令将源文件 hello.c 编译成目标文件,文件名为 test.o :
$ gcc -c -o test.o hello.c
可以用一条命令编译多个源文件,生成目标文件,这通常用于编写库文件或者一个项目中包含多个源文件。例如一个项目包含 file1.c、file2.c 和 file3.c,下面的命令可以将源文件生成 3 个目标文件:file1.o、file2.o 和 file3.o :
$ gcc -c file1.c file2.c file3.c
5. 多文件编译
GCC 可以自动编译链接多个文件,不管是目标文件还是源文件,都可以使用同一个命令编译到一个可执行文件中。
例如一个项目包含两个文件,文件 string.c 中有一个函数 StrLen 用于计算字符串的长度,而在 main.c 中调用这个函数将计算的结果显示出来。
1. 源文件string.c
文件 string.c 的内容如下。文件中主要包含了用于计算字符串长度的函数 StrLen() 。 StrLen() 函数的作用是计算字符串的长度,输入参数为字符串的指针,输出数值为计算字符串长度的计算结果。StrLen() 函数将字符串中的字符与 ’ \0 ’ 进行比较并进行字符长度计数, 获得字符串的长度。
/* string.c */
#define ENDSTRING '\0' /* 定义字符串 */
int StrLen(char *string)
{
int len = 0;
while(*string++ != ENDSTRING) /* 当 *string 的值为' \0 '时,停止计算 */
{
len++;
}
return len; /* 返回此值 */
}
2. 源文件 main.c
在文件 main.c 中是 main() 函数的代码,如下代码所示。main() 函数调用 Strlen() 函数计算字符串 Hello Dymatic 的长度,并将字符串的长度打印出来。
/* main.c */
#include <stdio.h>
extern int StrLen(char* str); /* 声明 Strlen 函数 */
int main(void)
{
char src[]="Hello Dymatic"; /* 字符串 */
printf("string length is:%d\n",StrLen(src)); /* 计算 src 的长度,将结果打印出来 */
return 0;
}
3. 编译运行
下面的命令将两个源文件中的程序编译成一个执行文件,文件名为 test 。
$ gcc -o test string.c main.c
执行编译出来的可执行文件 test ,程序的运行结果如下:
$ ./test
String length is:13
当然可以先将源文件编成目标文件,然后进行链接。例如,下面的过程先将 String.c 和 main.c 源文件编译成目标文件 string.o 和 main.o ,然后将 string.o 和 main.o 链接生成 test:
$ gcc -c string.c main.c
$ gcc -o test string.o main.o
6. 预处理
在 C 语言程序中,通常需要包含头文件并会定义一些宏。预处理过程将源文件中的头文件包含进源文件中,并且将文件中定义的宏进行扩展。
编译程序时选项 -E 告诉编译器进行预编译操作。例如如下命令将文件 string.c 的预处理结果显示在计算机屏幕上:
$ gcc -E string.c
如果需要指定源文件预编译后生成的中间结果文件名,需要使用选项 -o 。例如,下面的代码将文件 string.c 进行预编译,生成文件 string.i。string.i 内容如下:
$ gcc -o string.i -E string.c
# 1 "string.c"
# 1 "<built-in>"
# 1 "<命令行> "
# 1 "string.c"
int StrLen(char *string)
{
int len = 0;
while(*string++ != '\0')
{
len++;
}
return len;
}
可以发现之前定义的宏 ENDSTRING ,已经被替换成了 “ \0 ”。
7. 编译成汇编语言
编译过程将用户可识别的语言翻译成一组处理器可识别的操作码,通常翻译成汇编语言。汇编语言通常和机器操作码之间是一对一的关系。
生成汇编语言的 GCC 选项是 -S ,默认情况下生成的文件名和源文件一致,扩展名为 .s。 例如,下面的命令将 C 语言源文件 string.c 编译成汇编语言,文件名为 string.s
$ gcc -S string.c
下面是编译后的汇编语言文件 string.s 的内容。其中,第 1 行内容是 C 语言的文件名, 第 3 行和第 4 行是文件中的函数描述,标签 StrLen 之后的代码用于实现字符串长度的计算。
.file "string.c"
.text
.globl StrLen
.type StrLen, @function
StrLen:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
subl $16, %esp
movl $0, -4(%ebp)
jmp .L2
L3:
addl $1, -4(%ebp)
L2:
movl 8 (%ebp), %eax
movzbl (%eax), %eax
testb %al, %al
setne %al
addl $1, 8(%ebp)
testb %al, %al
jne .L3
movl -4(%ebp), %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE0:
.size StrLen,
.-StrLen
.ident "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3"
.section .note.GNU-stack,"",@progbits
8. 生成和使用静态链接库
静态库是 obj 文件的一个集合,通常静态库以“ .a ”为后缀。静态库由程序 ar 生成, 现在静态库己经不像之前那么普遍了,这主要是由于程序都在使用动态库。
静态库的优点是可以在不用重新编译程序库代码的情况下,进行程序的重新链接,这种方法节省了编译过程的时间(在编译大型程序的时候,需要花费很长时间)。
但是由于现在系统的强大,编译的时间已经不是问题。静态库的另一个优势是开发者可以提供库文件给使用的人员,不用开放源代码,这是库函数提供者经常采用的手段。如果其他开发人员要使用你的程序,而你又不想给其源码,提供静态库是一种选择。当然这也是程序模块化开发的一种手段,使每个软件开发人员的精力集中在自己的部分。在理论上,静态库的执行速度比共享库和动态库要快(1%〜5%),但实际上可能并非如此。由此看来,除了使用方便外,静态库可能并非一种好的选择。。
静态库:是在可执行程序运行前就已经加入到执行码中,成为执行程序的一部分;共享库:是在执行程序启动时加载到执行程序中,可以被多个执行程序共享使用。
然而,它们又各有优点。建议库开发人员创建共享库,比较明显的优势在于库是独立的,便于维护和更新;而静态库的更新比较麻烦,一般不做推荐。
静态库可以认为是一些目标代码的集合。按照习惯,一般以 “ .a ” 做为文件后缀名。使用 ar(archiver)命令可以创建静态库。因为共享库有着更大的优势,静态库已经不经常使用。但静态库使用简单,仍有使用的余地,并会一直存在。有些 Unix 系统,如 Solaris 10,已经基本废弃了静态库。
1. 生成静态链接库
生成静态库,或者将一个 obj 文件加到已经存在的静态库的命令为 “ ar 库文件 obj 文件 1 obj 文件 2 ”。创建静态库的最基本步骤是生成目标文件,这点前面己经介绍过。然后使用工具 ar 对目标文件进行归档。工具 ar 的 -r 选项,可以创建库,并把目标文件插入到指定库中。例如,将 string.o 打包为库文件 libstr.a 的命令为:
$ ar -rcs libstr.a string.o
2. 使用静态链接库
在编译程序的时候经常需要使用函数库,例如经常使用的 C 标准库等。GCC 链接时使用库函数和一般的 obj 文件的形式是一致的,例如对 main.c 进行链接的时候,需要使用之前己经编译好的静态链接库 libstr.a ,命令格式如下:
$ gcc -o test main.c libstr.a
也可以使用命令“ -l 库名 ”进行,库名是不包含函数库和扩展名的字符串。例如编译 main.c 链接静态库 libstr.a 的命令可以修改为:
$ gcc -o test main.c -lstr
上面的命令将在系统默认的路径下查找 str 函数库,并把它链接到要生成的目标程序上,可能系统会提示无法找到库文件 str,这是由于 str 库函数没有在系统默认的查找路径下,需要显示指定库函数的路径,例如库文件和当前编译文件在同一目录下:
$ gcc -o test main.c -L./ -lstr
㊨ 注意:在使用 -l 选项时,-o 选项的目的名称要在 -l 链接的库名称之前,否则 gcc 会认为 -I 是生成的目标而出错。
9. 生成动态链接库
动态链接库是程序运行时加载的库,当动态链接库正确安装后,所有的程序都可以使用动态库来运行程序。
动态链接库是目标文件的集合,目标文件在动态链接库中的组织方式是按照特殊方式形成的。
库中函数和变量的地址是相对地址,不是绝对地址,其真实地址在调用动态库的程序加载时形成。
动态链接库的名称有别名( soname )、真名( realname )和链接名( linkername )。
别名由一个前缀 lib,然后是库的名字,再加上个后缀“ .so ”构成。
真名是动态链接库的真实名称,一般总是在别名的基础上加上一个小版本号、发布版本等构成。
除此之外,还有一个链接名,即程序链接时使用的库的名字。
在动态链接库安装的时候,总是复制库文件到某个目录下,然后用个软链接生成别名,在库文件进行更新的时候,仅仅更新软链接即可。
1. 生成动态链接库
生成动态链接库的命令很简单,使用 -fPIC 选项或者选项 -fpic 和选项的作用是使得 gcc 生成的代码是位置无关的,例如下面的命令将 string.c 编译生成动态链接库:
$ gcc -shared -Wl,-soname,libstr.so -o libstr.so.1 string.c
其中,选项“ -soname,jibstr.so ”表示生成动态库时的别名是 libstr.so ; “ -o libstr.so.1 ” 选项则表示是生成名字为 libstr.so.1 的实际动态链接库文件;-shared 告诉编译器生成一个动态链接库。
生成动态链接库之后一个很重要的问题就是安装,一般情况下将生成的动态链接库复制到系统默认的动态链接库的搜索路径下,通常有 /lib、/usr/lib、/usr/local/lib,放到以上任何一个目录下都可以。
2. 动态链接库的配置
动态链接库不能随意使用,要在运行的程序中使用动态链接库,需要指定系统的动态链接库搜索的路径,让系统找到运行所需的动态链接库才可以。
系统中的配置文件 /etc/ld.so.conf 是动态链接库的搜索路径配置文件。在这个文件内,存放着可被 Linux 共享的动态链接库所在目录的名字(系统目录 /lib、/usr/lib 除外),多个目录名间以空白字符(空格、换行等)或冒号或逗号分隔。查看系统中的动态链接库配置文件的内容:
$ cat /etc/ld.so.conf
include /etc/ld.so.conf.d/*.conf
Ubuntu 的配置文件将目录 /etc/ld.so.conf.d 中的配置文件包含进来,对这个目录下的文件进行査看:
$ ls /etc/ld.so.conf.d/
i386-linux-gnu_GL.conf libc.conf
i686-linux-gnu.conf vmware-tools-libraries.conf
$ cat /etc/ld.so.conf.d/i686-linux-gnu.conf # 査看配置文件 i486-linux-gnu.conf
# Multiarch support
/lib/i386-linux-gnu
/usr/lib/i386-linux-gnu
/lib/i686-linux-gnu
/usr/lib/i686-linux-gnu
从上面的配置文件可以看出,在系统的动态链接库配置中,包含了该动态库 /Iib/i386-linux-gnu、/usr/lib/i386-linux-gnu 和 /lib/i686-linux-gnu、/usr/lib/i686-linux-gnu 四个目录。
3. 动态链接库管理命令
为了让新增加的动态链接库能够被系统共享,需要运行动态链接库的管理命令 ldconfig 。ldconfig 命令的作用是在系统的默认搜索路径,和动态链接库配置文件中所列出的目录里搜索动态链接库,创建动态链接装入程序需要的链接和缓存文件。搜索完毕后, 将结果写入缓存文件 /etc/ldsoxache 中,文件中保存的是己经排好序的动态链接库名字列表。ldconfig 命令行的用法如下,其中选项的含义参见下表(ldconfig 的选项含义)
ldconfig [-V |--verbose] [-n] [-N] [-X] [-f CONF] [-C CACHE] [-r ROOT] [-l]
[-p|--print-cache] [-c FORMAT] [--format=FORMAT] [-V] [-?|--help|--usage] path …
选项 | 含义 |
---|---|
-v | 此选项打印 ldconfig 的当前版本号,显示所扫描的每一个目录和动态链接库 |
-n | 此选项处理命令行指定的目录,不对系统的默认目录 /lib、/usr/lib 进行扫描,也不对配置文件 /etc/ld.so.conf 中所指定的目录进行扫描 |
-N | 此选项 ldconfig 不会重建缓存文件 |
-X | 此选项 ldconfig 不更新链接 |
-fCONF | 此选项使用用户指定的配置文件代替默认文件 /etc/ld.so.conf |
-C CACHE | 此选项使用用户指定的缓存文件代替系统默认的缓存文件 /etc/ld.so.cache |
-r ROOT | 此选项改变当前应用程序的根目录 |
-l | 此选项用于手动链接单个动态链接库 |
-p 或 --print-cache | 此选项用于打印出缓存文件中共享库的名字 |
如果想知道系统中有哪些动态链接库,可以使用 ldconfig 的 -p 选项来列出缓存文件中的动态链接库列表。下面的命令中表明在系统缓存中共有 682 个动态链接库。
$ ldconfig -p (列出当前系统中的动态链接库)
在缓冲区"/etc/ld.so.cache"中找到809个库 (缓存中的动态链接库的数目)
libzephyr.so.4 (libc6) => /usr/lib/libzephyr.so.4
libzeitgeist-1.0.so.1 (libc6) => /usr/lib/libzeitgeist-1.0.so.1
libz.so.l (libc6) => /lib/i386-linux-gnu/libz.so.1
libyelp.so.O (libc6) => /usr/lib/libyelp.so.0
libyajl.so.1 (libc6) => /usr/lib/i386-linux-gnu/libyaj1.so.1
使用 ldconfig 命令,默认情况下并不将扫描的结果输出。使用 -v 选项会将 ldconfig 在运行过程中扫描到的目录和共享库信息输出到终端,用户可以看到运行的结果和中间的信息。在执行 ldconfig 后,将刷新缓存文件 /etc/ld.so.cache 。
$ ldconfig -v
/usr/lib: (扫描 /usr/lib 目录中的动态链接库)
libdb-4.3.so -> libdb-4.3.so
libXcursor.so.1 -> libXcursor.so.1.0.2
/usr/lib/i686: (hwcap: 0x2000000000000) (扫描 /usr/lib/i486 目录中的动态链接库)
libssl.so.0.9.8 -> libssl.so.0.9.8
libcrypto.so.0.9.8 -> libcrypto.so.0.9.8
...
当用户的目录并不在系统动态链接库配置文件 /etc/ld.so.conf 中指定的时候,可以使用 ldconfig 命令显示指定要扫描的目录,将用户指定目录中的动态链接库放入系统中进行共享。命令格式的形式为:
ldconfig 目录名
这个命令将 ldconfig 指定的目录名中的动态链接库放入系统的缓存 /etc/ld.so.cache 中, 从而可以被系统共享使用。下面的代码将扫描当前用户的 lib 目录,将其中的动态链接库加入系统:
$ ldconfig ~/lib
㊨ 注意:如果在运行上述命令后,再次运行 ldconfig 而没有加参数,系统会将 /lib、/usr/lib 及 /etc/ld.so.conf 中指定目录中的动态库加入缓存,这时候上述代码中的动态链接库可能不被系统共享了。
4. 使用动态链接库
在编译程序时,使用动态链接库和静态链接库是一致的,使用“ -l 库名 ”的方式,在生成可执行文件的时候会链接库文件。例如下面的命令将源文件 main.c 编译成可执行文件 test,并链接库文件 libstr.a 或者 libstr.so :
$ gcc -o test main.c -L./ -lstr
-L 指定链接动态链接库的路径,-Istr 链接库函数 str。
但是运行 test 一般会出现如下问题:
./test: error while loading shared libraries: libstr.so: cannot open shared
object file: No such file or directory
这是由于程序运行时没有找到动态链接库造成的。程序编译时链接动态裢接库和运行时使用动态链接库的概念是不同的,在运行时,程序链接的动态链接库需要在系统目录下才行。有以下几种办法可以解决此问题。
█ 将动态链接库的目录放到程序搜索路径中,可以将库的路径加到环境变量 LD_LIBRARY_PATH 中实现,例如:
$ export LD_LIBRARY_PATH=/example/ex02: $ LD_LIBRARY_PATH
将存放库文件 libstnso 的路径 /example/ex02 加入到搜索路径中,再运行程序就没有之前的警告了。
█ 另一种方法是使用 ld-Linux.so.2 来加载程序,命令格式为:
/lib/ld-Linux.so.2 --library-path 路径 程序名
加载 test 程序的命令为:
/lib/ld-Linux.so.2 --library-path /example/ex02 test
㊨ 注意:如果系统的搜索路径下同时存在静态链接库和动态链接库,默认情况下会链接动态链接库。如果需要强制链接静态链接库,需要加上“ -static ”选项,即上述的编译方法改为如下的方式:
$ gcc -o test main.c -static -lstr
10. 动态加载库
动态加载库和一般的动态链接库所不同的是,一般动态链接库在程序启动的时候就要寻找动态库,找到库函数;而动态加载库可以用程序的方法来控制什么时候加载。动态加载库主要有函数 dlopen()、dlerror()、dlsym() 和 dlclose()。
1. 打开动态库 dlopen() 函数
函数 dlopen() 按照用户指定的方式打开动态链接库,其中参数 filename 为动态链接库的文件名,flag 为打开方式,一般为 RTLD_LASY,函数的返回值为库的指针。其函数原型如下:
void * dlopen(const char *filename, int flag);
例如,下面的代码使用 dlopen 打开当前目录下的动态库 libstr.so 。
void *phandle = dlopen ("./libstr. so", RTLD_LAZY);
2. 获得函数指针 dlsym()
使用动态链接库的目的是调用其中的函数,完成特定的功能。函数 dlsym() 可以获得动态链接库中指定函数的指针,然后可以使用这个函数指针进行操作。函数 dlsym() 的原型如下:
void * dlsym(void *handle, char *symbol);
其中参数 handle 为 dlopen() 打开动态库后返回的句柄,参数 symbol 为函数的名称,返回值为函数指针。
3. 使用动态加载库的一个例子
下面是一个动态加载库使用的例子。首先使用函数 dlopen() 来打开动态链接库,判断是否正常打开,可以使用函数 dlerror() 判断错误。如果上面的过程正常,使用函数 dlsym() 来获得动态链接库中的某个函数,可以使用这个函数来完成某些功能。其代码如下:
/* 动态加载库示例 */
#include <dlfcn.h> /* 动态加载库库头 */
int main(void)
{
char src[]="Hello Dymatic"; /* 要计算的字符串 */
int (*pStrLenFun) (char *str); /* 函数指针 */
void *phandle = NULL; /* 库句柄 */
char *perr = NULL; /* 错误信息指针 */
phandle = dlopen("./libstr.so", RTLD_LAZY); /* 打开 libstr.so 动态链接库 */
/* 判断是否正确打开 */
if(!phandle) /* 打开错误 */
{
printf("Failed Load library!\n"); /* 打印库不能加载信息 */
}
perr = dlerror(); /* 读取错误值 */
if(perr != NULL) /* 存在错误 */
{
printf("%s\n",perr);
return 0; /* 正常返回 */
}
pStrLenFun = dlsym(phandle, "StrLen"); /* 获得函数 strLen 的地址 */
perr = dlerror(); /* 读取错误信息 */
if(perr != NULL) /* 存在错误 */
{
printf("%s\n",perr); /* 打印错误函数获得的错误信息 */
return 0; /*返回*/
}
printf("the string length is: %d\n",pStrLenFun(src)); /* 调用函数 pStrLenFunc 计算字符串的长度 */
dlclose(phandle); /* 关闭动态加载库 */
return 0;
}
编译上述文件的时候需要链接动态库 libdl.so,使用如下的命令将上述代码编译成可执行文件 testdl。命令将 main.c 编译成可执行文件 testdl,并链接动态链接库 libdl.so。
$ gcc -o testdl main.c libstr.so -ldl
执行文件 testdl 的结果为:
$ ./testdl
string length is:13
使用动态加载库和动态链接库的结果是一致的。
11. GCC 常用选项
除了之前介绍的基本功能外,GCC 的选项配置是编译时很重要的选择,例如头文件路径、加载库路径、警告信息及调试等。接下来对常用的选项进行介绍。
1. -DMACRO 选项
定义一个宏,在多种预定义的程序中会经常使用。如下代码根据系统是否定义 Linux 宏来执行不同的代码。使用 -D 选项可以选择不同的代码段,例如 -DOS_LINUX 选项将执行代码段 ①。
#ifdef OS_LINUX
...代码段 ①
#else
...代码段 ②
#endif
█ -Idir:将头文件的搜索路径扩大,包含 dir 目录。
█ -Ldir:将链接时使用的链接库搜索路径扩大,包含 dir 目录。gcc 都会优先使用共享程序库。
█ -static:仅选用静态程序库进行链接,如果一个目录中静态库和动态库都存在,则仅选用静态库。
█ -g:包括调试信息。
█ -On:优化程序,程序优化后执行速度会更快,程序的占用空间会更小。通常 gcc 会进行很小的优化,优化的级别可以选择,即 n。最常用的优化级别是 2。
█ -Wall:打开所有 gcc 能够提供的、常用的警告信息。
2. GCC 的常用选项及含义
下表(常用的编译选项及含义)中是 GCC 的常用选项和含义,主要列出了警告选项、代码检查、ANSI 兼容等。 可以在编译程序的时候对 GCC 的选项进行设置,编写质量高的代码。
㊨ 注意:在编写代码的时候,不好的习惯会造成程序执行过程中发生错误。在一个比较大的项目中,当程序运行起来后再查找这些错误是很困难的。因此一种好的习惯是使用编译选项将代码的警告信息显示出来,并对代码进行改正。例如,打开编译选项 -Wall 和 -W 来显示所有的警告信息,甚至更严格一些,打开 -Werror 将编译时的警告信息作为错误信息来处理,中断编译。
12. 编译环境的搭建
在安装 Ubuntu 的时候,默认情况下会安装 GCC。可以使用 which 命令来查看系统中是否已经安装了 GCC:
$ which gcc
如果不存在,使用 apt 进行升级,获得 gcc 包并且安装:
$ apt-get install gcc
如果对 C++ 感兴趣可以安装 g++。在编译器安装完毕后,可以使用 GCC 进行程序的编译。