
GCC

gcc&gdb常规操作 静态库、动态库的创建和链接
GCC
GCC
是Linux下的编译工具集,是GNU Compiler Collection
的缩写,包含gcc g++
等编译器
gcc工作流程
gcc
编译器对程序的编译分为四个阶段:预处理(预编译)、编译、汇编、链接
预处理:展开头文件、宏替换、去注释
gcc
调用预处理器来完成,得到的还是文本格式的源文件编译:
gcc
调用编译器进行编译,得到汇编文件汇编:
gcc
调用汇编器进行汇编,得到二进制文件链接:
gcc
调用链接器对程序所需要的库进行链接,得到可执行文件
文件名后缀 | 说明 | gcc参数 |
---|---|---|
.c |
源文件 | - |
.i |
预处理后的源文件 | -E |
.s |
汇编文件 | -S |
.o |
二进制文件 | -c |
演示
1
2
3
4
5
6
7
8
9
10# 预处理
g++ -E filename.c -o filename.i
# 编译
g++ -S filename.i -o filename.s
# 汇编
g++ -c filename.s -o filename.o
# 链接
g++ filename.o -o filename
# 运行
./filename
gcc常用参数
gcc编译选项 | 选项的意义 |
---|---|
-E |
预处理 |
-S |
编译 |
-c |
汇编 |
-o |
指定输出文件名 |
-I |
指定include包含文件的搜索目录 |
-g |
生成调试信息,该程序可被调试器调试 |
-D |
在编译时指定一个宏 |
-w |
不生成任何警告信息 |
-Wall |
生成所有警告信息 |
On |
0~3,四个优化选项-O3优化级别最高 |
-l |
编译时指定使用的库 |
-L |
指定编译时搜索库的路径 |
-fPic |
生成与位置无关的代码 |
-shared |
生成共享库 |
-std |
指定C标准 |
多文件编译
gcc
可以自动编译链接多个文件,不管是目标文件还是源文件,都可以使用一个命令编译到一个可执行文件中
假设有string.c
string.h
main.c
三个文件
因为有文件是包含在源文件中的,因此在使用gcc编译程序的时候不需要指定头文件的名字
(在头文件无法被找到的时候需要使用参数
-I
指定其具体路径而不是名字)
直接链接生成可执行程序
1
2gcc -o res string.c main.c
./res先将源文件编成目标文件,然后进行链接得到可执行程序
1
2
3gcc -c string.c main.c
gcc -o res string.o main.o
./res
gcc与g++
gcc
g++
的一些区别
在代码编译阶段
- 后缀为
.c
的,gcc 把它当作是 C 程序,而 g++ 当作是 C++ 程序 - 后缀为
.cpp
的,两者都会认为是 C++ 程序,C++ 的语法规则更加严谨一些 - g++ 会调用 gcc,对于 C++ 代码,两者是等价的,也就是说 gcc 和 g++ 都可以编译 C/C++ 代码
在链接阶段
- gcc 和 g++ 都可以自动链接到标准 C 库
- g++ 可以自动链接到标准 C++ 库,gcc 如果要链接到标准 C++ 库需要加参数 -lstdc++
关于__cplusplus
宏的定义
- g++ 会自动定义
__cplusplus
宏,但是这个不影响它去编译 C 程序 - gcc 需要根据文件后缀判断是否需要定义
__cplusplus
宏
- 不管是
gcc
还是g++
都可以编译C程序,编译程序的规则和参数都相同g++
可以直接编译C++程序,gcc
编译C++程序需要添加额外参数-lstdc++
- 不管是
gcc
还是g++
都可以定义__cplusplus
宏
静态库和动态库
不管是在Linux还是Windows中的库文件其本质和工作模式都是相同的
程序中调用的库包括
静态库
动态库
两种在项目中使用库一般有两个目的,一个是为了使程序更加简洁不需要在项目中维护太多的源文件,另一方面是为了源代码保密。当我们拿到了库文件,还需要库中API的声明,也就是头文件。
静态库
在Linux下静态库由程序
ar
生成
- Linux中静态库以
lib
作为前缀,以.a
作为后缀,中间是库的名字,即libxxx.a
- Windows中静态库一般以
lib
作为前缀,lib
作为后缀,中间是库的名字,即libxxx.lib
生成静态链接库
生成静态库,需要先对源文件进行汇编操作(使用参数
-c
)得到二进制格式的目标文件(.o格式)然后通过
ar
工具将目标文件打包就可以得到静态库文件
c
:创建一个库,不管库是否存在都创建s
:创建目标文件索引,这在创建较大的库时能加快时间r
:在库中插入模块,默认新的成员添加在库的结尾处,如果模块名已经存在库中,则替换同名的模块
生成静态链接库的具体步骤如下
对源文件进行汇编,得到二进制文件
1
gcc <src(*.c)> -c
对二进制文件打包,得到静态库
1
ar rcs <libname.a> <bin(*.o)>
发布静态库
1
# 提供头文件和libxxx.a
静态库的使用
拿到发布的静态库
head.h
libcalc.a
将静态库、头文件、测试程序放在同一目录下进行测试
1 | gcc main.c -o app -L <path> -l<lib> # 编译测试程序 |
动态库
动态库是程序运行时加载的库,当动态库正确部署之后,运行的多个程序可以使用同一个加载到内存中的动态库
动态链接库是目标文件的集合,目标文件在动态链接库中的组织方式是按照特殊方式形成的。库中函数和变量的
地址使用的是相对地址(静态库中使用的是绝对地址),其真实地址是在程序加载动态库时形成的
- 在Linux中动态库以
lib
作为前缀,以so
作为后缀,中间为库名,即libxxx.so
- 在Windows中动态库一般以
lib
作为前缀,以dll
作为后缀,中间为库名,即libxxx.dll
生成动态链接库
生成动态链接库是直接使用
gcc
命令并且需要添加-fPic
和-shared
参数
-fPic
参数是使得gcc
生成的代码是与位置无关的,也就是相对位置-shared
是告诉编译器生成一个动态链接库
生成动态链接库的具体步骤如下
对源文件进行汇编,需要
-c
和-fPic
参数1
gcc <src(*.c)> -c -fpic
将得到的
.o
打包成动态库,使用gcc
加-shared
参数1
gcc -shared <bin(*.o)> -o <libname.so>
发布动态库和头文件
1
# 提供头文件和libxxx.so
动态链接库使用
拿到发布的动态库和头文件
1 | gcc main.c -o app -L <path> -l<lib> |
解决动态链接库无法加载问题
库的工作原理
静态库如何被加载
在程序编译的最后一个阶段也就是链接阶段,提供的静态库会被打包到可执行程序中。当可执行程序被执行
静态库中的代码也会一并被加载到内存中,因此不会出现静态库找不到无法被加载的问题
动态库如何被加载
在程序链接阶段
在
gcc
命令虽然指定了库路径(-L
),但是这个路径并没有记录到可执行程序,只是检查了这个路径下的库文件是否存在
同样对应的动态库文件也没有被打包到可执行程序中,只是在可执行程序中记录了库的名字
在程序执行后
程序执行的时候会先检测需要的动态库是否可以被加载,加载不到就会提示上边的错误信息
当动态库中的函数在程序中被调用了,这个时候动态库才被加载到内存,如果不被调用就不加载
动态库的检测和内存加载操作都是由动态连接器来完成的
动态链接器
动态链接器是一个独立于应用程序的进程,属于操作系统,当用户的程序需要加载动态库的时候动态链接器就开始
工作,显然动态链接器根本就不知道用户通过
gcc
编译程序的时候通过-L
参数指定的路径那么动态链接器是如何搜索某一个动态库的呢,在它内部有一个默认的搜索顺序,按照优先级从高到低的顺序
- 可执行文件内部的
DT_RPATH
段- 系统的环境变量
LD_LIBRARY_PATH
- 系统动态库的缓存文件
/etc/ld.so.cache
- 存储动态库、静态库的系统目录
/lib/
/usr/lib
等如果按照上面的顺序一次搜索后仍没找到,动态链接器就会提示动态库找不到的错误信息
解决方案
方案一、将库路径添加到环境变量
LD_LIBRARY_PATH
中找到相关配置文件
用户级别:
~/.bashrc
对当前用户有效系统级别:
/etc/profile
对所有用户有效修改配置文件
1
export LIBRARY_PATH=$LIBRARY_PATH:<path>
使配置文件生效
1
2source ~/.bashrc
source /etc/profile
方案二、更新
/etc/ld.so.cache
文件找到动态库的绝对路径
修改
/etc/ls.so.conf
文件,将上面路径加到文件中更新
/etc/ld.so.conf
中的数据到/etc/ls.so.cache
中1
sudo ldconfig
方案三、拷贝动态库到系统库目录
/lib
或者/usr/lib
中(或将库的软链接文件放进去)1
2
3
4
5# 库拷贝
sudo cp <path>/libxxx.so /usr/lib
# 创建软链接
sudo ln -s <path>/libxxx.so /usr/lib/libxxx.so
# 注意必须使用绝对路经
验证
1 | ldd <exec> |
优缺点
静态库
- 优点
- 静态库被打包到应用程序中加载速度很快
- 发布程序无需提供静态库,移植方便
- 缺点
- 相同的库文件数据可能在内存中被加载多份,消耗系统资源,浪费内存
- 库文件更新需要重新编译项目,生成新的可执行程序,浪费时间
动态库
- 优点
- 可实现不同进程间的资源共享
- 动态库升级简单,只需要替换库文件,无需重新编译应用程序
- 可以控制何时加载动态库,不调用库函数动态库不会被加载
- 缺点
- 加载速度比静态库慢,现在计算机性能可以忽略
- 发布程序需要提供依赖的动态库
GDB
gdb
是由GNU软件系统社区提供的调试器,同gcc
配套组成完整的开发环境
gdb
是一套字符界面的程序集,可以使用命令gdb
加载要调试的程序
调试准备
项目程序如果为了进行调试而编译时,必须打开调试选项(
-g
)此外,尽量不要使用编译器优化选项(
-On
)
-Wall
打开所有waring
启动和退出gdb
启动gdb
1 | gdb <exec> |
命令行传参
如果程序在启动时需要传递命令行参数,如果要调试这类程序,这些命令行参数必须要在应用程序启动之前通过调试
程序的
gdb
进程传递进去
1 | gcc <src> -o -g <exec> |
gdb中启动程序
在
gdb
中启动要调试的应用程序有两种方式,一种是使用run
,另一种是使用start
run
:可以缩写为r
,如果程序设置了断点会停在第一个断点的位置,如果没有设置断点,程序执行完start
:启动程序,最终会阻塞在main函数的第一行,等待输入后续其它gdb
指令如果想让程序start之后继续执行,或者在断点处继续执行,可以使用
continue
命令,简写为c
退出gdb
退出gdb调试,就是终止gdb进程,需要使用
quit
命令,简写为q
查看代码
gdb
调试没有IDE那样完善的可视化窗口,给调试的程序打断点又是调试之前必须要做的一项工作因此,
gdb
提供了查看代码的命令,可以轻松定位要调试代码行的位置
list
,简写为l
,通过这个命令我们可以查看项目中任意一个文件中的内容
当前文件
一般项目中有多个源文件,默认情况下通过
list
查看代码信息位于程序入口函数main
对应的那个文件如果不进行文件切换,
main
函数所在文件就是当前文件
1 | (gdb) list |
切换文件
1 | (gdb) list <file>:<lines> # 切换到指定文件并显示该行上下文 |
设置显示的行数
默认
list
只能一次查看10行代码,如果想显示更多,可以通过set listsize
来设置同样你如果想查看当前显示的行数可以使用
show listsize
来查看,这里的listsize
可以简写为list
1 | (gdb) set listsize n |
断点操作
想要通过
gdb
调试某一行或者得到某个变量在运行状态下的实际值,就需要在这一行设置断点程序指定到断点的位置就会阻塞,我们可以通过
gdb
的调试命令得到我们想要的信息
break
简写为b
,设置断点
设置断点
两种断点:常规断点(程序运行到这个位置就会被阻塞),条件断点(指定条件被满足才会在断点处阻塞)
设置普通断点到当前文件
1
2
3# break == b
(gdb) b <lines> # 断点打在该行
(gdb) b <func> # 断点打在该函数首行设置普通断点到某个非当前文件上
1
2(gdb) b <file>:<lines>
(gdb) b <file>:<func>设置条件断点
1
2(gdb) b <lines> if <var>==<val>
# 通常情况下,在循环中条件断点用的比较多
查看断点
断点设置完毕之后,可以通过
info break
命令查看设置的断点信息,其中info
可简写为i
1 | # info == i |
在显示的断点信息中有一些属性需要在其他操作中被使用
Num
:断点的编号,删除断点或者设置断点状态的时候都需要使用Enb
:当前断点的状态,y表示断点可用,n表示断点不可用What
:描述断点被设置在了哪个文件的哪一行或者哪个函数上
删除断点
确定设置的某个断点不再使用了,可将其删除,删除命令是
delete Num
,delete
可用简写为del
d
删除断点的方式有两种:删除一个或多个指定断点,删除一个连续的断点区间
1 | # delete == del == d |
设置断点状态
如果某个断点只是临时不需要了,可以将其设置为不可用状态,命令为
disable Num
使用
enable Num
可在需要时重新将其设置回可用状态,简写为dis
1 | # disable == dis |
调试命令
继续运行gdb
1 | # continue == c |
手动打印信息
打印变量值
1 | # print == p |
格式化字符(/fmt) | 说明 |
---|---|
/x |
以十六进制形式打印整数 |
/d |
以有符号,十进制形式打印整数 |
/u |
以无符号,十进制形式打印整数 |
/o |
以八进制形式打印整数 |
/t |
以二进制形式打印整数 |
/f |
以浮点数形式打印变量或表达式值 |
/c |
以字符形式打印变量或表达式的值 |
打印变量类型
如果在调试1过程中需要查看某个变量的类型,可以使用ptype
1 | (gdb) ptype 变量名 |
自动打印信息
设置变量名自动显示
当我们想频繁查看某个变量或表达式的值,使用
display
1 | (gdb) display 变量名 |
查看自动显示列表
1 | (gdb) info display |
Num
:变量或表达式的编号,gdb调试器为每个变量或表达式都分配有唯一的编号Enb
:表示当前变量(表达式)是处于激活还是禁用状态,y激活,n禁用Expression
:被自动打印值的变量或表达式的名字
取消自动显示
对于不需要再打印的变量或表达式,可以将其删除或禁用
删除自动显示列表中的变量或表达式
1
2
3
4
5(gdb) undisplay num1 num2 ...
(gdb) undisplay num1-num2
(gdb) delete display num1 num2 ...
(gdb) delete display num1-num2禁用自动显示列表中处于未激活状态下的变量或表达式
1
2(gdb) enable display num1 num2 ...
(gdb) display num1-num2
单步调试
step
step
s
命令执行一次,代码向下执行一次
1 | # step == s |
finish
finish
如果单步s
跳入函数,通过finish
跳出函数体
1 | (gdb) finish |
next
next
n
单步,但是不会进入函数体
1 | # next == n |
until
until
可直接跳出循环体,满足跳出循环内部不能有断点,必须在循环体的开始和结束行执行该命令
1 | (gdb) until |
设置变量值
直接更改某个特殊变量的值
set var 变量名=值
1 | (gdb) set var 变量名=值 |