实验目的

  • 了解嵌入式开发环境,掌握内核的下载与启动过程

  • 了解Linux内核编译过程,学会配置内核编译选项与内核剪裁等

  • 了解Linux文件系统的制作、烧录与挂载等

  • 掌握交叉编译工具链的使用,了解上位机/目标机概念

实验环境

实验基于Loongarch架构的龙芯教育派开发板平台,芯片为龙芯2k1000LA,Loongarch架构。上位机环境为Ubuntu 20.04,配备交叉编译工具链与minicom串口调试工具。

实验过程

在目标机上烧录Linux并挂载文件系统

本节中暂不讨论Linux内核的编译与文件系统的制作过程,直接使用实验提供的内核镜像与文件系统镜像进行烧录与挂载。

1
2
3
4
5
6
7
8
9
10
$ ifconfig  # 查看本机IP地址,这里我的机器上是192.168.208.22
$ minicom # 启动串口,按开发板重启按钮
# 等待出现“Press 'c' to command-line”提示时按c键进入PMON环境
PMON> ifconfig syn0 192.168.208.122
# 根据实验室环境,开发板地址可设为对应上位机地址+100,避免冲突
PMON> load tftp://192.168.208.22/vmlinuz # 下载内核镜像
PMON> initrd tftp://192.168.208.22/ramdisk_loongarch.cpio.gz
# 为龙芯开发板提供的RAMFS文件系统
PMON> g console=ttyS0,115200 rdinit=/sbin/init
# 启动内核,指定控制台端口与波特率,指定起始位置

接下来会出现提示Please press Enter to activate this console,我们按提示按Enter键即可进入开发板上的Linux环境。

体验交叉编译工具链

上位机环境为x86架构的Ubuntu系统,而目标机环境为Loongarch架构的Linux系统,虽然C语言编写一些简单的程序(比如HelloWorld)时,需要写的具体代码与架构无关,但是在不同的架构上肯定不能运行同一个程序,其中的关键就是把代码翻译成程序的编译过程。在上位机上编写程序,并编译成可以在目标机上运行的程序,这个过程就是交叉编译,使用的编译工具链就是交叉编译工具链。

在实验室的Ubuntu上位机上预装了针对Loongarch-GNU-Linux的交叉工具链,位于/opt/cross-tools路径下。首先我们把工具链路径添加到上位机的PATH环境变量中,这样每次调用就不用输入完整路径了:

1
$ export PATH=/opt/cross-tools/bin:$PATH

编写一个简单的hello.c文件

1
2
3
4
5
#include <stdio.h>
int main() {
printf("Hello from NJU!\n");
return 0;
}

接下来分别用默认工具链和交叉编译工具链编译出两份不同的程序。

1
2
3
4
5
6
7
8
$ gcc ./hello.c -o ./hello_x86 
# 这里使用的是上位机Ubuntu系统安装时自带的gcc工具链
$ loongarch64-linux-gnu-gcc ./hello.c -o ./hello_arch
# 这里使用的是/opt/cross-tools/bin中的工具链
$ cp ./hello_arch /srv/nfs4/loveapple/
$ cp ./hello_x86 /srv/nfs4/loveapple/
# 将编译出的程序拷贝到/srv/nfs4中的一个自建文件夹里
#以方便目标机上的Linux挂载网络文件系统并执行程序

在上位机上分别尝试执行两个程序。

1
2
3
4
$ ./hello_x86
Hello from NJU!
$ ./hello_arch
bash: cannot execute binary file: Exec format error

这里由于hell_arch是使用交叉工具链编译,适用于loongarch架构上运行的,所以运行时会产生报错。我们像之前所说在开发板上启动Linux系统,然后按如下步骤设置IP地址并挂载网络文件系统服务(NFS, Network File System):

1
2
3
> ifconfig eth0 192.168.208.122
> mount -t nfs 192.168.208.22:/mnt/nfs4/loveapple /mnt/loveapple
> ls /mnt/loveapple -l

这里我们可以看到命令前面的引导符号不是$而是#,这是因为我们处在root用户环境下。

执行ls命令后,此时应该能看见之前在上位机上放入的两个程序,我们来尝试运行。

1
2
3
4
5
6
7
8
9
/ > cd /mnt/loveapple
/mnt/loveapple > ./hello_x86
./hello_x86: line 13: ????B???: not found
./hello_x86: line 0: ? not found
./hello_x86: line 1: ELF???? not found
./hello_x86: line 15: syntax error: unexpected "("

/mnt/loveapple > ./hello_arch
Hello from NJU!

