build_embed_linux_system

Makefile语法

Makefile作为编译脚本,用于将C/C++代码编译生成可执行程序,虽然目前已有现代编译工具bazel,cmake,clion,或者使用高级脚本语言python, perl等,不在需要复杂的手动构建过程。不过对于嵌入式开发,如U-Boot,Linux,rootfs以及部分软件的交叉编译,Makefile仍然是最基础的工具。

理解掌握Makefile,对于系统的去理解大型项目的编译系统,掌握Linux环境下的项目如何构建,解决构建时问题,有着重要意义。学习Makefile并不是一个简单的过程,这里分如下章节讲解说明。

gcc_compiler

在Windows开发环境下,通常使用集成开发环境(IDE),如Visual Studio,MDK等,点击构建(Build)即可完成所有编译流程,其实这隐藏了系统软件的运行机理,而Makefile正是使用这些机理完成系统的构建。在Linux开发环境下,则需要自己实现编译脚本来完成环境的构建,这就需要理解背后的C/C++的编译过程。

C/C++的编译过程包含预处理,编译,汇编和链接四个步骤.

  1. ‘预处理’: 主要包含删除所有预定义的宏,如’#if’,’#define’,’#include’这类宏的处理
  2. ‘编译’ : 将预处理的文件经过词法分析,语法分析和优化生成相应的汇编文件
  3. ‘汇编’ : 将汇编文件转变成机器可执行的目标文件
  4. ‘链接’ : 将上述的目标文件,以及系统提供的库,进行链接最后输出可执行文件

上面就阐述了C/C++编译的全过程,下节则通过gcc编译来进一步说明。gcc作为是用于编译的工具集,包含多个工具。

这里有个小知识,理论上gcc和g++都可以用于编译c/c++文件,g++本身对于c语言编译支持,而gcc编译C++则需要去链接C++库,需要添加命令’-lstdc++’。对于GCC,其编译过程也符合上面提到的四个步骤,不过我们以C语言的编译流程来理解上节内容,以”hello.c”为例.

# 预编译
gcc -E hello.c -o hello.i   # 生成去掉额外信息的中间文件

# 编译
gcc -S hello.i -o hello.s   # 中间文件生成汇编文件

# 汇编
gcc -c hello.s -o hello.o   # 生成目标平台的指令链接文件

# 链接
gcc -s hello.o -o hello     # 将文件链接,构建目标平台的可执行文件

可以看到,gcc的编译流程也和上节的说明一致,从上面可以看出,预编译,编译和汇编都是对单个文件的操作,在链接才有涉及多个文件操作的可能,gcc单指令也整合了单文件的编译命令,因此上述操作也可以简化为如下操作。

# 生成指定链接文件
gcc hello.c -o hello.o      # 生成链接文件
# 链接
gcc hello.o -o hello        # 将文件链接,构建目标平台的可执行文件

当然,gcc也支持各种选项来支持各种扩展,如下所示。

-I"(Directory)"     # 指定包含的头文件目录
-L"(Library)"       # 指定包含的链接库的目录
-l"file"            # 指定链接的动态库,对应格式未lib[file].so
-std=(Version)      # 指定语言的版本
-Wl,-Map,(MapFile)  # 指定生成map文件
-lpthread           # 支持链接thread库
-lm                 # 支持链接math库
-O(Number)          # 支持优化等级,一般为0~3,值越大,优化等级越高
-D(String)          # 支持包含外部的宏定义,如果带值,则可以为-D(data=value)格式
-g					# 添加调试信息,使用gdb等调试工具必须加入此选项

上述基本包含的gcc的常用扩展,我们在实现Makefile语法时也是依赖上述扩展命令实现,另外在Makefile中一定要使用Tab进行命令的格式化,而不要使用空格,否则会因不识别而报错。

makefile_basic

在了解gcc的相关知识和命令说明,下面可以讲解Makefile的语法实现,Makefile主要是基于make命令实现(可以说所谓的Makefile语法,也就是make程序支持一套解析的语法),在当前目录下查询名为Makefile的文件,执行具体指令并完成编译的方法,下面描述Makefile的常用语法说明.

#1.标签
[label] :
  [function]

[target] : [label]
  [function]
#taget:执行的标签,是标签查找的执行标识和入口,make默认执行查找的就是all标签,当然也可以使用make all,实现相同的效果。
#label:需要查询执行的上一级的标签。
#function:执行的函数体,在所有上级标签执行完后进行执行的方法。
#对于标签,执行的顺序为target -> label(all loop) -> function。

