Linux基础

该文章转自:爱编程的大丙

一、GCC

1.GCC工作流程

GCC编译器对程序的编译,分为 4 个阶段:预处理(预编译)、编译和优化、汇编和链接。GCC 的编译器可以将这 4 个步骤合并成一个,其中每个步骤的工作如下:

  • 预处理: 主要做了三件事: 展开头文件 、宏替换 、去掉注释行;这个阶段需要GCC调用预处理器来完成, 最终得到的还是源文件, 文本格式
  • 编译: 这个阶段需要GCC调用编译器对文件进行编译, 最终得到一个汇编文件
  • 汇编: 这个阶段需要GCC调用汇编器对文件进行汇编, 最终得到一个二进制文件
  • 链接: 这个阶段需要GCC调用链接器对程序需要调用的库进行链接, 最终得到一个可执行的二进制文件

image-20240514143729335

image-20240514143756071

在Linux系统下,可以通过GCC指令直接操作一个.c源文件,可以之间编译为一个可执行的二进制文件,如gcc main.c -o main会生成一个main的可执行文件

也可以使用gcc 带参数的命令分开执行上述四个步骤,如下:

main.c

1
2
3
4
5
6
7
8
#include <stdio.h>
#include <stdlib.h>

int main(int argc,char *argv[])
{
printf("hello world!\n");
return 0;
}
  • 第一步:对源文件进行预处理,需要使用的gcc参数为 -E
1
2
# 预处理, -o 指定生成的文件名
gcc -E main.c -o main.i
  • 第二步: 编译预处理之后的文件, 需要使用的gcc参数为 -S
1
2
# 编译, 得到汇编文件
$ gcc -S main.i -o mian.s
  • 第三步: 对得到的汇编文件进行汇编, 需要使用的gcc参数为-c
1
2
#  汇编, 得到二进制文件
$ gcc -c maint.s -o mian.o
  • 第四步: 将得到的二进制文件和标准库进制链接, 得到可执行的二进制文件, 不需要任何参数
1
2
# 链接
$ gcc main.o -o main

2.gcc常用参数

编译程序时,gcc命令可以携带如下参数:

image-20240514150508959

1.指定一个宏

在程序中通过宏,去控制某段代码能够被执行。在下面这段程序中第8行判断是否定义了一个叫做 DEBUG的宏, 如果没有定义第9行代码就不会被执行了, 通过阅读代码能够知道这个宏是没有在程序中被定义的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// test.c
#include <stdio.h>
#define NUMBER 3

int main()
{
int a = 10;
#ifdef DEBUG
printf("我是一个程序猿, 我不会爬树...\n");
#endif
for(int i=0; i<NUMBER; ++i)
{
printf("hello, GCC!!!\n");
}
return 0;
}

如果不想在程序中定义这个宏, 但是又想让它存在,通过gcc的参数 -D就可以实现了,编译器会认为参数后边指定的宏在程序中是存在的

1
2
3
4
5
6
7
8
9
# 在编译命令中定义这个 DEBUG 宏, 
$ gcc test.c -o app -D DEBUG

# 执行生成的程序, 可以看到程序第9行的输出
$ ./app
我是一个程序猿, 我不会爬树...
hello, GCC!!!
hello, GCC!!!
hello, GCC!!!

-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:在库中插入模块(替换)。默认新的成员添加在库的结尾处,如果模块名已经在库中存在,则替换同名的模块。

image-20240514152557036

生成发布静态库的步骤如下

  • 需要将源文件进行汇编, 得到 .o 文件, 需要使用参数 -c
1
2
3
# 执行如下操作, 默认生成二进制的 .o 文件
# -c 参数位置没有要求
$ gcc 源文件(*.c) -c
  • 将得到的 .o 进行打包, 得到静态库
1
$ ar rcs 静态库的名字(libxxx.a) 原材料(*.o)
  • 发布静态库
1
2
3
# 发布静态库
1. 提供头文件 **.h
2. 提供制作出来的静态库 libxxx.a
(2)静态库制作
测试程序

在一个目录下,有如下源文件:

1
2
3
4
5
6
7
8
9
10
# 目录结构 add.c div.c mult.c sub.c -> 算法的源文件, 函数声明在头文件 head.h
# main.c中是对接口的测试程序, 制作库的时候不需要将 main.c 算进去
.
├── add.c
├── div.c
├── include
│   └── head.h
├── main.c
├── mult.c
└── sub.c

