Skip to content

LV120-编译时间与版本解析

一、为什么?

为什么要研究这个?因为 linux 内核的还没去学呢啊,主要是因为后面在搞驱动开发学习的时候需要这个。为什么?在驱动开发的时候,一般我是把驱动的源文件放在 linux 内核源码之外,通过调用内核的 makefile 来编译生成独立的内核模块,仓库在这里:imx6ull-driver-demo: i.mx6ull 驱动开发 demo

因为我是进行了 git 版本管理的,在实际的工作中,也经常遇到不知道用的驱动是什么版本的问题,用的是这一个版本,随着功能的完善,版本肯定要迭代更新的,可是回头谁知道自己用的哪个驱动版本啊,也许有别的办法能追溯,但起码我写这个笔记的时候还没有接触到。

目前想到的办法就是把 git 版本号以及编译时间这些信息写进去,git 版本号的获取可以在 Makefile 中这样获取:

makefile
#GIT_SHA = $(shell git rev-list HEAD | awk 'NR==1')
GIT_SHA = $(shell git rev-parse --short HEAD | awk 'NR==1')
GIT_SEQ = $(shell git rev-list HEAD | wc -l)
GIT_VER_INFO = $(GIT_SHA)-$(GIT_SEQ)
GIT_SVR_PATH = $(shell git remote -v | awk 'NR==1' | sed 's/[()]//g' | sed 's/\t/ /g' |cut -d " " -f2)


ifneq ($(GIT_VER_INFO),)
	CFLAGS += -DGIT_VERSION=\"$(GIT_VER_INFO)\"
else
	CFLAGS += -DGIT_VERSION=\"unknown\"
endif

ifneq ($(GIT_SVR_PATH),)
	CFLAGS += -DGIT_PATH=\"$(GIT_SVR_PATH)\"
else
	CFLAGS += -DGIT_PATH=\"unknown\"
endif

然后再 GCC 编译的时候加进去,就可以直接再 c 语言工程中调用了,但是吧有个问题,就是这样的-D 参数是要加在编译工具后面的,例如:

shell
gcc -DDEBUG source.c -o program

但是我们在内核源码之外写的驱动源文件,通过调用内核的 makefile 来编译的,经过实践发现,这里没办法直接把-D 参数传给内核 Makefile,至少写这个笔记的时候我还没发现怎么传进去让 ko 的源文件可以调用。所以我就去扒了 uboot 的源码,因为我们知道 uboot 是会生成两个文件的:

c
#define U_BOOT_DATE "Nov 19 2024"
#define U_BOOT_TIME "21:45:10"
#define U_BOOT_TZ "+0800"
#define U_BOOT_DMI_DATE "11/19/2024"
#define U_BOOT_BUILD_DATE 0x20241119
c
#define PLAIN_VERSION "2019.04-gf32bcf5b"
#define U_BOOT_VERSION "U-Boot " PLAIN_VERSION
#define CC_VERSION_STRING "arm-linux-gnueabihf-gcc (GNU Toolchain for the A-profile Architecture 8.3-2019.03 (arm-rel-8.36)) 8.3.0"
#define LD_VERSION_STRING "GNU ld (GNU Toolchain for the A-profile Architecture 8.3-2019.03 (arm-rel-8.36)) 2.32.0.20190321"

这里直接定义成了宏,生成的是头文件,这样我们直接在 C 工程中调用也可以,这样不就可以实现了吗,哈哈。所以就来看一下这两个文件怎么生成和创建的吧。

Tips:在 uboot 的 git 管理中,这两个文件没有必要纳入版本库的管理,这里只是为了方便学习。另外将这个纳入版本库后,这个远程仓库中的 version_autogenerated.h 文件中 git 版本号永远都比最新一次提交落后,因为每次编译完,这个文件中都是最新一次提交的 commit id,但是当我们传到远端的时候,在本地需要将所有的文件提交,这就会导致这个文件晚于最终的最新版本,当被重新编译的时候才会变成最新的。

二、timestamp

我们先来看这个关于时间的头文件都怎么生成的。

1. 编译信息

我们通过以下命令编译一次 uboot,获取编译过程信息,看看里面关于这个文件的信息:

shell
make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- mx6ull_14x14_evk_defconfig
make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j16 > make_info.txt

获取到这个 make_info.txt 文件后,我们打开搜索一下 timestamp_autogenerated.h,就会发现所有相关的信息都在这里:

shell
set -e; : '  CHK     include/generated/timestamp_autogenerated.h'; mkdir -p include/generated/; 	(if test -n "${SOURCE_DATE_EPOCH}"; then SOURCE_DATE="@${SOURCE_DATE_EPOCH}"; DATE=""; for date in gdate date.gnu date; do ${date} -u -d "${SOURCE_DATE}" >/dev/null 2>&1 && DATE="${date}"; done; if test -n "${DATE}"; then LC_ALL=C ${DATE} -u -d "${SOURCE_DATE}" +'#define U_BOOT_DATE "%b %d %C%y"'; LC_ALL = C ${DATE} -u -d "${SOURCE_DATE}" +'#define U_BOOT_TIME "%T "'; LC_ALL = C ${DATE} -u -d "${SOURCE_DATE}" +'#define U_BOOT_TZ "%z"'; LC_ALL = C ${DATE} -u -d "${SOURCE_DATE}" +'#define U_BOOT_DMI_DATE "%m/%d/%Y "'; LC_ALL = C ${DATE} -u -d "${SOURCE_DATE}" +'#define U_BOOT_BUILD_DATE 0x%Y%m%d'; else return 42; fi; else LC_ALL = C date +'#define U_BOOT_DATE "%b %d %C%y"'; LC_ALL = C date +'#define U_BOOT_TIME "%T"'; LC_ALL = C date +'#define U_BOOT_TZ "%z"'; LC_ALL = C date +'#define U_BOOT_DMI_DATE "%m/%d/%Y"'; LC_ALL = C date +'#define U_BOOT_BUILD_DATE 0x%Y%m%d'; fi) < Makefile > include/generated/timestamp_autogenerated.h.tmp; if [ -r include/generated/timestamp_autogenerated.h ] && cmp -s include/generated/timestamp_autogenerated.h include/generated/timestamp_autogenerated.h.tmp; then rm -f include/generated/timestamp_autogenerated.h.tmp; else : '  UPD     include/generated/timestamp_autogenerated.h'; mv -f include/generated/timestamp_autogenerated.h.tmp include/generated/timestamp_autogenerated.h; fi

整理一下:

