busybox最小Linux系统
2025-08-22 16:01:14,

环境

WSL(Ubuntu 22.04)

创建磁盘映像

可以使用fallocate为磁盘映像分配一块空间,或者使用dd if=/dev/zero of=$img bs=1M count=$size_in_MB直接得到一个大小为$size_in_MB大小的文件。

使用mkfs.ext4格式化映像文件,并使用mount -o loop $img mnt将文件挂载。

如果想要在磁盘映像中分区,则可以先使用fdiskcfdisk对磁盘映像进行分区,然后使用losetup -fP $img将文件挂载为回环设备。这里-f参数表示自动寻找可以挂载的回环设备号,-P参数表示探测文件中的分区并分别挂载为回环设备。挂载为回环设备后,再使用mount $loop1 $mnt1等命令挂载回环设备。

构建busybox

下载busybox源码并构建,这里使用的是busybox-1.36.1版本

这里采用的构建选项有

构建静态文件:

Symbol: STATIC [=y]
	Prompt: Build static binary (no shared libs)
	Defined at Config.in:362
	Location:
		-> Settings

这个版本默认支持了Unicode,可以不用更改

Symbol: UNICODE_SUPPORT [=y]
	Prompt: Support Unicode
	Defined at libbb/Config.in:311
	Location:
		-> Settings

添加了Unicode宽字符支持

Symbol: UNICODE_WIDE_WCHARS [=y]
	Prompt: Allow wide Unicode characters on output
	Defined at libbb/Config.in:390
		Depends on: UNICODE_SUPPORT
		Location:
		-> Settings
			-> Support Unicode (UNICODE_SUPPORT [=y])

其他构建选项均可以不更改

使用make构建后,再使用make install即可将完整的busybox、busybox符号链接等文件安装到busybox源码目录下的_install目录内。或者可以通过make install CONFIG_PREFIX=$install将busybox安装到指定目录中。比如这里我们可以使用make install CONFIG_PREFIX=$mnt将busybox安装到已经挂载的磁盘映像中。

构建Linux内核

下载Linux内核源码,这里使用Linux-6.12.7版本

根据自己喜好配置即可

创建rootfs

这里需要创建一个rootfs来作为Linux运行的环境。

查看busybox的安装目录可以发现,目前只有binsbinusr三个目录和Linuxrc一个符号链接。对比我们自己的Linux根目录可以发现,我们大概有以下目录

bin boot dev etc home lib mnt opt proc root run sbin sys tmp usr var

那么我们在$mnt目录下创建这些目录即可。

由于mount需要sudo$mnt目录下的文件很可能是root权限,后面一系列操作可能都需要root权限。

现在可以chroot$mnt目录下试试能否使用shell。

运行虚拟机

这里我们使用qemu虚拟机。

将启动命令写成一个脚本

#!/bin/sh
/usr/bin/qemu-system-x86_64\
  -kernel path/to/bzImage\
  -hda path/to/rootfs.img\
  -nographic\
  -append "console=ttyS0 root=/dev/sda init=/linuxrc"
  • -kernel选项表示设置Linux kernel为bzImage
  • -hda选项表示选择磁盘映像
  • -nographic表示不使用qemu窗口,而是将输出重定向到终端
  • -append表示传递给Linux内核的参数
    • console=ttyS0表示将输出重定向到串口设备ttyS0,这将使qemu将启动阶段的信息输出到终端
    • root=/dev/sda表示根文件系统的位置,虚拟机中一般是sda
    • init=linuxrc表示使用linuxrc作为init进程,也就是Linux下的第一个进程启动,这个linuxrc其实就是我们的busybox

启动配置

此时如果直接运行脚本启动虚拟机可能会报错,因为我们没有配置busybox作为init进程时的行为。

linuxrc会读取/etc/inittab文件,我们将该文件配置如下

::sysinit:/etc/init.d/rcS
::respawn:-/bin/sh
::restart:/sbin/init
::ctrlaltdel:/sbin/reboot
::shutdown:/bin/umount -a -r

该文件内每行有四个字段,格式为<id>:<runlevel>:<action>:<process>

  • <id>指编号,不重复即可
  • <runlevel>指运行级别,可以不指定,指定时表示运行级别为n时激活改行的规则
  • <action>包含一系列动作,表示对登记的<process>在一定条件下执行的动作
  • <process>即要运行的进程,前面加上-表示以交互方式运行

<action>包含以下动作

action 含义
respawn 当process终止后马上启动一个新的
wait 当进入指定的runlevels后process才会启动一次,并且到离开这个runlevels终止
initdefault 设定默认的运行级别,即我们开机之后默认进入的运行级别,不能是0,6,你懂的
sysinit 系统初始化,只有系统开机或重新启动的时候,这个process才会被执行一次
powerwait 当init接收到电源失败信号的时候执行相应的process,并且如果init有进程在运行,会等待这个进程完成之后,再执行相应的process
powerfail 当init接收到电源失败信号的时候执行相应的process,并且如果init有进程在运行,不会等待这个进程完成,它会直接执行相应的process
powerokwait 电源已经故障,但是在等待执行对应操作的时候突然来电了就执行对应的process
powerfailnow 当电源故障并且init被通知UPS电源已经快耗尽执行相对应的process
ctrlaltdel 当用户按下ctrl+alt+del这个组合键的时候执行对应的process
boot 只有在引导过程中,才执行该进程,但不等待该进程的结束;当该进程死亡时,也不重新启动该进程
bootwait 只有在引导过程中,才执行该进程,并等待进程的结束;当该进程死亡时,也不重新启动该进程
off 如果process正在运行,那么就发出一个警告信号,等待20秒后,再通过杀死信号强行终止该process。如果process并不存在那么就忽略该登记项
once 启动相应的进程,但不等待该进程结束便继续处理/etc/inittab文件中的下一个登记项;当该进程死亡时,init也不重新启动该进程