这里执行为x86编译的可执行文件时,会报错,而执行为loongarch编译的可执行文件时,则可正常执行。所谓交叉编译,就是在上位机上对程序代码进行编写开发,使用交叉工具链进行编译,从而传递给目标机进行执行。

编译Linux内核

Linux内核是开源的,一般来说可从网络上进行下载,这里由于实验室是内网环境,我们改为从教师机上使用基于ssh链接的scp工具获取Linux内核源码。

1
2
3
4
5
6
7
8
9
10
11
$ mkdir -p ~/loveapple/core
$ cd ~/loveapple/core # 使用自建的目录,与其他同学区分
$ ssh [email protected]
$ ls ~
# 先使用ssh登录到教师机上,用ls命令查看有那些文件,以方便scp下载指定路径
# 这里发现~(即/home/student)路径下有ramdisk_img.gz,ramdisk_loong.cpio.gz,busybox-1.29.2.tar.gz,linux_4.19.190.7.9.orig.tar.gz这几个比较有用的文件
$ exit
$ scp [email protected]:~/linux_4.19.190.7.9.orig.tar.gz .
#将教师机上的文件通过scp目录拷贝到当前路径
$ tar -zxvf ./linux_4.19.190.7.9.orig.tar.gz # tar命令解压
$ cd ./linux_4.19.190.7.9

这里我们成功进入了存放Linux内核源码的文件夹。在该文件夹中有Makefile文件规定可执行的make指令。这里为了交叉编译,我们首先要为make环境设置对应的环境变量ARCHCROSS_COMPILE

1
2
$ export ARCH=loongarch
$ export CROSS_COMPILE=loongarch64-linux-gnu-

这里ARCH指定了编译目标的架构,而CROSS_COMPILE指定了工具链的前缀,后缀则为GNU工具链统一的gccasar等等工具名称。通过export设置的环境变量不具有永久性,仅对当前会话有效,因此每次重新启动bash时,都需要重新export一次。

Linux内核编译时可选的配置选项很多,这里我们首先加载一下对这套板卡的缺省配置(defconfig)。

1
$ make loongson_2k1000_defconfig

接下来,还有很多可以自定义的配置项,我们可以使用menuconfig图形化界面加以配置。

1
$ make menuconfig

修改好配置之后,就可以进行编译了。可以用-j选项指定编译的线程数,加快编译进度。目标为vmlinuz内核文件。

1
$ make -j8 vmlinuz

编译完成之后,我们在tftpboot文件夹下新建一个自己的文件夹,依然是与其他同学做出区分,然后将编译好的vmlinuz文件拷贝的该文件夹下,这样之后就可以通过tftp协议来load自己的内核了。

1
2
$ mkdir /srv/tftpboot/loveapple
$ cp ./vmlinuz /srv/tftpboot/loveapple/vmlinuz_0909

制作文件系统

一般的文件系统需要的硬件资源比较多,这里我们选择可统一编译的轻量化的文件系统busybox。

之前ssh登录教师机时已经看到教师机上有busybox的压缩包,我们依然是使用scp命令传输下来。

1
2
3
4
5
$ mkdir ~/loveapple/busybox
$ cd ~/loveapple/busybox
$ scp [email protected]:~/busybox-1.29.2.tar.gz
$ tar -zxvf ./busybox-1.29.2.tar.gz
$ cd ./busybox-1.29.2

之前已经export过相应的环境变量了,这里如果没有重启过bash会话的话,可以不重新export,直接进行设置、编译与安装。

1
2
3
4
$ make defconfig
$ make menuconfig # 这里我推荐勾选Settings->Build Options->Build static binary (no shared libs)
$ make -j8
$ make install

如果没有特殊配置过安装路径的话,make install会默认将busybox安装于./_install下。

接下来我们还要对这个新系统做一些修改,规定一些启动时的初始化行为。在之前启动时的g命令中,我们曾规定过rdinit=/sbin/init,这是Linux内核启动并挂载文件系统后自动执行的脚本。虽然这个脚本是busybox编译安装时就自动生成的,但是我们依然还需要做出一些额外的配置。在_install文件夹下新建一个/etc目录,在该目录中新建inittab,rc,motd三个文件。接下来一定要注意,在提及这个etc目录时,一定要使用./etc/,而非/etc/(会指向上位机自己的etc文件夹)。

1
2
3
4
5
6
$ cd ./_install
$ mkdir etc
$ vim ./etc/inittab # 随后编辑inittab文件
$ vim ./etc/rc # 随后编辑rc文件
$ chmod +x ./etc/rc 给rc文件赋予可执行权限
$ vim ./etc/motd # 随后编辑motd文件

