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-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-6.12.7版本
根据自己喜好配置即可
这里需要创建一个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/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 ~
,但是,这里并没有我们的用户名和主机名。
此时我们执行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
中
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网页内容。