理解C语言(零) 导读(下):有用的C语言工具-从Make说起

理解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过一个函数意外只有当前线程会执行


参考

posted @ 2015-03-06 23:24  charlesxiong  阅读(1133)  评论(0编辑  收藏  举报