Linux中gcc编译流程,库的制作,makefile的编写以及gdb调试

本文详细介绍了GCC编译器从预处理、编译、汇编到链接的四个步骤,以及在各阶段使用的关键参数。同时,讨论了静态库和动态库的制作、使用方法及其优缺点。此外,还涵盖了makefile的编写规则和GDB调试工具的使用,包括断点设置、运行控制和查看程序状态等基本操作。
摘要由CSDN通过智能技术生成

1. gcc编译

1.1 gcc编译流程

GCC 编译器在编译一个C语言程序时需要经过以下 4 步:

  1. 将C语言源程序预处理,生成.i文件。
  2. 预处理后的.i文件编译成为汇编语言,生成.s文件。
  3. 将汇编语言文件经过汇编,生成目标文件.o文件。
  4. 将各个模块的.o文件链接起来生成一个可执行程序文件。
参数说明
-o file生成目标file文件
-E预处理后就停止,不会编译,默认输出到终端
-S编译之后就停止,不会汇编,生成汇编代码
-c执行汇编之后就停止,不会链接,生成目标文件
-I dir指定包含头文件的文件夹dir
-D name将name定义为宏,为1
-O优化等级,0~3,默认0级
-L包含的库路径
-l指定库名
-g用于gdb调试,不加此选项不能gdb调试
-Wall显示更多的警告
-lstdc++编译C++代码

1.1.1 预处理

头文件展开,宏替换,生成.i文件
gcc -o hello.i -E hello.c

1.1.2 编译

将预处理后的.i文件编译为汇编语言,并生成.s文件
gcc -o hello -S hello.i

1.1.3 汇编

将汇编语言经过汇编,生成目标文件.o文件
gcc -o hello -c hello.s

1.1.4 链接

将各个模块的.o文件链接起来生成一个可执行文件,默认生成a.out
gcc -o hello hello.o

2. 库的制作

2.1 静态库的制作

静态库制作步骤:

  1. 编译为.o文件
  2. 将.o文件打包

使用:编译时需要添加静态库名,-I包含头文件
优点:

  1. 执行速度快
  2. 发布应用时不需要发布库

缺点:

  1. 执行程序体积较大
  2. 库变更时需要重新编译程序

查看库文件信息:nm libfilename

2.1.1 静态库命名规则

Linux静态库命名规范,必须是"lib[your_library_name].a":lib为前缀,中间是静态库名,扩展名为.a。

2.1.2 创建静态库(.a)

通过上面的流程可以知道,Linux创建静态库过程如下:

  • 首先,将代码文件编译成目标文件.o(StaticMath.o)
    gcc -c StaticMath.c
    注意带参数-c,否则直接编译为可执行文件
  • 然后,通过ar工具将目标文件打包成.a静态库文件
    ar -rcs libstaticmath.a StaticMath.o
    生成静态库libstaticmath.a

2.1.3 使用静态库

Linux下使用静态库,只需要在编译的时候,指定静态库的搜索路径(-L选项)、指定静态库名(不需要lib前缀和.a后缀,-l选项)。

gcc TestStaticLibrary.cpp -L ../StaticLibrary -lstaticmath

  • -L:表示要连接的库所在目录
  • -l:指定链接时需要的动态库,编译器查找动态连接库时有隐含的命名规则,即在给出的名字前面加上lib,后面加上.a或.so来确定库的名称。

2.2 动态库的制作

动态库的制作步骤:

  1. 编译与位置无关的源代码,生成.o文件,生成关键参数:-fPIC
  2. 将.o文件打包。关键参数:-shared

使用:-L指定动态库路径;-l指定库名
例:gcc -o newapp main.c -L ./lib -lmvcalc -I ./include
优点:

  1. 执行程序体积小
  2. 库变更时,一般不需要重新编译程序

缺点:

  1. 执行时需要动态加载动态库,相对静态库速度较慢
  2. 发布应用时需要同时发布动态库

不能加载动态库问题:

  1. 将动态库拷贝到 /lib 目录下(不推荐)
  2. 将库路径增加到环境变量 LD_LIBARARY_PATH中(不推荐)
  3. 配置 /etc/ld.so.conf文件,增加库的绝对路径(需执行sudo ldconfig -v使配置生效)

2.2.1 动态库的命名规则

Linux动态库命名规范,必须是"lib[your_library_name].so":lib为前缀,中间是动态库名,扩展名为.so

2.2.2 制作动态库(.so)

  • 首先,将代码文件编译成目标文件.o(test.o)
    gcc -fPIC -c test.cpp -I ../include
    注意带参数-c,否则直接编译为可执行文件
  • 然后,通过gcc将目标文件打包成.so静态库文件
    gcc -shared -o libtest.so test.o
    生成静态库libtest.so

上面两个步骤可以合并为一个命令:
gcc -fPIC -shared -o libtest.so test.c

2.2.3 使用动态库

引用动态库编译成可执行文件(跟静态库方式一样):
gcc TestDynamicLibrary.c -L../DynamicLibrary -ltest
此时如果运行程序会报错,找不到库文件