add.c

1
2
3
4
5
6
7
#include <stdio.h>
#include "head.h"

int add(int a, int b)
{
return a+b;
}

sub.c

1
2
3
4
5
6
7
#include <stdio.h>
#include "head.h"

int subtract(int a, int b)
{
return a-b;
}

mult.c

1
2
3
4
5
6
7
#include <stdio.h>
#include "head.h"

int multiply(int a, int b)
{
return a*b;
}

div.c

1
2
3
4
5
6
7
#include <stdio.h>
#include "head.h"

double divide(int a, int b)
{
return (double)a/b;
}

head.h

1
2
3
4
5
6
7
8
9
10
11
#ifndef _HEAD_H
#define _HEAD_H
// 加法
int add(int a, int b);
// 减法
int subtract(int a, int b);
// 乘法
int multiply(int a, int b);
// 除法
double divide(int a, int b);
#endif

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include "head.h"

int main()
{
int a = 20;
int b = 12;
printf("a = %d, b = %d\n", a, b);
printf("a + b = %d\n", add(a, b));
printf("a - b = %d\n", subtract(a, b));
printf("a * b = %d\n", multiply(a, b));
printf("a / b = %f\n", divide(a, b));
return 0;
}
生成静态库
  • 第一步: 将源文件add.c, div.c, mult.c, sub.c 进行汇编, 得到二进制目标文件 add.o, div.o, mult.o, sub.o
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 1. 生成.o
$ gcc add.c div.c mult.c sub.c -c
sub.c:2:18: fatal error: head.h: No such file or directory
compilation terminated.

# 提示头文件找不到, 添加参数 -I 重新头文件路径即可
$ gcc add.c div.c mult.c sub.c -c -I ./include/

# 查看目标文件是否已经生成
$ tree
.
├── add.c
├── add.o # 汇编得到的,目标文件
├── div.c
├── div.o # 目标文件
├── include
│   └── head.h
├── main.c
├── mult.c
├── mult.o # 目标文件
├── sub.c
└── sub.o # 目标文件
  • 第二步: 将生成的目标文件通过 ar工具打包生成静态库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 2. 将生成的目标文件 .o 打包成静态库
$ ar rcs libcalc.a a.o b.o c.o # a.o b.o c.o在同一个目录中可以写成 *.o

# 查看目录中的文件
$ tree
.
├── add.c
├── add.o
├── div.c
├── div.o
├── include
│   └── `head.h ===> 和静态库一并发布
├── `libcalc.a ===> 生成的静态库
├── main.c
├── mult.c
├── mult.o
├── sub.c
└── sub.o
  • 第三步: 将生成的的静态库 libcalc.a和库对应的头文件head.h一并发布给使用者就可以了
1
2
3
# 3. 发布静态库
1. head.h => 函数声明
2. libcalc.a => 函数定义(二进制格式)
静态库使用

得到了一个可用的静态库之后, 需要将其放到一个目录中, 然后根据得到的头文件编写测试代码, 对静态库中的函数进行调用

1
2
3
4
5
6
7
8
# 1. 首先拿到了发布的静态库
`head.h` 和 `libcalc.a`

# 2. 将静态库, 头文件, 测试程序放到一个目录中准备进行测试
.
├── head.h # 函数声明
├── libcalc.a # 函数定义(二进制格式)
└── main.c # 函数测试

编译测试程序, 链接静态库 ,得到可执行文件:

1
2
3
4
5
6
7
8
# 3. 编译测试程序 main.c
$ gcc main.c -o app
/tmp/ccR7Fk49.o: In function `main':
main.c:(.text+0x38): undefined reference to `add'
main.c:(.text+0x58): undefined reference to `subtract'
main.c:(.text+0x78): undefined reference to `multiply'
main.c:(.text+0x98): undefined reference to `divide'
collect2: error: ld returned 1 exit status

错误分析:编译的源文件中包含了头文件 head.h, 这个头文件中声明的函数对应的定义(也就是函数体实现)在静态库中,程序在编译的时候没有找到函数实现,因此提示 undefined reference to xxxx

解决方案:在编译的时将静态库的路径和名字都指定出来

  • -L: 指定库所在的目录(相对或者绝对路径)
  • -l: 指定库的名字, 需要掐头(lib)去尾(.a) 剩下的才是需要的静态库的名字
1
2
3
4
5
6
7
8
9
10
11
12
13
# 4. 编译的时候指定库信息
-L: 指定库所在的目录(相对或者绝对路径)
-l: 指定库的名字, 掐头(lib)去尾(.a) ==> calc
# -L -l, 参数和参数值之间可以有空格, 也可以没有 -L./ -lcalc
$ gcc main.c -o app -L ./ -l calc

# 查看目录信息, 发现可执行程序已经生成了
$ tree
.
├── app # 生成的可执行程序
├── head.h
├── libcalc.a
└── main.c

2.动态库

动态链接库是程序运行时加载的库,当动态链接库正确部署之后,运行的多个程序可以使用同一个加载到内存中的动态库,因此在Linux中动态链接库也可称之为共享库。

动态链接库是目标文件的集合,目标文件在动态链接库中的组织方式是按照特殊方式形成的。库中函数和变量的地址使用的是相对地址(静态库中使用的是绝对地址),其真实地址是在应用程序加载动态库时形成的。

动态库命名规则

  • 在Linux中动态库以lib作为前缀, 以.so作为后缀, 中间是库的名字自己指定即可, 即: libxxx.so
  • 在Windows中动态库一般以lib作为前缀, 以dll作为后缀, 中间是库的名字需要自己指定, 即: libxxx.dll
(1)生成静态库

生成动态链接库是直接使用gcc命令并且需要添加-fPIC(-fpic) 以及-shared 参数

  • fPIC 或 -fpic 参数的作用是使得 gcc 生成的代码是与位置无关的,也就是使用相对位置
  • -shared参数的作用是告诉编译器生成一个动态链接库

image-20240514154531287

生成动态库步骤

  • 第一步:将源文件进行汇编操作, 需要使用参数 -c, 还需要添加额外参数 -fpic / -fPIC
1
2
# 得到若干个 .o文件
$ gcc 源文件(*.c) -c -fpic
  • 第二步:将得到的.o文件打包成动态库, 还是使用gcc, 使用参数 -shared 指定生成动态库(位置没有要求)
1
$ gcc -shared 与位置无关的目标文件(*.o) -o 动态库(libxxx.so)
  • 第三步:发布
1
2
3
# 发布
1. 提供头文件: xxx.h
2. 提供动态库: libxxx.so
(2)动态库制作

有如下代码目录:

1
2
3
4
5
6
7
8
9
10
11
# 举例, 示例目录如下:
# 目录结构 add.c div.c mult.c sub.c -> 算法的源文件, 函数声明在头文件 head.h
# main.c中是对接口的测试程序, 制作库的时候不需要将 main.c 算进去
.
├── add.c
├── div.c
├── include
│   └── head.h
├── main.c
├── mult.c
└── sub.c
  • 第一步:使用gcc将源文件进行汇编(参数-c), 生成与位置无关的目标文件, 需要使用参数 -fpic或者-fPIC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 1. 将.c汇编得到.o, 需要额外的参数 -fpic/-fPIC
$ gcc add.c div.c mult.c sub.c -c -fpic -I ./include/

# 查看目录文件信息, 检查是否生成了目标文件
$ tree
.
├── add.c
├── add.o # 生成的目标文件
├── div.c
├── div.o # 生成的目标文件
├── include
│   └── head.h
├── main.c
├── mult.c
├── mult.o # 生成的目标文件
├── sub.c
└── sub.o # 生成的目标文件
  • 第二步:使用gcc将得到的目标文件打包生成动态库, 需要使用参数 -shared
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 2. 将得到 .o 打包成动态库, 使用gcc , 参数 -shared
$ gcc -shared add.o div.o mult.o sub.o -o libcalc.so

# 检查目录中是否生成了动态库
$ tree
.
├── add.c
├── add.o
├── div.c
├── div.o
├── include
│   └── `head.h ===> 和动态库一起发布
├── `libcalc.so ===> 生成的动态库
├── main.c
├── mult.c
├── mult.o
├── sub.c
└── sub.o
  • 第三步:发布动态库以及头文件
1
2
3
# 3. 发布库文件和头文件
1. head.h
2. libcalc.so
(3)动态库的使用

得到了一个可用的动态库之后, 需要将其放到一个目录中, 然后根据得到的头文件编写测试代码, 对动态库中的函数进行调用

1
2
3
4
5
6
7
8
9
# 1. 拿到发布的动态库
`head.h libcalc.so
# 2. 基于头文件编写测试程序, 测试动态库中提供的接口是否可用
`main.c`
# 示例目录:
.
├── head.h ==> 函数声明
├── libcalc.so ==> 函数定义
└── main.c ==> 函数测试

