杨记

碎片化学习令人焦虑,系统化学习使人进步

0%

GNU和Makefile

GCC,GDB,Makefile的quickstart

快速入门后最好是看官方文档系统学习

GCC

When you invoke GCC, it normally does preprocessing, compilation, assembly and linking.

image-20220527104153016

四个过程:

  • 预处理 gcc -E test.c -o test.i.c .h文件预处理为.i文件 C/C++的语言代码
  • 编译 gcc -S test.i -o test.s 生成汇编文件.s 汇编语言代码
  • 汇编 gcc -c test.s -o test.o 将汇编文件编译为目标文件.o .obj 二进制
  • 链接 gcc test.o -o test 生成可执行文件 .exe .elf

直接生成可执行文件:

1
gcc test.c -o test //如果未指定输出文件名称,默认输出为a.out

-O编译优化,有四个级别0~3,级别越高优化越好,但编译时间越长,中间代码可读性越差。0级无优化效果:gcc -O1 test.c -o test

-I 指定头文件所在文件夹:

  • 如果文件不同在一个目录下,可以使用-I指定文件夹,如test.hinclude
  • 如:gcc test.c -I ./include -o test

-D 编译时定义宏

-Wall 提示警告信息

-g 加调试信息

-L 指定库所在文件夹

-l 指定要连接的库名

-m32-m64编译出32位或64位程序

库的制作

静态库

命名格式:lib+静态库名+.a,如libsort.a

制作步骤:(示例将a.c b.c c.c制作静态库libmytest.a

  1. 生成对应的.o文件,gcc a.c b.c c.c -c
  2. 得到静态库:ar rcs libmytest.a a.o b.o c.o
    • 打包.o文件的过程
    • ar工具包不包含在gcc
    • r —> 将文件插入静态库中
    • c —> 创建静态库,不管库是否存在
    • s —> 写入一个目标文件索引到库中,或者更新一个存在的目标文件索引
    • nm libmytest.a 查看库中的符号(函数,全局变量等)

使用静态库的方法:

  1. gcc 源文件 -L静态库路径 -l静态库名 -I头文件目录 -o 可执行文件名

    • 如:gcc main.c -L./ -lmytest -I./ -o app

    • 静态库名要去电前缀lib和后缀.a

  2. gcc 源文件 -I头文件 libxxx.a

    • libxxx.a也可以是路径,如:gcc main.c lib/libMyCalc.a -Iinclude -o app

生成的静态库需要跟对应的头文件同时发布,头文件存放的是函数接口(函数声明)

优点

  • 寻址方便,速度快
  • 库被打包到可执行程序中,直接发布可执行程序即可使用

缺点

  • 静态库的代码在编译过程中已经被载入可执行程序,因此体积较大
  • 如果静态函数库改变了,那么你的程序必须重新编译

使用场合

  • 在核心程序上使用,保证速度,可忽视空间
  • 主流应用于80、90年代,现在很少用

image-20220527154158019

动态库/共享库

命名格式:lib+静态库名+.so,如libsort.so ,win下是.dll文件

制作:

  1. 生成“与位置无关”的目标文件:gcc -fPIC a.c b.c c.c -c
  2. 制作动态库:gcc -shared -o libmytest.so a.o b.o c.o

使用动态库:

  1. gcc main.c lib/libMyCalc.so -o app -I./include
  2. gcc main.c -L./ -lMyCalc -o app -I./include

执行生成的可执行文件:./app —> 运行失败

报错:./app: error while loading shared libraries: lib/libMyCalc.so: cannot open shared object file: No such file or directory

问题原因:

  • 查看依赖的共享库:ldd app 发现 libMyCalc.so 找不到
  • 没有给动态链接器(ld-linux.so.2)指定好动态库 libMyCalc.so 的路径

解决:

法1) 直接将 libMyCalc.so 文件拷贝到/usr/lib//lib目录下,不推荐

法2)临时设置:export LD_LIBRARY_PATH=库路径,将当前目录加入环境变量,但是终端退出了就无效了。

法3)将export LD_LIBRARY_PATH=库路径写入~/.bashrc文件中,需要重启终端,永久有效

法4)动态链接器的配置文件 —> /etc/ld.so.conf,将动态库的绝对路径(如:/home/yang/test/lib)写进去,更新:sudo ldconfig -v

GDB

GDB: The GNU Project Debugger (sourceware.org)

基本命令

若要使用gdb调试程序,在编译源代码时必须带上-g参数 :gcc -g hello.c -o hello.out

