为Raspberry Pi 2编译内核模块

2015-04-21更新:原始的rpi-source项目已经由PeterOGB 接手维护,所以无须再用我下文中提到的我改过的rpi-source脚本,直接用原始的就可以了。其它文中提到的背景知识都仍然有效。

即把第一个命令改为:
$ wget https://raw.githubusercontent.com/notro/rpi-source/master/rpi-source
&& chmod a+x rpi-source

2015-07-29更新:Raspbian的内核版本已经升级到4.x,rpi-source还不能正确处理,需要进行以下额外的工作:

1. rpi-source需要获取的/proc/config.gz默认不存在了,需要额外加载模块来实现:

$ sudo modprobe configs

2. rpi-source在4.x内核下无法正确检测gcc版本,运行rpi-source时请加–skip-gcc选项。


 

在Linux下使用“360随身WiFi 2”》一文的留言区中,曾经有人问过,为什么编译出来的模块insmod/modprobe时报“Exec format error”,我不假思索的回复,请他检查编译模块时用的内核头文件与实际运行的内核是否完全匹配。这个答案倒也不算错,不过其实并没有解决问题,因为遇到的这个问题的人一般都已经用了“正确”的方式去编译他的模块,就算再重新做几遍,还是会遇到一样的问题。

最近我给Raspberry Pi 2编译内核模块时,遇到了一样的问题,花了很多时间才真正解决,在这里总结一下。以下描述的方法和内容,对Raspberry Pi (A/A+/B/B+)和Raspberry Pi 2都适用。

准备编译模块需要的内核树的方法(适用于Raspbian):

1. 下载我改过的rpi-source脚本
$ wget https://raw.githubusercontent.com/lifanxi/rpi-source/master/rpi-source
&& chmod a+x rpi-source

2. 运行rpi-source
$ ./rpi-source

3. 好了,可以进入模块源代码的目录进行模块编译了。

疑难排解:

1. rpi-source报gcc版本不一致

截止2015-03-12,Raspbian最新的内核是用gcc 4.8编译的(可以查看/proc/version确认),而Raspbian中自带的gcc是4.6的,需要升级到4.8。因为4.8的gcc已经backport了,所以可以直接sudo apt-get install gcc-4.8 g++-4.8,然后用update-alternatives设置优先级即可[1]。

2. 如果用rpi-source –skip-gcc忽略gcc版本检查,并强行用4.6的gcc会编译模块怎么样?

我的试验结果是模块可以编译,但在加载模块时会造成kernel oops,然后再用insmod/modprobe/rmmod/lsmod等命令时会挂住,只能重启解决。如果你编的模块是会自动加载的,重启前先把它删掉,不然启动时就会挂住。

3. rpi-source无法正常下载内核代码或Modules.symvers文件

有可能是你的内核版本太老,rpi-source只支持Raspberry Pi 3.10.37以上的内核。对于Raspberry Pi 2,它只支持3.18.6以上的内核。解决办法是先运行sudo rpi-update更新内核和固件,更新后请重启系统,然后再重新运行rpi-source。

4. 编译模块时报找不到arch/armv6l或arch/armv7l目录

尝试在make命令前加ARCH=arm参数,或尝试把/lib/modules/`uname -r`/build/arch中的arm软链为armv6l或armv7l后再编译。

背景知识:

1. Raspbian的内核包

不要按照使用Debian的习惯去找什么linux-image、linux-source之类的包,Raspbian的内核包是raspberrypi-bootloader,里面包含了内核、模块和一些启动文件,但没有Module.symvers和头文件。

2. rpi-update是啥

rpi-update是Raspbian内置的更新内核和相关固件的脚本,它的逻辑是去https://github.com/Hexxeh/rpi-firmware这个仓库下载最新的内核和固件,替换现有的版本。更新完成后会更新/boot/.firmware_revision,记下最新版本对应的Git Hash,以后rpi-update或rpi-source都会根据这个Hash去GitHub找对应文件。

3. Raspberry Pi的官方内核去哪里找

http://github.com/raspberrypi,里面的linux对应内核源代码,firmware是编译好的内核和相关文件。而rpi-update用的https://github.com/Hexxeh/rpi-firmware其实是firmware中部分文件的一个镜像,分出一个镜像仓库可以让rpi-update脚本的实现变得比较简单[2]。

4. rpi-source做了些啥

根据rpi-update记录在/boot/.firmware_revision中的内核版本Git Hash(如果没有用rpi-update更新过内核,就从raspberrypi-bootloaderq包的changlog中解析出Hash),去raspberrypi/linux仓库中获取对应的源代码,把/lib/modules/`uname -r`/build和/lib/modules/`uname -r`/source对应的软链建好,从/proc/config.gz获取当前内核配置,去raspberrypi/firmware仓库中获取对应的Modules.symvers跟内核代码放在一起,然后make modules_prepare准备好编译模块所需要的内核树。

5. 你改的rpi-source改了些啥

rpi-source的作者已经宣布不再维护这个脚本,并且这个脚本不支持Raspberry Pi 2,所以我在GitHub上Fork了一份,做了以下改动:

  • 修改了脚本自动更新URL到我Fork出来的版本;
  • 检查/proc/cpuinfo,判断当前硬件是Raspberry Pi还是Raspberry Pi 2;
  • 可以通过-b参数强行指定Raspberry Pi的硬件版本;
  • 根据不同的硬件,下载不同版本的Modules.symvers;
  • 如果用参数指定了要求用默认配置来配置内核树,则对不同硬件版本的Raspberry Pi调用不同的命令[3]。

6. Raspberry Pi和Raspberry Pi 2的内核有啥区别

Raspberry Pi 2的SOC是BCM2709,基于ARM 7(armv7l),而一代是BCM2708,ARM 6(armv6l),所以二代的内核中用了一些armv7l中特有特性。目前在打包的时候两个版本内核文件是打包在一起的,只是用后缀7或v7来区别,启动的时候会按实际硬件选择。

7. Module.symvers是干嘛用的?

一句话讲不清,有兴趣请参考[4]。总之,没有Module.symvers或用错了Module.symvers都可能会造成你加载模块时报Exec format error。如果你遇到了这样的情况,请确认rpi-source的执行过程中有没有失败的步骤。armv7l和armv6l版本的内核用的Module.symvers是不通用的,在raspberrypi/firmware中分别命名为Module.symvers和Modules7.symvers,但放到内核树中使用时需要命名为Module.symvers,如果是你自己准备内核树,务必要小心,我自己在这个问题上犯了错误,浪费了很多时间。当然,如果用我改过的rpi-source,那它已经帮你搞定了这件事。

8. 我用了rpi-update和rpi-source后编出来的模块还是无法加载。

目前我用本文描述的方法编译了过天猫魔盘(rtl8192eu)、360随身WiFi 2(mt7601u)这两种无线网卡的驱动,都工作正常。如果你遇到了别的问题,不妨在这里留言,可以一起讨论一下。

另外,终级大法一定是重新完整的编译整个内核,不过如果你想在Raspberry Pi上完成这个工作,那必须等有充分的耐心。所以,最好是在PC上进行交叉编译[3]。

[1] https://github.com/notro/rpi-source/wiki

[2] https://github.com/Hexxeh/rpi-firmware/blob/master/README.md

[3] https://github.com/raspberrypi/documentation/blob/master/linux/kernel/building.md

[4] http://www.ibm.com/developerworks/cn/linux/l-cn-kernelmodules/