当系统加载可执行代码时候,能够知道其所依赖的库的名字,但是还需要知道绝对路径。此时就需要系统动态载入器(dynamic linker/loader)。
对于elf格式的可执行程序,是由ld-linux.so*来完成的,它先后搜索elf文件的 DT_RPATH段—>环境变量LD_LIBRARY_PATH—>/etc/ld.so.cache文件列表—>/lib/,/usr/lib 目录找到库文件后将其载入内存。

如何让系统能够找到它:

  • 如果安装在/lib或者/usr/lib下,那么ld默认能够找到,无需其他操作。
  • 如果安装在其他目录,需要将其添加到/etc/ld.so.cache文件中,步骤如下:
  1. 编辑/etc/ld.so.conf文件,加入库文件所在目录的路径
  2. 运行ldconfig -v ,该命令会重建/etc/ld.so.cache文件
  • 如果修改LD_LIBRARY_PATH环境变量:
    export LD_LIBRARY_PATH=lib_dir_obsolute_path:$LD_LIBRARY_PATH
    注:LD_LIBRARY_PATH环境变量在重启终端后会失效,可以将该命令加入.bashrc文件中使其生效

ldd app可以检查可执行程序app的链接

3. makefile的编写

3.1 makefile规则

  1. 如果这个工程没有编译过,那么我们的所有c文件都要编译并被链接
  2. 如果这个工程的某几个c文件被修改,那么我们只编译被修改的c文件,并链接目标程序
  3. 如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的c文件,并链接目标程序

make命令会自动智能地根据当前的文件修改的情况来确定哪些文件需要重编译,从而自动编译所需要的文件和链接目标程序

3.1.1 makefile命名规则

make默认只能识别makefileMakefile,如果需要使用其他文件需要使用-f指定文件名

3.1.2 makefile书写规则

makefile三要素:

  • 目标
  • 依赖
  • 规则命令
target ... : prerequisites ...
   command
   ...
   ...

target
可以是一个object file(目标文件),也可以是一个执行文件,还可以是一个标签(label)
prerequisites
生成该target所依赖的文件和/或target
command
该target要执行的命令(任意的shell命令)

实例:

# objects是定义的一个变量,'\'是换行符
objects = main.o kbd.o command.o display.o \
    insert.o search.o files.o utils.o
# $(objects) 使用变量
edit : $(objects)
    gcc -o edit $(objects)
main.o : main.c defs.h
    gcc -c main.c
kbd.o : kbd.c defs.h command.h
    gcc -c kbd.c
command.o : command.c defs.h command.h
    gcc -c command.c
display.o : display.c defs.h buffer.h
    gcc -c display.c
insert.o : insert.c defs.h buffer.h
    gcc -c insert.c
search.o : search.c defs.h buffer.h
    gcc -c search.c
files.o : files.c defs.h buffer.h command.h
    gcc -c files.c
utils.o : utils.c defs.h
    gcc -c utils.c
# clean目标没有依赖,这种目标称为伪目标
clean :
    rm edit $(objects)

GNU的make很强大,它可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必要去在每一个 .o 文件后都写上类似的命令,因为,make会自动识别,并自己推导命令。

只要make看到一个 .o 文件,它就会自动的把 .c 文件加在依赖关系中,如果make找到一个 whatever.o ,那么 whatever.c 就会是 whatever.o 的依赖文件。并且 gcc -c whatever.c 也会被推导出来,于是,makefile不用写得这么复杂。

objects = main.o kbd.o command.o display.o \
    insert.o search.o files.o utils.o

edit : $(objects)
    gcc -o edit $(objects)

main.o : defs.h
kbd.o : defs.h command.h
command.o : defs.h command.h
display.o : defs.h buffer.h
insert.o : defs.h buffer.h
search.o : defs.h buffer.h
files.o : defs.h buffer.h command.h
utils.o : defs.h

.PHONY : clean
clean :
    rm edit $(objects)

这种方法,也就是make的“隐晦规则”。上面文件内容中, .PHONY 表示 clean 是个伪目标文件。
以上代码还可以进一步简写:

SrcFiles=$(wildcard *.c) # SrcFiles为当前路径下所有.c文件
ObjFiles=$(patsubst %.c,%.o,$(SrcFiles)) # 将SrcFiles中的.c替换为.o

edit : $(ObjFiles)
	gcc -o edit &(ObjFiles)
%.o:%.c
	gcc -c $< -o $@
# $< 和 $@ 则是自动化变量, $< 表示第一个依赖文件, $@ 表示目标集,这类变量只能在规则命令中出现
.PHONY : clean # 定义伪目标防止有歧义
clean:
	rm edit $(ObjFiles)

规则示例:

%.d: %.c
    @set -e; rm -f $@; \
    $(CC) -M $(CPPFLAGS) $< > $@.$$$$; \
    sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
    rm -f $@.$$$$

