Linux内核源码顶层 Makefile分析并单独编译调试内核自带的驱动
本文约900字,一起走读Linux内核顶层Makefile,并实现单个编译调试内核自动的驱动。
关注公众号, 即可获得与Linux相关的电子书籍以及常用开发工具,文末有文档清单。
本文基于内核版本:6.6.123 ubuntu版本:22.04。
走读内核驱动源码,为了更熟悉,我们会去尝试修改源码增加调试打印等,但如果不做特殊处理,就得make modules整体编译,如果clean过再整体编译需要的时间比较久,所以单独拎出来修改编译安装卸载驱动是很有必要的。
先来走读一下内核的顶层Makefile是如何来编译构建的。
一 内核版本及最低版本检查
Makefile的最前面是版本号相关内容,如果需要手动更改内核版本号,比如应付安全ebma这类扫描工具,需要更新内核才能减少扫出来的错误数量而不实际更新新版本内核源码,修改最前面的三个数字6,6,123就可以了,记得要全部重新编译包括驱动,否则扫描软件依然能扫描出旧的内核版本。
# SPDX-License-Identifier: GPL-2.0# 内核主版本号VERSION = 6# 次版本号PATCHLEVEL = 6# 修订版本号SUBLEVEL = 123# 额外版本后缀(如 -rc1、-stable)EXTRAVERSION =# 内核版本名称(仅标识)NAME = Pinguïn Aangedreven
# 检查 GNU Make 版本 >= 3.82ifeq ($(filter undefine,$(.FEATURES)),)$(error GNU Make >= 3.82 is required. Your Make version is $(MAKE_VERSION))endif# 禁止以 __ 开头的目标(内部使用)$(if $(filter __%, $(MAKECMDGOALS)), \$(error targets prefixed with '__' are only for internal use))
二 默认目标 & 构建基本变量
# 默认目标:__allPHONY := __all__all:# 记录当前 Makefile 路径this-makefile := $(lastword$(MAKEFILE_LIST))# 源码根目录绝对路径export abs_srctree := $(realpath $(dir $(this-makefile)))# 编译输出目录(默认当前目录)export abs_objtree := $(CURDIR)
三 关闭内置规则、环境清理
ifneq ($(sub_make_done),1)# 关闭 make 内置规则 + 内置变量(提速+防坑)MAKEFLAGS += -rR# 固定字符集,避免语言干扰unexport LC_ALLLC_COLLATE=CLC_NUMERIC=Cexport LC_COLLATE LC_NUMERIC# 关闭 grep 环境变量干扰unexport GREP_OPTIONS
四 静默输出 / Verbose 控制(make V=1)
# V=1:显示完整编译命令# V=2:显示为什么重新构建ifeq ("$(origin V)", "command line")KBUILD_VERBOSE = $(V)endifquiet = quiet_Q = @# 如果 V=1,不静默,显示完整命令ifneq ($(findstring 1, $(KBUILD_VERBOSE)),)quiet =Q =endifexport quiet Q KBUILD_VERBOSE
五 外部模块编译支持 M=
当将内核部分需要的驱动配置为M时关联的编译设置:
# M=dir:编译外部模块ifeq ("$(origin M)", "command line")KBUILD_EXTMOD := $(M)endif# 只允许一个 M= 目录$(if $(word 2, $(KBUILD_EXTMOD)), \$(error building multiple external modules is not supported))# 移除路径末尾斜杠ifneq ($(filter %/, $(KBUILD_EXTMOD)),)KBUILD_EXTMOD := $(shell dirname $(KBUILD_EXTMOD).)endifexport KBUILD_EXTMOD
六 out-of-tree 编译 O=
配置编译内核输出的路径:
# O=dir:指定输出目录ifeq ("$(origin O)", "command line")KBUILD_OUTPUT := $(O)endif# 创建输出目录并计算绝对路径ifneq ($(KBUILD_OUTPUT),)abs_objtree := $(shell mkdir -p $(KBUILD_OUTPUT) && cd $(KBUILD_OUTPUT) && pwd)endif
七 二次 Make 递归(解决目录、版本问题)
# 需要递归一次 makeneed-sub-make := 1export sub_make_done := 1endif # sub_make_done# 进入最终构建目录,递归执行真正的构建__sub-make:$(Q)$(MAKE) $(no-print-directory) -C $(abs_objtree) \-f $(abs_srctree)/Makefile $(MAKECMDGOALS)
八 源码树 & 输出树区分
# 源码目录srctree := .# 输出目录objtree := .VPATH := $(srctree)# 输出目录 != 源码目录 → 外部构建ifneq ($(abs_srctree),$(abs_objtree))building_out_of_srctree := 1endifexport building_out_of_srctree srctree objtree VPATH
九 目标分类:是否需要 .config/ 编译器
# clean / help / tags 等不需要 .configno-dot-config-targets := $(clean-targets) \cscope gtags TAGS tags help%# 安装目标不需要编译器no-compiler-targets := $(no-dot-config-targets) install# 单文件目标:make file.osingle-targets := %.a %.i %.ko %.lds %.lst %.mod %.o
十 混合目标处理(make clean all)
# 如果同时传入 clean + all 等混合目标# 循环逐个执行__build_one_by_one:$(Q)set -e; \for i in $(MAKECMDGOALS); do \$(MAKE) -f $(srctree)/Makefile $$i; \done
十一 工具链定义(CC、LD、AR、RUSTC 等)
# 架构ARCH ?= $(SUBARCH)# 交叉编译前缀CROSS_COMPILE ?=# C 编译器CC = $(CROSS_COMPILE)gcc# 链接器LD = $(CROSS_COMPILE)ld# 归档AR = $(CROSS_COMPILE)ar# Rust 编译器RUSTC = rustc
十二 核心编译标志 CFLAGS / CPPFLAGS
# 内核必须的宏KBUILD_CPPFLAGS := -D__KERNEL__# 内核 C 标准KBUILD_CFLAGS := -std=gnu11# 无符号 char、不重叠、无 PIEKBUILD_CFLAGS += -funsigned-charKBUILD_CFLAGS += -fno-strict-aliasingKBUILD_CFLAGS += -fno-PIE
十三 模块编译标志(重点:我们单独编译 ttynull 用的就是这个)
# 模块专用:定义 MODULE 宏KBUILD_AFLAGS_MODULE := -DMODULEKBUILD_CFLAGS_MODULE := -DMODULEKBUILD_RUSTFLAGS_MODULE := --cfg MODULE
十四 最终构建入口
# 内核默认目标:vmlinuxall: vmlinux# 外部模块(M=)目标:modulesifeq ($(KBUILD_EXTMOD),)__all: allelse__all: modulesendif
说明: vmlinux 是 Linux 内核的「原始形态」,它不直接用于系统启动,却是内核开发、调试、性能优化和模块编译的核心基础。
十五 模块编译关键流程
# 当定义 KBUILD_EXTMOD(即 M=xxx)# 只编译模块,不编译内核KBUILD_BUILTIN :=KBUILD_MODULES := 1# 目标:modules → modpost → .komodules: modpost$(Q)$(MAKE) -f $(srctree)/scripts/Makefile.modfinal
说明:
模块编译完整链路:
[1].make -C kernel M=dir modules
[2].顶层 Makefile 识别 M= → KBUILD_EXTMOD=dir
[3].只走模块编译分支
[4].调用 scripts/Makefile.modpost
[5].检查 MODULE_LICENSE(如果驱动源码最后没有增加MODULE_LICENSE(“GPL v2”);这句话就会编译报错)
[6].链接生成 .ko
十六 clean 清理逻辑
clean:# 清理 .o .ko .mod.c .cmd 等@find $(or $(KBUILD_EXTMOD), .) \\( -name '*.[aios]' -o -name '*.ko' -o -name '.*.cmd' \) | xargs rm -rf
关键点总结:
[1].顶层 Makefile 不直接编译代码,只做【全局调度】
[2].真正编译逻辑在 scripts/Makefile.* 和子目录 Makefile
[3].外部模块 M= 是单独编译驱动的核心机制
[4].modpost 会强制检查 MODULE_LICENSE,没有就报错
我们编译 ttynull.ko 走的就是:
顶层 Makefile → scripts/Makefile.modpost → 链接生成 .ko
单个驱动独立编译方法:
现在如果我们需要调试驱动,比如ttynull.c,又想只编译这一个驱动,那么我们将驱动源码拷贝出去以免破坏内核源码,然后再编写一个Makefile文件即可 。
PWD := $(shell pwd)# 指向内核源码目录KDIR ?= ../../linux-6.6.123obj-m += ttynull.oall:$(MAKE) -C $(KDIR) M=$(PWD) modulesclean:$(MAKE) -C $(KDIR) M=$(PWD) clean


以上为全文内容。
这里是女程序员的笔记本
15年+嵌入式软件工程师兼二胎宝妈
分享读书心得、工作经验,自我成长和生活方式。
希望我的文字能对你有所帮助
夜雨聆风
