环境
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
将文件挂载。
如果想要在磁盘映像中分区,则可以先使用fdisk
或cfdisk
对磁盘映像进行分区,然后使用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:362Location:-> Settings
这个版本默认支持了Unicode,可以不用更改
Symbol: UNICODE_SUPPORT [=y]Prompt: Support UnicodeDefined at libbb/Config.in:311Location:-> Settings
添加了Unicode宽字符支持
Symbol: UNICODE_WIDE_WCHARS [=y]Prompt: Allow wide Unicode characters on outputDefined at libbb/Config.in:390Depends on: UNICODE_SUPPORTLocation:-> 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的安装目录可以发现,目前只有bin
,sbin
和usr
三个目录和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
表示根文件系统的位置,虚拟机中一般是sdainit=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
脚本里的内容。这也是没有inittab
时linuxrc
的默认动作。
接下来我们配置/etc/init.d/rcS
脚本的内容
#!/bin/shPATH=/sbin:/bin:/usr/sbin:/usr/bin:$PATH
LD_LIBRARY_PATH=/lib:/usr/lib:$LD_LIBRARY_PATHrunlevel=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 ~
,但是,这里并没有我们的用户名和主机名。
此时我们执行id
和hostname
命令会发现,我们现在虽然是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
中
#!/bin/sh
...
# hostname
hostname -F /etc/hostname
这代表从/etc/hostname
加载主机名
网络配置
同样在/etc/init.d/rcS
中添加以下配置
# network
ifconfig lo up
ifconfig eth0 192.168.1.100 netmask 255.255.255.0 up
route add default gw 192.168.1.1 eth0
ip地址随意填写,网关地址填写为qemu外部提供的网卡地址
网卡配置
在WSL中,需要创建一张虚拟网卡设备作为虚拟机的网关。
我们创建一张tap设备,向网卡配置脚本中写入以下内容
ip tuntap add dev tap0 mode tap
ip link set dev tap0 up
ip a add dev tap0 192.168.1.1/24
iptables -t nat -A POSTROUTING -o eth0 -s 192.168.1.1/24 -j MASQUERADE
echo 1 > /proc/sys/net/ipv4/ip_forward
这个脚本创建了一张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网页内容。