从零开始构建最简树莓派linux

经过一番折腾,终于弄清了树莓派Linux是如何启动的。

过去一直搞不明白的一个问题就是,树莓派第一阶段的bootloader在哪里?为什么会有这个疑问呢?是因为,安装树莓派Linux的时候,只需要用dd命令把SD卡镜像刷入SD卡中,插入树莓派,上电即可运行。而SD卡上,没什么特别的地方,无非是两个分区,第一个分区是VFAT文件系统,作为/boot目录,第二个分区是EXT4文件系统,作为/目录。要启动树莓派的Linux内核(在/boot/kernel.img),至少必须要先挂载/boot目录,然后把kernel.img放入RAM里面执行才行。那么,挂载和加载内核这件事,是谁做的?

后来看了一个帖子,终于明白了。先贴上截图:

翻译一下:

1、当树莓派刚刚上电时,ARM内核(也就是CPU)是关闭的,而GPU内核是开启的。此时SDRAM也是不可用的;

2、GPU开始执行第一阶段的bootloader,这段代码被固化在ROM中。第一阶段bootloader读取SD卡,并且加载第二阶段bootloader——bootcode.bin——放入二级缓存,并运行之;

3、bootcode.bin启用SDRAM,并从SD卡中读取第三阶段的bootloader——loader.bin——放入RAM,并运行之;

4、loader.bin读取GPU固件——start.elf;

5、start.elf读取config.txt、cmdline.txt和kernel.img。

loader.bin并不做很多事。它能够处理.elf文件,这能够把start.elf加载到内存的顶端(CPU是从地址0开始使用内存的)。有一个计划想要把对elf文件的加载支持放入bootcode.bin中,这样就不再需要loader.bin了。不过这个计划的优先级很低(我猜这可能可以节约100ms的启动时间)。

OK,翻译结束~~~~~~~~~~~~~~~

事实上,现在的树莓派确实已经没有了loader.bin了,下图就是一个最简化的/boot目录所需的文件,只有5个:

其中bootcode.bin和start.elf是属于firmware(固件),可以自己下源码编译,也可以下载已经编译完了的二进制文件,还可以,呵呵,就是直接从已经存在的树莓派系统里面复制过来~他们的功能上面已经讲解过了。而config.txt和cmdline.txt就是两个文本文件,config.txt是start.elf的配置文件,而cmdline.txt是传给linux内核的参数。kernel.img就是我们自己定制编译的linux内核,当然,要通过一个工具加工一下。

config.txt说简单可以很简单,留一个空文件都可以,说复杂可以很复杂,因为可以设置很多很多参数。这里贴一个树莓派官方的文档,专门讲解这个config.txt的:《config.txt》

=======================阶段一:SD卡分区==========================

为了演示确实是从“零”开始构建的,我不得不格式化掉SD卡。。。

树莓派要求SD卡的第一个分区必须是fat32文件系统,并把它挂载到/boot目录。而之后分几个区可以自由发挥,只要在cmdline.txt中指定哪个分区挂载为/目录,那么系统就能启动起来。一般来说,SD卡被分为两个区,第一个是fat32文件系统作为/boot,第二个是ext4文件系统作为/。

首先,把SD卡插入读卡器,然后再把读卡器插入电脑。

然后,查看以下当前有哪些存储设备,以确定哪个是SD卡。(其实最简单的办法就是在未插SD卡时“ls /dev | grep sd”看看有那些存储器,然后插入SD卡再“ls /dev | grep sd”,多出来的那个就是SD卡了)。我这里是/dev/sdb。

使用fdisk分区:

sudo fdisk /dev/sdb

之后就是交互式的操作了:

1、输入p,回车,看看当前SD卡有哪些分区:

2、输入d,回车,输入1,回车,即可删除/dev/sdb1分区;同理,输入d,回车,输入2,回车,即可删除/dev/sdb2。以此类推,直到把所有分区删除完。

3、此时再输入p,回车,会发现已经没有分区了:

4、输入n,回车,输入p,回车,输入1,回车,回车,输入+40M,回车,于是就新建了一个/dev/sdb1分区,大小40MB;

5、输入n,回车,输入p,回车,输入2,回车,回车,输入+4G,回车,于是就新建了一个/dev/sdb2分区,大小4GB;

6、输入p,回车,可以看一下分区表是否正确:

看来除了/dev/sdb1的文件系统类型不对以外,其他都对了(id=83就是EXT4);

7、输入t,回车,输入1,回车,输入c,回车,于是/dev/sdb1被设置为了W95 FAT32 (LBA);

8、再输入p,回车,再次看一下分区是否正确:

看来已经正确了;

9、输入w,将新的分区表写入SD卡。

分区完以后,就要格式化文件系统了:

sudo umount /dev/sdb1
sudo umount /dev/sdb2
sudo mkfs.vfat /dev/sdb1
sudo mkfs.ext4 /dev/sdb2

=======================阶段二:编译linux内核========================

在之前一篇博客《树莓派linux内核编译与升级》的阶段二已经讲解了怎么编译树莓派linux的内核了,可以作为一个参考。

首先新建一个目录diy_rpi_linux并进入目录:

cd ~
mkdir diy_rpi_linux
cd diy_rpi_linux

下载树莓派linux的内核源码并解压:

wget http://github.com/raspberrypi/linux/archive/rpi-4.1.y.zip
jar xvf rpi-4.1.y.zip

下载制作内核镜像的工具,其中包括交叉编译工具:

wget http://github.com/raspberrypi/tools/archive/master.zip -O tools.zip
unzip tools.zip

设置好环境变量,把交叉编译器的路径加入PATH中:

PATH=$PATH:~/diy_rpi_linux/tools-master/arm-bcm2708/arm-bcm2708hardfp-linux-gnueabi/bin

然后开始编译工作!

cd linux-rpi-4.1.y/
make ARCH=arm CROSS_COMPILE=arm-bcm2708hardfp-linux-gnueabi- bcmrpi_defconfig
make ARCH=arm CROSS_COMPILE=arm-bcm2708hardfp-linux-gnueabi- -j4

完成后,要来制作内核镜像了。因为编译出来的linux内核的格式不是树莓派能够识别的格式(树莓派用的kernel.img是在linux内核前加了一些信息),所以需要另外处理一下:

cd ../tools-master/mkimage/
./imagetool-uncompressed.py ../../linux-rpi-4.1.y/arch/arm/boot/Image

此时,在~/diy_rpi_linux/tools-master/mkimg目录下,就多了一个kernel.img的文件。这个就是待会儿要放入SD卡的boot目录的内核镜像。

=====================阶段三:构建/boot目录======================

有了内核,就可以来构建/boot目录了。上面也说了,只需要bootcode.bin、start.elf、config.txt、cmdline.txt和kernel.img这么五个文件。

bootcode.bin和start.elf可以从树莓派的firmware中得到。

首先下载firmware-master.zip并解压:

cd ~/diy_rpi_linux
wget https://github.com/raspberrypi/firmware/archive/master.zip -O firmware.zip
unzip firmware.zip

而后就可以在~/diy_rpi_linux/firmware-master/boot目录下找到我们要的东西了:

新建一个目录,用来存放这5个文件:

mkdir sd_boot
cd firmware-master/boot
cp bootcode.bin start.elf ../../sd_boot/

接着,创建一个空白的config.txt和一个只有一行参数的cmdline.txt:

cd ../../sd_boot
touch config.txt
echo "console=ttyAMA0,115200 root=/dev/mmcblk0p2 rootfstype=ext4 rootwait" > cmdline.txt

最后,把kernel.img复制进来:

cp ../tools-master/mkimage/kernel.img .

至此,该有的都有了,挂载/dev/sdb1,全部复制进去,卸载SD卡:

sudo mkdir /mnt/sd_boot
sudo mount /dev/sdb1 /mnt/sd_boot
sudo cp * /mnt/sd_boot/
sync
sudo umount /dev/sdb1

就是还差/目录。先不管了,看看内核能不能运行起来再说把!!

拔出SD卡,插入树莓派,启动minicom:

能够启动并打印一大堆启动信息,但是最后因为找不到init程序而挂起。但至少,内核启动起来了!!

=======================阶段三:构建/目录==========================

之所以出现上面的原因,是因为,内核找不到init程序。init进程是内核启动后第一个启动的进程,而所有的进程都是init的子进程。

构建/目录,大致就是构建bin、sbin、etc、var、home等目录,以及proc、dev等特殊目录。在《从零开始构建linux(一)——编译linux内核》的阶段三和阶段四已经讲述了怎么编译busybox和构建根目录了。这里依旧使用busybox。

首先要下载busybox的源码包并解压:

cd ~/diy_rpi_linux
wget http://busybox.net/downloads/busybox-1.20.2.tar.bz2
tar xvf busybox-1.20.2.tar.bz2

这时就有了busybox-1.20.2目录了,进入目录并开始配置编译选项:

cd busybox-1.20.2
make defconfig
make menuconfig

出现如下画面:

首先设置静态编译:Busybox Settings–>Build Options下的第一个选项Build BusyBox as a static binary(no shared libs)选上。因为我们的linux环境暂时还没有加入各种运行时库,所以必须静态链接。

然后是设置交叉编译工具:Busybox Settings–>Build Options下的第四个选项Cross Compiler prefix填写为“arm-bcm2708hardfp-linux-gnueabi-”,如图:

退出,保存。开始编译:

make -j4
make install

make install之后,目录下就多了一个_install目录,看看里面的结构,就是一个linux的/目录需要的了:

接下来,再创建几个常用的目录:

cd ~/diy_rpi_linux
mkdir sd_root
cp -r busybox-1.20.2/_install/* sd_root/
cd sd_root
mkdir proc mnt var tmp dev sys etc

同时在/下还必须要有一个init文件,这个init文件可以是一个可执行的二进制文件,也可以是一个shell脚本,或者是指向前面两者的链接。init文件会在linux内核初始化就绪后被执行。方便起见,我们就把init做成一个指向bin/sh的软连接:

ln -s bin/sh init

dev目录下还必须有几个必要的设备console,null,tty,tty1,tty2,tty3,tty4:

cd dev
mknod console c 5 1
mknod null c 1 3
mknod tty c 5 0
mknod tty1 c 4 1
mknod tty2 c 4 2
mknod tty3 c 4 3
mknod tty4 c 4 4

把sd_root中的内容复制到/dev/sdb2中:

sudo mkdir /mnt/sd_root
sudo mount /dev/sdb2 /mnt/sd_root
cd ..
sudo cp -a * /mnt/sd_root/
sync
sudo umount /dev/sdb2

OK,拔出SD卡,插入树莓派,启动minicom,欣赏一下吧:

注意最后一句,“Please press Enter to activate this console”,就是说按回车键激活终端。那就照做呗,敲一下Enter,居然出现了命令提示符“#”,执行命令试试:

不仅可以打印目录列表,还能读写文件~~~

你可能很好奇,为什么不需要用户登录?因为我把init直接指向了/bin/sh,而不是/bin/login,所以直接跳过了用户登录,于是乎,一切畅行无阻,再也没有权限管理的概念了~~~

但是目前,文件系统上还没有内核模块。