#2.变量
[variable]=[value]
$(variable) 
#variable定义变量名,value定义变量,在使用时通过$(variable)即可读取变量。
#对于=的说明
# = 基本的赋值
# := 赋值,并覆盖之前的值
# ?= 之前未赋值,则使用等号后的值,否则使用之前值
# += 将等号后面的值添加到之前值后

#3.自动化变量
# $< 表示依赖对象集合中的第一个文件,如果依赖文件是以模式(即“%” )定义的,那么"$<"就是符合模式的一系列的文件集合
# $@ 表示规则中的目标集合,即分别代表%.c和%.o中的每个项
# $% 当目标是函数库的时候表示规则中的目标成员名,如果目标不是函数库文件,那么其值为空。
# $? 所有比目标新的依赖目标集合,以空格分开
# $^ 所有依赖文件的集合,使用空格分开,如果在依赖文件中有多个重复的文件,“$^”会去除重复的依赖文件,值保留一份
# $* 这个变量表示目标模式中"%"及其之前的部分,如果目标是 test/a.test.c,目标模式为 a.%.c,那么"$*"就是 test/a.test
%.o : %.c
  gcc -c $< -o $@ 

#4.ifeq/ifneq语法
#用于对比变量是否一致的方法,基于这类语法,可以实现不同配置项的编译
ifeq ($(CCPLUS),g++)
	$(info used deivce compiler:$(CCPLUS))
endif

ifneq ($(CCPLUS),g++)
	CCPLUS := g++
endif

#5.编译logger输出
$(info:[string])    #输出字符串信息,不会报错。
$(warning:[string]) #以警告形式输出下暗示信息。
$(error:[string])   #错误输出,同时将中止当前编译

现在定义一个比较简单的项目文件结构,包含hello/hello.c,hello/hello.h,main.c三个文件,按照上述的gcc的说明和Makefile语法,编译文件实现如下。

#step1: make查找all标签,并执行target.
all:target                                                  

#step3: 执行hello.o标签,查找hello.c文件,执行下述命令
hello/hello.o:hello/hello.c                                 
	gcc -I"hello/" -O1 -c hello/hello.c -o hello/hello.o

#step4: 执行main.o标签,查找main.c文件,执行下述命令,生成链接文件
main.o:main.c                                               
	gcc -I"hello/" -O1 -c main.c -o main.o

#step2: 查找target标签,执行hello/hello.o,main.o.
target:hello/hello.o main.o                                 
#step5: 链接文件,生成可执行文件
	gcc -o target hello/hello.o main.o -O1              

clean:
	rm -rf target
	rm -rf hello/hello.o main.o

注意: Makefile中,命令必须以Tab开始,不能够为空格,否则会报错。

上述就是最基础的Makefile文件,当执行make时,查询all对应的tag并执行,然后遍历执行所有的标签,最后完成所有文件的编译。整个系统由label标签和gcc编译命令构成,如果项目中文件较少,使用这种方式是可行的,不过对于有几十上百文件的项目,手写每个.o对应的执行是不现实的,所以可以使用上面的变量和自动化变量方法替代,依据此可修改为如下格式。

#variable
#定义编译的生成可执行文件名
EXCUTABLE ?= target

#包含链接的所有文件
OBJECTS = hello/hello.o main.o

#包含需要访问的文件头目录
INCLUDE_PATH = -I"hello/"

#支持的C编译选项
CFLAGS += -O1 -lm

#指定编译工具
CC ?= gcc                                           

#label
all : $(EXCUTABLE)

#自动化变量,匹配所有的.c生成的链接文件
#等同于上述的main.o:main.c和hello/hello.o:hello/hello.c标签
%.o : %.c                                           
	$(CC) $(INCLUDE_PATH) $(CFLAGS) -c $< -o $@

#生成可执行文件
#等同于上述target:hello/hello.o main.o标签
$(EXCUTABLE):$(OBJECTS)
	$(CC) -o $(EXCUTABLE) $(OBJECTS) $(CFLAGS)

clean:
	rm -rf $(EXCUTABLE)
	rm -rf $(OBJECTS)

上面就是基本的Makefile文件,支持C项目的编译,不过对于大型项目,往往还有基于shell的预处理,编译条件管理,按照上述路径层层剖析,另外将Makefile中的相同部分提取出来,通过include语法,可以将Makefile应用到不同的工程,提高项目中各部分复用,这就涉及了编译系统的构建,下面进一步进行说明。

library

在上述编译步骤中,讲述的是大部分C/C++项目的编译流程,另外C++还支持以静态库或者动态库的方式进行编译,然后在实际使用过程中链接到项目中,编译动态库和静态库的流程如下。

