Makefile 基本笔录

命令行编译

main.c文件

#include <stdio.h>
int main() 
{
   printf("Hello, World!\r\n");
   return 0;
}

命令行 编译运行

$ gcc -o main main.c
$ ./main
Hello, World!

拆成两个.c文件

//main.c
#include "print.h"
int main() 
{
   func_print();
   return 0;
}

//print.h
#ifndef PRINT_H
#define PRINT_H
extern void func_print(void);
#endif

//print.c
#include <stdio.h>
#include "print.h"
void func_print(void)
{
    printf("Hello, World!\r\n");
}

命令行编译运行

# https://stackoverflow.com/questions/18777326/compiling-multiple-c-files-with-gcc
# -c, Compile and assemble, but do not link.
$ gcc main.c -o main.o -c
$ gcc print.c -o print.o -c
$ gcc -o main main.o print.o
$ ./main
Hello, World!

# 或者更简单的
$ gcc -o main main.c print.c
$ ./main
Hello, World!

但如果不在同一个目录, 如下

$ tree
.
├── inc
│   └── print.h
├── main.c
└── src
    └── print.c

那么编译运行就变成了

# -I指定头文件路径, -Iinc 也就是 -I./inc
# 等价于 gcc -Iinc -o main main.c src/print.c
$ gcc -I./inc -o main main.c ./src/print.c
$ ./main
Hello, World!

# 如果有库文件, 用-l

# 可以用pkg-config自动获取include目录列表
# $ pkg-config --cflags python
# -I/usr/include/python2.7 -I/usr/include/x86_64-linux-gnu/python2.7
# $ pkg-config --libs python
# -lpython2.7

Makefile初探

固然可以把命令行写进一个.sh脚本, 但是又不太好管控(.o文件)哪些文件更新哪些没更新, 默认每次都要全部编译, 不合理, Makefile的引进至少解决了增量编译和并行编译的问题

Makefile文件由表单的规则集合组成, 粗略地说,规则告诉make通过执行命令脚本从先决条件集合中生成目标集合.

targets: prerequisites
	command-script	# 或者叫 recipe

接下来我们写Makefile

$ tree
.
├── inc
│   └── print.h
├── main.c
├── Makefile
└── src
    └── print.c
    
$ vi Makefile
main: main.o print.o
	gcc main.o print.o -o main

main.o: main.c inc/print.h
	gcc main.c -Iinc -o main.o -c

print.o: src/print.c inc/print.h
	gcc src/print.c -Iinc -o print.o -c
	
# 很多时候看到的不是gcc, 而是cc或者$(CC), 在linux下这是等价的, cc是gcc的一个链接
# main.o: main.c inc/print.h 后面的.h也可以不写
	
$ make
gcc main.c -Iinc -o main.o -c
gcc src/print.c -Iinc -o print.o -c
gcc main.o print.o -o main
# 如果再次执行, 可以看出是增量编译
$ make
make: 'main' is up to date.
$ ./main
Hello, World!

# 多了两个.o文件和可执行的main文件
$ tree
.
├── inc
│   └── print.h
├── main
├── main.c
├── main.o
├── Makefile
├── print.o
└── src
    └── print.c

我们经常可以看到make all, make clean之类的, 这个是在Makefile文件里面起的假名或者叫伪目标(PHONY):

# 添加三个PHONY: all, run, clean
$ vi Makefile
.PHONY: all run clean
all: main

main: main.o print.o
	gcc main.o print.o -o main

main.o: main.c inc/print.h
	gcc main.c -Iinc -o main.o -c

print.o: src/print.c inc/print.h
	gcc src/print.c -Iinc -o print.o -c

run:
	./main

clean:
	rm -f main *.o
	
$ make clean
rm -f main *.o
$ make all
gcc main.c -Iinc -o main.o -c
gcc src/print.c -Iinc -o print.o -c
gcc main.o print.o -o main
$ make run
./main
Hello, World!
$ make clean
rm -f main *.o

Makefile PHONY与Shell脚本

某次逛知乎, 发现讲网络的yanfeizhang/coder-kung-fu: 开发内功修炼 (github.com) 上的几个脚本用Makefile组织, 顿时一声卧槽, 这种用PHONY来让Makefile打开Shell脚本的新天地, 摘录一个Makefile:

.PHONY: create-net1
create-net1:
	ip netns add net1
	ip netns exec net1 iptables -L
	ip netns exec net1 ip link list
	ip netns exec net1 ifconfig

.PHONY: create-veth
create-veth:
	ip link add veth1 type veth peer name veth1_p
	ip link set veth1 netns net1
	ip link list
	ip netns exec net1 ip link list

.PHONY: start-veth
start-veth:
	ip addr add 192.168.0.100/24 dev veth1_p
	ip netns exec net1 ip addr add 192.168.0.101/24 dev veth1
	ip link set dev veth1_p up 
	ip netns exec net1 ip link set dev veth1 up 
	ifconfig
	ip netns exec net1 ifconfig
	
.PHONY: ping
ping:
	ip netns exec net1 ping 192.168.0.100 -I veth1

.PHONY: clean
clean:
	ip link delete veth1_p
	ip link list 
	ip netns del net1
	ip netns list

ping的话就是 make ping, 清理网络就用make clean… emm…

Makefile 变量 = := ?= +=

类似于C中的宏定义? 最简单的定义一个变量var=c, 然后变量值的获取可以用${var}, 那么g${var}${var}, 就等效于gcc

下面来看下=, :=, ?=, +=的区别

