祝各位道友念头通达
GitHub Gitee 语雀 打赏

C Makefile 写法

技巧

如何判断传入参数,然后设定不同值

ARCH=ARM
PLAT_FORM_ARM=ARM
PLAT_FORM_X86=X86

PARA=
DEBUG=
# 查看 DEBUG 属性值是否为 DEBUG 值
ifeq ($(DEBUG), DEBUG)
PARA += -DDEBUG
endif
# 查看 ARCH 是否为ARM值
ifeq ($(ARCH), $(PLAT_FORM_ARM))
PARA += -DPLATFORM_ARM
endif
# 查看 ARCH 是否为X86值
ifeq ($(ARCH), $(PLAT_FORM_X86))
PARA += -DPLATFORM_X86
CC=gcc
endif

make笔记

中文文档: https://seisman.github.io/how-to-write-makefile/introduction.html
Makefile里主要包含了五个东西:显式规则、隐晦规则、变量定义、文件指示和注释。

  1. 显式规则。显式规则说明了如何生成一个或多个目标文件。这是由Makefile的书写者明显指出要生成的文件、文件的依赖文件和生成的命令。
  2. 隐晦规则。由于我们的make有自动推导的功能,所以隐晦的规则可以让我们比较简略地书写 Makefile,这是由make所支持的。
  3. 变量的定义。在Makefile中我们要定义一系列的变量,变量一般都是字符串,这个有点像你C语言中的宏,当Makefile被执行时,其中的变量都会被扩展到相应的引用位置上。
  4. 文件指示。其包括了三个部分,一个是在一个Makefile中引用另一个Makefile,就像C语言中的include一样;另一个是指根据某些情况指定Makefile中的有效部分,就像C语言中的预编译#if一样;还有就是定义一个多行的命令。有关这一部分的内容,我会在后续的部分中讲述。
  5. 注释。Makefile中只有行注释,和UNIX的Shell脚本一样,其注释是用 # 字符,这个就像C/C++中的 // 一样。如果你要在你的Makefile中使用 # 字符,可以用反斜杠进行转义,如: # 。

GUN make工作方式

GNU的make工作时的执行步骤如下:(想来其它的make也是类似)

  1. 读入所有的Makefile。
  2. 读入被include的其它Makefile。
  3. 初始化文件中的变量。
  4. 推导隐晦规则,并分析所有规则。
  5. 为所有的目标文件创建依赖关系链。
  6. 根据依赖关系,决定哪些目标要重新生成。
  7. 执行生成命令。

1-5步为第一个阶段,6-7为第二个阶段。第一个阶段中,如果定义的变量被使用了,那么,make会把其展开在使用的位置。但make并不会完全马上展开,make使用的是拖延战术,如果变量出现在依赖关系的规则中,那么仅当这条依赖被决定要使用了,变量才会在其内部展开。

makefile 中常用函数使用

origin

$(origin V)
"environment"	# 环境变量
"command line"	# 命令行
"file" 			# 本文件中定义
"default"		# 默认变量
"undefined"		# 未定义变量
"automatic"		# 自动化变量

firstword

$(firstword x$(MAKEFLAGS))   ## 取 MAKEFLAGS 第一个单词

函数“firstword”实现的功能等效于"$(word 1, NAMES…)"

foreach 使用案例

给需要的目录加入通配符,方便后面对源文件查找处理

SUBDIR=./src ./include
sources=$(foreach x, $(SUBDIR), $(x)*.c)

wildcard

和 ls 有些类似, 查找某些目录下对应的文件

