懒人版Linux驱动Makefile
1、背景
就是懒。看了很多教程示例,自己也亲手写过,觉得见过的Linux
驱动Makefile
都大同小异,
既不想每次都要在一大片相同之中找不同而浪费精力,也不想重复这种毫无意义的复制粘贴,
于是乎就催生了这样一份通(懒)用(人)版Makefile
。
2、脚本内容
第一手核心内容先上,原理解析以及完整版脚本内容见后面:
STRIP ?= arm-linux-gnueabihf-strip
NDEBUG ?= y
KERNEL_ROOT ?= ${HOME}/src/linux
ifeq (${DRVNAME},)
export DRVNAME := $(basename $(notdir $(shell find ./ -name "*.c" | grep -v '_app\.c$$' | head -n 1)))
ifeq (${DRVNAME},)
$(error DRVNAME not specified and not deductive)
endif
endif
obj-m := ${DRVNAME}.o
ccflags-y += -D__VER__=\"${__VER__}\"
ifeq (${NDEBUG},)
ccflags-y += -O0 -g
endif
APP_NAME ?= $(basename $(shell find ./ -name "*.c" | grep '_app\.c$$' | head -n 1))
APP_OBJS ?= ${APP_NAME}.o
APP_CC ?= arm-linux-gnueabihf-gcc
ifeq (${NDEBUG},)
APP_DEBUG_FLAGS ?= -O0 -g
else
APP_DEBUG_FLAGS ?= -O2 -DNDEBUG
endif
APP_CFLAGS ?= -D_REENTRANT -D__VER__=\"${__VER__}\" -fPIC -Wall \
${APP_DEBUG_FLAGS} ${APP_DEFINES} ${APP_INCLUDES} ${OTHER_APP_CFLAGS}
all: ${DRVNAME}.ko ${APP_NAME}
${DRVNAME}.ko:
make -C ${KERNEL_ROOT} M=`pwd` modules
[ -f $@ ] || mv $$(ls *.ko | head -n 1) $@
[ -z "${NDEBUG}" ] || ${STRIP} -d $@
${APP_NAME}: ${APP_OBJS}
${APP_CC} -o $@ -fPIE $^ ${APP_LDFLAGS}
[ -z "${NDEBUG}" ] || ${STRIP} -s $@
%.o: %.c
${APP_CC} ${APP_CFLAGS} -c -o $@ $<
debug:
make NDEBUG=""
clean:
rm -f ${APP_NAME} ${APP_OBJS}
make -C ${KERNEL_ROOT} M=`pwd` clean
将以上内容保存到一个名为linux_driver.mk
的文件。
3、用法
在详细讲解脚本的原理之前,先讲一下如何使用,以获得一些感性认识, 有助于后面的理解。
3.1 最简单的小测试场景
这种场景最常见,新手入门和老手日常小测试都属于这类,特点是文件少、
代码简单甚至就是玩具代码
(Toy Code
),Hello World
就是一个典型例子。
假设你有很多小测试代码,目录结构如下:
/path/to/test/root/directory
|-- test1
| `-- test1.c
|-- test2
| |-- test2.c
| `-- test2_app.c
. .
. .
. .
`-- testN
`-- testN.c
以上所示的文件其实有一定的规律和命名要求,但现在先不管,
只需知道要为符合这些规律和要求的测试代码写Makefile
是很简单的事即可,
基本不必做太多的改动和定制化。如果是做ARM
嵌入式Linux
驱动的,
甚至不需要写Makefile
,而只需要作一个软链接即可,
以test1
为例(假设linux_driver.mk
和Linux
源码均保存在~/src
目录):
$ ln -s ${HOME}/src/linux_driver.mk /path/to/test1/Makefile
如果是做其他平台的驱动,例如X86
,就需要稍微多一点工作。
可以创建一个${HOME}/src/x86_driver.mk
,并写入以下内容:
STRIP := strip
APP_CC := gcc
include ${HOME}/src/linux_driver.mk
最后再为每个测试创建一个Makefile
软链接,仍以test1
为例:
$ ln -s ${HOME}/src/x86_driver.mk /path/to/test1/Makefile
3.2 用于正式项目的场景
这种场景稍微复杂一些,不能直接套用模板,要做一些定制化, 例如组织多个源文件、修改编译参数、定义应用程序或驱动的版本号等, 但也不难。假设项目目录层次如下:
/path/to/project/source/code/directory
|-- common
| `-- __ver__.mk # 内含软件版本号的定义,这里不必深究
|-- app
|-- kernel # Linux内核源码目录
`-- drivers
|-- driver1
| |-- driver1_main.c
| |-- driver1_utils.c
| |-- driver1_app_main.c
| |-- driver1_app_utils.c
| `-- Makefile
|-- driver2 # 内容与driver1相似
. .
. .
. .
`-- driverN # 内容与driver1相似
先将linux_driver.mk
放入以上common
目录。
再按实际情况修改每个测试的Makefile
,以driver1
为例:
KERNEL_ROOT := ${PWD}/../../kernel
DRVNAME := driver1
${DRVNAME}-objs := driver1_main.o driver1_utils.o
APP_NAME := driver1_app
APP_OBJS := driver1_app_main.o driver1_app_utils.o
# 根据需要再设置其它变量:STRIP、APP_CC、APP_DEFINES、……
include ${PWD}/../../common/__ver__.mk
include ${PWD}/../../common/linux_driver.mk
4、原理解析
- 核心:尽可能地自动推断出待编译的目标和相关资源!
为此,对文件和目录的名称、路径等组织形式要作一定的约束,如下:
- 若
Linux
内核源码目录的路径是${HOME}/src/linux
,则可自动使用, 否则要对KERNEL_ROOT
变量手动赋值以指明路径。 - 若驱动源码文件只有一个且文件名不以
_app.c
结尾, 则可自动推断最终的驱动名称,否则要对DRVNAME
变量手动赋值以指定驱动名。 详见ifeq (${DRVNAME},)
语句块。 - 驱动名称确定之后,
obj-m
变量的值也随之确定。 - 若配备用于演示驱动用法的应用程序,并且其源码文件只有一个、
文件名以
_app.c
结尾,则可自动推断最终的应用程序名称, 否则要对APP_NAME
变量手动赋值以指定应用名称。反之,若不配备应用程序, 则按以上逻辑就会推断出一个空名称,亦即导致了一个空目标的出现, 在编译阶段会自动跳过,无需手动置空。 - 应用程序的中间产物默认为
${APP_NAME}.o
,若是多个源码文件的场景, 需要对APP_OBJS
变量手动赋值。
- 若
- 其他:
- 尽可能地利用
Linux
内核的编译框架,不随便重复造轮子增加复杂度, 重点语句是:make -C ${KERNEL_ROOT} M=`pwd` modules
- 利用
ccflags-y
变量为驱动追加编译参数,例如自定义版本号、 编译时保持调试符号等,以便于程序调试和版本管理。 - 一个
Makefile
同时集成驱动和应用的编译逻辑, 而网上常见的教程通常会分开两个Makefile
,或直接使用命令来编译应用程序, 错失了make
程序可自动检测依赖关系链之中每个节点是否有更新的机制带来的好处。 - 利用
Makefile
自带的模式规则
语法,使得应用程序的.c
转.o
更简洁、通用, 详见%.o: %.c
语句块。 - 增加了
调试/发布
模式的选择逻辑,以便在发布时能剥除调试符号, 既缩小文件的体积,也增加了破解难度。
- 尽可能地利用
5、完整脚本
详见懒编程秘笈项目的makefile/linux_driver.mk
,
内容与本文的脚本无多大差别,主要是参数和注释更详细,且后续若有更新,仅更新GitHub
项目,
本文的脚本内容不会再同步。
此外,由于Markdown
插件的影响,本文的某些水平制表符
(Tab
)可能会转成空格,
导致直接复制本文的脚本内容来使用可能会报错,建议直接使用GitHub
项目的脚本。
6、更新说明
-
2023-07-15:可便捷地进行不同架构的交叉编译,默认编译可在宿主机运行的驱动和应用, 若要交叉编译则需显式指定目标,这与之前的默认编译
ARM
的行为是不同的。 此外,除了前文列举的可定制化的变量有部分的名称有变化之外,还新增了一些变量, 详情可见完整脚本内的注释,或者在运行make
命令时带上V=1
进行查看。 -
2023-07-19:由于这个
Makefile
在实际应用中可能会在不同文件或不同位置导入, 一次完整的编译可能会出现多次的递归make
,make
程序和Linux
内核版本更新时会改动某些逻辑, 因而可能会出现一些奇奇怪怪的问题,要追溯具体原因非常困难,所以只能绕开问题。 已经遇上的有两个问题:一个是如果不在include
这个Makefile
前先显式指定一些变量, 例如obj-m
、ccflags-y
等,这些变量的值就传递不进Linux
内核编译系统(原因未明), 编译系统也就推导不出要编译的目标驱动或缺失预期的参数;另一个是当使用某些较旧(例如4.1.15)的内核源码时, 在编译过程中会抛出参数列表太长
(Argument list too long
)的错误。对于前一个, 只能在自己写上层Makefile
时多写几行;对于后一个,已经添加了一个名为old
的目标, 用户在有需要的时候直接执行make old
即可。