【webserver 前置知识 01】Linux系统编程入门
题外话,PA里面也有很不错的Linux基础基础
传送门:https://nju-projectn.github.io/ics-pa-gitbook/ics2019/linux.html
静态库与动态库
什么玩意?
库文件是计算机上的一类文件,可以简单的把库文件看成一种代码仓库,它提供给使用者一些可以直接拿来用的变量、函数或类。
◼ 库是特殊的一种程序,编写库的程序和编写一般的程序区别不大,只是库不能单独运行。
◼ 库文件有两种,静态库和动态库(共享库)。
静态库与动态库的区别
复制代码到程序的时机不同
静态库在程序的链接阶段被复制到了程序中;
动态库在链接阶段没有被复制到程序中,而是程序在运行时由系统动态加载到内存中供程序调用
库的好处
1.代码保密
2.方便部署和分发
静态库的制作与使用
命名规则
◆ Linux : libxxx.a(库文件的名字)
lib : 前缀(固定)
xxx : 库的名字,自己起(注意,在使用gcc编译时需要的是xxx,不含lib前缀)
.a : 后缀(固定)
◆ Windows : libxxx.lib
静态库的制作
◆ gcc 获得 .o 文件
◆将 .o 文件打包,使用 ar 工具(archive)
ar rcs libxxx.a xxx.o xxx.o
r –将文件插入备存文件中
c –建立备存文件
s –索引
root@ubuntu:/home/ag/cpp/Linux/lesson04/calc# tree
.
├── add.c
├── div.c
├── head.h
├── main.c
├── mult.c
└── sub.c
0 directories, 6 files
root@ubuntu:/home/ag/cpp/Linux/lesson04/calc# gcc -c add.c div.c mult.c sub.c
root@ubuntu:/home/ag/cpp/Linux/lesson04/calc# ls
add.c div.c head.h mult.c sub.c
add.o div.o main.c mult.o sub.o
root@ubuntu:/home/ag/cpp/Linux/lesson04/calc# tree
.
├── add.c
├── add.o
├── div.c
├── div.o
├── head.h
├── main.c
├── mult.c
├── mult.o
├── sub.c
└── sub.o
0 directories, 10 files
root@ubuntu:/home/ag/cpp/Linux/lesson04/calc# ar rcs libcalc.a add.o sub.o mult.o div.o
root@ubuntu:/home/ag/cpp/Linux/lesson04/calc# tree
.
├── add.c
├── add.o
├── div.c
├── div.o
├── head.h
├── libcalc.a
├── main.c
├── mult.c
├── mult.o
├── sub.c
└── sub.o
0 directories, 11 files
静态库的使用
在写项目的时候,一般会进行分包操作
一个常见的项目树如下:
root@ubuntu:/home/ag/cpp/Linux/lesson05/library# tree
.
├── include //放头文件
│ └── head.h
├── lib //放库文件
├── main.c //测试文件
└── src //源代码
├── add.c
├── div.c
├── mult.c
└── sub.c
3 directories, 6 files
把之前制作的静态库文件libcalc.a
复制到 lib 目录下即可,然后去编译
ps:使用库文件时,还有同时有其依赖的头文件
编译文件
root@ubuntu:/home/ag/cpp/Linux/lesson05/library# gcc main.c -o app
main.c:2:10: fatal error: head.h: No such file or directory
#include "head.h"
^~~~~~~~
compilation terminated.
这里报错的原因是:编译时,需要把头文件的代码拷贝到main.c中,但是我们include的时候用的是相对路径,main.c与head.h并不在统一目录下,因此是识别不到的
解决方法1:让编译文件(main.c)与头文件(head.h)在同一目录下
解决方式2(优雅):使用gcc提供的参数选项 -I 在编译时查找指定目录下的头文件
gcc main.c -o app -I ./include
又报错
root@ubuntu:/home/ag/cpp/Linux/lesson05/library# gcc main.c -o app -I ./include
/tmp/cct81thC.o: In function `main':
main.c:(.text+0x3a): undefined reference to `add'
main.c:(.text+0x5c): undefined reference to `subtract'
main.c:(.text+0x7e): undefined reference to `multiply'
main.c:(.text+0xa0): undefined reference to `divide'
collect2: error: ld returned 1 exit status
原因:main中使用了上述函数,但是编译时只找到了 .h 中的声明,没有找到库文件中的定义
还需要追加gcc参数,使用 -l 指定使用哪个库文件(只需要库名字,前缀lib不需要)进行编译, -L 指定到某个目录下找
root@ubuntu:/home/ag/cpp/Linux/lesson05/library# gcc main.c -o app -I ./include -l calc -L ./lib
root@ubuntu:/home/ag/cpp/Linux/lesson05/library# ls
app include lib main.c src
./app可以正常执行
动态库的制作与使用
命名规则
◆ Linux : libxxx.so(库文件的名字)
lib : 前缀(固定)
xxx : 库的名字,自己起(注意,在使用gcc编译时需要的是xxx,不含lib前缀)
.so : 后缀(固定,在linux下是一个可执行文件)
◆ Windows : libxxx.dll
动态库的制作
◆ gcc 获得 .o 文件,得到和位置无关的代码
gcc -c –fpic/-fPIC axx.c bxx.c
◆ gcc得到动态库
gcc -shared a.o b.o -o libcalc.so
root@ubuntu:/home/ag/cpp/Linux/lesson06/calc# gcc -c -fpic add.c div.c sub.c mult.c
root@ubuntu:/home/ag/cpp/Linux/lesson06/calc# ls
add.c add.o div.c div.o head.h main.c mult.c mult.o sub.c sub.o
root@ubuntu:/home/ag/cpp/Linux/lesson06/calc# gcc -shared add.o sub.o mult.o div.o -o libcalc.so
root@ubuntu:/home/ag/cpp/Linux/lesson06/calc# ls
add.c div.c head.h main.c mult.o sub.o
add.o div.o libcalc.so mult.c sub.c
然后把 .so 拷贝到要使用的地方
root@ubuntu:/home/ag/cpp/Linux/lesson06/calc# cd ..
root@ubuntu:/home/ag/cpp/Linux/lesson06# cd library/
root@ubuntu:/home/ag/cpp/Linux/lesson06/library# cp ../calc/libcalc.so ./lib/
root@ubuntu:/home/ag/cpp/Linux/lesson06/library# tree
.
├── app
├── include
│ └── head.h
├── lib
│ └── libcalc.so
├── main.c
└── src
├── add.c
├── div.c
├── mult.c
└── sub.c
3 directories, 8 files
编译main.c
root@ubuntu:/home/ag/cpp/Linux/lesson06/library# gcc main.c -o main
main.c:2:10: fatal error: head.h: No such file or directory
#include "head.h"
^~~~~~~~
compilation terminated.
报错,和静态库使用的时候的原因一样
需要指定头文件位置、库文件位置、库文件名字
root@ubuntu:/home/ag/cpp/Linux/lesson06/library# gcc main.c -o main -I include/
/tmp/ccOgCtBV.o: In function `main':
main.c:(.text+0x3a): undefined reference to `add'
main.c:(.text+0x5c): undefined reference to `subtract'
main.c:(.text+0x7e): undefined reference to `multiply'
main.c:(.text+0xa0): undefined reference to `divide'
collect2: error: ld returned 1 exit status
root@ubuntu:/home/ag/cpp/Linux/lesson06/library# gcc main.c -o main -I include/ -L lib/ -l calc
root@ubuntu:/home/ag/cpp/Linux/lesson06/library# ls
include lib main main.c src
可见,成功编译了,但是运行编译后的文件又报错
root@ubuntu:/home/ag/cpp/Linux/lesson06/library# ./main
./main: error while loading shared libraries: libcalc.so: cannot open shared object file: No such file or directory
动态库加载失败原因
造成上述错误的原因与动态库加载原理有关
库文件工作原理
◼ 静态库:GCC 进行链接时,会把静态库中代码打包到可执行程序中
◼ 动态库:GCC 进行链接时,动态库的代码不会被打包到可执行程序中
程序启动之后,动态库会被动态加载到内存中,通过 ldd (list dynamic dependencies)命令检查动态库依赖关系
root@ubuntu:/home/ag/cpp/Linux/lesson06/library# ldd main
linux-vdso.so.1 (0x00007fff808bb000)
libcalc.so => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007efca544e000)
/lib64/ld-linux-x86-64.so.2 (0x00007efca5a41000)
可以看到,我们创建的动态库文件没有被找到,而标准的c库文件是被识别到的
如何定位共享库文件?
当系统加载可执行代码时候,能够知道其所依赖的库的名字,但是还需要知道绝对路径。此时就需要系
统的动态载入器来获取该绝对路径。
对于elf格式的可执行程序,是由 ld-linux.so 来完成的,它先后搜索 elf 文件的
DT_RPATH段 ——> 环境变量LD_LIBRARY_PATH ——> /etc/ld.so.cache文件列表 ——> /lib/,/usr/lib目录,找到库文件后将其载入内存。
解决动态库加载失败问题
临时添加环境变量
root@ubuntu:/home/ag# export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/ag/cpp/Linux/lesson06/library/lib
root@ubuntu:/home/ag# echo $LD_LIBRARY_PATH
:/home/ag/cpp/Linux/lesson06/library/lib
此时可以在当前终端看到,编译后的文件main已经与动态库链接
root@ubuntu:/home/ag/cpp/Linux/lesson06/library# ls
include lib main main.c src
root@ubuntu:/home/ag/cpp/Linux/lesson06/library# ldd main
linux-vdso.so.1 (0x00007ffd93bf7000)
libcalc.so => /home/ag/cpp/Linux/lesson06/library/lib/libcalc.so (0x00007f32c4b61000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f32c4770000)
/lib64/ld-linux-x86-64.so.2 (0x00007f32c4f65000)
关闭终端再打开其他终端,仍然会报错
永久配置环境变量
用户级别
回到home目录下(或者在当前目录下操作~/.bashrc
也行)
root@ubuntu:/home/ag/cpp/Linux/lesson06/library# cd
root@ubuntu:~# ls
snap
root@ubuntu:~# ll
total 68
drwx------ 7 root root 4096 Feb 5 21:14 ./
drwxr-xr-x 24 root root 4096 Feb 2 16:50 ../
-rw------- 1 root root 8887 Feb 9 06:36 .bash_history
-rw-r--r-- 1 root root 3106 Apr 9 2018 .bashrc
drwx------ 4 root root 4096 Nov 9 04:38 .cache/
drwx------ 3 root root 4096 Nov 7 17:50 .gnupg/
-rw-r--r-- 1 root root 148 Aug 17 2015 .profile
drwx------ 6 root root 4096 Nov 8 03:06 snap/
drwx------ 2 root root 4096 Nov 7 18:41 .ssh/
-rw------- 1 root root 9616 Feb 5 21:14 .viminfo
drwxr-xr-x 5 root root 4096 Feb 9 19:51 .vscode-server/
-rw-r--r-- 1 root root 183 Nov 7 18:45 .wget-hsts
-rw------- 1 root root 52 Nov 7 18:38 .Xauthority
找到隐藏文件 .bashrc ,编辑该文件
vim .bashrc
使用shift+g跳转到最后一行,按o往下插入一行
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/ag/cpp/Linux/lesson06/library/lib
保存退出,使修改后的bashrc生效:
root@ubuntu:~# . .bashrc
root@ubuntu:~#
注:
1、. .bashrc
等于source .bashrc
2、在终端1中配置bashrc后,在另一终端2中也要重新source一次才可以正常链接
系统级别
这时需要配置系统的profile文件
sudo vim etc/profile
同样在最后一行加入export环境变量即可
然后source一下(不用加sudo)
root@ubuntu:/etc# source etc/profile
如果识别不到,就source一下
静态库与动态库对比
程序编译成可执行程序的过程
静态库处理过程
动态库处理过程
静态库的优缺点
优点:
◆静态库被打包到应用程序中加载速度快
◆发布程序无需提供静态库,移植方便
缺点:
◆消耗系统资源,浪费内存
◆更新、部署、发布麻烦
动态库的优缺点
优点:
◆可以实现进程间资源共享(共享库)
◆更新、部署、发布简单
◆可以控制何时加载动态库
缺点:
◆加载速度比静态库慢
◆发布程序时需要提供依赖的动态库
Makefile
什么是 Makefile
一个工程中的源文件不计其数,其按类型、功能、模块分别放在若干个目录中,Makefile 文件定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为 Makefile 文件就像一个 Shell 脚本一样,也可以执行操作系统的命令。
Makefile 带来的好处就是“自动化编译” ,一旦写好,只需要一个 make 命令,整个工程完全自动编译,极大的提高了软件开发的效率。make 是一个命令工具,是一个解释 Makefile 文件中指令的命令工具,一般来说,大多数的 IDE 都有这个命令,比如 Delphi 的 make,Visual C++ 的 nmake,Linux 下 GNU 的 make。
Makefile 文件命名和规则
文件命名
makefile 或者 Makefile
Makefile 规则
一个 Makefile 文件中可以有一个或者多个规则
目标 ...: 依赖 ...
命令(Shell 命令)
...
⚫ 目标:最终要生成的文件(伪目标除外)
⚫ 依赖:生成目标所需要的文件或是目标
⚫ 命令:通过执行命令对依赖操作生成目标(命令前必须 Tab 缩进)
Makefile 中的其它规则一般都是为第一条规则服务的。
一个简单的Makefile例子
app: sub.c add.c mult.c div.c main.c
gcc sub.c add.c mult.c div.c main.c -o app
工作原理
◼ 命令在执行之前,需要先检查规则中的依赖是否存在
- 如果存在,执行命令
- 如果不存在,向下检查其它的规则,检查有没有一个规则是用来生成这个依赖的,如果找到了,则执行该规则中的命令
◼ 检测更新,在执行规则中的命令时,会比较目标和依赖文件的时间
- 如果依赖的时间比目标的时间晚,需要重新生成目标
- 如果依赖的时间比目标的时间早,目标不需要更新,对应规则中的命令不需要被
执行
升级一下之前的简单例子,检测每个依赖是否存在并且重新生成被修改的目标
app: sub.c add.c mult.c div.c main.c
gcc sub.c add.c mult.c div.c main.c -o app
sub.o : sub.c
gcc -c sub.c -o sub.o
add.o : add.c
gcc -c add.c -o add.o
add.o : div.c
gcc -c div.c -o div.o
mult.o : mult.c
gcc -c mult.c -o mult.o
main.o : main.c
gcc -c main.c -o main.o
但是这么写非常不简洁,文件一多就很麻烦
还得改进
变量
自定义变量
变量名 = 变量值
例如,var = hello
预定义变量
AR : 归档维护程序的名称,默认值为 ar
CC : C 编译器的名称,默认值为 cc
CXX : C++ 编译器的名称,默认值为 g++
$@ : 目标的完整名称
$< : 第一个依赖文件的名称
$^ : 所有的依赖文件
获取变量的值
$ (变量名)
例如,$(var)
模式匹配
' % '是通配符,可以匹配一个字符串
例如:%.o
可以匹配出文件结尾为 .o 的所有文件
所以我们可以进行如下改写
add.o : add.c
gcc -c add.c -o add.o
#等于
%.o : %.c
gcc -c add.c -o add.o
gcc -c add.c -o add.o
也应该简化掉
于是第三版简单例子可以写为:
src=sub.o add.o mult.o div.o main.o
target=app
$(target): $(src)
$(CC) $(src) -o $(target)
%.o : %.c
$(CC) -c $< -o $@
解释一下:
%.o : %.c
匹配到了src中的每一个文件
$<
则按顺序往下执行指令
src获取的方式还可以再优化
函数
$(wildcard PATTERN...)
功能:获取指定目录下指定类型的文件列表
参数:PATTERN 指的是某个或多个目录下的对应的某种类型的文件,如果有多个目录,一般使用空格间隔
返回:得到的若干个文件的文件列表,文件名之间使用空格间隔
示例:
$(wildcard *.c ./sub/*.c)
返回值格式: a.c b.c c.c d.c e.c f.c
$(patsubst <pattern>,<replacement>,<text>)
功能:查找%
,表示任意长度的字串。如果%
,那么,%
将是\
来转义,以\%
来表示真实含义的%
字符)
返回:函数返回被替换过后的字符串
示例:
$(patsubst %.c, %.o, x.c bar.c)
返回值格式: x.o bar.o
于是第四版简单例子可以改成:
#定义变量
# sub.o add.o mult.o div.o main.o
src=$(wildcard ./*.c)
objs=$(patsubst %.c, %.o,$(src))
target=app
$(target): $(objs)
$(CC) $(objs) -o $(target)
%.o:%.c
$(CC) -c $< -o $@
.PHONY:clean #伪目标,不生成clean
clean:
rm $(objs) -f
GDB调试
什么是 GDB
◼ GDB 是由 GNU 软件系统社区提供的调试工具,同 GCC 配套组成了一套完整的开发环境,GDB 是 Linux 和许多类 Unix 系统中的标准开发环境。
◼ 一般来说,GDB 主要帮助你完成下面四个方面的功能:
1.启动程序,可以按照自定义的要求随心所欲的运行程序
2.可让被调试的程序在所指定的调置的断点处停住(断点可以是条件表达式)
3.当程序被停住时,可以检查此时程序中所发生的事
4.可以改变程序,将一个 BUG 产生的影响修正从而测试其他 BUG
准备工作
◼ 通常,在为调试而编译时,我们会()关掉编译器的优化选项(-O
), 并打开调试选项(-g
)。另外,-Wall
在尽量不影响程序行为的情况下选项打开所有warning,也可以发现许多问题,避免一些不必要的 BUG。
◼ gcc -g -Wall program.c -o program
◼ -g
选项的作用是在可执行文件中加入源代码的信息,比如可执行文件中第几条机器指令对应源代码的第几行,但并不是把整个源文件嵌入到可执行文件中,所以在调试时必须保证 gdb 能找到源文件。
GDB 命令 –启动、退出、查看代码
启动和退出
gdb 可执行程序
quit
给程序设置参数/获取设置参数
set args 10 20
show args
查看当前文件代码
list/l (从默认位置显示)
list/l 行号 (从指定的行显示)
list/l 函数名(从指定的函数显示)
查看非当前文件代码
list/l 文件名:行号
list/l 文件名:函数名
设置显示的行数
show list/listsize
set list/listsize 行数
GDB命令 –断点操作
设置断点
b/break 行号
b/break 函数名
b/break 文件名:行号
b/break 文件名:函数
查看断点
i/info b/break
删除断点
d/del/delete 断点编号
设置断点无效/生效
dis/disable 断点编号
ena/enable 断点编号
设置条件断点(一般用在循环的位置)
b/break 10 if i==5 //比如有个for循环,这里是循环到i=5才会停在断点上
GDB命令 –调试命令
运行GDB程序
start(程序停在第一行)
run(遇到断点才停)
继续运行,到下一个断点停
c/continue
向下执行一行代码(不会进入函数体)
n/next
变量操作
p/print 变量名(打印变量值)
ptype 变量名(打印变量类型)
向下单步调试(遇到函数进入函数体)
s/step
finish(跳出函数体)
自动变量操作
display 变量名(自动打印指定变量的值)
i/info display
undisplay 编号
其它操作
set var 变量名=变量值 (循环中用的较多)
until (跳出循环)