Linux内核模块常见问题

跟踪 dmesg 日志

~# cat /dev/kmsg

printk 输出等级

~ # cat /proc/sys/kernel/printk
4       4       1       7

4 个值分别为:

  1. 控制台日志级别:优先级高于该值的消息将被打印至控制台;
  2. 缺省的消息日志级别:将用该值来打印没有优先级的消息;
  3. 最低的控制台日志级别:控制台日志级别可能被设置的最小值;
  4. 缺省的控制台:控制台日志级别的缺省值。

内核模块签名的问题

使用 insmod 命令加载内核模块时出现报错:

[root@localhost ~]# insmod mbcache.ko
ksign: module signed with unknown public key
- signature keyid: 0fb015c8f72fe172 ver=4
insmod: error inserting 'mbcache.ko': -1 Unknown error 126

此时可以使用 objcopy 命令移除内核模块中的签名:

[root@localhost ~]# objcopy -R .note.module.sig mbcache.ko

移除之后如果内核版本一致没有其它错误就可以加载了。

内核模块安装路径

编译内核时可以使用 INSTALL_MOD_PATH 参数指定安装路径:

[root@localhost ~]# make modules_install INSTALL_MOD_PATH=/media

编译 scripts 程序

交叉编译内核时如果内核开发包 scripts 目录下的 fixdep 等程序非本机架构,将无法进行编译内核模块,可以使用本机编译器重新编译 scripts 程序:

~# make SUBDIRS=scripts

__DATE____TIME__ 编译报错

新版本 gcc 编译包含 __DATE__ 或者 __TIME__ 宏的源程序时,会报下面的错误:

error: macro "__DATE__" might prevent reproducible builds
error: macro "__TIME__" might prevent reproducible builds

解决办法为增加 EXTRA_CFLAGS 参数为:-Wno-error=date-time(报警但不报错) 或者 -Wno-date-time(不报警)。

inline 函数编译报错

程序里包含 inline 函数编译时可能遇到这种错误:

warning: inline function ‘xxx’ declared but never defined

解决办法为增加 EXTRA_CFLAGS 参数 -fgnu89-inline

acs_map 编译报错

使用 devtoolset 编译 kernel 在 make menuconfig 时可能报错:

ld: scripts/kconfig/lxdialog/checklist.o: undefined reference to symbol 'acs_map'

为命令增加参数:

make HOST_LOADLIBES="-lcurses -ltinfo" menuconfig

编译内核工具

例如交叉编译内核中的 fixdepmodpost 工具:

~# make -C /usr/src/kernels/XXX M=scripts/basic CROSS_COMPILE=x86_64-linux-
~# make -C /usr/src/kernels/XXX M=scripts/mod CROSS_COMPILE=x86_64-linux-

交叉编译 objtool 等 tools 中的工具:

~# make -C tools/objtool CROSS_COMPILE=x86_64-linux- HOSTCC=x86_64-linux-gcc HOSTLD=x86_64-linux-ld HOSTAR=x86_64-linux-ar

%p 打印指针显示 ptrval 的问题

  • 换成 %pK 进行打印输出,是否输出由 kptr_restrict sysctl 控制;
  • 换成 %px 可以强制打印输出。

内核版本号附带加号

内核版本号自动附加加号一般由于修改了 git 版本库中的内核源代码,可以通过在内核源代码根目录增加 .scmversion 隐藏文件,或者通过 make 命令避免:

make LOCALVERSION=

覆盖内核包含路径

可以修改模块的 Makefile:

PRE_CFLAGS = -I/usr/src/ofa_kernel/default/include -include linux/compat-2.6.h
LINUXINCLUDE := $(PRE_CFLAGS) $(LINUXINCLUDE)

通过修改 LINUXINCLUDE 实现某些模块(例如 OFED)使用第三方的包含路径进行编译。

依赖其它模块的符号

如果编译某个外部模块需要另一个外部模块,为了防止找不到符号的问题,有两种办法:

  1. 将另一个模块的 Module.symvers 拷贝到当前模块路径,进行编译;
  2. 修改当前模块的 Makefile,增加 KBUILD_EXTRA_SYMBOLS = /path/to/other/module/Module.symvers,进行编译。

显示模块信息

