Linux静态库与动态库、makefie文件以及gdb调试工具
1、Linux静态库和动态库的制作和使用
1.1 什么是库文件
在理解什么是库文件之前,我们首先要了解C语言程序的编译过程。下面简单介绍一下此过程:
库是一组预先编译好的函数的集合,这些函数是按照可重用的原则编写的。函数库最简单的形式是一组处于“准备好使用”状态的目标文件。当程序需要使用函数库中的某个函数时,编译器和链接器会将程序代码和函数库结合在一起生成一个单独的可执行文件。
库文件分为标准系统库文件和用户生成的库文件。
标准系统库文件一般存储在/lib和/usr/lib目录中。C语言编译(即链接程序)需要知道要搜索哪些库文件,因为在默认情况下,它只搜索标准C语言库。对于库文件若是仅把它放进标准库文件目录中就想直接使用是不行的,因为库文件必须遵循特定的命名规范,并且需要在命令行中明确指定。
所谓“无名天地之始,有名万物之母”,万事万物存在于这个世界中,人类首先要做的是给他们起名字,库文件也不例外。库文件的名字以lib开头,随后的部分指明这是什么库(例如,c代表c语言库,m代表数学库)。文件名的最后部分以 . 开始,之后给出库文件的类型:
- .a代表静态函数库;
- .so代表共享函数库。
1.2 如何调用库文件
函数库通常会以静态库和动态库的两种格式存在。对于函数库的调用有两种方式:
- 通过给出的完整的库文件路径名搜索库文件;
- 用 -l 标志告诉编译器要搜索的库文件。
举例:这里给出一个例子,调用我封装的一个数学计算函数库libeval.a。
若是不进行eval函数库的调用,可执行程序是无法生成的,过程如下:
dragon@dragonLinux:~/eval$ g++ -o eval Test_eval.cpp
/tmp/ccZIMgAc.o:在函数‘main’中:
Test_eval.cpp:(.text+0x326):对‘eval(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, double*)’未定义的引用
collect2: 错误:ld 返回 1
下面用绝对路径调用库文件,过程如下:
dragon@dragonLinux:~/eval$ g++ -o eval Test_eval.cpp /usr/local/lib/libeval.a
dragon@dragonLinux:~/eval$ ./eval
31.415926/10 = 3.14159
3.14159
用-leval来进行函数库的调用,过程如下:
dragon@dragonLinux:~/eval$ g++ -o eval Test_eval.cpp -leval
dragon@dragonLinux:~/eval$ ./eval
31.415926/10 = 3.14159
3.14159
-leval是简写的方式,它代表的是库目录(/usr/local/lib)中名为libeval.a的函数库。
dragon@dragonLinux:/usr/local/lib$ ls /usr/local/lib/libeval.a -l
-rw-r--r-- 1 root root 202040 10月 15 18:37 /usr/local/lib/libeval.a
此外,-l加库名的标志另一个好处是如果有库文件,编译器会自动选择动态库。
除了以上两种调用位于标准位置的库文件之外,也可以通过使用-L标志为编译器增加库文件的搜索路径。例如:
g++ -o eval Test_eval.cpp -L/home/dragon/eval -leval
这条命令用/home/dragon/eval目录中的libeval库来编译和链接eval程序。
1.3 静态库
静态库也称作归档文件(archive),它们的文件名都是以.a结尾。使用ar
命令(代表archive,即建立归档文件)创建静态库。下面通过一个实例演示如何创建与使用静态库,在本例中,我们将创建一个小型函数库,它包含两个函数,之后我们在程序中调用其中一个函数。
1)首先我们为这两个函数分别创建各自的源文件(将它们分别命名为free.c和style.c)
下面是第一个源程序:
#include <stdio.h>
void free(int arg)
{
printf("free style : %d\n",arg);
}
下面是第二个源程序:
#include <stdio.h>
void style(char *arg)
{
printf("free style %s\n", arg);
}
2)生成目标文件
利用gcc
命令编译以上两个函数生成要包含在库文件中的目标文件。通过gcc -c来完成,-c的作用是生成.o结尾的目标文件。过程如下:
dragon@dragonLinux:~/arch$ gcc -c free.c style.c
dragon@dragonLinux:~/arch$ ls
free.c free.o style.c style.o
3)为库文件创建一个头文件 lib.h,这个头文件将会对你的库文件中的函数进行声明,其他想要使用此库文件的程序都必须包含这个头文件。内容如下:
void myfree(int );
void style(char *);
4)现在编写一个调用style函数的程序program.c。它包含库的头文件,并且调用其中的一个函数。program.c内容如下:
#include <stdio.c>
#include "lib.h"
int main()
{
style("hello world");
return 0;
}
5)现在我们来编译并测试这个程序。
dragon@dragonLinux:~/arch$ gcc -c program.c
dragon@dragonLinux:~/arch$ gcc -o program program.o style.o
dragon@dragonLinux:~/arch$ ./program
free style hello world
6)创建静态库文件。使用ar
命令创建一个归档文件并将目标文件添加进去。
dragon@dragonLinux:~/arch$ ar crv libfree.a free.o style.o
a - free.o
a - style.o
7)使用静态库生成可执行程序。
dragon@dragonLinux:~/arch$ gcc -o program program.o libfree.a
dragon@dragonLinux:~/arch$ ./program
free style hello world
同样,也可以使用-l
选项来访问函数库,但因其未保存在准确位置,所以必须使用-L
选项来告诉编译器在何处找到它,如下所示:
dragon@dragonLinux:~/arch$ gcc -o program program.o -L. -lfree
dragon@dragonLinux:~/arch$ ./program
free style hello world
其中-L.
选项告诉编译器在当前目录(.)中查找函数库,-lfree
选项告诉编译器使用名为libfree.a的函数库。
- 注:静态库在编译的时候,主程序文件与静态库一起编译,把主程序与主程序中用到的库函数一起整合进了目标文件。这样做优点是在编译后的可执行程序可以独立运行,因为所使用的函数都已经被编译进去了。缺点是,如果所使用的静态库发生更新改变,我们的程序必须重新编译。
1.4 动态库
下面为大家介绍动态库。上一节提到的静态库的一个缺点是,当你同时运行许多程序且这些程序包含同一函数库的函数时,内存中就会有同一函数的多份副本,这会消耗大量的内存以及磁盘空间。
动态库的链接方式是,程序本身不再包含函数代码,而是引用运行时可访问的共享代码。当编译好的程序被装载到内存中执行时,函数引用被解析并产生对动态库的调用,如果有必要,动态库才被加载到内存中。这是动态库的一个好处。
动态库的命名方式与静态库类似,前缀相同,为“lib”,后缀变为“.so” ,中间部分为动态库名。
1)把程序文件free.c和style.c编译成动态库的指令:
gcc -fPIC -shared -o libfree.so free.c style.c
2)编译生成可执行程序:
使用动态库的方法与使用静态库的方法相同。如果动态库文件和静态库文件同时存在,系统优先使用动态库编译。
gcc -o program program.c -L/home/dragon/arch -lfree
3)执行./program
时,系统出现如下错误:
dragon@dragonLinux:~/arch$ ./program
./program: error while loading shared libraries: libfree.so: cannot open shared object file: No such file or directory
这是因为采用了动态链接库的可执行程序在运行时需要指定动态库文件的目录,大家还记得上次课讲到的一个环境变量LD_LIBRARY_PATH 吗?这里我们就需要修改这个环境变量,使得Linux系统中指定动态库文件的目录列表包含我们指定的目录。
采用以下命令设置LD_LIBRARY_PATH环境变量:
export LD_LIBRARY_PATH=$LD_LIBRABY_PATH:/home/dragon/arch
再次执行./program
命令:
dragon@dragonLinux:~/arch$ export LD_LIBRARY_PATH=$LD_LIBRABY_PATH:/home/dragon/arch
dragon@dragonLinux:~/arch$ ./program
free style hello world
4)动态库的另一个好处是动态库的更新可以独立于依赖它的应用程序。例如,我来修改动态库中style函数的代码,改为
void style(char *arg)
{
printf("hello free style , %s\n", arg);
}
重新编译动态库:
gcc -fPIC -shared -o libfree.so free.c style.c
无需重新编译book265,直接执行程序:
dragon@dragonLinux:~/arch$ ./program
hello free style , hello world
动态库在编译的时候只做语法检查,并没有被编译进目标代码,当程序执行到动态库中的函数时才调用该函数库里的代码。动态函数库并没有整合进程序,所以程序的运行环境必须提供动态库路径。如果所使用的动态库发生更新改变,程序不需要重新编译,所以动态库升级比较方便。
1.5 静态库与动态库的优缺点小结
1.5.1 静态库的优缺点
1、优点
静态链接相当于复制一份库文件到可执行程序中,不需要像动态库那样有动态加载和识别函数地址的开销,也就是说采用静态链接编译的可执行程序运行更快。
2、缺点
1)静态链接生成的可执行程序比动态链接生成的大很多,运行时占用的内存也更多。
2)库文件的更新不会反映到可执行程序中,可执行程序需要重新编译。
1.5.2动态库的优缺点
1、优点
1)相对于静态库,动态库在更新后(修复bug,增加新的功能),调用它的可执行不需要重新编译。
2)全部的可执行程序共享动态库的代码,运行时占用的内存空间更少。
2、缺点
1)使可执行程序在不同平台上移植变得更复杂,因为它需要为每个不同的平台提供相应平台的共享库。
2)增加可执行程序运行时的时间和空间开销,因为应用程序需要在运行过程中查找依赖的库函数,并加载到内存中。
2、makefie文件的编写
2.0 什么是makefile?
一个软件工程中的源文件不计数,其按照类型、功能、模块分别放在若干个目录和文件中,makefile定义了一系列的规则来指定,哪些文件需要先编译,那些文件需要后编译,那些文件需要重新编译,甚至进行更复杂的功能操作,这就有了我们的系统编译的工具。
在linux和unix中,有一个强大的实用程序,叫make,可以用它来管理多模块程序的编译和链接,直至生成可执行文件。
make程序需要一个编译规则说明文件,称为makefile,makefile文件中描述了整个软件工程的编译规则和各个文件之间的依赖关系。
makefile就像是一个shell脚本一样,其中可以执行操作系统的命令,它带来的好处就是我们能够实现“自动化编译”,一旦写好,只要一个make命令,整个软件工程就完全自动编译,极大的提高了软件开发的效率。
2.1 makefile的编写
makefile文件的规则可以非常复杂,比C程序还要复杂,我通过示例来介绍它的简单用法。
文件名:makefile,内容如下:
all: opcServer0_1 opcServer0_2 opcServerT
opcServer0_1:opcServer0_1.cpp
g++ -g -o opcServer0_1 opcServer0_1.cpp open62541.c -lpthread
opcServer0_2:opcServer0_2.cpp
g++ -g -o opcServer0_2 opcServer0_2.cpp open62541.c -lpthread
opcServerT:opcServerT.cpp
g++ -g -o opcServerT opcServerT.cpp open62541.c
clean:
rm -f opcServer0_1 opcServer0_2 opcServerT
第一行:
all: opcServer0_1 opcServer0_2 opcServerT
用all可以一次性创建多个文件,如果未指定all目标,make命令将只创建它在文件makefile中找到的第一个目标。
第二行:
opcServer0_1:opcServer0_1.cpp
opcServer0_1:表示需要编译的目标程序。
如果要编译目标程序opcServer0_1,需要依赖源文件opcServer0_1.cpp,当opcServer0_1.cpp的内容发生了变化,执行make的时候就会重新编译opcServer0_1。
第三行:
g++ -g -o opcServer0_1 opcServer0_1.cpp open62541.c -lpthread
这是一个编译命令,和在shell中输入的命令一样,但是要注意一个问题,在g++之前要用tab键,一定要用tab,空格不行。
第四至第七行与第二第三行含义相同。
第八行:
clean
清除目标文件,清除的命令由第八行之后的脚本来执行。
第九行:
rm -f opcServer0_1 opcServer0_2 opcServerT
清除目标文件的脚本命令,注意了,rm -f是直接删除,无须逐一确认。
2.2 make命令
makefile准备好了后,在命令提示符下执行make就可以编译makefile中all参数指定的目标文件。
执行make编译目标程序:
dragon@dragonLinux:~/OPC/OPC_T/open62541$ make
g++ -g -w -o opcServer0_1 opcServer0_1.cpp open62541.c -lpthread
g++ -g -w -o opcServer0_2 opcServer0_2.cpp open62541.c -lpthread
g++ -g -w -o opcServerT opcServerT.cpp open62541.c
再执行一次make:
dragon@dragonLinux:~/OPC/OPC_T/open62541$ make
make: Nothing to be done for 'all'.
因为全部的目标程序都是最新的,所以提示没有目标可以编译。
执行make clean,执行清除目标文件的指令。
dragon@dragonLinux:~/OPC/OPC_T/open62541$ make clean
rm -f opcServer0_1 opcServer0_2 opcServerT
再执行make重新编译。
dragon@dragonLinux:~/OPC/OPC_T/open62541$ make
g++ -g -w -o opcServer0_1 opcServer0_1.cpp open62541.c -lpthread
g++ -g -w -o opcServer0_2 opcServer0_2.cpp open62541.c -lpthread
g++ -g -w -o opcServerT opcServerT.cpp open62541.c
修改opcServer0_1.cpp的代码,然后再执行make:
dragon@dragonLinux:~/OPC/OPC_T/open62541$ make
g++ -g -w -o opcServer0_1 opcServer0_1.cpp open62541.c -lpthread
注意,因为opcServer0_1依赖的源程序之一opcServer0_1.cpp改变了,所以opcServer0_1重新编译。
opcServer0_2和opcServerT没有重新编译,因为这两个文件依赖的源文件并没有改变。
2.3 makefile文件中的宏
makefile中的宏常被用于设置编译器的选项,可以通过语句MACRONAME=value在makefile中定义宏,引用宏的方法是使用$(MACRONAME)
或${MACRONAME}
。
我通过示例来介绍它的简单用法。
CC = g++
CFLAGS = -g -w
all: opcServer0_1 opcServer0_2 opcServerT
opcServer0_1:opcServer0_1.cpp
$(CC) $(CFLAGS) -o opcServer0_1 opcServer0_1.cpp open62541.c -lpthread
opcServer0_2:opcServer0_2.cpp
$(CC) $(CFLAGS) -o opcServer0_2 opcServer0_2.cpp open62541.c -lpthread
opcServerT:opcServerT.cpp
$(CC) $(CFLAGS) -o opcServerT opcServerT.cpp open62541.c
clean:
rm -f opcServer0_1 opcServer0_2 opcServerT
第一行
CC = g++
定义变量CC,赋值g++。
第二行
CFLAGS = -g -w
定义变量CFLAGS,赋值-g -w。
第五行
$(CC) $(CFLAGS) -o opcServer0_1 opcServer0_1.cpp open62541.c -lpthread
( C C ) 和 (CC)和 (CC)和(CFLAGS)就是使用宏CC和CFLAGS的值,执行make命令:
dragon@dragonLinux:~/OPC/OPC_T/open62541$ make
g++ -g -w -o opcServer0_1 opcServer0_1.cpp open62541.c -lpthread
g++ -g -w -o opcServer0_2 opcServer0_2.cpp open62541.c -lpthread
g++ -g -w -o opcServerT opcServerT.cpp open62541.c
在makefile文件中,使用宏的好处有两个:
1)如果在很多编译指令采用了变量,只要修改变量的值,就相当于修改全部的编译指令;
2)把比较长的、公共的编译指令采用变量来表示,可以让makefile更简洁。
3、使用GDB进行调试
我们在编写程序的时候不可能是一帆风顺的,gcc编译器可以发现程序代码的语法错误,但不能发现程序的业务逻辑错误,调试程序是软件开发的内容之一。调试程序的方法有很多种,例如可以用printf语句跟踪程序的运行步骤和显示变量的值,本节介绍一个功能强大的调试工具gdb。
用gcc编译源程序的时候,编译后的可执行文件不会包含源程序代码,如果您打算编译后的程序可以被调试,编译的时候要加-g的参数,例如:
g++ -g -o opcServerT opcServerT.cpp open62541.c
在命令提示符下输入gdb opcServerT就可以调试opcServerT程序了。
dragon@dragonLinux:~/OPC/OPC_T/open62541$ gdb opcServerT
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from opcServerT...done.
(gdb)
基本调试命令见下表:
命令 | 命令缩写 | 命令说明 |
---|---|---|
set args | 设置主程序的参数。 | |
break | b | 设置断点,b 20 表示在第20行设置断点,可以设置多个断点。 |
run | r | 开始运行程序, 程序运行到断点的位置会停下来,如果没有遇到断点,程序一直运行下去。 |
next | n | 执行当前行语句,如果该语句为函数调用,不会进入函数内部执行。 |
step | s | 执行当前行语句,如果该语句为函数调用,则进入函数执行其中的第一条语句。注意了,如果函数是库函数或第三方提供的函数,用s也是进不去的,因为没有源代码,如果是您自定义的函数,只要有源码就可以进去。 |
p | 显示变量值,例如:p name表示显示变量name的值。 | |
continue | c | 继续程序的运行,直到遇到下一个断点。 |
set valueName = value | 设置变量的值,假设程序有两个变量:int ii; char name[21];set var ii=10 把ii的值设置为10;set var name=“dtagon” 把name的值设置为"dragon",注意,不是strcpy。 | |
quit | q | 退出gdb环境。 |
4、作业
1)将自己常用的工具函数进行整合,分别封装成静态库与动态库,在自己的项目中进行调用。
2)将项目编写到makefile文件中,使用make进行项目编译。
3)使用调试已有项目,尝试使用gdb调试多线程程序。