Linux基础
Linux基础
一、GCC
1.GCC工作流程
GCC编译器对程序的编译,分为 4 个阶段:预处理(预编译)、编译和优化、汇编和链接。GCC 的编译器可以将这 4 个步骤合并成一个,其中每个步骤的工作如下:
- 预处理: 主要做了三件事: 展开头文件 、宏替换 、去掉注释行;这个阶段需要GCC调用预处理器来完成, 最终得到的还是源文件, 文本格式
- 编译: 这个阶段需要GCC调用编译器对文件进行编译, 最终得到一个汇编文件
- 汇编: 这个阶段需要GCC调用汇编器对文件进行汇编, 最终得到一个二进制文件
- 链接: 这个阶段需要GCC调用链接器对程序需要调用的库进行链接, 最终得到一个可执行的二进制文件
在Linux系统下,可以通过GCC指令直接操作一个.c
源文件,可以之间编译为一个可执行的二进制文件,如gcc main.c -o main
会生成一个main
的可执行文件
也可以使用gcc
带参数的命令分开执行上述四个步骤,如下:
main.c
1 |
|
- 第一步:对源文件进行预处理,需要使用的gcc参数为 -E
1 | # 预处理, -o 指定生成的文件名 |
- 第二步: 编译预处理之后的文件, 需要使用的gcc参数为 -S
1 | # 编译, 得到汇编文件 |
- 第三步: 对得到的汇编文件进行汇编, 需要使用的gcc参数为-c
1 | # 汇编, 得到二进制文件 |
- 第四步: 将得到的二进制文件和标准库进制链接, 得到可执行的二进制文件, 不需要任何参数
1 | # 链接 |
2.gcc常用参数
编译程序时,gcc命令可以携带如下参数:
1.指定一个宏
在程序中通过宏,去控制某段代码能够被执行。在下面这段程序中第8行判断是否定义了一个叫做 DEBUG的宏, 如果没有定义第9行代码就不会被执行了, 通过阅读代码能够知道这个宏是没有在程序中被定义的
1 | // test.c |
如果不想在程序中定义这个宏, 但是又想让它存在,通过gcc的参数 -D就可以实现了,编译器会认为参数后边指定的宏在程序中是存在的
1 | 在编译命令中定义这个 DEBUG 宏, |
-D
的应用场景:
在发布程序的时候, 一般都会要求将程序中所有的log输出去掉, 如果不去掉会影响程序的执行效率,很显然删除这些打印log的源代码是一件很麻烦的事情,解决方案是这样的:
- 将所有的打印log的代码都写到一个宏判定中, 可以模仿上边的例子
- 在编译程序的时候指定 -D 就会有log输出
- 在编译程序的时候不指定 -D, log就不会输出
3.gcc与g++的区别
二、动态库与静态库
在项目中使用库的目的:
- 使程序更加简洁不需要在项目中维护太多的源文件
- 代码保密
1.静态库
在Linux中静态库由程序 ar
生成,关于静态库的命名规则如下:
- 在Linux中静态库以
lib
作为前缀, 以.a
作为后缀, 中间是库的名字自己指定即可, 即:libxxx.a
- 在Windows中静态库一般以lib作为前缀, 以
lib
作为后缀, 中间是库的名字需要自己指定, 即:libxxx.lib
(1)生成静态库链接
生成静态库,需要先对源文件进行汇编操作 (使用参数 -c
) 得到二进制格式的目标文件 (.o
格式), 然后在通过 ar
工具将目标文件打包就可以得到静态库文件了 (libxxx.a
)
使用ar
工具创建静态库的时候需要三个参数:
- 参数c:创建一个库,不管库是否存在,都将创建。
- 参数s:创建目标文件索引,这在创建较大的库时能加快时间。
- 参数r:在库中插入模块(替换)。默认新的成员添加在库的结尾处,如果模块名已经在库中存在,则替换同名的模块。
生成发布静态库的步骤如下:
- 需要将源文件进行汇编, 得到 .o 文件, 需要使用参数 -c
1 | 执行如下操作, 默认生成二进制的 .o 文件 |
- 将得到的 .o 进行打包, 得到静态库
1 | ar rcs 静态库的名字(libxxx.a) 原材料(*.o) |
- 发布静态库
1 | 发布静态库 |
(2)静态库制作
测试程序
在一个目录下,有如下源文件:
1 | 目录结构 add.c div.c mult.c sub.c -> 算法的源文件, 函数声明在头文件 head.h |
add.c
1 |
|
sub.c
1 |
|
mult.c
1 |
|
div.c
1 |
|
head.h
1 |
|
main.c
1 |
|
生成静态库
- 第一步: 将源文件
add.c
,div.c
,mult.c
,sub.c
进行汇编, 得到二进制目标文件add.o
,div.o
,mult.o
,sub.o
1 | 1. 生成.o |
- 第二步: 将生成的目标文件通过 ar工具打包生成静态库
1 | 2. 将生成的目标文件 .o 打包成静态库 |
- 第三步: 将生成的的静态库 libcalc.a和库对应的头文件head.h一并发布给使用者就可以了
1 | 3. 发布静态库 |
静态库使用
得到了一个可用的静态库之后, 需要将其放到一个目录中, 然后根据得到的头文件编写测试代码, 对静态库中的函数进行调用
1 | 1. 首先拿到了发布的静态库 |
编译测试程序, 链接静态库 ,得到可执行文件:
1 | 3. 编译测试程序 main.c |
错误分析:编译的源文件中包含了头文件 head.h, 这个头文件中声明的函数对应的定义(也就是函数体实现)在静态库中,程序在编译的时候没有找到函数实现,因此提示 undefined reference to xxxx
解决方案:在编译的时将静态库的路径和名字都指定出来
- -L: 指定库所在的目录(相对或者绝对路径)
- -l: 指定库的名字, 需要掐头(lib)去尾(.a) 剩下的才是需要的静态库的名字
1 | 4. 编译的时候指定库信息 |
2.动态库
动态链接库是程序运行时加载的库,当动态链接库正确部署之后,运行的多个程序可以使用同一个加载到内存中的动态库,因此在Linux中动态链接库也可称之为共享库。
动态链接库是目标文件的集合,目标文件在动态链接库中的组织方式是按照特殊方式形成的。库中函数和变量的地址使用的是相对地址(静态库中使用的是绝对地址),其真实地址是在应用程序加载动态库时形成的。
动态库命名规则:
- 在Linux中动态库以
lib
作为前缀, 以.so
作为后缀, 中间是库的名字自己指定即可, 即:libxxx.so
- 在Windows中动态库一般以
lib
作为前缀, 以dll
作为后缀, 中间是库的名字需要自己指定, 即:libxxx.dll
(1)生成静态库
生成动态链接库是直接使用gcc
命令并且需要添加-fPIC(-fpic)
以及-shared
参数
- fPIC 或 -fpic 参数的作用是使得 gcc 生成的代码是与位置无关的,也就是使用相对位置
- -shared参数的作用是告诉编译器生成一个动态链接库
生成动态库步骤:
- 第一步:将源文件进行汇编操作, 需要使用参数 -c, 还需要添加额外参数 -fpic / -fPIC
1 | 得到若干个 .o文件 |
- 第二步:将得到的.o文件打包成动态库, 还是使用gcc, 使用参数 -shared 指定生成动态库(位置没有要求)
1 | gcc -shared 与位置无关的目标文件(*.o) -o 动态库(libxxx.so) |
- 第三步:发布
1 | # 发布 |
(2)动态库制作
有如下代码目录:
1 | 举例, 示例目录如下: |
- 第一步:使用gcc将源文件进行汇编(参数-c), 生成与位置无关的目标文件, 需要使用参数 -fpic或者-fPIC
1 | 1. 将.c汇编得到.o, 需要额外的参数 -fpic/-fPIC |
- 第二步:使用gcc将得到的目标文件打包生成动态库, 需要使用参数 -shared
1 | 2. 将得到 .o 打包成动态库, 使用gcc , 参数 -shared |
- 第三步:发布动态库以及头文件
1 | 3. 发布库文件和头文件 |
(3)动态库的使用
得到了一个可用的动态库之后, 需要将其放到一个目录中, 然后根据得到的头文件编写测试代码, 对动态库中的函数进行调用
1 | 1. 拿到发布的动态库 |
编译测试:
1 | 3. 编译测试程序 |
错误原因:与静态库相同,没有告知库路径,以及链接库
添加相关信息,重新编译链接测试:
1 | 在编译的时候指定动态库相关的信息: 库的路径 -L, 库的名字 -l |
重点错误:
gcc通过指定的动态库信息生成了可执行程序, 但是可执行程序运行却提示无法加载到动态库
3.解决动态库无法加载的问题
(1)库的工作原理
静态库如何被加载
在程序编译的最后一个阶段也就是链接阶段,提供的静态库会被打包到可执行程序中。当可执行程序被执行,静态库中的代码也会一并被加载到内存中,因此不会出现静态库找不到无法被加载的问题。
动态库如何被加载
在程序编译的最后一个阶段也就是链接阶段:
- 在gcc命令中虽然指定了库路径(使用参数 -L ), 但是这个路径并没有记录到可执行程序中,只是检查了这个路径下的库文件是否存在
- 同样对应的动态库文件也没有被打包到可执行程序中,只是在可执行程序中记录了库的名字
可执行程序被执行起来之后:
- 程序执行的时候会先检测需要的动态库是否可以被加载,加载不到就会提示上边的错误信息
- 当动态库中的函数在程序中被调用了, 这个时候动态库才加载到内存,如果不被调用就不加载
- 动态库的检测和内存加载操作都是由动态连接器来完成的
(2)动态链接库
动态链接器是一个独立于应用程序的进程, 属于操作系统, 当用户的程序需要加载动态库的时候动态连接器就开始工作了,很显然动态连接器根本就不知道用户通过 gcc 编译程序的时候通过参数 -L
指定的路径。
那么动态链接器是如何搜索某一个动态库的呢,在它内部有一个默认的搜索顺序,按照优先级从高到低的顺序分别是:
- 可执行文件内部的
DT_RPATH
段 - 系统的环境变量
LD_LIBRARY_PATH
- 系统动态库的缓存文件
/etc/ld.so.cache
- 存储动态库/静态库的系统目录
/lib/
,/usr/lib
等
按照以上四个顺序, 依次搜索, 找到之后结束遍历, 最终还是没找到, 动态连接器就会提示动态库找不到的错误信息。
(3)解决方法
可执行程序生成之后, 根据动态链接器的搜索路径,可以提供三种解决方案,只需要将动态库的路径放到对应的环境变量或者系统配置文件中,同样也可以将动态库拷贝到系统库目录(或者是将动态库的软链接文件放到这些系统库目录中)。
方法1
将库路径添加到环境变量 LD_LIBRARY_PATH
中
第一步:找到相关配置文件
- 用户级别:
~/.bashrc
—> 设置对当前用户有效 - 系统级别:
/etc/profile
—> 设置对所有用户有效
第二步:使用 vim 打开配置文件, 在文件最后添加这样一句话
1 | 把路径写进去就行了 |
第三步:使修改的配置文件生效
- 修改了用户级别的配置文件, 关闭当前终端, 打开一个新的终端配置就生效了
- 修改了系统级别的配置文件, 注销或关闭系统, 再开机配置就生效了
- 不想执行上边的操作, 可以执行一个命令让配置重新被加载
1 | 修改的是哪一个就执行对应的那个命令 |
方法2
更新/etc/ld.so.cache
文件
- 找到动态库所在的绝对路径(不包括库的名字)比如:
/home/robin/Library/
- 使用
vim
修改/etc/ld.so.conf
这个文件, 将上边的路径添加到文件中(独自占一行)
1 | 1. 打开文件 |
- 更新 /etc/ld.so.conf中的数据到 /etc/ld.so.cache 中
1 | 必须使用管理员权限执行这个命令 |
方法3
拷贝动态库文件到系统库目录/lib/
或者 /usr/lib
中 (或者将库的软链接文件放进去)
1 | 库拷贝 |
(4)验证
在启动可执行程序之前, 或者在设置了动态库路径之后, 可以通过一个命令检测程序能不能够通过动态链接器加载到对应的动态库, 这个命令叫做 ldd
1 | 语法: |
4.优缺点
(1)静态库
优点:
- 静态库被打包到应用程序中加载速度快
- 发布程序无需提供静态库,移植方便
缺点:
- 相同的库文件数据可能在内存中被加载多份, 消耗系统资源,浪费内存
- 库文件更新需要重新编译项目文件, 生成新的可执行程序, 浪费时间
(2)动态库
优点:
- 可实现不同进程间的资源共享
- 动态库升级简单, 只需要替换库文件, 无需重新编译应用程序
- 设计人员可以控制何时加载动态库, 不调用库函数动态库不会被加载
缺点:
- 加载速度比静态库慢, 以现在计算机的性能可以忽略
- 发布程序需要提供依赖的动态库
三、MakeFile
用到去查看,使用CMakeLists.txt
更简便
四、GDB
gdb 是由 GNU 软件系统社区提供的调试器,同 gcc 配套组成了一套完整的开发环境,可移植性很好,支持非常多的体系结构并被移植到各种系统中(包括各种类 Unix 系统与 Windows 系统里的 MinGW 和 Cygwin )。此外,除了 C 语言之外,gcc/gdb 还支持包括 C++、Objective-C、Ada 和 Pascal 等各种语言后端的编译和调试。 gcc/gdb 是 Linux 和许多类 Unix 系统中的标准开发环境,Linux 内核也是专门针对 gcc 进行编码的。
GDB 是一套字符界面的程序集,可以使用命令 gdb 加载要调试的程序,直接跳转至文档