使用Makefile


C 工程结构

一个典型的C工程(lib库)的工程结构可参考如下:

c-project
├── LECENCE
├── Makefile
├── README.md
├── bin
├── build
├── src
│   └── dbg.h
└── tests
  • bin:生成的可执行文件目录
  • build:库文件(.a)存放目录,编译过程中产生
  • src: 存放源码
  • test: 存放单元测试文件

使用Makefile

当一个工程很大,有很多文件时,使用gcc编译就局限了。这个时候通常使用makefile,makefile中,需要把这些文件组织到一起。makefile是一个纯文本文件,本质是一个shell脚本(关于Shell脚本可以参考这里,符号对大小写敏感,下面是一个简单的Makefile文件:

CC=gcc
CFLAGS=-Wall -g
LDFLAGS=
SOURCES=main.cpp hello.cpp factorial.cpp
OBJECTS=$(SOURCES:.cpp=.o)
EXECUTABLE=hello
all: $(SOURCES) $(EXECUTABLE)
$(EXECUTABLE): $(OBJECTS)
    $(CC) $(LDFLAGS) $(OBJECTS) -o $@
.cpp.o:
    $(CC) $(CFLAGS) $< -o $@
  • CC: 编译器名称
    • % gcc main.c 在makefile中的写法为:$(CC) main.c
  • CFLAGS: 编译参数,也就是上面提到的gcc的编译选项。
    • CFLAGS=-Wall -g
    • 通常用来指定头文件的位置,常用的是-I, -g
  • LDFLAGS: 链接参数,告诉链接器lib的位置
    • 常用的有-I,-L
  • SOURCES: 所有.c文件路径
  • OBJECTS: 所有.o文件路径
  • EXECUTABLE: 编译后的可执行文件名称
  • action: allclean为action,可以用make action来执行某个action
    • 如果不指定action,默认执行Makefile中的第一个action

Mekfile的工作原理

当执行make时,编译器会在当前目录下寻找Makefile,找到之后从第一个TARGET开始解析其依赖的变量或其他条件。Makefile的一条Rule的格式为

target: prerequisites
	recipe
	…
	…
  • target有两个含义
    • 要build的文件名+后缀
    • 某个action(比如上面提到的clean
  • prerequisites为target的依赖,包含:
    • 在执行target前需要执行的action
    • recipe中需要的参数
  • recipe则为这个action具体执行的shell命令,每条命令单独一行,前面用tab空格
myprogam: main.o foo.o #编译myprogam文件,依赖main.o,foo.o两个文件
    gcc -o myprogram main.o foo.o #shell命令

main.o: main.c foo.h #产生main.o 需要main.c, foo.h
    gcc -c main.c

foo.o: foo.c foo.h
    gcc -c foo.c

A Makefile Demo

CFLAGS=-g -O2 -Wall -Wextra -Isrc -rdynamic -DNDEBUG $(OPTFLAGS)
LIBS=-ldl $(OPTLIBS)

PREFIX?=/usr/local
SOURCES=$(wildcard src/**/*.c src/*.c)
OBJECTS=$(patsubst %.c,%.o,$(SOURCES))

TEST_SRC=$(wildcard tests/*_tests.c)
TESTS=$(patsubst %.c,%,$(TEST_SRC))

TARGET=build/libYOUR_LIBRARY.a
SO_TARGET=$(patsubst %.a,%.so,$(TARGET))

# The Target Build
all: $(TARGET) $(SO_TARGET) tests

dev: CFLAGS=-g -Wall -Isrc -Wall -Wextra $(OPTFLAGS)
dev: all

$(TARGET): CFLAGS += -fPIC
$(TARGET): build $(OBJECTS)
	ar rcs $@ $(OBJECTS)
	ranlib $@

$(SO_TARGET): $(TARGET) $(OBJECTS)
	$(CC) -shared -o $@ $(OBJECTS)

build:
	@mkdir -p build
	@mkdir -p bin

# The Unit Tests
.PHONY: tests
tests: CFLAGS += $(TARGET)
tests: $(TESTS)
	sh ./tests/runtests.sh

# The Cleaner
clean:
	rm -rf build $(OBJECTS) $(TESTS)
	rm -f tests/tests.log
	find . -name "*.gc*" -exec rm {} \;
	rm -rf `find . -name "*.dSYM" -print`

# The Install
install: all
	install -d $(DESTDIR)/$(PREFIX)/lib/
	install $(TARGET) $(DESTDIR)/$(PREFIX)/lib/

# The Checker
check:
	@echo Files with potentially dangerous functions.
	@egrep '[^_.>a-zA-Z0-9](str(n?cpy|n?cat|xfrm|n?dup|str|pbrk|tok|_)\
		|stpn?cpy|a?sn?printf|byte_)' $(SOURCES) || true

上面的Makefile是个人使用的版本,不同开发人员可能会维护各自的版本,结构大同小异

  • SOURCES:GNU Make中提供了一个函数叫wildcard,它可以将所有符合由其参数描述的文件展开,以空格间隔。因此可以用这个函数展开src路径下所有的.c文件
  • OBJECTS: 使用了另一个GNU Make中的函数patsubst,它需要三个参数:
    • 第一个是一个需要匹配的式样;第二个表示用什么来替换它,第三个是一个需要被处理的由空格分隔的列表
    • 使用patsubst可以方便的生成.o文件列表
  • $(OPTFLAGS),$(OPTLIBS)为执行make命令时的附加参数
    • make OPTFLAGS=-pthread
    • 如果附加参数和Makefile中某项option崇明,则会覆盖掉Makefile中该option的值
      • make PREFIX=/tmp install #installs the lib to /tmp
  • TARGET/SO_TARGET: 编译后库文件目录
  • all: 默认的make命令,依次执行$(TARGET) $(SO_TARGET) tests这三条rule
  • $(TARGET): 具体的编译指令,依赖build和$(OBJECT),前者创建文件夹,后者为shell命令参数

C 程序调试

现代可视化的编辑器如Visual Studio, XCode集成了很方便好用的调试工具,使用这些工具可以极大的提升效率,不必纠结于命令行+GDB/LLDB的方式

GDB & LLDB

  • GDB tricks
    • gdb --args [PROGRAM] Normally, gdb takes arguments you give it and assumes they are for itself. Using –args passes them to the program.
    • thread apply all bt .Dump a backtrace for all threads. It’s very useful.
    • gdb --batch --ex run --ex bt --ex q --args [PROGRAM] [ARGS]Run the program so that if it bombs, you get a backtrace.
  • GDB参考
    • run [args] Start your program with [args].
    • break [file:] function Set a break point at [file:]function. You can also use b.
    • backtrace Dump a backtrace of the current calling stack. Shorthand is bt.
    • print expr Print the value of expr. Shorthand is p.
    • continue Continue running the program. Shorthand is c.
    • next Next line, but step over function calls. Shorthand is n.
    • step Next line, but step into function calls. Shorthand is s.
    • quit Exit GDB.
    • help List the types of commands. You can then get help on the class of command as well as the command.
    • cd, pwd, make just like running these commands in your shell.
    • shell Quickly start a shell so you can do other things.
    • clear Clear a breakpoint.
    • info break, info watch Show information about breakpoints and watchpoints.
    • attach pid Attach to a running process so you can debug it.
    • detach Detach from the process.
    • list List out the next ten source lines. Add a to list the previous ten lines.
  • LLDB参考
    • run [args] Start your program with [args].
    • breakpoint set --name [file:]function Set a break point at [file:]function. You can also use b, which is way easier.
    • thread backtrace Dump a backtrace of the current calling stack. Shorthand is bt.
    • print expr Print the value of expr. Shorthand is p.
    • continue Continue running the program. Shorthand is c.
    • next Next line, but step over function calls. Shorthand is n.
    • step Next line, but step into function calls. Shorthand is s.
    • quit Exit LLDB.
    • help List the types of commands. You can then get help on the class of command as well as the command itself.
    • cd, pwd, make just like running these commands in your shell.
    • shell Quickly start a shell so you can do other things.

使用GDB调试需要有debug symbol,GCC编译时加入-g生成符号文件(例如XCode会生成 .dSYM文件)

Resource