进入gdb交互界面gdb .out文件gdb 进入环境之后 file 文件 加载文件

  • 没有调试信息 Reading symbols from base64...(no debugging symbols found)...done.
  • 可以调试 Reading symbols from test.out...done.

也可以使用readelf -S 文件名 | grep debug查看是否能调试

0)set args 可指定运行时参数。(如:set args 10 20 30 40 50 )

  • show args 命令可以查看设置好的运行参数
  • pwd 显示当前所在目录

1)start 运行程序,停在第一执行语句

2)run r 运行程序

3)break b 打断点 行号 或 函数名 或 文件名:行号

  • b 12
  • b main
  • b test.c:main
  • b class::function 在类class的function函数的入口处停住
  • b namespace::class:;function 在名称空间为namespace的类class的function函数的入口处停住
  • b 12 if num>0 # 设置程序在断点停止的条件
  • info breakpoints 查看所有断点,会显示断点编号Num

4)delete 1 删除编号为1的断点

5)disable [range...] dis disable所指定的停止点,如果什么都不指定,表示disable所有的停止点

6)enable [range...] ena enable所指定的停止点,如果什么都不指定,表示enable所有的停止点

7)print p 打印变量的值,或变量的地址

  • print a 查看变量a的值
  • print &a 查看变量a的地址
  • print *a 查看指针a指向的内容
  • print 'test.c'::i
  • print 'sum'::i

8)ptype 打印变量的类型

9)continue c 从断点处继续运行

10)next n 单步调试(逐过程,函数直接执行)

11)step s 单步执行(逐语句,进入函数内部)

12)list l 查看源代码,当前行,上2行下8行

  • list - 显示当前行前面的源程序 ,上5行下5行
  • list 6,21 列出6到21行的代码
  • list test.c:main 列出test.c文件的main函数
  • set listsize count 设置一次显示源代码行数,默认10行
  • show listsize 查看当前listsize设置

13)quit q 退出gdb交互界面

14)help [name] 查看命令的帮助

15)info 查看信息

  • info breakpoints 查看断点信息

16)display 追踪变量的值,当程序停住时,或是在你单步跟踪时,这些变量会自动显示

  • info display 查看所有追踪的变量及对应Num
  • undisplay Numdelete display Num 取消追踪某个变量
  • disable display num 不显示追踪的变量
  • enable display num 显示追踪的变量

17)until u 跳出循环 类似break

18)finish 跳出函数

19)set var v=值 修改程序中变量的值,v是程序的变量名

2、调试core文件

编译没有问题,运行程序崩溃

1)先修改shell限制

image-20220401172548704

ulimit -c unlimited

2)更改core dump生成路径

core dump默认会生成在程序的工作目录,但是有些程序存在切换目录的情况,导致core dump生成的路径没有规律

示例:

1
echo /data/coredump/core.%e.%p> /proc/sys/kernel/core_pattern
  • 更改core文件生成路径,放在/date/coredump文件夹中(要先创建文件夹)
  • %e表示程序名,%p表示进程id

不能生成core文件,https://blog.csdn.net/qq_35621436/article/details/120870746

1
$ gdb [exec file] [ core file]   #  如 gdb ./test test.core
  • 再输入wherebacktrace命令,gdb就会输出coredump的位置

3、调试正在运行的程序

后台运行 ./test.out & 有输出的话:nohup ./test.out &

查看进程PID ps -ef | grep 文件名

gdb -p 进程号 可能需要root权限

GDB调试指南(入门,看这篇够了)_程序猿编码的博客-CSDN博客_gdb调试

父子进程追踪

使用gdb调试的时候,gdb只能跟踪一个进程。可以在fork函数调用之前,通过指令设置gdb调试工具跟踪父进程或者是跟踪子进程。默认跟踪父进程。

set follow-fork-mode child 命令设置gdb在fork之后跟踪子进程。

set follow-fork-mode parent 设置跟踪父进程。

注意,一定要在fork函数调用之前设置才有效。

Makefile

概述 — 跟我一起写Makefile 1.0 文档 (seisman.github.io)

跟我一起写 Makefile(完整版)_FixCarMaster的博客-CSDN博客_跟我一起写makefile

安装sudo apt install make

文件命名:Makefilemakefile

三要素:目标、依赖、命令

注意:最终目标要写在最上面

注释:#

执行make —> 通过makefile生成目标文件

  • make 使用makefile文件
  • make -f mm 指定一个名字不为makefile的文件
  • make clean —> 清除编译生成的中间.o文件和最终目标文件
    • 如果当前目录下有同名clean文件,该命令不执行
    • 在命令前面加-:表示此条命令出错,make也会继续执行后续的命令。如:-rm a.o b.o

