環境
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: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的安裝目錄可以發現,目前只有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
中
#!/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網頁內容。