环境搭建完后,我们就正式进入主题了。现在我们需要配置kernel源码,编译,并用qemu运行我们自己编译的kernel。这样我们就能够对我们的kernel进行测试,并做出对应的修改。
进入kernel源码目录,我们需要找最新的kernel稳定版本。在写这篇文章的时候,最新的稳定版本是3.10.10。我们可以通过git切换到3.10.10。由于我们编译的内核需要运行在ARM上,所以我们应该到arch/arm/configs下找到对应我们设备的kernel配置文件。但是我们没有实际意义上的设备,而是用qemu模拟的设备,所以我们应该选择qemu能够模拟的设备的配置文件。这里我们选择常用的versatile_defconfig。
对应的命令如下:
cd ~/armsource/kernel
# checkout a tag and create a branch
git checkout v3.10.10 -b linux-3.10.10# create .config file
make versatile_defconfig ARCH=arm
配置完了,我们就可以编译了。编译的时候,我们可以用多个线程来加速编译,具体用多少个就要看你主机的配置了。这里我们用12个线程编译,命令如下:
make -j12 ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabi-
注意,如果交叉编译环境没有配置好,这个地方会提示找不到对应的gcc编译器。这里-j12是指定编译线程为12个,ARCH是指定目标架构为arm,所用的交叉编译器arm-none-linux-gnueabi-。
OK,kernel已经编译好了,那么我们需要用qemu把它跑起来。关于qemu的具体使用,请看qemu的官方文档[3],这里直接给出命令:
qemu-system-arm -M versatilepb -kernel arch/arm/boot/zImage -nographic
这里-M是指定模拟的具体设备型号,versatile系列的pb版本,-kernel指定的是对应的内核,-nographic是把qemu输出直接导向到当前终端。
好,命令成功执行了。但是,好像没有任何有效输出。我们通过C-a x来退出qemu。编译的kernel好像不怎么好使,配置文件肯定有问题。打开.config配置文件,发现传递给kernel的参数没有指定console,难怪没有输出。我们定位到CMDLINE,并加入console参数:
CONFIG_CMDLINE="console=ttyAMA0 root=/dev/ram0"
保存.config,重新编译kernel,并用qemu加载。现在终于有输出了。如果不出意外,kernel应该会停在找不到根文件系统,并跳出一个panic。为什么会找不到根文件系统?因为我们压根就没有给它传递过,当然找不到。
那现在是不是应该制作我们自己的根文件系统了。先别急,为了让后面的路好走一点,我们这里还需对内核进行一些配置。首先,我们需要用ARM EABI去编译kernel,这样我们才能让kernel运行我们交叉编译的用户态程序,因为我们所有的程序都是用gnueabi的编译器编译的。具体可以看wikipedia相关页面[4],你也可以简单的理解为嵌入式的ABI。其次,我们需要把对kernel module的支持去掉,这样可以把相关的驱动都编译到一个文件里,方便我们之后的加载。
当然,你可以使能kernel的debug选项,这样就可以调试内核了,并打印很多调试信息。这里就不再说了,如果感兴趣,可以看我之前写的关于kernel调试的文章[5]。
总结起来,这一次我们对.config做了如下修改:
# CONFIG_MODULES is not setCONFIG_AEABI=y
CONFIG_OABI_COMPAT=y
CONFIG_PRINTK_TIME=y
CONFIG_EARLY_PRINTK=y
CONFIG_CMDLINE="earlyprintk console=ttyAMA0 root=/dev/ram0"
二、通过busybox制作initramfs镜像
如果你注意到了之前传递给kernel的参数,你会发现有一个root=/dev/ram0的参数。没错,这就是给kernel指定的根文件系统,kernel检查到这个参数的时候,会到指定的地方加载根文件系统,并执行其中的init程序。这样就不会出现刚才那种情况,找不到根文件系统了。
我们的目标就是让kernel挂载我们的ramfs根文件系统,并且在执行init程序的时候,调用busybox中的一个shell,这样我们就有一个可用的shell来和系统进行交互了。
整个ramfs中的核心就是一个busybox可执行文件。busybox就像是一把瑞士军刀,可以把很多linux下的命令(比如:cp, rm, whoami等)全部集成到一个可执行文件[6]。这为制作嵌入式根文件系统提供了很大的便利,开发者不用单独编译每一个要支持的命令,还不用考虑库的依赖关系。基本上每一个制作嵌入式系统的开发者的首选就是busybox。
busybox也是采用Kconfig来管理配置选项,所以配置和编译busybox和kernel没有多大区别。busybox很灵活,你可以自由取舍你想要支持的命令,并且还可以添加你自己写的程序。在编译busybox的时候,为了简单省事,我们这里采用静态编译,这样就不用为busybox准备其他libc,ld等依赖库了。
具体命令如下:
cd ~/armsource/busybox
# using stable version 1.21
git checkout origin/1_21_stable -b busybox-1.21# using default configure
make defconfig ARCH=arm
# compile busybox in static
make menuconfig
make -j12 ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabi-
编译完后,我们就得到一个busybox静态链接的文件。
接下来,我们需要一个init程序。这个程序将是kernel执行的第一个用户态的程序,我们需要它来产生一个可交互的shell。在桌面级别的linux发行版本,使用的init程序一般是System V init(传统的init),upstart(ubuntu),systemd(fedora)等。busybox也带有一个init程序,但是我们想自己写一个。既然自己写,那有两种实现方式,用C和libc实现,或者写一个shell脚本。
为了简单,这里选择后者,具体脚本如下:
#!/bin/shechoecho"###########################################################"echo"## THis is a init script for initrd/initramfs ##"echo"## Author: wengpingbo@gmail.com ##"echo"## Date: 2013/08/17 16:27:34 CST ##"echo"###########################################################"echo
PATH="/bin:/sbin:/usr/bin:/usr/sbin"if [ ! -f"/bin/busybox" ];thenecho"cat not find busybox in /bin dir, exit"exit1fi
BUSYBOX="/bin/busybox"echo"build root filesystem..."$BUSYBOX --install -sif [ ! -d /proc ];thenecho"/proc dir not exist, create it..."$BUSYBOX mkdir /proc
fiecho"mount proc fs..."$BUSYBOX mount -t proc proc /proc
if [ ! -d /dev ];thenecho"/dev dir not exist, create it..."$BUSYBOX mkdir /dev
fi# echo "mount tmpfs in /dev..."# $BUSYBOX mount -t tmpfs dev /dev$BUSYBOX mkdir -p /dev/pts
echo"mount devpts..."$BUSYBOX mount -t devpts devpts /dev/pts
if [ ! -d /sys ];thenecho"/sys dir not exist, create it..."$BUSYBOX mkdir /sys
fiecho"mount sys fs..."$BUSYBOX mount -t sysfs sys /sys
echo"/sbin/mdev" > /proc/sys/kernel/hotplug
echo"populate the dev dir..."$BUSYBOX mdev -secho"drop to shell..."$BUSYBOX sh
exit0
我们把这个脚本保存在~/armsource目录下。在这个脚本中,我们通过busybox –install -s来构建基本文件系统,挂载相应的虚拟文件系统,然后就调用busybox自带的shell。
现在我们已经编译好了busybox,并准备好了相应的init脚本。我们需要考虑根文件系统的目录结构了。kenel支持很多种文件系统,比如:ext4, ext3, ext2, cramfs, nfs, jffs2, reiserfs等,还包括一些伪文件系统: sysfs, proc, ramfs等。而在kernel初始化完成后,会尝试挂载一个它所支持的根文件系统。根文件系统的目录结构标准是FHS,由一些kernel开发者制定,感兴趣的可以看wikipedia相关页面[7]。
由于我们要制作一个很简单的ramfs,其中只有一个busybox可执行文件,所以我们没必要过多的考虑什么标准。只需一些必须的目录结构就OK。这里,我们使用的目录结构如下:
├── bin
│ ├── busybox
│ └── sh ->busybox
├── dev
│ └── console
├── etc
│ └── init.d
│ └── rcS
├── init
├── sbin
└── usr
├── bin
└── sbin
你可以通过如下命令来创建这个文件系统:
cd ~/armsource/ramfs
mkdir -pv bin dev etc/init.d sbin user/{bin,sbin}
cp ~/armsource/busybox/busybox bin/
ln -s busybox bin/sh
mknod -m 644 dev/console c 51cp ~/armsource/init .
touch etc/init.d/rcS
chmod +x bin/busybox etc/init.d/rcS init
现在我们有了基本的initramfs,万事具备了,就差点东风了。这个东风就是怎样制作intramfs镜像,并让kernel加载它。
在kernel文档中,对initramfs和initrd有详细的说明[8][9]。initramfs其实就是一个用gzip压缩的cpio文件。我们可以把initramfs直接集成到kernel里,也可以单独加载initramfs。在kernel源码的scripts目录下,有一个gen_initramfs_list.sh脚本,专门是用来生成initramfs镜像和initramfs list文件。你可以通过如下方式自动生成initramfs镜像:
sh scripts/gen_initramfs_list.sh -o ramfs.gz ~/armsource/ramfs
然后修改kernel的.config配置文件来包含这个文件:
CONFIG_INITRAMFS_SOURCE="ramfs.gz"
重新编译后,kernel就自动集成了你制作的ramfs.gz,并会在初始化完成后,加载这个根文件系统,并产生一个shell。
你也可以用gen_initramfs_list.sh脚本生成一个列表文件,然后CONFIG_INITRAMFS_SOURCE中指定这个列表文件。也可以把你做的根文件系统自动集成到kernel里面。命令如下:
sh scripts/gen_initramfs_list.sh ~/armsource/ramfs > initramfs_list
对应的内核配置:CONFIG_INITRAMFS_SOURCE=”initramfs_list”
但是这里并不打算这么做,我们自己手动制作initramfs镜像,然后外部加载。命令如下:
cd ~/armsource/ramfs
find . | cpio -o -H newc | gzip -9 > ramfs.gz
选项-H是用来指定生成的格式。
手动生成ramfs.gz后,我们就可以通过qemu来加载了,命令如下:
qemu-system-arm -M versatilepb -kernel arch/arm/boot/zImage -nographic -initrd ramfs.gz
现在我们的系统起来了,并且正确执行了我们自己写的脚本,进入了shell。我们可以在里面执行基本常用的命令。是不是有点小兴奋。
三、配置物理文件系统,切换根文件系统
不是已经配置了根文件系统,并加载了,为什么还需要切换呢?可能你还沉浸在刚才的小兴奋里,但是,很不幸的告诉你。现在制作的小linux系统还不是一个完全的系统,因为没有完成基本的初始化,尽管看上去好像已经完成了。
在linux中initramfs和initrd只是一个用于系统初始化的小型文件系统,通常用来加载一些第三方的驱动。为什么要通过这种方式来加载驱动呢?因为由于版权协议的关系,如果要把驱动放到kernenl里,意味着你必须要开放源代码。但是有些时候,一些商业公司不想开源自己的驱动,那它就可以把驱动放到initramfs或者initrd。这样既不违背kernel版权协议,又达到不开源的目的。也就是说在正常的linux发行版本中,kernel初始化完成后,会先挂载initramfs/initrd,来加载其他驱动,并做一些初始化设置。然后才会挂载真真的根文件系统,通过一个switch_root来切换根文件系统,执行第二个init程序,加载各种用户程序。在这中间,linux kernel跳了两下。
既然他们跳了两下,那我们也跳两下。第一下已经跳了,现在的目标是制作物理文件系统,并修改initramfs中的init脚本,来挂载我们物理文件系统,并切换root文件系统,执行对应的init。
为了省事,我们直接把原先的initramfs文件系统复制一份,当作物理根文件系统。由于是模拟,所以我们直接利用dd来生成一个磁盘镜像。具体命令如下:
dd if=/dev/zero of=~/armsource/hda.img bs=1 count=10M
mkfs -t ext2 hda.img
mount hda.img /mnt
cp -r ~/armsource/ramfs/* /mnt
umount /mnt
这样hda.img就是我们制作的物理根文件系统,ext2格式。现在我们需要修改原先在initramfs中的init脚本,让其通过busybox的switch_root功能切换根文件系统。这里需要注意的是,在切换根文件系统时,不能直接调用busybox的switch_root,而是需要通过exec来调用。这样才能让最终的init进程pid为1。
修改后的init脚本如下:
#!/bin/shechoecho"###########################################################"echo"## THis is a init script for sd ext2 filesystem ##"echo"## Author: wengpingbo@gmail.com ##"echo"## Date: 2013/08/17 16:27:34 CST ##"echo"###########################################################"echo
PATH="/bin:/sbin:/usr/bin:/usr/sbin"if [ ! -f"/bin/busybox" ];thenecho"cat not find busybox in /bin dir, exit"exit1fi
BUSYBOX="/bin/busybox"echo"build root filesystem..."$BUSYBOX --install -sif [ ! -d /proc ];thenecho"/proc dir not exist, create it..."$BUSYBOX mkdir /proc
fiecho"mount proc fs..."$BUSYBOX mount -t proc proc /proc
if [ ! -d /dev ];thenecho"/dev dir not exist, create it..."$BUSYBOX mkdir /dev
fi# echo "mount tmpfs in /dev..."# $BUSYBOX mount -t tmpfs dev /dev$BUSYBOX mkdir -p /dev/pts
echo"mount devpts..."$BUSYBOX mount -t devpts devpts /dev/pts
if [ ! -d /sys ];thenecho"/sys dir not exist, create it..."$BUSYBOX mkdir /sys
fiecho"mount sys fs..."$BUSYBOX mount -t sysfs sys /sys
echo"/sbin/mdev" > /proc/sys/kernel/hotplug
echo"populate the dev dir..."$BUSYBOX mdev -secho"dev filesystem is ok now, log all in kernel kmsg" >> /dev/kmsg
echo"you can add some third part driver in this phase..." >> /dev/kmsg
echo"begin switch root directory to sd card" >> /dev/kmsg
$BUSYBOX mkdir /newroot
if [ ! -b "/dev/mmcblk0" ];thenecho"can not find /dev/mmcblk0, please make sure the sd
card is attached correctly!" >> /dev/kmsg
echo"drop to shell" >> /dev/kmsg
$BUSYBOX sh
else$BUSYBOX mount /dev/mmcblk0 /newroot
if [ $? -eq0 ];thenecho"mount root file system successfully..." >> /dev/kmsg
elseecho"failed to mount root file system, drop to shell" >> /dev/kmsg
$BUSYBOX sh
fifi# the root file system is mounted, clean the world for new root file systemecho"" > /proc/sys/kernel/hotplug
$BUSYBOX umount -f /proc
$BUSYBOX umount -f /sys
$BUSYBOX umount -f /dev/pts
# $BUSYBOX umount -f /devecho"enter new root..." >> /dev/kmsg
exec$BUSYBOX switch_root -c /dev/console /newroot /init
if [ $? -ne0 ];thenecho"enter new root file system failed, drop to shell" >> /dev/kmsg
$BUSYBOX mount -t proc proc /proc
$BUSYBOX sh
fi
现在我们可以通过qemu来挂载hda.img,为了简单,我们这里把这个设备虚拟为sd卡,这也是为什么上面的init脚本挂载物理根文件系统时,是找/dev/mmcblk0了。具体命令如下:
qemu-system-arm -M versatilepb -kernel arch/arm/boot/zImage -nographic -initrd ramfs.gz -sd hda.img
如果不出意外,你可以看到这个自己做的linux系统,通过调用两个init脚本,跳到最终的hda.img上的文件系统。