sources += $(wildcard ./src/*.c ./include/*c)

返回的是src和include目录下所有的.c文件,相对于当前目录

":=" 和 "=" 符号使用

1、"="
 make会将整个makefile展开后,再决定变量的值。也就是说,变量的值将会是整个makefile中最后被指定的值。看例子:
     x = foo
     y = $(x) bar
     x = xyz

在上例中,y的值将会是 xyz bar ,而不是 foo bar 。

2、":="
":="表示变量的值决定于它在makefile中的位置,而不是整个makefile展开后的最终值。

     x := foo
     y := $(x) bar
     x := xyz
在上例中,y的值将会是 foo bar ,而不是 xyz bar 了。

makefile基础

写法规则

目标 … : 依赖 …
    命令

c语言编译过程

# 预处理 -> 编译 -> 汇编 -> 链接

# 预处理: 将头文件替换中内容全部替换成实际内容 (.i 结尾), 不会检查错误
示例: gcc -E hello.c -o hello.i
# 编译: 检查错误,生成汇编文件 (.s 结尾)
示例: gcc -S hello.i -o hello.s
# 汇编: 将汇编文件编译成二进制文件 (.o 结尾)
示例: gcc - c hello.s -o hello.o
# 编译: 将各个文件的二进制文件链接起来生成可执行文件
示例: gcc hello.o xxx.o ... -o hello

使用make的好处

如果你是对自己的代码很熟悉的话, 体量比较小, 使用脚本控制gcc 编译也是可以的

  1. 主要它本身有一套很灵活的机制, 当代码体量很大的时候使用make能够更加体现出它的便捷之处
  2. 增量编译: 使用makefile可以只对已修改的文件做编译,不过前提是要保留之前编译的 .o 文件

增量编译的原理:make根据生成 .o 文件的日期和依赖文件的日期对比,看是否需要重新生成对应 .o文件。

makefile 写法示例

progName= StorgProg
# 将 .o 文件放在 build 文件夹中
objects	= main.o logger.o thread_recvcmd.o util-files.o util_mmap.o util-gpio.o util-keepoff.o util-string.o thread_manager.o sys_resource.o common.o stru_queue.o sys_init.o drv_pcie.o pcie_proce.o vlan_proce.o info_apper.o oper_device.o
run:	$(objects)
	cc -o StorgProg -g  -w -D_FILE_OFFSET_BITS=64 -D_LARGE_FILE -pthread -lm $(objects)

# .o 文件为中间文件,当增量编译时需要保留 .o 文件
main.o:			  	main.c 
logger.o:		 	logger.c logger.h
thread_recvcmd.o: 	thread_recvcmd.c thread_recvcmd.h
util-files.o:		util-files.c util-files.h
util_mmap.o:		util_mmap.c util_mmap.h
util-gpio.o:		util-gpio.c util-gpio.h
util-keepoff.o:		util-keepoff.c util-keepoff.h
util-string.o:		util-string.c util-string.h
thread_manager.o:	thread_manager.c thread_manager.h
sys_resource.o:		sys_resource.c sys_resource.h
common.o:			common.c common.h
stru_queue.o:		stru_queue.c stru_queue.h
sys_init.o:			sys_init.c sys_init.h
drv_pcie.o:			drv_pcie.c drv_pcie.h
pcie_proce.o:		pcie_proce.c pcie_proce.h
vlan_proce.o:		vlan_proce.c vlan_proce.h
info_apper.o:		info_apper.c info_apper.h
oper_device.o:		oper_device.c oper_device.h

clean:
	-rm -rf $(objects)

cleanAll: 
	-rm -rf $(objects)
	-rm -rf StorgProg

伪目标

这样显式地定义伪目标的好处有两点:

  1. 如果同时存在一个普通目标clean,非显示定义的伪目标将无法执行,显式定义伪目标可以解决这个问题,在出现同名普通目标时,它将覆盖普通目标得以执行,同时make将输出警告信息。
  2. 告诉make这就是一个伪目标,不要试图对其做其他处理,这样可以提高编译效率,减少编译时间。
.PHONY : clean    #需要指定 clean为伪目标,提供给make解析
clean:
   -rm -rf xxx

vpath

作用:在一些大的工程中,有大量的源文件,我们通常的做法是把这许多的源文件分类,并存放在不同的目录中。所以,当make需要去找寻文件的依赖关系时,你可以在文件前加上路径,但最好的方法是把一个路径告诉make,让make在自动去找。
vpath 是 makefile 中特殊变量, 用于指定源文件在那些目录下搜索

VPATH = src:../headers  #在src 和 ../headers 目录中查找

make debug模式,输出详细错误日志

make --debug=v

指定头文件目录

作用: 代码编译的时候会引用头文件, 比如某个目录下的 test.h 被其他文件引用编译的话需要详细到具体的目录才行, 但是使用 -I(需要指定引用.h的目录), 这个时候,就可以直接省略掉相对或者绝对的路径

// 如果不使用 -I指定
#include "xxx/test.h"
// 如果使用 -Ixxx 编译指定, 使用如下引用即可
#include “test.h”

如何指定多个include目录

addprefix: 关键字,给指定数组每个元素前面加指定的前缀
语法 $(addprefix fixstring,string1 string2 ...)

INCLUDES=./include ./src
$(objects):	%.o:  %.c
	$(CC) -c $(CFLAGS) $< -o $@ $(addprefix -I, $(INCLUDES))

增量编译写法

1.正常风格

logger.o:		 	logger.c logger.h
cc -c -g socket.c -o socket.o

如上,当对依赖文件作出修改的时候,如 logger.c 和 logger.h 中的文件某些内容, logger.o 会 增量编译, 如果不写 logger.h 或着 logger.c(这个看平台,GUN make会自动推导),则修改对应文件的时候,logger.o 文件并不会重新编译。
另外, 如果不加 -g, 只是在最后链接的时候加 -g, 调试的时候也调试不了。

2. make自动推导

GUN make 会自动推导命令,根据run指令, 会自动推导出对应.c文件和编译参数, 所以上述可以简写, 将对应的.h 加入即可。

logger.o:		logger.h

3. 再次优化依赖文件 .h

$(objects) : defs.h
kbd.o command.o files.o : command.h
display.o insert.o search.o files.o : buffer.h

可以根据不同的 .o 文件,导入对应的 .h

4. 优化 .o 文件

.o 按照上述文件需要自己手动添加对应的 .o 文件
通过make提供的关键字

obj_test := $(wildcard *.c)   #找出当前目录下所有 .c 文件
obj_test1:=$(patsubst %.c,%.o,$(wildcard *.c))  #根据当前目录下所有 .c 文件生成对应的 .o 文件
obj_test2 := $(wildcard *.o)  #找出当前目录下所有的 .o 文件

5. 如何指定多个目录源编译

# 步骤1
## 方式1
objects += $(patsubst %.c,%.o,$(wildcard src/*.c))
objects += $(patsubst %.c,%.o,$(wildcard include/*.c))

## 方式2
objects += $(patsubst %.c,%.o,$(wildcard src/*.c include/*.c))

#步骤2
run:	$(objects)
	$(CC) $(CFLAGS) -o $(progName) -w -D_FILE_OFFSET_BITS=64 -D_LARGE_FILE -pthread -lm $(objects)

6. 通配符优化 .o 文件

$<: 自动化变量,表示依赖文件
$@: 自动化变量,表示目标集

# 方案1
$(objects):	%.o:  %.c
  $(CC) -c $(CFLAGS) $< -o $@

# 方案2
$(filter %.o,$(files)): %.o: %.c
    $(CC) -c $(CFLAGS) $< -o $@

# 如果除了 .o 还有其他文件
files = foo.elc bar.o lose.o
$(filter %.o,$(files)): %.o: %.c
    $(CC) -c $(CFLAGS) $< -o $@
$(filter %.elc,$(files)): %.elc: %.el
    emacs -f batch-byte-compile $<

7. 优化依赖文件

如果使用GUN的编译器,需要使用 -MM, -M会把一些标准库的头文件包含进来

gcc -M main.c
#输出的是
main.o: main.c xx1.h xx2.h

makefile中引用其他文件

使用 include <filename>, 或者是 -include <filename>, -的意思就是包含makefile文件如果包含致命错误,跳过继续执行。

系统,gcc,make版本

系统版本: Centos8

[root@node2]# cat /proc/version
Linux version 4.18.0-305.3.1.el8.x86_64 (mockbuild@kbuilder.bsys.centos.org) (gcc version 8.4.1 20200928 (Red Hat 8.4.1-1) (GCC)) #1 SMP Tue Jun 1 16:14:33 UTC 2021
[root@node2]# gcc -v
使用内建 specs。
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/8/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none
OFFLOAD_TARGET_DEFAULT=1
目标:x86_64-redhat-linux
配置为:../configure --enable-bootstrap --enable-languages=c,c++,fortran,lto --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-shared --enable-threads=posix --enable-checking=release --enable-multilib --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-gcc-major-version-only --with-linker-hash-style=gnu --enable-plugin --enable-initfini-array --with-isl --disable-libmpx --enable-offload-targets=nvptx-none --without-cuda-driver --enable-gnu-indirect-function --enable-cet --with-tune=generic --with-arch_32=x86-64 --build=x86_64-redhat-linux
线程模型:posix
gcc 版本 8.4.1 20200928 (Red Hat 8.4.1-1) (GCC)
[root@node2]# make -v
GNU Make 4.2.1
为 x86_64-redhat-linux-gnu 编译
Copyright (C) 1988-2016 Free Software Foundation, Inc.
许可证:GPLv3+:GNU 通用公共许可证第 3 版或更新版本<http://gnu.org/licenses/gpl.html>。
本软件是自由软件:您可以自由修改和重新发布它。
在法律允许的范围内没有其他保证。
posted @ 2022-11-09 17:11  韩若明瞳  阅读(170)  评论(0)    收藏  举报