格式:

1
2
目标:依赖
命令

简单样例

1
2
app:main.c sub.c add.c mul.c
gcc main.c sub.c add.c mul.c -o app

如果某个文件修改,要重新编译,当文件很多时很耗时。修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
app:main.o add.o sub.o mul.o
gcc main.o add.o sub.o mul.o -o app

main.o:main.c
gcc -c main.c

add.o:add.c
gcc -c add.c

mul.o:mul.c
gcc -c mul.c

sub.o:sub.c
gcc -c sub.c

当某个.c文件改变时,执行make只会对应生成.o及最后的链接,其它未修改的文件不会再次生成,节省了编译的时间。

原理

若想生成目标,检查规则中的依赖条件是否存在,如果不存在,寻找是否有规则用来生成该依赖文件

检查规则中的目标是否需要更新,必须检查它的所有依赖,依赖中有任意一个被更新,则目标必须更新(判断条件:依赖文件比目标文件时间晚)

image-20220527140023044

image-20220527140035116

image-20220527140043582

伪目标

1
2
3
.PHONY:
伪目标:
命令

示例:clearAllclear 就是伪目标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 注释
# 文件名就是Makefile
hello:hello.o
g++ hello.o -o hello

hello.o:hello.s
g++ -c hello.s -o hello.o

hello.s:hello.i
g++ -S hello.i -o hello.s

hello.i:hello.cpp
g++ -E hello.cpp -o hello.i

.PHONY:
clearAll:
rm hello.o hello.i hello.s hello

clear:
rm hello.o hello.i hello.s

使用Makefile

1
2
3
4
5
6
7
$ make hello
g++ -E hello.cpp -o hello.i
g++ -S hello.i -o hello.s
g++ -c hello.s -o hello.o
g++ hello.o -o hello
$ make clearAll
rm hello.o hello.i hello.s hello

变量

  • 赋值 = 如:obj = a.o b.o c.o
  • 追加 += 如:obj += d.o
  • 常量 := 如:target := app
  • 使用: $(变量名)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Makefile
CC := g++
target = hello
reply = hello.o
source = hello.cpp
remove += $(reply)

$(target):$(reply)
$(CC) $(reply) -o $(target)

$(reply):$(source)
$(CC) -c $(source) -o $(reply)

# 即使当前目录有clean cleanAll 两个文件
# make clean
# make cleanAll
# 也能够执行到这两个伪命令
.PHONY:clean cleanAll
clearAll:
rm $(remove) hello

clear:
rm $(remove)

Makefile 维护的一些变量

  • 通常格式都是大写,部分有默认值。如:
    • CC :默认值 cc 意思即gcc
    • CPPFLAGS : 预处理器需要的选项。如:-I
    • CFLAGS:编译的时候使用的参数 –Wall –g -c
    • LDFLAGS :链接库使用的选项 –L -l
  • 用户可以修改这些变量的默认值。如:CC = gcc

模式匹配

%.c %.o 任意的.c.o文件 *.c *.o 所有的 .c .o文件

自动变量

  • $< 规则中的第一个依赖
  • $@ 规则中的目标
  • $^ 规则中所有依赖
1
2
3
4
5
6
app:main.o sub.o mul.o
gcc $^ -o $@
%.o:%.c
gcc -c $< -o $@
# $< 表示依次取出依赖条件
# $@ 表示依次取出目标值

函数

makefile中所有的函数必须都有返回值

wildcard:查找指定目录下指定类型的文件,一个参数

  • src = $(wildcard ./src/*.c) 查找当前目录的src文件夹下的.c文件

patsubst:匹配替换,从src中找到所有.c 结尾的文件,并将其替换为.o

  • obj = $(patsubst %.c ,%.o ,$(src)) 把src变量中所有后缀为.c的文件替换成.o
  • ob = $(patsubst ./src/%.c, ./obj/%.o, $(src)) 指定.o 文件存放的路径 ./obj/%.o
1
2
3
4
5
6
7
8
9
10
11
12
13
14
target = app
src=$(wildcard ./*.c)
obj=$(patsubst ./%.c, ./$.o, $(src))
CC = gcc
CPPFLAGS = -I
$(target):$(obj)
$(CC) $(obj) -o $(target)

%.o:%.c
$(CC) -c $< -o $@

.PHONY:clean
clean:
-rm $(obj) $(target)

欢迎关注我的其它发布渠道