嵌入式Linux——gcc和Makefile
1.gcc编译过程
1.预处理:
-
所有的
#include
被替换为对应的头文件内容。 -
所有的宏(
#define
)被展开。 -
条件编译(如
#ifdef
)被处理。 -
注释被删除。
2.编译:
将预处理后的源代码转换为汇编代码,生成一个汇编代码文件(.s
文件)
3.汇编:
将汇编代码转换为机器代码(二进制文件),生成一个目标文件(.o
文件)
4. 链接
将多个目标文件和库文件合并,生成最终的可执行文件:
-
所有的目标文件被合并。
-
符号引用(如
printf
函数)被解析并链接到标准库或其他库。 -
生成的文件可以直接运行。
2.Makefile的使用
2.1 规则
一个简单的 Makefile 文件包含一系列的“规则”
命令:依赖
(Tab键)命令
命令被执行的 2 个条件:依赖文件比目标文件新,或是 目标文件还没生成
make命令执行Makefile:
- 第一次make:全部规则的命令都会执行
- 修改其中一个文件后再make:根据第一条规则的依赖往下一个一个找,若下面的依赖比目标更新(.c文件比.o文件新),则执行该规则的命令
main: main.o input.o calcu.o #根据这些依赖一个一个往下找、对比
gcc -o main main.o input.o calcu.o
main.o: main.c
gcc -c main.c
input.o: input.c
gcc -c input.c
calcu.o: calcu.c
gcc -c calcu.c
clean:
rm *.o
rm main
2.2 语法
2.2.1 通配符
依赖文件太多怎么办?
使用通配符
test: a.o b.o
gcc -o test $^
%.o : %.c
gcc -c -o $@ $<
%.o:表示所有的.o文件
%.c:表示所有的.c文件
$@:表示目标
$<:表示第1个依赖文件
$^:表示所有依赖文件
这样就只需要一条规则就OK了
第一条指令在搜寻a.o的时候就会把a带入到“%”中,以此类推,依次执行
2.2.2 假想目标: .PHONY
test: a.o b.o
gcc -o test $^
%.o : %.c
gcc -c -o $@ $<
clean:
rm *.o test
想清除文件
*1)执行 make :生成第一个可执行文件。
*2)执行 make clean : 清除所有文件,即执行: rm *.o test。
执行:make [目标] 也可以不跟目标名,若无目标默认第一个目标。我们直接执行make的时候,会在 makefile里面找到第一个目标然后执行下面的指令生成第一个目标。当我们执行 make clean 的时候, 就会在 Makefile 里面找到 clean 这个目标,然后执行里面的命令。
这个写法有些问题,原因是我们的 目录里面没有 clean 这个文件,这个规则执行的条件成立,他就会执行下面的命令来删除文件。
如果:该目录下面有名为clean文件怎么办呢?
这时候make clean不会起反应,因为一个规则能够执行的条件:
*1)目标文件不存在
*2)依赖文件比目标新
现在我们的目录里面有名为“clean”的文件,目标文件是有的,并且没有依赖文件,没有办法判断依赖文件的时间。
解决办法:我们需要把目标定义为假象目标,用关键字PHONY
.PHONY: clean #把clean定义为假象目标。他就不会判断名为“clean”的文件是否存在,
在Makfile结尾添加.PHONY: clean语句,重新执行:make clean,就会执行删除操作。
2.2.3 变量
1), 简单变量(即使变量):
A := xxx # A的值即刻确定,在定义时即确定
对于即使变量使用 “:=” 表示,它的值在定义的时候已经被确定了
2)延时变量
B = xxx # B的值使用到时才确定 对于延时变量使用“=”表示。
它只有在使用到的时候才确定,在定义/等于时并没有 确定下来。
:= : 即时变量
= : 延时变量
?= : 延时变量, 如果是第1次定义才起效, 如果在前面该变量已定义则忽略这句
+= : 附加, 它是即时变量还是延时变量取决于前面的定义
?= : 如果这个变量在前面已经被定义了,这句话就会不会起效果,
示例
A := $(C)
B = $(C)
C = abc
#D = 100ask
D ?= weidongshan
all:
@echo A = $(A) //@的作用是make时候不显示该过程
@echo B = $(B)
@echo D = $(D)
C += 123
执行结果:
A =
B = abc 123
D = weidongshan
解释:
1) A := $(C):
A为即时变量,在定义时即确定,由于刚开始C的值为空,所以A的值也为空。
2) B = $(C):
B为延时变量,只有使用到时它的值才确定,当执行make时,会解析Makefile里面的所用变量,所以先 解析C= abc,然后解析C += 123,此时,C = abc 123,当执行:\@echo B = $(B) B的值为 abc 123。
3) D ?= weidongshan:
D变量在前面没有定义,所以D的值为weidongshan,如果在前面添加D = 100ask,最后D的值为 100ask。
2.3 处理头文件(依赖文件)
一般的Makefile中,不会包含头文件的更新,那应该怎么处理
可以像下面这段代码一样:
c.o: c.c c.h
把 c.h 加入到目标 c.o 的依赖中,这样每次检查c.o是就会判断c.h是否更新
但是,当头文件一多起来,不可能每个头文件一个一个写进去
这时候就应该换一种方法,如下:
gcc -M c.c # 打印出依赖
gcc -M -MF c.d c.c # 把依赖写入文件c.d
gcc -c -o c.o c.c -MD -MF c.d # 编译c.o, 把依赖写入文件c.d
objs = a.o b.o c.o
# 生成依赖文件的列表。通过 patsubst 函数将 objs 中的每个目标文件(如 a.o)转换为对应的依赖文件(如 .a.o.d)
dep_files := $(patsubst %,.%.d, $(objs))
# 使用 wildcard 函数过滤出实际存在的依赖文件。如果某个 .d 文件不存在,wildcard 会将其从 dep_files 中移除
dep_files := $(wildcard $(dep_files))
# 定义默认目标 test,它依赖于 objs 中列出的所有目标文件(a.o, b.o, c.o)
# 当运行 make 时,会首先构建这些目标文件,然后使用 gcc 将它们链接成可执行文件 test
test: $(objs)
gcc -o test $^ # $^ 表示所有依赖文件(即 a.o b.o c.o)
# 如果 dep_files 不为空(即存在依赖文件),则包含这些依赖文件
# 依赖文件通常由 gcc 的 -MD -MF 选项生成,用于自动处理头文件依赖
ifneq ($(dep_files),)
include $(dep_files)
endif
# 定义如何从 .c 文件生成 .o 文件的规则
# $@ 表示目标文件(如 a.o),$< 表示第一个依赖文件(如 a.c)
# -c 选项表示只编译不链接,-o 指定输出文件名
# -MD 选项生成依赖文件,-MF 指定依赖文件的名称(如 .a.o.d)
%.o : %.c
gcc -c -o $@ $< -MD -MF .$@.d
# 定义 clean 目标,用于删除所有 .o 文件和可执行文件 test
clean:
rm *.o test
# 定义 distclean 目标,用于删除所有依赖文件(如 .a.o.d, .b.o.d, .c.o.d)
distclean:
rm $(dep_files)
.PHONY: clean
看注释应该能看得懂
虽然 %.o : %.c
规则中只显式指定了 .c
文件作为依赖,但通过 gcc
的 -MD -MF
选项,Makefile
能够自动检测 .h
文件的更新并重新编译。
CFLAGS:编译参数
示例
CFLAGS = -Werror -Iinclude
通过CFLAGS配置了两个作用:
-Werror
:将所有警告视为错误,确保代码质量。
-Iinclude
:将include
目录添加到头文件搜索路径中。
CFLAGS
可以帮助更好地控制编译过程,确保代码的正确性和可维护性
2.4 解析Makefile
*通用Makefile设计思想:
1.在顶层和子目录Makefile文件中确定要编译的文件、目录,比如:
顶层Makefile:文件、目录
子目录Makefile:文件
obj-y += main.o
obj-y += a/
Makefile 文件总是被 Makefile.build 包含的。
2.在 Makefile.build 中设置编译规则,有 3 条编译规则:
i. 怎么编译子目录? 进入子目录编译:
$(subdir-y):
make -C $@ -f $(TOPDIR)/Makefile.build
ii. 怎么编译当前目录中的文件?
%.o : %.c
$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<
iii. 当前目录下的.o和子目录下的built-in.o要打包起来到顶层目录
built-in.o : $(cur_objs) $(subdir_objs)
$(LD) -r -o $@ $^
3. 顶层Makefile中把顶层目录的built-in.o链接成APP:
$(TARGET) : built-in.o
$(CC) $(LDFLAGS) -o $(TARGET) built-in.o
一个通用的Makefile:
- 顶层目录的Makefile
- 顶层目录的Makefile.build
- 各级子目录的Makefile
源码分析(看注释):
Makefile:
CROSS_COMPILE = # 交叉编译工具头,如:arm-linux-gnueabihf-
AS = $(CROSS_COMPILE)as # 把汇编文件生成目标文件
LD = $(CROSS_COMPILE)ld # 链接器,为前面生成的目标代码分配地址空间,将多个目标文件链接成一个库或者一个可执行文件
CC = $(CROSS_COMPILE)gcc # 编译器,对 C 源文件进行编译处理,生成汇编文件
CPP = $(CC) -E
AR = $(CROSS_COMPILE)ar # 打包器,用于库操作,可以通过该工具从一个库中删除或则增加目标代码模块
NM = $(CROSS_COMPILE)nm # 查看静态库文件中的符号表
STRIP = $(CROSS_COMPILE)strip # 以最终生成的可执行文件或者库文件作为输入,然后消除掉其中的源码
OBJCOPY = $(CROSS_COMPILE)objcopy # 复制一个目标文件的内容到另一个文件中,可用于不同源文件之间的格式转换
OBJDUMP = $(CROSS_COMPILE)objdump # 查看静态库或则动态库的签名方法
# 共享到sub-Makefile
export AS LD CC CPP AR NM
export STRIP OBJCOPY OBJDUMP
# -Wall : 允许发出 GCC 提供的所有有用的报警信息
# -O2 : “-On”优化等级
# -g : 在可执行程序中包含标准调试信息
# -I : 指定头文件路径(可多个)
CFLAGS := -Wall -O2 -g
CFLAGS += -I $(shell pwd)/include
# LDFLAGS是告诉链接器从哪里寻找库文件,这在本Makefile是链接最后应用程序时的链接选项。
LDFLAGS :=
# 共享到sub-Makefile
export CFLAGS LDFLAGS
# 顶层路径
TOPDIR := $(shell pwd)
export TOPDIR
# 最终目标
TARGET := test
# 本次整个编译需要源 文件 和 目录
# 这里的“obj-y”是自己定义的一个格式,和“STRIP”这些一样,*但是 一般内核会搜集 ”obj-”的变量*
obj-y += main.o # 需要把当前目录下的 main.c 编进程序里
obj-y += sub.o # 需要把当前目录下的 sub.c 编进程序里
obj-y += subdir/ # 需要进入 subdir 这个子目录去寻找文件来编进程序里,具体是哪些文件,由 subdir 目录下的 Makefile 决定。
#obj-y += $(patsubst %.c,%.o,$(shell ls *.c))
# 第一个目标
all : start_recursive_build $(TARGET)
@echo $(TARGET) has been built !
# 处理第一个依赖,**转到 Makefile.build 执行**
start_recursive_build:
make -C ./ -f $(TOPDIR)/Makefile.build
# 处理最终目标,把前期处理得出的 built-in.o 用上
$(TARGET) : built-in.o
$(CC) -o $(TARGET) built-in.o $(LDFLAGS)
# 清理
clean:
rm -f $(shell find -name "*.o")
rm -f $(TARGET)
# 彻底清理
distclean:
rm -f $(shell find -name "*.o")
rm -f $(shell find -name "*.d")
rm -f $(TARGET)
Makefile.build
注意,include 命令,相对路径为 执行本 Makefile.build 的路径
# 伪目标
PHONY := __build
__build:
# 清空需要的变量
obj-y :=
subdir-y :=
EXTRA_CFLAGS :=
# 包含同级目录Makefile
# 这里要注意,相对路径为 执行本 Makefile.build 的路径
include Makefile
# 获取当前 Makefile 需要编译的子目录的目录名
# obj-y := a.o b.o c/ d/
# $(filter %/, $(obj-y)) : c/ d/
# __subdir-y : c d
# subdir-y : c d
__subdir-y := $(patsubst %/,%,$(filter %/, $(obj-y)))
subdir-y += $(__subdir-y)
# 把子目录的目标定为以下注释
# built-in.o d/built-in.o
subdir_objs := $(foreach f,$(subdir-y),$(f)/built-in.o)
# 获取当前目录需要编进程序的文件名作为,并写为目标
# a.o b.o
cur_objs := $(filter-out %/, $(obj-y))
# 使修改头文件 .h 后,重新make后可以重新编译(重要)
dep_files := $(foreach f,$(cur_objs),.$(f).d)
# 列出存在的文件
dep_files := $(wildcard $(dep_files))
ifneq ($(dep_files),)
include $(dep_files)
endif
PHONY += $(subdir-y)
# 第一个目标
__build : $(subdir-y) built-in.o
# 优先编译 子目录的内容
$(subdir-y):
make -C $@ -f $(TOPDIR)/Makefile.build
# 链接成 目标
built-in.o : $(cur_objs) $(subdir_objs)
$(LD) -r -o $@ $^
dep_file = .$@.d
# 生成 cur_objs 目标
%.o : %.c
$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<
.PHONY : $(PHONY)
形象化:
流程:
1. 各级子目录的Makefile
参考
# 可以不添加下面两个变量
EXTRA_CFLAGS :=
CFLAGS_test.o :=
obj-y += test.o
obj-y += subdir/
- obj-y += file.o 表示把当前目录下的file.c编进程序里,
- obj-y += subdir/ 表示要进入subdir这个子目录下去寻找文件来编进程序里,是哪些文件由subdir目录下的Makefile决定。
- EXTRA_CFLAGS, 它给当前目录下的所有文件(不含其下的子目录)设置额外的编译选项, 可以不设置
- CFLAGS_xxx.o, 它给当前目录下的xxx.c设置它自己的编译选项, 可以不设置
注意
- "subdir/"中的斜杠"/"不可省略
- 顶层Makefile中的CFLAGS在编译任意一个.c文件时都会使用
- CFLAGS EXTRA_CFLAGS CFLAGS_xxx.o 三者组成xxx.c的编译选项
2.顶层目录的Makefile
主要作用:
- 整个工程的参数初期定义:架构、工具链、编译工具、编译参数、需要导出的变量等等
- 定义最终目标
- 跳转到 Makefile.build
3.顶层目录的Makefile.build **
注意:该文件虽然放在顶层,但是也是提供给各级子目录使用的。
主要功能:
- 把某个目录及它的所有子目录中、需要编进程序去的文件都编译出来,把各个subdir/built-in.o和当前目录的目标 .o 合并打包为当前目录的built-in.o 。
使用提示
- 执行"make"来编译,执行 make clean 来清除,执行 make distclean 来彻底清除。