除了 modinfo 命令,也可以使用 objdump 获取:

~# objdump -s --section=.modinfo nvme-core.ko

也可以获取符号版本信息:

~# objdump -h --section=__versions nvme-core.ko
Sections:
Idx Name          Size      VMA               LMA               File off  Algn
 13 __versions    00001640  0000000000000000  0000000000000000  0000d480  2**5
                  CONTENTS, ALLOC, LOAD, READONLY, DATA

下载目录的某次 commit 内容

如果不想完整 clone kernel 代码,又需要下载源码某个目录某次 commit 内容,新增 get-linux-kernel-commit.sh 脚本:

#!/bin/sh
K_ID="$1"
K_DIR="$2"
K_GIT="$3"
if [ "x$K_GIT" = "x" -o "x$K_GIT" = "x-" ]; then
    K_GIT="https://git.kernel.org"
fi
K_PATH="$4"
if [ "x$K_PATH" = "x" -o "x$K_PATH" = "x-" ]; then
    K_PATH="/pub/scm/linux/kernel/git/torvalds/linux.git"
fi
K_PATH="${K_PATH}/plain/"
K_SUB=$5

get_commit() {
    wget --no-check-certificate -q -O - "${K_GIT}${K_PATH}$1?id=$K_ID" | grep -v '>\.\./</a>' | sed -n '/\/plain\//{s/^.*href='\''//;s/'\''.*$//;p}' | while read T_PATH; do
        C_PATH="${T_PATH%*/?id=*}"
        if [ $C_PATH != $T_PATH ]; then
            if [ "x$K_SUB" != "x0" ]; then
                C_PATH="${C_PATH##*/}"
                mkdir $C_PATH
                OPWD=`pwd`
                cd $C_PATH
                get_commit $1/$C_PATH
                cd $OPWD
            fi
        else
            wget --no-check-certificate --content-disposition "${K_GIT}${T_PATH}"
        fi
    done
}

get_commit $K_DIR

使用方法(第一个参数为 commit id,第二个参数为源代码目录路径,第三个和第四个参数为 git 版本库地址,默认为 Linux kernel git,可以使用其它版本库地址,第五个可选参数指定 0 表示不递归下载下面的子目录):

get-linux-kernel-commit.sh e2f6ad4624dfbde3a6c42c0cfbfc5553d93c3cae fs/xfs

module_layout

获取当前内核的 module_layout

可以通过内核的 Module.symvers 文件获取:

~# grep module_layout ~/linux/Module.symvers
0x623133ef      module_layout   vmlinux EXPORT_SYMBOL

获取内核模块的 module_layout

使用 objdump 查看内核模块的 __versions 段的数据,一般 module_layout 符号是第一个,对应下面的第一个 4 字节数据:

~# objdump -s --section=__versions nvme-core.ko | head

nvme-core.ko:     file format elf64-little

Contents of section __versions:
 0000 0b0b58e6 00000000 6d6f6475 6c655f6c  ..X.....module_l
 0010 61796f75 74000000 00000000 00000000  ayout...........
 0020 00000000 00000000 00000000 00000000  ................
 0030 00000000 00000000 00000000 00000000  ................
 0040 b6a28784 00000000 666c7573 685f776f  ........flush_wo
 0050 726b0000 00000000 00000000 00000000  rk..............

module_layout 符号不一致的问题

如果内核模块对应的内核版本相同,但使用的头文件不同,加载时可能会出现 disagrees about version of symbol module_layout 报错,如果需要强制加载,可以修改模块文件的 module_layout 符号版本:

root@server:~# modprobe --dump-modversions new.ko | grep module_layout
0x022a8bbb      module_layout
root@server:~# modprobe --dump-modversions old.ko | grep module_layout
0xe89184a2      module_layout

内核模块的符号版本信息位于 ELF 的 __versions 段中,可以使用 objdump 命令显示所在位置:

[root@centos7-dev centos]# objdump -h new.ko

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
 12 __versions    00000b00  0000000000000000  0000000000000000  00004978  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA

例如从上面例子中的 0x4978 位置就可以找到符号版本信息,module_layout 符号一般是第一个,使用 16 进制编辑器改为与当前内核符号的版本就可以加载了。