inittab是初始化系统设置的关键配置文件,比如系统初始化过程,执行脚本所用的解释器,关机对应的指令等等。我编写的inittab内容如下:

1
2
3
4
5
6
7
# /etc/inittab

::sysinit:/etc/init.d/rcS
::askfirst:-/bin/sh

::ctrlaltdel:/sbin/reboot
::shutdown:/bin/umount -a -r

rc文件则是刚刚我们规定的/etc/init.d/rcS软连接到的真正脚本文件。因此我们还要进行rcSrc的软连接。

1
2
$ mkdir ./etc/init.d
$ ln -s ./etc/rc ./etc/init.d/rcS

我编写的rc文件如下:

1
2
3
4
5
6
7
8
9
#!/bin/sh
hostname Loongson
mount -t proc proc /proc
mount -t sysfs sysfs /sys
cat /etc/motd

#ifconfig eth0 192.168.208.123
mkdir /LoveApple
#mount 192.168.208.22:/srv/nfs4/loveapple /LoveApple -o nolock,proto=tcp

最后三行我本来适用于自动执行NFS挂载操作的,但是实测发现rc脚本执行太早,网卡还没有配置好就尝试执行这几条指令了,导致NFS挂载失败并在控制台中弹出报错,于是我便注释掉了两句,只留下了创建文件夹的语句。

最后是motd文件(Message Of ToDay),我们在rc中让脚本cat出这个文件的内容,因此我们在motd中写的内容会在系统启动时显示,类似于一个标识。这里我编写的motd文件如下:

1
2
3
4
5
6
7
===================================
Welcome to Linux World!
===================================
Ported by LoveApple 14434
===================================
Say Hello to Everybody! Love U All!
===================================

接下来我们将编译出的ramdisk_img挂载,并将_install文件夹中安装的文件复制到挂载点中,最后制作归档文件,以方便目标机通过tftp加载。