shell
set -e; : '  CHK     include/generated/timestamp_autogenerated.h'; 
mkdir -p include/generated/; 	
(if test -n "${SOURCE_DATE_EPOCH}"; then 
	SOURCE_DATE="@${SOURCE_DATE_EPOCH}"; 
	DATE=""; 
	for date in gdate date.gnu date; do 
		${date} -u -d "${SOURCE_DATE}" >/dev/null 2>&1 && DATE="${date}"; 
	done; 
	if test -n "${DATE}"; then 
		LC_ALL=C ${DATE} -u -d "${SOURCE_DATE}" +'#define U_BOOT_DATE "%b %d %C%y"';
        LC_ALL=C ${DATE} -u -d "${SOURCE_DATE}" +'#define U_BOOT_TIME "%T"'; 
        LC_ALL=C ${DATE} -u -d "${SOURCE_DATE}" +'#define U_BOOT_TZ "%z"'; 
        LC_ALL=C ${DATE} -u -d "${SOURCE_DATE}" +'#define U_BOOT_DMI_DATE "%m/%d/%Y"';
        LC_ALL=C ${DATE} -u -d "${SOURCE_DATE}" +'#define U_BOOT_BUILD_DATE 0x%Y%m%d'; 
	else 
		return 42; 
	fi; 
else 
	LC_ALL=C date +'#define U_BOOT_DATE "%b %d %C%y"'; 
	LC_ALL=C date +'#define U_BOOT_TIME "%T"'; 
	LC_ALL=C date +'#define U_BOOT_TZ "%z"'; 
	LC_ALL=C date +'#define U_BOOT_DMI_DATE "%m/%d/%Y"'; 
	LC_ALL=C date +'#define U_BOOT_BUILD_DATE 0x%Y%m%d'; 
fi) 
< Makefile > include/generated/timestamp_autogenerated.h.tmp; 
if [ -r include/generated/timestamp_autogenerated.h ] && cmp -s include/generated/timestamp_autogenerated.h include/generated/timestamp_autogenerated.h.tmp; then 
	rm -f include/generated/timestamp_autogenerated.h.tmp; 
else : '  UPD     include/generated/timestamp_autogenerated.h'; 
	mv -f include/generated/timestamp_autogenerated.h.tmp include/generated/timestamp_autogenerated.h; 
fi

接下来我们就来看一看这个编译信息怎么出来的。

2. timestamp_h

先找一下 timestamp_autogenerated.h 定义的位置,这个文件位置的定义是在 Makefile · timestamp_h

makefile
timestamp_h := include/generated/timestamp_autogenerated.h

看到这里定义了这个 timestamp_h 变量,我们接着搜一下这个 timestamp_h 关键词,会发现它也是一个目标,在 Makefile 中的规则如下:

makefile
$(timestamp_h): $(srctree)/Makefile FORCE
	$(call filechk,timestamp.h)

我们前面分析顶层 Makefile 的时候分析过,这个 Makefile · srctree 最后是 .,所以这里简单先展开一下就是:

makefile
include/generated/timestamp_autogenerated.h: ./Makefile FORCE
	$(call filechk,timestamp.h)

接下来就是看这个 filechk 和 timestamp.h 是什么了。

3. filechk

3.1 Makefile 中的自定义函数

我们先回顾一下 Makefile 中的自定义函数,在 Makefile 中是可以自己定义一个函数然后调用的。比如:

makefile
define filechk_timestamp.h
	@echo "this is a makefile function!"
	@echo "\$(0)=$(0):表示函数名"
    @echo "\$(1)=$(1):表示传给函数的第一个参数"
    @echo "\$(2)=$(2):表示传给函数的第二个参数"
endef

然后可以通过以下语句调用:

makefile
$(call func_name,arg1,arg2,...)

一般而言,call 本身并不具备编译功能,call 只是将自定义函数在当前位置展开,遇到 Makefile 内置函数会自动执行。其中 arg1、arg2、...是传入函数的参数。我们可以尝试一下,新建一个 Makefile 文件,输入以下内容:

makefile
define filechk_timestamp
	@echo "this is a makefile function!"
	@echo "$(0):表示函数名"
    @echo "$(1):表示传给函数的第一个参数"
    @echo "$(2):表示传给函数的第二个参数"
endef

test:
	$(call filechk_timestamp,arg1,arg2)

我们直接执行 make,就会得到如下打印信息:

shell
this is a makefile function!
filechk_timestamp:表示函数名
arg1:表示传给函数的第一个参数
arg2:表示传给函数的第二个参数

前面说到 call 只是将自定义函数在当前位置展开,所以上面的函数其实等价于:

makefile
#define filechk_timestamp
#	 @echo "this is a makefile function!"
#	 @echo "$(0):表示函数名"
#    @echo "$(1):表示传给函数的第一个参数"
#    @echo "$(2):表示传给函数的第二个参数"
#endef

test:
	@echo "this is a makefile function!"
	@echo "filechk_timestamp:表示函数名"
    @echo "arg1:表示传给函数的第一个参数"
    @echo "arg2:表示传给函数的第二个参数"

3.2 filechk

接下来我们来看一下这个 filechk 函数,它定义在 scripts/Kbuild.include · filechk

makefile
###
# filechk is used to check if the content of a generated file is updated.
# Sample usage:
# define filechk_sample
#	echo $KERNELRELEASE
# endef
# version.h : Makefile
#	$(call filechk,sample)
# The rule defined shall write to stdout the content of the new file.
# The existing file will be compared with the new one.
# - If no file exist it is created
# - If the content differ the new file is used
# - If they are equal no change, and no timestamp update
# - stdin is piped in from the first prerequisite ($<) so one has
#   to specify a valid file as first prerequisite (often the kbuild file)
define filechk
	$(Q)set -e;				\
	$(kecho) '  CHK     $@';		\
	mkdir -p $(dir $@);			\
	$(filechk_$(1)) < $< > $@.tmp;		\
	if [ -r $@ ] && cmp -s $@ $@.tmp; then	\
		rm -f $@.tmp;			\
	else					\
		$(kecho) '  UPD     $@';	\
		mv -f $@.tmp $@;		\
	fi
endef

可以看到这里的 $(1) 应该就是前面的 timestamp.h,所以这里就是:

makefile
define filechk
	$(Q)set -e;				\
	$(kecho) '  CHK     $@';		\
	mkdir -p $(dir $@);			\
	$(filechk_timestamp.h) < $< > $@.tmp;		\
	if [ -r $@ ] && cmp -s $@ $@.tmp; then	\
		rm -f $@.tmp;			\
	else					\
		$(kecho) '  UPD     $@';	\
		mv -f $@.tmp $@;		\
	fi
endef

3.3 变量的值

哪些变量都是啥啊?其实这这些 $@$< 都是在规则中确定的,我们省点事,加点打印信息好了,我们修改 scripts/Kbuild.include · filechk 中 filechk 函数如下:

shell
define filechk
	echo "" \
	echo "Q=$(Q)" \
	echo "kecho=$(kecho)" \
	echo "$$@=$@" \
	echo "$$<=$<" \
	echo "" \
	$(Q)set -e;				\
	$(kecho) '  CHK     $@';		\
	mkdir -p $(dir $@);			\
	$(filechk_$(1)) < $< > $@.tmp;		\
	if [ -r $@ ] && cmp -s $@ $@.tmp; then	\
		rm -f $@.tmp;			\
	else					\
		$(kecho) '  UPD     $@';	\
		mv -f $@.tmp $@;		\
	fi
endef

可以找到有这么一条打印信息:

shell
echo "" echo "Q=" echo "kecho=:" echo "$@=include/generated/timestamp_autogenerated.h" echo "$<=Makefile" echo "" set -e; : '  CHK     include/generated/timestamp_autogenerated.h'; mkdir -p include/generated/; 	(if test -n "${SOURCE_DATE_EPOCH}"; then SOURCE_DATE="@${SOURCE_DATE_EPOCH}"; DATE=""; for date in gdate date.gnu date; do ${date} -u -d "${SOURCE_DATE}" >/dev/null 2>&1 && DATE="${date}"; done; if test -n "${DATE}"; then LC_ALL=C ${DATE} -u -d "${SOURCE_DATE}" +'#define U_BOOT_DATE "%b %d %C%y"'; LC_ALL = C ${DATE} -u -d "${SOURCE_DATE}" +'#define U_BOOT_TIME "%T "'; LC_ALL = C ${DATE} -u -d "${SOURCE_DATE}" +'#define U_BOOT_TZ "%z"'; LC_ALL = C ${DATE} -u -d "${SOURCE_DATE}" +'#define U_BOOT_DMI_DATE "%m/%d/%Y "'; LC_ALL = C ${DATE} -u -d "${SOURCE_DATE}" +'#define U_BOOT_BUILD_DATE 0x%Y%m%d'; else return 42; fi; else LC_ALL = C date +'#define U_BOOT_DATE "%b %d %C%y"'; LC_ALL = C date +'#define U_BOOT_TIME "%T"'; LC_ALL = C date +'#define U_BOOT_TZ "%z"'; LC_ALL = C date +'#define U_BOOT_DMI_DATE "%m/%d/%Y"'; LC_ALL = C date +'#define U_BOOT_BUILD_DATE 0x%Y%m%d'; fi) < Makefile > include/generated/timestamp_autogenerated.h.tmp; if [ -r include/generated/timestamp_autogenerated.h ] && cmp -s include/generated/timestamp_autogenerated.h include/generated/timestamp_autogenerated.h.tmp; then rm -f include/generated/timestamp_autogenerated.h.tmp; else : '  UPD     include/generated/timestamp_autogenerated.h'; mv -f include/generated/timestamp_autogenerated.h.tmp include/generated/timestamp_autogenerated.h; fi

可以看到这些就是我们添加的打印信息:

shell
echo "" echo "Q=" echo "kecho=:" echo "$@=include/generated/timestamp_autogenerated.h" echo "$<=Makefile" echo ""

3.4 filehck 展开

所以这里我们对这个函数展开:

makefile
define filechk
	set -e;				\
	: '  CHK     include/generated/timestamp_autogenerated.h';		\
	mkdir -p $(dir include/generated/timestamp_autogenerated.h);			\
	$(filechk_$(1)) < Makefile > include/generated/timestamp_autogenerated.h.tmp;		\
	if [ -r include/generated/timestamp_autogenerated.h ] && cmp -s include/generated/timestamp_autogenerated.h include/generated/timestamp_autogenerated.h.tmp; then	\
		rm -f include/generated/timestamp_autogenerated.h.tmp;			\
	else					\
		: '  UPD     include/generated/timestamp_autogenerated.h';	\
		mv -f include/generated/timestamp_autogenerated.h.tmp include/generated/timestamp_autogenerated.h;		\
	fi
endef
  • set -e:使脚本在遇到任何命令失败时立即终止。

  • 这个 : 是啥意思???: 在 shell 中是一个不执行任何操作的命令,在这里应该只是用于记录日志或者调试信息,因为从前面的打印信息可以看出这里会打印出来这条提示信息。

  • -r include/generated/timestamp_autogenerated.h:检查这个文件是否可读。

  • cmp -scmp 这是一个用于比较两个文件内容的命令,但它不会输出任何内容,而是通过退出状态码来表示文件是否相同。-s 选项:静默模式,不输出任何内容,只通过退出状态码来表示结果。这个命令退出状态码为 0 表示文件内容完全相同,1 表示文件内容不同,>1 表示发生错误。这里的逻辑就是判断新生成的文件 timestamp_autogenerated.h.tmp 与 timestamp_autogenerated.h 文件是否相同,若相同,这次就没有必要更新了,就把中间文件删掉,若不相同,就把 timestamp_autogenerated.h.tmp 重命名为 timestamp_autogenerated.h。

3.5 filechk_$(1)

我们再来看一下这个 filechk_$(1),在调用 filechk 函数的时候是这样的:

makefile
$(call filechk,timestamp.h)

我们将 $(1) 替换后,就是:

makefile
$(filechk_timestamp.h) < Makefile > include/generated/timestamp_autogenerated.h.tmp;

这里我们看一下这个 filechk_timestamp.h,它定义在顶层 Makefile · filechk_timestamp.h

makefile
# The SOURCE_DATE_EPOCH mechanism requires a date that behaves like GNU date.
# The BSD date on the other hand behaves different and would produce errors
# with the misused '-d' switch.  Respect that and search a working date with
# well known pre- and suffixes for the GNU variant of date.
define filechk_timestamp.h
	(if test -n "$${SOURCE_DATE_EPOCH}"; then \
		SOURCE_DATE="@$${SOURCE_DATE_EPOCH}"; \
		DATE=""; \
		for date in gdate date.gnu date; do \
			$${date} -u -d "$${SOURCE_DATE}" >/dev/null 2>&1 && DATE="$${date}"; \
		done; \
		if test -n "$${DATE}"; then \
			LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define U_BOOT_DATE "%b %d %C%y"'; \
			LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define U_BOOT_TIME "%T"'; \
			LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define U_BOOT_TZ "%z"'; \
			LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define U_BOOT_DMI_DATE "%m/%d/%Y"'; \
			LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define U_BOOT_BUILD_DATE 0x%Y%m%d'; \
		else \
			return 42; \
		fi; \
	else \
		LC_ALL=C date +'#define U_BOOT_DATE "%b %d %C%y"'; \
		LC_ALL=C date +'#define U_BOOT_TIME "%T"'; \
		LC_ALL=C date +'#define U_BOOT_TZ "%z"'; \
		LC_ALL=C date +'#define U_BOOT_DMI_DATE "%m/%d/%Y"'; \
		LC_ALL=C date +'#define U_BOOT_BUILD_DATE 0x%Y%m%d'; \
	fi)