inittab第一行表示在系统启动时,运行/etc/init.d/rcS脚本里的内容。这也是没有inittablinuxrc的默认动作。

接下来我们配置/etc/init.d/rcS脚本的内容

#!/bin/sh

PATH=/sbin:/bin:/usr/sbin:/usr/bin:$PATH
LD_LIBRARY_PATH=/lib:/usr/lib:$LD_LIBRARY_PATH

runlevel=S
umask 022
export PATH LD_LIBRARY_PATH runlevel

# devices
mount -a
mkdir /dev/pts
mount -t devpts devpts /dev/pts
mount -o remount,rw /

mdev -s

我们的脚本配置了环境变量,设备等,需要在系统启动时进行的配置,开启的服务,都可以在该文件中进行配置。

配置完成后一定要赋予/etc/init.d/rcS运行权限,否则启动过程中会报错。

此时启动虚拟机可以看到,我们已经进入了shell。

其他配置文件

虽然我们的Linux已经正常启动,但是不要高兴的太早。

我们在shell中执行export PS1='\u@\h \W',重新登陆,我们预期会显示root@host ~,但是,这里并没有我们的用户名和主机名。

此时我们执行idhostname命令会发现,我们现在虽然是uid=0 gid=0的用户,但是我们没有用户名,主机名也是(none)。执行ifconfig会发现,我们也没有可用网络。

接下来我们将进行这些方面的配置。

我们的Linux已经可以启动,而且busybox内置了vi作为编辑器,接下来的配置可以不通过宿主机,直接在虚拟机中完成。

用户配置

由于root用户本来就存在,我们不能用adduser创建用户,于是我们手动创建用户属性文件。

Linux通过识别/etc/passwd中的用户来判断用户名,我们手动创建这个文件。

添加以下内容

root:x:0:0::/root:/bin/sh

这个文件有7个字段,格式为<user>:<passswd>:<uid>:<gid>:<desc>:<home>:<shell>

其中<passwd>字段内容为加密后的密码,如果设为空则表示不需要密码也可以登录,如果为x表示密码存储在/etc/shadow文件中。

如果我们不创建/etc/shadow文件,passwd命令会将加密的密码存储在/etc/passwd中,所以我们打算创建一个/etc/passwd

我们的Linux和busybox都支持解析/etc/shadow文件,接下来我们手动创建这个文件。

添加以下内容

root::1::::::

这个文件内每行9个字段,格式为login:encyrptedpassword:lastchangedate:min_age:max_age:warning:inactivity:expiration_date:reserved,第一个字段为用户名,第二个字段为加密后的密码,如果为空会登录失败,为*!时情况不确定,Linux console上写*!表示没有密码,但实际测试后发现,为这两个符号时,busybox的login会提示bad salt

后面的几个字段都与密码修改时间有关,分别为

  • lastchange表示上次修改密码的日期的时间,如果该值为0,则表示用户下次登录时必须更改密码
  • minage表示更改密码的间隔日期,为空或为0表示随时可以更改密码
  • maxage表示必须更改密码的日期
  • warning表示在密码到期前n天警告用户需要更改密码
  • inactivity表示密码过期后,n天内可以再更改密码
  • expiration_date表示到期日期,到期后无法再登录
  • reserved最后一个字段为保留字段

有这个文件后我们就可以使用passwd命令更改密码,然后再查看/etc/shadow可以发现密码已经改变了。

然后我们就可以通过登录的方式进入操作系统。

更改/etc/inittab如下

::sysinit:/etc/init.d/rcS
::respawn:/sbin/getty -L console 0 vt100
::restart:/sbin/init
::ctrlaltdel:/sbin/reboot
::shutdown:/bin/umount -a -r

这表示不直接打开一个shell,而是在console这个tty上打开一个login

主机配置

一般我们将主机名写在/etc/hostname中,但是busybox不自动读取这个文件。

于是我们添加配置到/etc/init.d/rcS

Symbol: UNICODE_SUPPORT [=y]
	Prompt: Support Unicode
	Defined at libbb/Config.in:311
	Location:
		-> Settings
0

这代表从/etc/hostname加载主机名

网络配置

同样在/etc/init.d/rcS中添加以下配置

Symbol: UNICODE_SUPPORT [=y]
	Prompt: Support Unicode
	Defined at libbb/Config.in:311
	Location:
		-> Settings
1

ip地址随意填写,网关地址填写为qemu外部提供的网卡地址

网卡配置

在WSL中,需要创建一张虚拟网卡设备作为虚拟机的网关。

我们创建一张tap设备,向网卡配置脚本中写入以下内容

Symbol: UNICODE_SUPPORT [=y]
	Prompt: Support Unicode
	Defined at libbb/Config.in:311
	Location:
		-> Settings
2

这个脚本创建了一张tap0网卡,并分配了ip地址192.168.1.1,就是我们的虚拟机的网关地址。

iptables命令创建了一条nat规则,将内部发出的源地址为192.168.1.0/24网段的数据包改为从eth0发出,这样就可以让虚拟机连接到外部网络了。

此时进入虚拟机,执行ping 192.168.1.1发现有网络连接。

然后执行cat nameserver 8.8.8.8 > /etc/resolv.conf配置域名解析服务器。

此时执行ping www.baidu.com就可以ping通了。

由于busybox没有自带curl,执行echo -e "GET / HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n" | nc www.baidu.com 80代替,可以收到HTML网页内容。