1. gcc编译
1.1 gcc编译流程
GCC 编译器在编译一个C语言程序时需要经过以下 4 步:
- 将C语言源程序预处理,生成.i文件。
- 预处理后的.i文件编译成为汇编语言,生成.s文件。
- 将汇编语言文件经过汇编,生成目标文件.o文件。
- 将各个模块的.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 静态库的制作
静态库制作步骤:
- 编译为.o文件
- 将.o文件打包
使用:编译时需要添加静态库名,-I包含头文件
优点:
- 执行速度快
- 发布应用时不需要发布库
缺点:
- 执行程序体积较大
- 库变更时需要重新编译程序
查看库文件信息: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 动态库的制作
动态库的制作步骤:
- 编译与位置无关的源代码,生成.o文件,生成关键参数:-fPIC
- 将.o文件打包。关键参数:-shared
使用:-L指定动态库路径;-l指定库名
例:gcc -o newapp main.c -L ./lib -lmvcalc -I ./include
优点:
- 执行程序体积小
- 库变更时,一般不需要重新编译程序
缺点:
- 执行时需要动态加载动态库,相对静态库速度较慢
- 发布应用时需要同时发布动态库
不能加载动态库问题:
- 将动态库拷贝到 /lib 目录下(不推荐)
- 将库路径增加到环境变量 LD_LIBARARY_PATH中(不推荐)
- 配置 /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
文件中,步骤如下:
- 编辑
/etc/ld.so.conf
文件,加入库文件所在目录的路径- 运行
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规则
- 如果这个工程没有编译过,那么我们的所有c文件都要编译并被链接
- 如果这个工程的某几个c文件被修改,那么我们只编译被修改的c文件,并链接目标程序
- 如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的c文件,并链接目标程序
make命令会自动智能地根据当前的文件修改的情况来确定哪些文件需要重编译,从而自动编译所需要的文件和链接目标程序
3.1.1 makefile命名规则
make
默认只能识别makefile
和Makefile
,如果需要使用其他文件需要使用-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)
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参考链接