1
2
3
4
5
6
$ mount ramdisk_img # 根据/etc/fstab的配置,会将ramdisk_img挂载到/mnt/ramdisk/下
$ cp ./_install/* /mnt/ramdisk/ -r
$ cd /mnt/ramdisk/
$ find . | cpio -H newc -o | gzip -9 > /srv/tftpboot/loveapple/initrd_0916.cpio.gz # 直接放入tftp文件夹即可
$ cd -
$ umount /mnt/ramdisk # 及时卸载挂载点

然后按第一小节所述进入开发板的PMON环境,加载我们自己编译的内核与文件系统。

1
2
3
4
PMON> ifconfig syn0 192.168.208.122
PMON> load tftp://192.168.208.22/loveapple/vmlinuz_0909
PMON> initrd tftp://192.168.208.22/loveapple/initrd_0916.cpio.gz
PMON> g console=ttyS0,115200 rdinit=/sbin/init

注意这里我们的文件是存放在/srv/tftpboot底下的子文件夹里的,使用tftp协议传输时要输入相对路径。稍等几秒让Linux内核与文件系统启动,我们就可以第一次看到我们自己编译的内核与文件系统了。

制作动态链接库

前面编译安装busybox时我们选择构建静态二进制文件,不使用shared libs。这为我们省去了拷贝动态链接库的烦恼,但是也会让之后我们在目标机上运行其他需要动态链接库的程序遇到一些困难。接下来我们介绍一下使用动态链接库的办法。

进入到busybox的源码文件夹,使用如下命令查看busybox需要的动态链接器与共享库:

1
2
$ readelf -l busybox | grep interpreter
$ readelf -d busybox | grep NEEDED

为了存放链接器与共享库,我们需要在/usr/下建立lib/lib64/文件夹,还需要在根目录建立/lib64/文件夹,并从交叉工具链附带的库文件中将这些链接器与共享库文件拷贝过来。注意上面命令所列出的链接器与共享库均为软链接,不能直接拷贝,而要用ls -l指令看到具体的链接关系,并拷贝其对应的目标文件,再手动建立相应的软链接关系。

1
2
3
4
5
6
7
8
9
10
11
$ cd ./_install
$ mkdir -p /usr/lib64
$ mkdir /usr/lib
$ ln -s /usr/lib64 ./lib64
$ ls -l /opt/cross-tools/loongarch64-linux-gnu/sysroot/lib64
$ cp /opt/cross-tools/loongarch64-linux-gnu/sysroot/lib64/{ld-2.28.so, libc-2.28.so, libm-2.28.so, libresolv-2.28.so} ./usr/lib64
$ cd ./usr/lib64
$ ln -s ld-2.28.so ld.so.1
$ ln -s libc-2.28.so libc.so.6
$ ln -s libm-2.28.so libm.so.6
$ ln -s libresolv-2.28.so libresolv.so.2

接下来我们再像之前一样重新制作一个归档文件(.cpio.gz),并在开发板上启动,实验效果与之前相同,但是新启动的文件系统中有lib64与其具体内容。

遇到的问题与感悟

  • 在开发板上执行命令是使用串口(通过USB连接到上位机)进行数据传输,但是loadinitrd指令通过tftp服务传输文件要借助网络。当loadinitrd命令没有反应时,首先检查开发板有没有插上网线,其次通过ping命令检查网络是否通畅,再其次检查tftp://\<IP\>/后面的路径与文件名是否正确。正常来说如果命令正确执行并正在加载时,会出现一串16进制数字,一个括号包裹的文件类型((ELF)或(BIN)),以及一个会转圈的字符(轮流显示|/-)。

  • 在上位机开发时,如果要进行文件夹切换,如果不记得每一级文件夹的结构,比如其目录下有什么子目录与文件名之类的,不需要一点点cd一点点ls,而可以cd <部分路径>然后按tab键,Ubuntu会把可能的补全列在下面,并且保留你已经输入过的命令部分。善用tab补全可以节省很多手打文件名的烦恼,也可以避免不少输入错误导致的问题。

  • 当命令执行失败或者遇到系统无法启动之类的问题时,可以认真看看报错信息。

  • 当我第一次从构建静态二进制文件转为使用动态共享链接库时,一直给我报错说找不到libm.so.6,但是我明明拷贝了文件并建立了软链接。后来发现是/usr底下少建立了/lib文件夹。

  • 我尝试把文件系统的归档文件整合进内核编译过程中,首先把initrd.cpio.gz文件拷贝到Linux编译的文件夹中,然后make menuconfig并在相关设置中制定RAMFS的文件路径,保存退出,重新make -j8,生成新的vmlinuz内核,当我尝试仅load这个vmlinuz内核并尝试用g命令启动时,会与之前正确步骤中一样输出一大堆调试信息,但是缺少Press Enter to enable this console提示,个人推测是内核和文件系统都成功加载了,但是console指定有问题,或者是启动脚本的路径没有正确被找到。由于实验时间限制,尚未解决这一问题。

实验总结

本次实验基于龙芯2K1000LA开发板,系统地完成了嵌入式Linux开发环境的搭建与核心流程的实践。通过本次实验,我主要获得了以下收获:

  • 掌握了嵌入式Linux开发的基本流程:亲身体验了从内核配置编译、文件系统制作到最终烧录启动的完整过程,对嵌入式系统的组成和启动流程有了直观且深入的理解。

  • 理解了交叉编译的核心概念:通过在上位机(x86)编译出能在目标机(LoongArch)上运行的程序,深刻体会了交叉编译工具链在跨平台开发中的关键作用,以及不同架构下二进制文件的不可兼容性。

  • 提升了问题分析与解决能力:在实验过程中,遇到了如tftp加载失败、动态链接库缺失、文件系统启动异常等问题。通过分析错误信息、检查网络配置和文件路径,我学会了系统地排查和解决问题的方法,并认识到启动脚本执行时机的重要性。

  • 熟悉了关键工具的使用:熟练掌握了menuconfig进行内核剪裁、BusyBox构建轻量级文件系统、以及使用cpio和gzip制作根文件系统镜像等核心技能。

总而言之,本实验不仅让我掌握了具体的操作技能,更重要的是建立了对嵌入式系统”自上而下”的整体认识,为后续更深层次的嵌入式开发打下了坚实的基础。

实验过程照片

由于实验上位机不连接外网,不方便截图传输到个人电脑上,因此这里均使用手机拍屏而非截屏。所幸照片仅作为实验过程的证据,而非实验过程的描述,相对而言不算特别关键。

PMON环境启动
PMON环境下配置网络
在开发板上启动现成的Linux内核与文件系统
在上位机上运行两种程序的不同结果
在目标机上运行两种程序的不同结果
下载Linux内核压缩包并解压
安装busybox
制作ramdisk_img镜像
编写Message of Today
第一次成功启动我自己的内核与文件系统
在我自己的内核与文件系统上执行一些简单命令
将tftpboot中我的文件整合到一个单独目录中,并进行加载与启动
在目标机上尝试新建用户与组,设置密码
目标机也要合理关机,不要粗暴拔电哦