endef
  • if test -n 是判断字符串是否为空,我找了下顶层 Makefile,好像也没找到这个 SOURCE_DATE_EPOCH 的定义,SOURCE_DATE_EPOCH 它是一个环境变量,通常用于构建过程中,以确保生成的二进制文件和相关文件具有确定的时间戳。加个打印信息就会发现,这里为空,所以走的是 else 的部分,非空的部分就没去仔细研究了。
  • LC_ALL=C:这个是一个环境变量设置,用于覆盖所有本地化设置,使其使用 C 本地化。这意味着程序的行为将遵循 POSIX 标准,忽略用户的本地化设置。这对于确保跨平台一致性尤其有用,特别是在处理文本数据时。
  • date +'#define ...':这个就是以后面指定的格式获取时间字符串,放到终端执行以下就知道了:
shell
sumu@sumu-virtual-machine:~/7Linux/imx6ull-uboot$ LC_ALL=C date +'#define U_BOOT_DATE "%b %d %C%y"'
#define U_BOOT_DATE "Nov 22 2024"

因为走的是 else 分支,所以这里我们可以简化一下:

makefile
define filechk_timestamp.h
	(LC_ALL=C date +'#define U_BOOT_DATE "%b %d %C%y"'; \
    LC_ALL=C date +'#define U_BOOT_TIME "%T"'; \
    LC_ALL=C date +'#define U_BOOT_TZ "%z"'; \
    LC_ALL=C date +'#define U_BOOT_DMI_DATE "%m/%d/%Y"'; \
    LC_ALL=C date +'#define U_BOOT_BUILD_DATE 0x%Y%m%d';)
endef

3.6 timestamp_autogenerated.h

timestamp_autogenerated.h 这个文件怎么生成的?上面已经分析了里面的内容怎么来的,那文件怎么创建的?其实在这里 scripts/Kbuild.include · filechk

makefile
$(filechk_$(1)) < $< > $@.tmp;

前面我们已经可以将这个展开了:

makefile
$(filechk_timestamp.h) < Makefile > include/generated/timestamp_autogenerated.h.tmp;

把这个 filechk_timestamp.h 也替换一下:

makefile
(LC_ALL=C date +'#define U_BOOT_DATE "%b %d %C%y"'; \
    LC_ALL=C date +'#define U_BOOT_TIME "%T"'; \
    LC_ALL=C date +'#define U_BOOT_TZ "%z"'; \
    LC_ALL=C date +'#define U_BOOT_DMI_DATE "%m/%d/%Y"'; \
    LC_ALL=C date +'#define U_BOOT_BUILD_DATE 0x%Y%m%d';) < Makefile > include/generated/timestamp_autogenerated.h.tmp;

这个用了两个重定向操作符 <>< 是输入重定向操作符,用于将文件内容作为命令的标准输入。这在处理文件内容时非常有用,可以避免手动输入数据或从命令行传递大量数据。> 是重定向操作符,用于将命令的输出重定向到文件或其他目标。

所以这里的含义就是,将当前目录下的 Makefile 文件作为左边命令的输入(我并没有发现它有什么具体的作用,后面发现了再补充),将命令的结果重定向到 include/generated/timestamp_autogenerated.h.tmp 文件中去。当文件不存在的时候就会直接创建。

4. 规则展开

到这里为止,就可以把这个 timestamp_h 规则完全展开了,但是这里展开写的太长了,我们直接写一个单独的 demo 来演示效果:

makefile
define filechk_timestamp_autogenerated.h
	(if test -n "$${SOURCE_DATE_EPOCH}"; then \
		SOURCE_DATE="@$${SOURCE_DATE_EPOCH}"; \
		DATE=""; \
		for date in gdate date.gnu date; do \
			$${date} -u -d "$${SOURCE_DATE}" >/dev/null 2>&1 && DATE="$${date}"; \
		done; \
		if test -n "$${DATE}"; then \
			LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define U_BOOT_DATE "%b %d %C%y"'; \
			LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define U_BOOT_TIME "%T"'; \
			LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define U_BOOT_TZ "%z"'; \
			LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define U_BOOT_DMI_DATE "%m/%d/%Y"'; \
			LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define U_BOOT_BUILD_DATE 0x%Y%m%d'; \
		else \
			return 42; \
		fi; \
	else \
		LC_ALL=C date +'#define U_BOOT_DATE "%b %d %C%y"'; \
		LC_ALL=C date +'#define U_BOOT_TIME "%T"'; \
		LC_ALL=C date +'#define U_BOOT_TZ "%z"'; \
		LC_ALL=C date +'#define U_BOOT_DMI_DATE "%m/%d/%Y"'; \
		LC_ALL=C date +'#define U_BOOT_BUILD_DATE 0x%Y%m%d'; \
	fi)
endef

define filechk
	set -e;				\
	: '  CHK     $@';		\
	mkdir -p $(dir $@);			\
	$(filechk_$(1)) < $< > $@.tmp;		\
	if [ -r $@ ] && cmp -s $@ $@.tmp; then	\
		rm -f $@.tmp;			\
	else					\
		: '  UPD     $@';	\
		mv -f $@.tmp $@;		\
	fi
endef

srctree := .
timestamp_h := timestamp_autogenerated.h

$(timestamp_h): $(srctree)/Makefile FORCE
	$(call filechk,timestamp_autogenerated.h)
	
PHONY += FORCE
FORCE:

.PHONY: $(PHONY)

其中 FORCE 是一个伪目标,他可以保证每次都执行 timestamp_h 规则,可以保证每次都更新 time_stamp.h 文件。

三、version

接下来我们看一下这个 uboot 的版本是怎么显示和获取的。我们前面知道 version_autogenerated.h 这个文件的内容如下:

c
#define PLAIN_VERSION "2019.04-gf32bcf5b"
#define U_BOOT_VERSION "U-Boot " PLAIN_VERSION
#define CC_VERSION_STRING "arm-linux-gnueabihf-gcc (GNU Toolchain for the A-profile Architecture 8.3-2019.03 (arm-rel-8.36)) 8.3.0"
#define LD_VERSION_STRING "GNU ld (GNU Toolchain for the A-profile Architecture 8.3-2019.03 (arm-rel-8.36)) 2.32.0.20190321"

