理解C语言(零) 导读(下):有用的C语言工具-从Make说起
1 Make
在GNU中提供了一个用于管理多个C源代码文件的项目管理工具,用户只需按照一定的语法规则编写这个Makefile文件。输入make命令,系统会自动的根据当前文件的修改情况确定哪些文件需要重编译,一旦文件被修改,make工具只会执行依赖于该文件的一系列规则,这样节省了整个编译和链接时间。
1.1 Make规则
Makefile是由若干规则组成,每个规则定义了生成对应目标文件和它的依赖关系、产生目标文件需要执行的命令。
它的核心在于只要依赖的文件时间比目标更新,则执行产生目标的命令,不存在执行后续既定的命令。注:这里的目标既可以是一个目标文件,也可以是可执行文件,也可能是伪目标(命令必须从tab键开头)
目标: 依赖文件列表
<tab>命令
如执行
#Makefile 文件1
appex: main.o app.o mod.o lib.o
@echo "正在编译模块..."
gcc -o appex main.o app.o mod.o lib.o
main.o: main.c app.h
gcc -c main.c
app.o: app.c app.h
gcc -c app.c
mod.o: mod.c
gcc -c mod.c
lib.o: lib.c lib.h
gcc -c lib.c
clean:
rm -f *.o
有几点需要说明:
- 关于GCC的参数使用,参考理解C语言(零) 导读(上)的第一节
- Make目标规则中支持三个通配符:*、 ?、 [...]
- 若未指定目标,则运行
make
命令默认执行第一个目标。运行make clean
命令,清除所有的目标文件(clean是一个伪目标,并不生成clean这个文件,它只是一个标签)。还有很多这样的命令,如
all
: 一般是编译所有的目标;
install
: 把已经编译号的目标执行文件拷贝到指定目标中去
tar
: 把源程序打包备份,tar文件;
dist
: 创建一个压缩文件
TAGS
: 更新所有的目标,以备完整的编译使用
为避免和目标文件重名的情况,通常使用一个特殊的标记.PHONY
来显式地指明这是一个伪目标,如:
.PHONY: doc
doc:
command
.PHONY: distclean clean
distclean: clean
$(MAKE) -C test distclean
rm -rf autom4te.cache/
rm -f Makefile
rm -f $(CILLYDIR)/App/$(CILLYMOD)/CilConfig.pm
rm -f config.h
rm -f config.log
rm -f config.mk
rm -f config.status
rm -f doc/header.html
rm -f doc/index.html
rm -f src/machdep-ml.c src/cilversion.ml
rm -f stamp-h
clean: $(CILLYDIR)/Makefile
rm -rf $(OBJDIR)
rm -f $(BINDIR)/$(CILLY).*
rm -rf lib/cil share/
rm -f META
rm -rf doc/html/
rm -rf doc/cilcode.tmp/
rm -f doc/cil.version.*
rm -f doc/cilpp.*
$(MAKE) -C $(CILLYDIR) clean
rm -f $(CILLYDIR)/App/$(CILLYMOD).pm
rm -f $(CILLYDIR)/Makefile.old
$(MAKE) -C test clean
- 规则支持多目标,因为有可能我们的多个目标同时依赖于一个文件,我们就把它合并起来,建议使用自动变量
$@
表示目前规则中所有的目标集合,如:
bigoutput littleoutput : text.g
generate text.g -$(subst output,,$@) > $@
- 定义多目标规则-使用到了自动化变量
$<
(表示所有的依赖目标集),$@
(表示所有的目标集合)。例如
objects= foo.o bar.o
all: $(objects)
$(objects): %.o : %.c
$(CC) -c $(CFLAGS) $< -o $@
该例子的意思是:我们的目标从$objects
中获取-所有.o结尾的目标,就是foo.o/bar.o
,依赖的模式是对应的%.c文件。目标文件较多时,采取这种静态模式规则更为方便,灵活
@
字符在命令行前表示在命令执行前输出信息到屏幕上- 如果命令模式里含有多个命令需要连续执行,应使用
;
分隔命令 - 嵌套make执行-每个子目录中都有一个Makefile,根目录有一个Makefile,根目录的Makefile应如下书写,它表示先进入这个子目录中,再执行make命令
search:
cd suddir && $(MAKE)
在Makefile中,主要包含了以下内容: 变量定义、显式规则、隐含规则。
1.2 变量
A. 变量基础
变量名的命名规则:变量名=变量值(字符串),引用变量时在变量前加上$符号,也可用"()"或者"{}"把变量给包起来(为了安全使用)。如果使用变量来定义变量的值,有两种方式: "="或者":="
# =符号允许变量可以使用后面的变量定义,但出现递归引用,就不行
foo = $(bar)
bar = $(ugh)
ugh = Huh?
all :
echo $(foo)
x := foo
y := $(x) bar
x := later
# 使用:=,前面的变量就不能使用后面的变量
操作符"?="表示如果变量没有定义过,则变量值就是后面的形式,若先前被定义,什么都不做。追加变量sh值使用"+=",如:
# 结合条件选择是否追加相应参数,摘自CIL代码
CILHOME := ..
CILLY := $(CILHOME)/bin/cilly
ifdef _MSVC
include Makefile.msvc
else
ifdef _GNUCC
include Makefile.gcc
endif
endif
CILLY += --mode=$(COMPILERNAME) --decil
CILLY += --save-temps $(EXTRAARGS)
目标变量:我们还可以为目标设置局部变量,这个变量只会作用在这条规则和连带规则中,而不影响其他规则链以外的值。如:
prog: CFLAGS = -g
prog: prog.o foo.o
$(CC) $(CFLAGS) prog.o foo.o -o prog
prog.o : prog.c
$(CC) $(CFLAGS) prog.c
如果我们想在多个目标中定义,则使用模式变量,如%.o: CFLAG= -O
B. 条件判断
ifeq (arg1,arg2)... else ...endif
如果两个参数的值相等,则表达式为真,相反的是ifneq
ifdef var-name ... else ... endif
如果定义了某变量值非空,则表达式为真,相反的是ifndef
例如:
ifeq ($(CC),gcc)
libs=$(libs_for_gcc)
else
libs=$(normal_libs)
endif
ifndef NOCHECK
CILLY += --strictcheck
endif
ifdef OCAMLDEBUG
CILLY+= --ocamldebug
endif
1.3 隐含规则、模式规则及其使用的变量
GNU make中定义了内置各种隐含规则,在不给出产生目标文件的命令时由make自动添加,例如未定义如何产生目标的命令,如:
demo.o: demo.c app.h
# make会自动添加如下规则
# $(CC) $(CFLAGS) $(CPPFLAGS) ... -c $< -o $@
我们看到在隐含规则中基本上都使用了一些预定义的变量,你可以在文件中进行重新定义。只要设定了这些预定义变量,就会对隐含规则起作用。这些预定义变量分为两种类型:命令相关,参数相关。
命令相关:
- AR: 函数库打包程序,默认命令ar;AS: 汇编语言编译程序,默认as
- CC: C编译器,默认cc; CXX: C++编译器,默认g++; CPP: C程序的预处理,默认$(CC) -E
- RM: 删除文件命令,默认rm -f
参数相关:
- CFLAGS: C编译器参数,可添加加入非标准的目录
-I dir
或者调试信息-g
选项 - CPPFLAGS: C预处理参数;CXXFLAGS: C++编译器参数
- LDFLAGS: 链接器参数,通常可添加
-lxxx
指定的库文件(如-lm)或者指定的库搜索路径-L dir
- LEX: 词法分析器,默认lex; 语法分析器,默认yacc
还注意到刚才已经多次提到了自动变量,它的值是与规则中的目标和依赖对象有关,即把模式中定义的一些列文件自动地取出,直至所有的符合模式的文件都取完,自动化变量只出现在规则的命令中。如下:
$@
: 匹配规则中的目标文件集合$^
: 所有的依赖目标集合,以空格分隔,有重复去除($+,不去除重复)$<
: 第一个依赖文件,如果依赖目标是以模式%定义的,表示符合模式的一系列文件$*
: 不包含扩展名的目标文件名称$?
: 所有比目标新的依赖目标的集合,以空格分隔。
希望只对更新过的依赖文件操作,$?就很有用,例如一个库文件lib,由其他几个目标文件更新,那么把几个目标打包的高效率的规则如下:
lib : x.o y.o z.o
ar r lib $?
结合这些采取不同的规则修改上面我们定义的Makefile文件
- 使用自动变量
OBJS= main.o app.o mod.o lib.o
appex: $(OBJS)
@echo "正在编译模块..."
$(CC) -o $@ $^
main.o: main.c app.h
$(CC) -c -o $@ $<
app.o: app.c app.h
$(CC) -c -o $@ $<
mod.o: mod.c
$(CC) -c -o $@ $<
lib.o: lib.c lib.h
$(CC) -c -o $@ $<
clean:
rm -f *.o
- 使用隐含规则
OBJS= main.o app.o mod.o lib.o
appex: $(OBJS)
@echo "正在编译模块..."
$(CC) -o $@ $^
main.o: main.c app.h
app.o: app.c app.h
mod.o: mod.c
lib.o: lib.c lib.h
clean:
rm -f *.o
- 使用模式规则,把具有相同行为特点的规则,进行通配表示
*.o : *.c
$(CC) -c $< -o $@
OBJS= main.o app.o mod.o lib.o
appex: $(OBJS)
@echo "正在编译模块..."
$(CC) -o $@ $^
main.o: main.c app.h
app.o: app.c app.h
mod.o: mod.c
lib.o: lib.c lib.h
clean:
rm -f *.o
2 GDB
例如我有三个文件:stack.h、stack.c、teststack.c,通过Makefile编译或(gcc -g -o stack),生成可执行文件stack ,下面将进行调试
2.1 运行程序
加载可执行文件:gdb stack
如果显示No symbol table is loaded
,其实是因为GCC编译时没加入-g
选项。
解决办法是在编译时一定要加-g选项以加入调试信息,即修改Makefile里面的CFLAGS选项=-g,因为如果没有-g,你将看不见程序的函数名、变量名,所代替的全是运行时的内存地址。
加入-g参数后,会显示Reading symbols from stack...(no debugging symbols found)...done
查看当前源代码:l func-name
(函数名,默认为main)
默认l
(list) : 显示main函数,直接回车表示,表示重复上一次命令
开始和停止命令:
命令 | 效果 |
---|---|
r |
运行程序,可在此给出命令行参数(r,run命令的缩写) |
q |
退出GDB(q,quit命令的缩写) |
k |
停止程序(k,kill命令的缩写) |
设置调试的命令行参数:
gdb命令行中的gdb --args <运行文件> <参数>
gdb环境中的set args
命令
run执行时加入参数
2.2 设置断点和调试执行
断点: b,break命令的缩写;d,delete命令的缩写
命令 | 效果 |
---|---|
b 16 |
在16行处设置断点 |
b sum |
在函数sum入口处设置断点 |
b *0x8048394 |
在地址0x8048394处设置断点 |
d id |
删除断点的标号(注:不是行号或函数名) |
d |
删除所有断点 |
info b |
查看断点信息 |
调试执行:
命令 | 效果 |
---|---|
n |
单步执行 |
c |
继续执行 |
s |
进入函数内部 |
finish |
运行直到当前函数返回,即跳出某个函数或断点 |
2.3 检查代码和数据
检查代码:
命令 | 效果 |
---|---|
disas |
反汇编当前函数 |
disas sum |
反汇编函数sum |
disas 0x8048394 0x80483a4 |
反汇编指定地址范围内的代码 |
info frame |
查看当前栈帧的信息 |
bt |
查看函数堆栈信息 |
检查数据:p,print命令的缩写;x-输出地址信息
命令 | 效果 |
---|---|
p 变量名 |
输出变量的值,总是需要一个变量名 |
p 0x100 |
输出0x100的十进制表示 |
p /x 555 |
输出555的八进制表示 |
p /t y |
输出y的二进制表示 |
注:p显示的变量信息均是在当前n命令(c,还未执行到这一行语句)前的内容
变量设置:直接使用set $name
例如想逐个打印数组的元素,可以如下:
(gdb) set $i=0
(gdb) p arr[$i++]
2.4 多线程调试
建议使用多线程库时使用-lpthread
定位解析多线程头文件
命令 | 效果 |
---|---|
info thread |
查看当前进程中的线程 |
thread <ID> |
切换调试的线程为指定ID的线程 |
b xxx.c:5 thread all<ID> |
在xxx.c第5行处为所有经过这里的线程设置断点 |
set scheduler-locking off/on/step |
使用单步或继续命令调试当前被调试线程的时候,其他线程也是同时执行的,怎么只让被调试程序执行 |
参数说明:off 不锁定任何线程,所有线程都执行;on 只有当前被调试程序执行;step 单步的时候,除了next过一个函数意外只有当前线程会执行
参考
- Make篇参考- [GNU/Linux编程]第5章第2小节
- GDB中应该知道的几个调试方法