###编译生成静态库
#将C++文件编译为中间文件
g++ test.cpp -o test.o
g++ test1.cpp -o test1.o

#通过ar文件打包中间文件生成lib文件
ar -cr libtest.a test.o test1.o

###静态库打包生成程序
g++ libtest.a main.o -o target

可以看到静态库的使用和中间文件没有什么区别,另外ar可以将多个中间文件打包成一个静态库,如果将上述流程转换为Makefile语法则可以如下实现。

lib-test=libtest.a
lib-test-src=test.o test1.o
excutable=target
CCPLUS ?= g++

all: $(excutable)

%.o : %.cpp  
	$(CCPLUS) $(IncludePath) $(CPPFlags) -c $< -o $@

$(lib-test): $(lib-test-src)
	ar -cr $(lib-test) $(lib-test-src)

#具体执行函数
$(excutable):main.o $(lib-test)
	g++ libtest.a main.o -o $(excutable)

上述就是生成静态库,并打包生成程序的流程,下面讲解动态。

###编译生成动态库
#将C++文件编译为中间文件
g++ test.cpp -o test.o
g++ test1.cpp -o test1.o

#将C++文件编译为动态库
g++ test.o test1.o -fPIC -shared -o libtest.so

###链接动态库,生成程序
g++ main.o -L. -ltest -o target

动态库是cpp文件生成动态链接库,格式需要满足lib[file].so的格式,在使用时用-l[file]编译,将上述流程转换为Makefile语法则可以如下实现。

dlib-test=libtest.so
dlib-test-src=test.o test1.o
excutable=target
CCPLUS ?= g++

all: $(excutable)

%.o : %.cpp  
	$(CCPLUS) $(IncludePath) $(CPPFlags) -c $< -o $@

$(dlib-test): $(dlib-test-src)
	$(CCPLUS) $(dlib-test-src) -fPIC -shared -o $(dlib-test)

#具体执行函数
$(excutable):main.o $(dlib-test)
	g++ main.o -L. -ltest -o $(excutable)

关于Makefile的运用,可以参考目录file/Chapter1-2下实现。

cd file/Chapter1-3/

#编译静态库
make BUILD=s

#编译动态库
make BUILD=d

#编译可执行文件
make

#将动态库加载的系统中并执行
sudo cp libdynamictest.so /usr/lib/libdynamictest.so
./target

extend_makefile

上面讲解的Makefile,需要将编译的文件都写入到Makefile中,这样虽然逻辑更清晰,但项目文件过多时,维护文件也是比较繁琐的工作。这里可以借用makefile自带的命令机制,支持自动化的脚本生成。

$(shell "pwd")      					#执行shell命令
$(addsuffix <suffix>,<name>) 			#将suffix后缀加到name的对象中并赋值到返回地址中
$(addsuffix /inc,$(CUR_DIR))
$(wildcard $(CUR_DIR)/*.c) 				#通配符,匹配指定目录下满足要求的文件
$(sub <from>,<to>,<text>)				#替换字符串,将text中的"from"字符串替换成"to"字符串,返回替换后的字符串
$(patsubst %.c,%.o,$(notdir $(SRC))) 	#使用模式替换字符串,将%.c格式替换成%.o格式,并返回
file := $(foreach n,$(obj),$(n).o)		#循环遍历值并返回
$(notdir <names>) 						#提取文件名中非目录部分
$(dir <names>) 							#提取文件名中目录部分

基于上述脚本命令,实现的Makefile如下所示。

CUR_DIR := $(shell "pwd")
INC_DIR := $(addsuffix /inc,$(CUR_DIR))
SRC := $(wildcard $(CUR_DIR)/*.c)
OBJ := $(patsubst %.c,%.o,$(notdir $(SRC)))
CC := gcc
CFLAGS := -Wall -I$(INC_DIR)

all: target

%.o : %.c
	$(CC) $(CFLAGS) -c $< -o $@

target: $(OBJ)
	$(CC) $^ -o $@

clean:
	rm -rf *.o target

至此我们便讲解了大部分Makefile语法实现;在实际项目中,往往结合上面这些常用的Makefile语法,提取出mk文件,再通过config配置进行开关选项的管理,这就是uboot和kernal中的编译构建方法。理解了这一点,再去理解编译选项就可以有更清晰的认识。基于此经验,就可以去进一步去分析大型项目如U-Boot,Linux Kernel,Busybox中的Makefile,可以做到知其然亦知其所以然,学习更进一步。

next_chapter

返回目录

直接开始下一小节:menuconfig界面管理