编译测试:

1
2
3
4
5
6
7
8
# 3. 编译测试程序
$ gcc main.c -o app
/tmp/ccwlUpVy.o: In function `main':
main.c:(.text+0x38): undefined reference to `add'
main.c:(.text+0x58): undefined reference to `subtract'
main.c:(.text+0x78): undefined reference to `multiply'
main.c:(.text+0x98): undefined reference to `divide'
collect2: error: ld returned 1 exit status

错误原因:与静态库相同,没有告知库路径,以及链接库

添加相关信息,重新编译链接测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 在编译的时候指定动态库相关的信息: 库的路径 -L, 库的名字 -l
$ gcc main.c -o app -L./ -lcalc

# 查看是否生成了可执行程序
$ tree
.
├── app # 生成的可执行程序
├── head.h
├── libcalc.so
└── main.c

# 执行生成的可执行程序, 错误提示 ==> 可执行程序执行的时候找不到动态库
$ ./app
./app: error while loading shared libraries: libcalc.so: cannot open shared object file: No such file or directory

重点错误

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
2
# 把路径写进去就行了
export LD_LIBRARY_PATH =$LD_LIBRARY_PATH :动态库的绝对路径

第三步:使修改的配置文件生效

  • 修改了用户级别的配置文件, 关闭当前终端, 打开一个新的终端配置就生效了
  • 修改了系统级别的配置文件, 注销或关闭系统, 再开机配置就生效了
  • 不想执行上边的操作, 可以执行一个命令让配置重新被加载
1
2
3
4
# 修改的是哪一个就执行对应的那个命令
# source 可以简写为一个 "." 其作用是让文件内容被重新加载
$ source ~/.bashrc (. ~/.bashrc)
$ source /etc/profile (. /etc/profile)
方法2

更新/etc/ld.so.cache文件

  • 找到动态库所在的绝对路径(不包括库的名字)比如:/home/robin/Library/
  • 使用vim修改 /etc/ld.so.conf 这个文件, 将上边的路径添加到文件中(独自占一行)
1
2
3
4
# 1. 打开文件
$ sudo vim /etc/ld.so.conf

# 2. 添加动态库路径, 并保存退出
  • 更新 /etc/ld.so.conf中的数据到 /etc/ld.so.cache 中
1
2
# 必须使用管理员权限执行这个命令
$ sudo ldconfig
方法3

拷贝动态库文件到系统库目录/lib/或者 /usr/lib 中 (或者将库的软链接文件放进去)

1
2
3
4
5
# 库拷贝
sudo cp /xxx/xxx/libxxx.so /usr/lib

# 创建软连接
sudo ln -s /xxx/xxx/libxxx.so /usr/lib/libxxx.so
(4)验证

在启动可执行程序之前, 或者在设置了动态库路径之后, 可以通过一个命令检测程序能不能够通过动态链接器加载到对应的动态库, 这个命令叫做 ldd

1
2
3
4
5
6
7
8
9
# 语法:
$ ldd 可执行程序名

# 举例:
$ ldd app
linux-vdso.so.1 => (0x00007ffe8fbd6000)
libcalc.so => /home/robin/Linux/3Day/calc/test/libcalc.so (0x00007f5d85dd4000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5d85a0a000)
/lib64/ld-linux-x86-64.so.2 (0x00007f5d85fd6000) ==> 动态链接器, 操作系统提供

4.优缺点

(1)静态库

优点

  • 静态库被打包到应用程序中加载速度快
  • 发布程序无需提供静态库,移植方便

缺点

  • 相同的库文件数据可能在内存中被加载多份, 消耗系统资源,浪费内存
  • 库文件更新需要重新编译项目文件, 生成新的可执行程序, 浪费时间

image-20240514162255127

(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 加载要调试的程序,直接跳转至文档

文档链接