$ vi Makefile
x = foo
a = aha
c = cha
y = ${x} bar
z := ${x} bar
a ?= ${x}
b ?= ${x}
c += ${x}
x = oof

.PHONY: test
test:
	@echo x=${x}
	@echo y=${y}
	@echo z=${z}
	@echo a=${a}
	@echo b=${b}
	@echo c=${c}

$ make test
x=oof
y=oof bar
z=foo bar
a=aha
b=oof
c=cha oof

其中:

  • @echo表示不回显命令, 直接输出命令结果
  • =, 取整个Makefile最后指定的值, 类似于懒加载, 所以y的值是等到${x}的最后展开oof bar
  • :=, 立即展开, z的值是${x}立即展开foo bar
  • ?=, 取默认值, 如果变量之前被赋值, 该默认值不生效
  • +=, 追加

Makefile 自动变量

自动变量(Automatic Variables):

  • $@, 规则目标的文件名(target)
  • $%, 目标成员名称
  • $<, 第一个先决条件的名称(first prerequisite)
  • $^, 所有先决条件的名称(prerequisites)

参考: Automatic Variables (GNU make)

简单举例

$ vi Makefile
.PHONY: x y test

x:
	@echo 'abc'

y:
	@echo def

test: x y
	@echo ======
	@echo "\$$@:" $@
	@echo "\$$^:" $^
	@echo "\$$<:" $<

$ make test
abc
def
======
$@: test
$^: x y
$<: x

其中:

  • \$是转义
  • @ 指target, 目标文件, 也就是test
  • ^, 指所有依赖文件, 也就是x y
  • <, 指第一个依赖文件, 也就是x

Makefile 举例

$ tree
.
├── inc
│   ├── abc.h
│   └── xyz.h
├── Makefile
└── src
    ├── abc.c
    ├── main.c
    └── xyz.c
    
$ vi inc/abc.h 
#ifndef ABC_H
#define ABC_H
extern void print_abc(void);
#endif

$ vi inc/xyz.h
#ifndef XYZ_H
#define XYZ_H
extern void print_xyz(void);
#endif

$ vi src/abc.c
#include <stdio.h>
void print_abc(void)
{
    printf("abc\r\n");
}

$ vi src/xyz.c
#include <stdio.h>
void print_xyz(void)
{
    printf("xyz\r\n");
}

$ vi src/main.c
#include "abc.h"
#include "xyz.h"
int main()
{
    print_abc();
    print_xyz();
    return 0;
}

$ vi Makefile
.PHONY: all run clean 

BUILD_DIR = build
SRC_DIR = src

TARGET:=${BUILD_DIR}/main
SRC := $(wildcard $(SRC_DIR)/*.c)
OBJ := $(SRC:$(SRC_DIR)/%.c=$(BUILD_DIR)/%.o)

C_INCLUDES = \
-Iinc

CC = gcc
CFLAGS = $(C_INCLUDES)

all: ${TARGET}

$(TARGET): $(OBJ)
	${CC} $^ -o $@

$(OBJ): $(BUILD_DIR)/%.o : $(SRC_DIR)/%.c | $(BUILD_DIR)
	${CC} -c ${CFLAGS} $< -o $@

$(BUILD_DIR):
	mkdir -p $@

run:
	@./${TARGET}

clean:
	-rm -fR $(BUILD_DIR)
	
$ make
mkdir -p build
gcc -c -Iinc src/abc.c -o build/abc.o
gcc -c -Iinc src/xyz.c -o build/xyz.o
gcc -c -Iinc src/main.c -o build/main.o
gcc build/abc.o build/xyz.o build/main.o -o build/main
$ make run
abc
xyz
$ make clean
rm -fR build

其中:

  • * %都是通配符, *.c表示所有后缀为.c的文件,
  • wildcard, 中文意思通配符, 在变量的定义和函数引用时,通配符(* %)将失效, 除非加上wildcard
  • $(var:a=b) 或 ${var:a=b}, 含义是把变量var中的每一个值结尾用b替换掉a, OBJ那一行赋值就是把源文件夹的所有.c替换为build文件夹的同名.o
  • | $(BUILD_DIR) 的含义是如果找不到定义或执行失败, 先去找$(BUILD_DIR)?

备忘

源文件(c/cpp)变成可执行文件需要经历的步骤:

  • Preprocess, 预处理, -E参数(默认不生成文件, 可以指定到某个文件, 如gcc -E main.c -o main.i)
  • Compile, 编译, -S参数(生产.s汇编文件)
  • Assemble, 汇编, -c参数(编译和汇编, 不链接, 生成.o的obj文件)
  • Link, 链接, -o参数? (默认.out文件, 用-o指定可执行文件名)

具体到MCU开发:

  • MCU开发中的.s后缀的文件常用来定义中断向量表等
  • MCU开发bootloader时经常会用到.ld或者.lsl后缀的链接脚本(link script), 指定入口点, 内存, 分区等
  • MCU的可执行文件一般有 BIN文件, HEX文件, S19文件, ELF文件等

汇编文件参考通用的汇编语法, 链接脚本可以参考:

并行编译如make -j8, 不管多少核, 很多人都喜欢用的并行编译命令, 或者根据核的数量来的

make -j $(nproc)

Linux内核中Makefile常见的 obj-y, 用Kconfig(.config)控制obj-(y/n/m)编译进Kernel还是Module或者不编译, 还有+=文件夹, 这些都是Kbuild的组织语法?

make -C 进入文件夹编译, 编译后离开该文件夹

参考

欢迎扫描二维码关注微信公众号, 及时获取最新文章:
在这里插入图片描述

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值