这个规则的意思是,所有的.d文件依赖于.c 文件,rm -f $@的意思是删除所有的目标,也就是.d文件,第二行的意思是,为每个依赖文件$<,也就是.c文件生成依赖文件,$@表示模式 %.d文件,如果有一个C文件是name.c,那么%就是name$$$$意为一个随机编号,第二行生成的文件有可能是name.d.12345,第三行使用sed命令做了一个替换,关于sed命令的用法请参看相关的使用文档。第四行就是删除临时文件。
将每一个.c文件生成一个可执行文件:

SrcFiles=$(wildcard *.c)
TargetFiles=$(patsubst %.c,%,$(SrcFiles))

all:$(TargetFiles)

%:%.c
    gcc -o $@ $^
 
clean:
	rm -f $(TargetFiles)

makefile参考链接

4. gdb调试

4.1 启动gdb

对C/C++程序的调试,需要在编译前就加上-g选项:
gcc -g hello.cpp -o hello
调试可执行文件:
gdb <program>
program也就是你的执行文件,一般在当前目录下。
调试core文件(core是程序非法执行后core dump后产生的文件):
gdb <program> <core dump file>
gdb program core.11127
调试服务程序:
gdb <program> <PID>
gdb hello 11127
如果你的程序是一个服务程序,那么你可以指定这个服务程序运行时的进程ID。gdb会自动attach上去,并调试他。program应该在PATH环境变量中搜索得到。

4.2 gdb交互命令

启动gdb后,进入到交互模式,通过以下命令完成对程序的调试;注意高频使用的命令一般都会有缩写,熟练使用这些缩写命令能提高调试的效率;

4.2.1 运行

命令说明
r(run)其作用是运行程序,当遇到断点后,程序会在断点处停止运行,等待用户输入下一步的命令
c(continue)继续执行,到下一个断点处(或运行结束)
n(next)单步跟踪程序,当遇到函数调用时,也不进入此函数体;此命令同 step 的主要区别是,step 遇到用户自定义的函数,将步进到函数中去运行,而 next 则直接调用函数,不会进入到函数体内
s(step)单步调试如果有函数调用,则进入函数;与命令n不同,n是不进入调用的函数的
until当你厌倦了在一个循环体内单步跟踪时,这个命令可以运行程序直到退出循环体
until+行号运行至某行,不仅仅用来跳出循环
finish运行程序,直到当前函数完成返回,并打印函数返回时的堆栈地址和返回值及参数值等信息
call fun(param)调用程序中可见的函数,并传递“参数”,如:call gdb_test(55)
q(quit)退出gdb

4.2.2 设置断点

命令说明
b n(break n)在第n行处设置断点(可以带上代码路径和代码名称: b OAGUPDATE.cpp:578)
b fn1 if a>b条件断点设置
b(break func)
delete 断点号n删除第n个断点
disable 断点号n暂停第n个断点
enable 断点号n开启第n个断点
clear 行号n清除第n行的断点
info b(info breakpoints)显示当前程序的断点设置情况
delete breakpoints清除所有断点

4.2.3查看源代码

命令说明
l(list)其作用就是列出程序的源代码,默认每次显示10行
list 行号将显示当前文件以“行号”为中心的前后10行代码,如:list 12
list 函数名将显示“函数名”所在函数的源代码,如:list main
list不带参数,将接着上一次 list 命令的,输出下边的内容

4.2.4打印表达式

命令说明
p(print 表达式)其中“表达式”可以是任何当前正在被测试程序的有效表达式,比如当前正在调试C语言的程序,那么“表达式”可以是任何C语言的有效表达式,包括数字,变量甚至是函数调用
print a将显示整数 a 的值
print ++a将把 a 中的值加1,并显示出来
print name将显示字符串 name 的值
print gdb_test(22)将以整数22作为参数调用 gdb_test() 函数
print gdb_test(a)将以变量 a 作为参数调用 gdb_test() 函数
display 表达式在单步运行时将非常有用,使用display命令设置一个表达式后,它将在每次单步进行指令后,紧接着输出被设置的表达式及值。如: display a
watch 表达式设置一个监视点,一旦被监视的“表达式”的值改变,gdb将强行终止正在被调试的程序。如: watch a
whatis查询变量或函数
info function查询函数
info locals显示当前堆栈页的所有变量

4.2.5 查询运行信息

命令说明
where/bt当前运行的堆栈列表
bt backtrace显示当前调用堆栈
`up/down改变堆栈显示的深度
set args 参数指定运行时的参数
show args查看设置好的参数
info program来查看程序的是否在运行,进程号,被暂停的原因

4.2.6 分割窗口

命令说明
layout用于分割窗口,可以一边查看代码,一边测试
layout src显示源代码窗口
layout asm显示反汇编窗口
layout regs显示源代码/反汇编和CPU寄存器窗口
layout split显示源代码和反汇编窗口
Ctrl + L刷新窗口

注:交互模式下直接回车的作用是重复上一指令,对于单步调试非常方便
gdb参考链接

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT灰猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值