可以看到它包含了 ubuntu 的版本号、git 版本信息、编译器和链接器的版本信息。下面我们来分析一下这些都是怎么生成的。

1. 编译信息

和上面一样,我们通过以下命令编译一次 uboot,获取编译过程信息,看看里面关于这个文件的信息:

shell
make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- mx6ull_14x14_evk_defconfig
make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j16 > make_info.txt

获取到这个 make_info.txt 文件后,我们打开搜索一下 version_autogenerated.h,就会发现所有相关的信息都在这里:

shell
set -e; : '  CHK     include/generated/version_autogenerated.h'; mkdir -p include/generated/; 	(echo \#define PLAIN_VERSION \" 2019.04 "" \"; echo \#define U_BOOT_VERSION \" U-Boot \" PLAIN_VERSION; echo \#define CC_VERSION_STRING \" $(LC_ALL=C arm-linux-gnueabihf-gcc --version | head -n 1)\"; echo \#define LD_VERSION_STRING \"$(LC_ALL = C arm-linux-gnueabihf-ld.bfd --version | head -n 1)\"; ) < include/config/uboot.release > include/generated/version_autogenerated.h.tmp; if [ -r include/generated/version_autogenerated.h ] && cmp -s include/generated/version_autogenerated.h include/generated/version_autogenerated.h.tmp; then rm -f include/generated/version_autogenerated.h.tmp; else : '  UPD     include/generated/version_autogenerated.h'; mv -f include/generated/version_autogenerated.h.tmp include/generated/version_autogenerated.h; fi

整理一下:

shell
set -e; : '  CHK     include/generated/version_autogenerated.h'; 
mkdir -p include/generated/; 	
(echo \#define PLAIN_VERSION \" 2019.04 "" \"; 
echo \#define U_BOOT_VERSION \" U-Boot \" PLAIN_VERSION; 
echo \#define CC_VERSION_STRING \"$(LC_ALL = C arm-linux-gnueabihf-gcc --version | head -n 1)\";
echo \#define LD_VERSION_STRING \"$(LC_ALL = C arm-linux-gnueabihf-ld.bfd --version | head -n 1)\"; ) < include/config/uboot.release > include/generated/version_autogenerated.h.tmp; 
if [ -r include/generated/version_autogenerated.h ] && cmp -s include/generated/version_autogenerated.h include/generated/version_autogenerated.h.tmp; then 
	rm -f include/generated/version_autogenerated.h.tmp; 
else 
	: '  UPD     include/generated/version_autogenerated.h'; 
	mv -f include/generated/version_autogenerated.h.tmp include/generated/version_autogenerated.h; 
fi

接下来我们就来看一看这个编译信息怎么出来的。

2. version_h

和时间信息一样,我们先在顶层 Makefile 中找到 version_h 的定义: Makefile · version_h

makefile
version_h := include/generated/version_autogenerated.h

这里定义了这个 version_h 变量,我们接着搜一下这个 version_h 关键词,会发现它也是一个目标,在 Makefile 中的规则如下:

makefile
$(version_h): include/config/uboot.release FORCE
	$(call filechk,version.h)

发现这里和前面的 Makefile · timestamp.h 一样,都是调用了 filechk 函数,这里简单展开一下:

makefile
include/generated/version_autogenerated.h: include/config/uboot.release FORCE
	$(call filechk,version_h)

3. filechk

filechk 函数前面已经找过了,它定义在 scripts/Kbuild.include · filechk

makefile
define filechk
	$(Q)set -e;				\
	$(kecho) '  CHK     $@';		\
	mkdir -p $(dir $@);			\
	$(filechk_$(1)) < $< > $@.tmp;		\
	if [ -r $@ ] && cmp -s $@ $@.tmp; then	\
		rm -f $@.tmp;			\
	else					\
		$(kecho) '  UPD     $@';	\
		mv -f $@.tmp $@;		\
	fi
endef

可以看到这里的 $(1) 应该就是前面的 version_h,所以这里就是:

makefile
define filechk
	$(Q)set -e;				\
	$(kecho) '  CHK     $@';		\
	mkdir -p $(dir $@);			\
	$(filechk_version_h) < $< > $@.tmp;		\
	if [ -r $@ ] && cmp -s $@ $@.tmp; then	\
		rm -f $@.tmp;			\
	else					\
		$(kecho) '  UPD     $@';	\
		mv -f $@.tmp $@;		\
	fi
endef

3.1 变量的值

还是和前面一样,这些 $@$< 都是在规则中确定的,我们省点事,加点打印信息好了,我们修改 scripts/Kbuild.include · filechk 中 filechk 函数如下:

shell
define filechk
	echo "" \
	echo "Q=$(Q)" \
	echo "kecho=$(kecho)" \
	echo "$$@=$@" \
	echo "$$<=$<" \
	echo "" \
	$(Q)set -e;				\
	$(kecho) '  CHK     $@';		\
	mkdir -p $(dir $@);			\
	$(filechk_$(1)) < $< > $@.tmp;		\
	if [ -r $@ ] && cmp -s $@ $@.tmp; then	\
		rm -f $@.tmp;			\
	else					\
		$(kecho) '  UPD     $@';	\
		mv -f $@.tmp $@;		\
	fi
endef

可以找到有这么一条打印信息:

shell
echo "" echo "Q=" echo "kecho=:" echo "$@=include/generated/version_autogenerated.h" echo "$<=include/config/uboot.release" echo "" set -e; : '  CHK     include/generated/version_autogenerated.h'; mkdir -p include/generated/; 	(echo \#define PLAIN_VERSION \" 2019.04 "" \"; echo \#define U_BOOT_VERSION \" U-Boot \" PLAIN_VERSION; echo \#define CC_VERSION_STRING \" $(LC_ALL=C arm-linux-gnueabihf-gcc --version | head -n 1)\"; echo \#define LD_VERSION_STRING \"$(LC_ALL = C arm-linux-gnueabihf-ld.bfd --version | head -n 1)\"; ) < include/config/uboot.release > include/generated/version_autogenerated.h.tmp; if [ -r include/generated/version_autogenerated.h ] && cmp -s include/generated/version_autogenerated.h include/generated/version_autogenerated.h.tmp; then rm -f include/generated/version_autogenerated.h.tmp; else : '  UPD     include/generated/version_autogenerated.h'; mv -f include/generated/version_autogenerated.h.tmp include/generated/version_autogenerated.h; fi

可以看到这些就是我们添加的打印信息:

shell
echo "" echo "Q=" echo "kecho=:" echo "$@=include/generated/version_autogenerated.h" echo "$<=include/config/uboot.release" echo ""

3.2 filehck 展开

所以这里我们对这个函数展开:

makefile
define filechk
	set -e;				\
	: '  CHK     include/generated/version_autogenerated.h';		\
	mkdir -p $(dir include/generated/version_autogenerated.h);			\
	$(filechk_$(1)) < include/config/uboot.release > include/generated/version_autogenerated.h.tmp;		\
	if [ -r include/generated/version_autogenerated.h ] && cmp -s include/generated/version_autogenerated.h include/generated/version_autogenerated.h.tmp; then	\
		rm -f include/generated/version_autogenerated.h.tmp;			\
	else					\
		: '  UPD     include/generated/version_autogenerated.h';	\
		mv -f include/generated/version_autogenerated.h.tmp include/generated/version_autogenerated.h;		\
	fi
endef

这里和前面都是一样的。

3.3 filechk_$(1)

3.3.1 filechk_version.h

我们再来看一下这个 filechk_$(1),在调用 filechk 函数的时候是这样的:

makefile
$(call filechk,version.h)

我们将 $(1) 替换后,就是:

makefile
$(filechk_version.h) < include/config/uboot.release > include/generated/version_autogenerated.h.tmp;

这里我们看一下这个 filechk_version.h,它定义在顶层 Makefile · filechk_version.h

makefile
define filechk_version.h
	(echo \#define PLAIN_VERSION \"$(UBOOTRELEASE)\"; \
	echo \#define U_BOOT_VERSION \"U-Boot \" PLAIN_VERSION; \
	echo \#define CC_VERSION_STRING \"$$(LC_ALL=C $(CC) --version | head -n 1)\"; \
	echo \#define LD_VERSION_STRING \"$$(LC_ALL=C $(LD) --version | head -n 1)\"; )
endef
  • PLAIN_VERSION:这个的值由 UBOOTRELEASE 决定,我们后面再说。
  • U_BOOT_VERSION:这个是最终的 uboot 版本信息,它的内容与 PLAIN_VERSION 有关。
  • CC_VERSION_STRING 是编译器的版本,我们可以在终端执行一下看看打印信息:
shell
sumu@sumu-virtual-machine:~/7Linux/imx6ull-uboot$ LC_ALL=C arm-linux-gnueabihf-gcc --version | head -n 1
arm-linux-gnueabihf-gcc (GNU Toolchain for the A-profile Architecture 8.3-2019.03 (arm-rel-8.36)) 8.3.0
  • LD_VERSION_STRING 是链接器的版本:
shell
sumu@sumu-virtual-machine:~/7Linux/imx6ull-uboot$ LC_ALL=C arm-linux-gnueabihf-ld --version | head -n 1
GNU ld (GNU Toolchain for the A-profile Architecture 8.3-2019.03 (arm-rel-8.36)) 2.32.0.20190321

接下来我们再来看看前面的 uboot 版本怎么得到的。

3.3.2 UBOOTRELEASE

接下来我们看一下这个 UBOOTRELEASE 变量,它在 Makefile · UBOOTRELEASE 中有定义:

makefile
# Read UBOOTRELEASE from include/config/uboot.release (if it exists)
UBOOTRELEASE = $(shell cat include/config/uboot.release 2> /dev/null)

可以看到它其实是获取了 include/config/uboot.release 文件的内容。接下来就是分析 uboot.release 的来源了。我们来看一下这个文件的内容:

image-20241122215559817

所以这里就对应了:

c
#define PLAIN_VERSION "2019.04-gf32bcf5b"

这个版本没对应上,可以发现是本地的上一个版本,这是因为,我们本地提交后,会产生一个新的 git 版本号,但是我们编译的时候是还没有提交的,所以这里就会晚一个版本啦,这个影响不大,这只是提交到版本库的记录会落后,我们拉到本地重新编译,这个文件就会更新成最新版本啦。

3.4 uboot.release

接下来就是看一看 uboot.release 中的内容来自于哪里?

3.4.1 uboot.release 规则

我们这部分来分析一下。这个文件其实是编译后生成的,我们在 Makefile · uboot.release 可以看到它的定义:

makefile
# Store (new) UBOOTRELEASE string in include/config/uboot.release
include/config/uboot.release: include/config/auto.conf FORCE
	$(call filechk,uboot.release)

发现这里也是调用了 filechk 函数,并且传入的参数为 uboot.release。

3.4.3 filechk 展开

我们先看一下这个 filechk 函数,和前面是一样的,定义在 scripts/Kbuild.include · filechk

makefile
define filechk
	$(Q)set -e;				\
	$(kecho) '  CHK     $@';		\
	mkdir -p $(dir $@);			\
	$(filechk_$(1)) < $< > $@.tmp;		\
	if [ -r $@ ] && cmp -s $@ $@.tmp; then	\
		rm -f $@.tmp;			\
	else					\
		$(kecho) '  UPD     $@';	\
		mv -f $@.tmp $@;		\
	fi
endef

我们根据调用命令:

makefile
$(call filechk,uboot.release)

对其进行展开,我们前面知道:

makefile
Q=
kecho=:
$@=include/config/uboot.release
@<=include/config/auto.conf
$(1)=uboot.release

所以这里是:

makefile
define filechk
	set -e;				\
	: '  CHK     include/config/uboot.release';		\
	mkdir -p $(dir include/config/uboot.release);			\
	$(filechk_uboot.release) < include/config/auto.conf > include/config/uboot.release.tmp;		\
	if [ -r include/config/uboot.release ] && cmp -s include/config/uboot.release include/config/uboot.release.tmp; then	\
		rm -f include/config/uboot.release.tmp;			\
	else					\
		: '  UPD     include/config/uboot.release';	\
		mv -f include/config/uboot.release.tmp include/config/uboot.release;		\
	fi
endef
3.4.2 filechk_$(1)

我们来看一下 Makefile · filechk_uboot.release

makefile
define filechk_uboot.release
	echo "$(UBOOTVERSION)$$($(CONFIG_SHELL) $(srctree)/scripts/setlocalversion $(srctree))"
endef
makefile
UBOOTVERSION = $(VERSION)$(if $(PATCHLEVEL),.$(PATCHLEVEL)$(if $(SUBLEVEL),.$(SUBLEVEL)))$(EXTRAVERSION)

这里其实就是把 Makefile · VERSION 这些版本号组合起来。

makefile
# SHELL used by kbuild
CONFIG_SHELL := $(shell if [ -x "$$BASH" ]; then echo $$BASH; \
	  else if [ -x /bin/bash ]; then echo /bin/bash; \
	  else echo sh; fi ; fi)

就不分析那几个变量了,我们直接从编译信息里找一下,我们去前面生成的 make_info.txt 中搜一下这个 scripts/setlocalversion 关键词:

shell
set -e; : '  CHK     include/config/uboot.release'; mkdir -p include/config/; 	echo "2019.04$(/bin/bash ./scripts/setlocalversion .)" < include/config/auto.conf > include/config/uboot.release.tmp; if [ -r include/config/uboot.release ] && cmp -s include/config/uboot.release include/config/uboot.release.tmp; then rm -f include/config/uboot.release.tmp; else : '  UPD     include/config/uboot.release'; mv -f include/config/uboot.release.tmp include/config/uboot.release; fi

整理一下就是:

shell
set -e; 
: '  CHK     include/config/uboot.release'; 
mkdir -p include/config/; 	
echo "2019.04$(/bin/bash ./scripts/setlocalversion .)" < include/config/auto.conf > include/config/uboot.release.tmp; 
if [ -r include/config/uboot.release ] && cmp -s include/config/uboot.release include/config/uboot.release.tmp; then 
	rm -f include/config/uboot.release.tmp; 
else 
	: '  UPD     include/config/uboot.release'; 
	mv -f include/config/uboot.release.tmp include/config/uboot.release; 
fi

可以看到:

makefile
$(filechk_uboot.release) < include/config/auto.conf > include/config/uboot.release.tmp;
# 对应的就是
echo "2019.04$(/bin/bash ./scripts/setlocalversion .)" < include/config/auto.conf > include/config/uboot.release.tmp;

所以其实有:

makefile
define filechk_uboot.release
	echo "$(UBOOTVERSION)$$($(CONFIG_SHELL) $(srctree)/scripts/setlocalversion $(srctree))"
endef

# 对应一下就是
define filechk_uboot.release
	echo "2019.04$(/bin/bash ./scripts/setlocalversion .)"
endf

# UBOOTVERSION=2019.04
# CONFIG_SHELL=/bin/bash
# srctree=.

其实这里就是执行了 scripts/setlocalversion 中的这个脚本并把脚本的结果和 UBOOTVERSION 拼接起来。然后通过 > 将拼接后的字符串重定向到 include/config/uboot.release.tmp 文件中。

3.5 setlocalversion

我们来看一下这个脚本 scripts/setlocalversion 在做什么:

shell
# This scripts adds local version information from the version
# control systems git, mercurial (hg) and subversion (svn).
#

# If something goes wrong, send a mail the kernel build mailinglist
# (see MAINTAINERS) and CC Nico Schottelius
# <nico-linuxsetlocalversion -at- schottelius.org>.
#

#

根据前面的信息,可以看到这个其实是获取了 git 或者 hg 或者 sv 版本号,我们直接终端执行一下:

image-20241122230044913

所以这里得到的 ""-gde6d2f71-dirty 和前面的 2019.04 结合起来就是 2019.04""-gde6d2f71-dirty

4. 生成 git 版本号

我们了解了 uboot 的 git 版本号生成过程,我们自己来写一个:

makefile
UBOOTRELEASE=$(shell git rev-parse --verify --short HEAD)

define filechk_version_autogenerated.h
	(echo \#define PLAIN_VERSION \"$(UBOOTRELEASE)\"; \
	echo \#define U_BOOT_VERSION \"U-Boot \" PLAIN_VERSION; \
	echo \#define CC_VERSION_STRING \"$$(LC_ALL=C $(CC) --version | head -n 1)\"; \
	echo \#define LD_VERSION_STRING \"$$(LC_ALL=C $(LD) --version | head -n 1)\"; )
endef

define filechk
	set -e;				\
	: '  CHK     $@';		\
	mkdir -p $(dir $@);			\
	$(filechk_$(1)) < $< > $@.tmp;		\
	if [ -r $@ ] && cmp -s $@ $@.tmp; then	\
		rm -f $@.tmp;			\
	else					\
		: '  UPD     $@';	\
		mv -f $@.tmp $@;		\
	fi
endef

srctree := .
version_h := version_autogenerated.h

CC := arm-linux-gnueabihf-gcc
LD := arm-linux-gnueabihf-ld

export LD CC

$(version_h): $(srctree)/Makefile FORCE
	echo "UBOOTRELEASE="$(UBOOTRELEASE)
	$(call filechk,version_autogenerated.h)
	
PHONY += FORCE
FORCE:

.PHONY: $(PHONY)

四、应用实例

这里先写一个在内核模块中打印编译时间和版本信息的实例。整体可以看这里:linux-dev-org/imx6ull-driver-demo/03_module_basic/03_printk_prj_info

1. Kbuild.include

makefile
define filechk_timestamp_autogenerated.h
	(if test -n "$${SOURCE_DATE_EPOCH}"; then \
		SOURCE_DATE="@$${SOURCE_DATE_EPOCH}"; \
		DATE=""; \
		for date in gdate date.gnu date; do \
			$${date} -u -d "$${SOURCE_DATE}" >/dev/null 2>&1 && DATE="$${date}"; \
		done; \
		if test -n "$${DATE}"; then \
			LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define  KERNEL_KO_DATE "%b %d %C%y"'; \
			LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define KERNEL_KO_TIME "%T"'; \
			LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define KERNEL_KO_TZ "%z"'; \
			LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define KERNEL_KO_DMI_DATE "%m/%d/%Y"'; \
			LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define KERNEL_KO_BUILD_DATE 0x%Y%m%d'; \
		else \
			return 42; \
		fi; \
	else \
		LC_ALL=C date +'#define KERNEL_KO_DATE "%b %d %C%y"'; \
		LC_ALL=C date +'#define KERNEL_KO_TIME "%T"'; \
		LC_ALL=C date +'#define KERNEL_KO_TZ "%z"'; \
		LC_ALL=C date +'#define KERNEL_KO_DMI_DATE "%m/%d/%Y"'; \
		LC_ALL=C date +'#define KERNEL_KO_BUILD_DATE 0x%Y%m%d'; \
	fi)
endef

define filechk_version_autogenerated.h
	(echo \#define PLAIN_VERSION \"$(KERNEL_KO_RELEASE)\"; \
	echo \#define KERNEL_KO_VERSION \"\" PLAIN_VERSION; \
	echo \#define CC_VERSION_STRING \"$$(LC_ALL=C $(CC) --version | head -n 1)\"; \
	echo \#define LD_VERSION_STRING \"$$(LC_ALL=C $(LD) --version | head -n 1)\"; )
endef

define filechk
	$(Q)set -e;				\
	: '  CHK     $@';		\
	mkdir -p $(dir $@);			\
	$(filechk_$(1)) < $< > $@.tmp;		\
	if [ -r $@ ] && cmp -s $@ $@.tmp; then	\
		rm -f $@.tmp;			\
	else					\
		: '  UPD     $@';	\
		mv -f $@.tmp $@;		\
	fi
endef

2. Makefile

makefile
# 模块名和模块测试APP名称
MODULE_NAME       := printk_prj_info

ARCH              ?= arm
MAKE_PARAM        :=
CURRENT_PATH      := $(shell pwd)
KERNEL_KO_RELEASE  = $(shell git rev-parse --verify --short HEAD)
ifeq ("$(origin V)", "command line")
  KBUILD_VERBOSE = $(V)
endif
ifndef KBUILD_VERBOSE
  KBUILD_VERBOSE = 0
endif

ifeq ($(KBUILD_VERBOSE),1)
  quiet =
  Q =
else
  quiet=quiet_
  Q = @
endif

#=====================================================
INCDIRS 		  := ./ 			   
SRCDIRS			  := ./

INCLUDE			  := $(patsubst %, -I %, $(INCDIRS))
#=====================================================
# NFS 共享目录
TFTP_SERVER       ?= ~/3tftp
NFS_SERVER        ?= ~/4nfs

TFTP_DIR          ?= $(TFTP_SERVER)
ROOTFS_ROOT_DIR   ?= $(NFS_SERVER)/imx6ull_rootfs
#ROOTFS_MODULE_DIR ?= $(ROOTFS_ROOT_DIR)/lib/modules/4.19.71-00007-g51f3cd8ec-dirty
ROOTFS_MODULE_DIR ?= $(ROOTFS_ROOT_DIR)/drivers_demo
#=====================================================
ifeq ($(KERNELRELEASE),)

# 选择可执行文件运行的平台
ifeq ($(ARCH), arm)
KERNELDIR            ?= ~/7Linux/imx6ull-kernel 
MAKE_PARAM           += ARCH=arm
MAKE_PARAM           += CROSS_COMPILE=arm-linux-gnueabihf-
CROSS_COMPILE_PREFIX ?= arm-linux-gnueabihf-
else
KERNELDIR            ?= /lib/modules/$(shell uname -r)/build
CROSS_COMPILE_PREFIX ?=
endif

CC                   := $(CROSS_COMPILE_PREFIX)gcc
LD                   := $(CROSS_COMPILE_PREFIX)ld

include Kbuild.include

srctree              := .
timestamp_h          := timestamp_autogenerated.h
version_h            := version_autogenerated.h

export CC LD srctree

# 编译模块和测试程序
all: $(timestamp_h) $(version_h) modules

modules:
	$(MAKE) $(MAKE_PARAM) -C $(KERNELDIR) M=$(CURRENT_PATH) modules $(INCLUDE)

modules_install:
	$(MAKE) $(MAKE_PARAM) -C $(KERNELDIR) M=$(CURRENT_PATH) modules INSTALL_MOD_PATH=$(ROOTFS_MODULE_DIR) modules_install

$(timestamp_h): $(srctree)/Makefile FORCE
	$(call filechk,timestamp_autogenerated.h)
	
$(version_h): $(srctree)/Makefile FORCE
	$(call filechk,version_autogenerated.h)

# 拷贝相关文件到nfs共享目录
install:
	@sudo cp -v $(MODULE_NAME).ko $(ROOTFS_MODULE_DIR)

uninstall:
	@sudo rm -vf $(ROOTFS_MODULE_DIR)/$(MODULE_NAME).ko 

PHONY += FORCE
FORCE:

PHONY += clean
clean:
	rm -rf *.o *.ko *.o.d
	rm -rf .*.cmd  *.mod.* *.mod modules.order Module.symvers .tmp_versions .cache.mk 
	rm -rf $(timestamp_h)
	rm -rf $(version_h)

.PHONY: $(PHONY)

help:
	@echo "\033[1;32m================================ Help ================================\033[0m"
	@echo "Ubuntu may need to add sudo:"
	@echo "insmod <module_name>.ko   # Load module"
	@echo "rmmod <module_name>       # Uninstall the module"
	@echo "dmesg -C                  # Clear the kernel print information"
	@echo "lsmod                     # Check the kernel modules that have been inserted"
	@echo "dmesg                     # View information printed by the kernel"
	@echo "file <module_name>.ko     # View \".Ko\" file information"
	@echo ""
	@echo "make ARCH=x86_64          # x86_64 platform"
	@echo "make                      # arm platform"
	@echo "\033[1;32m======================================================================\033[0m"

print:
	@echo "KERNELDIR = $(KERNELDIR)"
	@echo "INCLUDE   = $(INCLUDE)"
else
CONFIG_MODULE_SIG = n
obj-m            += $(MODULE_NAME).o
endif

3. printk_prj_info.c

c
#include <linux/kernel.h>
#include <linux/init.h>     /* module_init module_exit */
#include <linux/module.h>   /* MODULE_LICENSE */
#include "./timestamp_autogenerated.h"
#include "./version_autogenerated.h"

// 模块入口函数
static int __init printk_demo_init(void)
{
    printk("*** [%s:%d]Build Time: %s %s, git version:%s ***\n", __FUNCTION__, __LINE__, KERNEL_KO_DATE, KERNEL_KO_TIME, KERNEL_KO_VERSION);
    printk("printk_demo module init!\n");

    printk(KERN_EMERG"KERN_EMERG:%s\r\n", KERN_EMERG);
    printk(KERN_ALERT"KERN_ALERT:%s\r\n", KERN_ALERT);
    printk(KERN_CRIT"KERN_CRIT:%s\r\n", KERN_CRIT);
    printk(KERN_ERR"KERN_ERR:%s\r\n", KERN_ERR);
    printk(KERN_WARNING"KERN_WARNING:%s\r\n", KERN_WARNING);
    printk(KERN_NOTICE"KERN_NOTICE:%s\r\n", KERN_NOTICE);
    printk(KERN_INFO"KERN_INFO:%s\r\n", KERN_INFO);
    printk(KERN_DEBUG"KERN_DEBUG:%s\r\n", KERN_DEBUG);
	return 0;
}

// 模块出口函数
static void __exit printk_demo_exit(void)
{
	printk("printk_demo exit!\n");
}

// 将__init 定义的函数指定为驱动的入口函数
module_init(printk_demo_init);


// 将__exit 定义的函数指定为驱动的出口函数
module_exit(printk_demo_exit);

/* 模块信息(通过 modinfo printk_demo 查看) */
MODULE_LICENSE("GPL");               /* 源码的许可证协议 */
MODULE_AUTHOR("sumu");               /* 字符串常量内容为模块作者说明 */
MODULE_DESCRIPTION("Description");   /* 字符串常量内容为模块功能说明 */
MODULE_ALIAS("module's other name"); /* 字符串常量内容为模块别名 */