從原始碼構建一個極簡的Linux作業系統

ITPUB社群發表於2023-02-23

現代生活環境中,各式各樣的電子裝置包圍著我們每個人。但凡稍微複雜一點的裝置,比如一臺電視機、路由器或智慧手機,它很有可能執行的就是Linux作業系統,這種想法一直在我腦海出現。

讓我更加困惑的是,在這些裝置或伺服器上執行的Linux作業系統的核心居然都是由同一個原始碼構建而成的,這些原始碼被放在了一個叫做kernel.org網站的倉庫中。

如此多樣的裝置上執行的作業系統都是由相同的原始碼組合而成的!當然,這種說法有點片面,實際上核心通常由特定Linux發行版的開發人員以及特定裝置的開發人員擴充套件和修改的,但核心中卻有很多通用的原始碼。

我一直想靠自己從原始碼開始構建一個Linux作業系統,但過程實在是太複雜,而且越做越亂,究其原因是我有很多沒有掌握的領域。終於在某一時刻,我積累了足夠的知識,現在我可以實現我的夢想了。因此,在這篇文章中,我將展示如何在計算機上從原始碼編譯和執行一個極簡的Linux。

雖然它不會具備所有的功能,但它擁有最重要的東西——命令列介面。相信我,在真正的計算機上獲得一個可以工作的Linux命令列介面將是一種不可思議的體驗。

令人驚訝的是,獲得Linux命令列所需的最小集合只有兩個檔案:Linux核心檔案和根目錄檔案系統初始映象檔案。顯然,這需要一個引導裝載程式來載入這兩個檔案並啟動核心的執行,必要時還可以向它傳遞初始根目錄檔案系統的映象和其他引數。


 1 
極簡Linux作業系統


為了能以某種方式使用此作業系統,你需要四個元件:

載入程式(Bootloader):是一個特殊的程式,它允許處理器去執行位於作業系統核心檔案中的機器指令。

核心(Kernel):此程式的程式碼包含:

  • 用於處理器可以使用的各種物理I/O裝置(裝置驅動程式)的抽象;

  • 用於儲存的資料結構的抽象(檔案系統);

  • 程式指令(程式、執行緒)的時間分離抽象;

  • 其他抽象。

由於核心的存在,應用程式的開發人員通常不關心計算機上安裝了哪種顯示卡、鍵盤或硬碟。他們只編寫與I/O裝置、程式、檔案、套接字等工作的程式碼。

初始根目錄檔案系統(Initial root filesystem):此檔案系統是必須的,以便核心能夠在引導作業系統啟動時載入所必須的檔案。其中最重要的就是Linux核心模組,這些模組沒有包含在核心檔案中,而是用於進一步載入,以及在作業系統啟動時建立第一個程式檔案(init)。

作業系統核心介面的一組實用程式集:它允許我們使用在作業系統核心中發現的抽象。根據作業系統所執行的裝置的複雜性和用途的不同,這個集合也有所不同。這些實用程式定義功能和使用者介面。例如,路由器會有一個程式;手機會有另一個程式;個人電腦還會有一個程式。


 2 
載入Linux作業系統


在不同的架構和計算機上,Linux作業系統的引導都可能不同,但對於x86架構的計算機,大多數情況下它引導是這樣的:

  1. 電腦開機。

  2. BIOS或UEFI在計算機上找到作業系統載入程式,並向其傳輸控制指令。

  3. 作業系統引導裝載程式將Linux核心檔案和初始檔案系統映象檔案(initrd檔案)載入到RAM中。

  4. 作業系統引導裝載程式將控制傳輸到Linux作業系統核心。

  5. 作業系統核心執行初始化。

  6. 作業系統核心訪問初始檔案系統映象中的檔案(載入映象)。

  7. 核心在初始檔案系統中查詢init檔案,並基於該檔案啟動第一個使用者程式。

  8. init進程裝載一個已經持久化的檔案系統,繼續初始化作業系統,從Linux檔案系統的根目錄轉移到已裝載的檔案系統,並啟動初始化所需的其他程式。


 3 
Linux發行版


發行版是一個Linux核心以及安裝在計算機或裝置上的一組庫、實用元件和程式。

目前,各種發行版在市場上流行。我們可以從DistroWatch[1]上檢視這些清單。

現代的Linux發行版通常以ISO映象的形式釋出,並允許我們安裝更新和額外的程式(包),但我們現在要做的是一個極簡的發行版,因此對我們沒什麼意義。

關於如何從頭開始構建Linux發行版的最完整的說明,可以參考這裡[2]。構建Linux發行版是一個有趣的過程,可以讓我們學習到很多新的東西,但它非常耗時,這意味著我們需要很大的意志力才能從頭到尾完成。

我試圖將建立分發套件的簡化過程做到極致:我們不會掛載一個永久的檔案系統,但作為一個init檔案,我們將使用一個指令碼檔案,該指令碼檔案將執行最小初始化並啟動sh (shell)。


 4 
作業系統引導載入


多年來,Linux已經可以移植到許多硬體平臺上。然而,每個平臺的Linux引導都是不同的。對於x86可能有如下不同:

  1. 它會被用來引導BIOS或者UEFI嗎?

  2. BIOS或UEFI將在哪個儲存裝置上尋找載入程式(硬碟,快閃記憶體驅動器隨身碟,光碟機,網路)?

  3. 如何標記硬碟或隨身碟(MBR或GPT)?

  4. 核心文件和帶有初始根檔案系統映象的檔案(稱為initrd)位於什麼媒介上,在哪種檔案系統(FAT、NTFS、EXT、CDFS,等等)中?


 5 
初始根檔案系統的結構


初始的根檔案系統包含Linux後續操作所需的最小量級的目錄和檔案。在我們的例子中,這些是bin、dev、proc和sys目錄。bin目錄包含了使用Linux核心的實用工具。


 6 
實用程式包


極簡版的Linux是一個核心和一組命令列實用程式包。核心和命令列實用程式由不同的程式設計師團隊開發。

最常見的集合是:

  • GNU Core Utils;

  • BusyBox。

BusyBox結構簡單,佔用磁碟空間少,常用於嵌入式裝置。為了簡單起見,我們將使用這個。


 7 
建立Linux構建環境


這聽起來很矛盾,Linux通常被編譯成Linux。要做到這一點,我們需要一個Linux作業系統,其中包含允許我們構建Linux核心的程式和一組用於構建的實用程式包。

例如,在Ubuntu 22.10上,我們需要安裝以下軟體包:make、build-essential、bc、bison、flex、libssl-dev、libelf-dev、wget、cpio、fdisk、extlinux、dosfstool和qemu-system-x86。

注意,對於其他版本的Linux系統,包的集合可能不同。


 8 
Ubuntu 22.10上構建極簡Linux


安裝所需的程式包。

cd ~
$ mkdir -p simple-linux/build/sources
$ mkdir -p simple-linux/build/downloads
$ mkdir -p simple-linux/build/out
$ mkdir -p simple-linux/linux
$ sudo apt update
$ sudo apt install --yes make build-essential bc bison flex libssl-dev libelf-dev wget cpio fdisk extlinux dosfstools qemu-system-x86

從Linux Kernel上下載原始碼,再從Internet上下載BusyBox。

cd simple-linux/build
$ wget -P downloads  
$ wget -P downloads 

將壓縮包解壓為原始碼。

$ tar -xvf downloads/linux-5.15.79.tar.xz -C sources
$ tar -xjvf downloads/busybox-1.35.0.tar.bz2 -C sources

為Linux核心構建BusyBox的二進位制檔案。這個過程需要一些時間,大約10分鐘或更多,所以耐心等待就好。

cd sources/busybox-1.35.0
$ make defconfig
$ make LDFLAGS=-static
$ cp busybox ../../out/
cd ../linux-5.15.79
$ make defconfig
$ make -j8 || exit
$ cp arch/x86_64/boot/bzImage ~/simple-linux/linux/vmlinuz-5.15.79

建立init檔案。

$ mkdir -p ~/simple-linux/build/initrd
cd ~/simple-linux/build/initrd
$ vi init

除了vim編輯器,還可以使用其他的文字編輯器,例如getit。

init檔案:

#! /bin/sh
mount -t sysfs sysfs /sys
mount -t proc proc /proc
mount -t devtmpfs udev /dev
sysctl -w kernel.printk="2 4 1 7"
/bin/sh
poweroff -f

建立目錄和檔案結構。

$ chmod 777 init
$ mkdir -p bin dev proc sys
cd bin
$ cp ~/simple-linux/build/out/busybox ./
for prog in $(./busybox --list); do ln -s /bin/busybox $progdone

將該檔案目錄在initrd檔案中,這是我們的cpio歸檔檔案。

cd ..
$ find . | cpio -o -H newc > ~/simple-linux/linux/initrd-busybox-1.35.0.img

從QEMU模擬器中啟動映象的製作。

cd ~/simple-linux/linux
$ qemu-system-x86_64 -kernel vmlinuz-5.15.79 -initrd initrd-busybox-1.35.0.img -nographic -append 'console=ttyS0'


 9 
U建立一個引導映象


如果我們想在真正的硬體上執行Linux,那麼可能最簡單的方法是建立一個映象,放在引導驅動器上並啟動它。在我看來,建立這樣的映象並從驅動器啟動是最困難的過程,需要我們具備更高階的知識。

建立映象時,需要做以下幾個決定:

  • 哪個會啟動引導(BIOS或UEFI)?

  • 將使用哪個驅動器(光碟驅動器、隨身碟還是硬碟驅動器)?

  • 如何對驅動器(MBR、GPT)進行分割槽?

  • 使用哪個載入程式?

  • 將使用什麼檔案系統,Linux和引導載入程式檔案將放在哪裡?

我用了一個安裝了MBR和EXTLINUX引導裝載程式的隨身碟,檔案放在了一個FAT32分割槽中。BIOS為我啟動了引導過程(如果你的計算機有UEFI BIOS,則為選擇Legacy boot選項)。

建立可引導隨身碟映象的方法如下:

建立一個映象檔案。

$ dd if=/dev/zero of=boot-disk.img bs=1024K count=50

在映象檔案裡建立一個啟動分割槽。

$ echo "type=83,bootable" | sfdisk boot-disk.img

在boot-disk.img檔案,給啟動分割槽上設定迴環裝置。

$ losetup -D
$ LOOP_DEVICE=$(losetup -f)
$ losetup -o $(expr 512 \* 2048) ${LOOP_DEVICE} boot-disk.img

給迴環裝置建立一個檔案系統。

$ mkfs.vfat ${LOOP_DEVICE}

掛載迴環裝置。

$ mkdir -p /mnt/os
$ mount -t auto ${LOOP_DEVICE} /mnt/os

在boot-disk.img檔案裡,把Linux核心檔案和initrd檔案複製到第一個分割槽。

$ cp vmlinuz-5.15.79 initrd-busybox-1.35.0.img /mnt/os

在boot-disk.img檔案裡,將載入程式EXTLINUX放進來。

$ mkdir -p /mnt/os/boot
$ extlinux --install /mnt/os/boot

為載入程式建立配置檔案,這樣我們可以指定到底需要載入什麼。

echo "DEFAULT linux" >> /mnt/os/boot/syslinux.cfg
echo "  SAY Booting Simple Linux via SYSLINUX" >> /mnt/os/boot/syslinux.cfg
echo "  LABEL linux"  >> /mnt/os/boot/syslinux.cfg
echo "  KERNEL /vmlinuz-5.15.79" >> /mnt/os/boot/syslinux.cfg
echo "  APPEND initrd=/initrd-busybox-1.35.0.img nomodeset" >> /mnt/os/boot/syslinux.cfg

解除安裝迴環裝置。

$ umount /mnt/os
$ losetup -D

在boot-disk.img檔案中的硬碟開頭部分,安裝MBR引導載入器。

$ dd if=/usr/lib/syslinux/mbr/mbr.bin of=boot-disk.img bs=440 count=1 conv=notrunc

boot-disk.img檔案將會包含隨身碟的引導映象。


 10 
使用Docker構建Linux


上述方法包含許多命令和引數;敲這麼多程式碼很容易出錯。我們可以將命令組合成bash指令碼,為了能夠在Windows 10或11作業系統上構建Linux,建議使用Docker Desktop。

Docker的本質如下:

  1. 在Dockerfile中,描述程式或指令碼的基礎環境結構。

  2. 使用Docker實用工具,基於Dockerfile,以特定格式建立該環境的映象。

  3. 使用相同的工具,可以啟動基於映象的程式或指令碼例項,這些例項執行在隔離的環境中,在Docker術語中稱為Docker容器。

  4. 建立好的映象可以儲存在存映象倉庫中並進行復用。由同一個映象建立的Docker容器是可以在其他計算機上同樣執行。

  5. Dockerfile易於閱讀和學習,也易於釋出。

下面是Dockerfile的內容:

FROM ubuntu:22.10
RUN apt update && apt install --yes make build-essential bc bison flex libssl-dev libelf-dev wget
RUN apt install --yes cpio fdisk extlinux dosfstools qemu-system-x86
RUN apt install --yes vim
ARG APP=/app
ARG LINUX_DIR=$APP/linux
ARG FILES_DIR=$APP/files
ARG SCRIPTS_DIR=$APP/scripts
ENV BUILD_DIR=$APP/build
ENV LINUX_DIR=$LINUX_DIR
ENV FILES_DIR=$FILES_DIR
ENV LINUX_VER=5.15.79
ENV BUSYBOX_VER=1.35.0
ENV BASH_ENV="$SCRIPTS_DIR/bash-env/env" 
COPY ./scripts $APP/scripts
COPY ./files $APP/files
RUN mkdir -p $LINUX_DIR
RUN  ln -s $APP/scripts/start-linux.sh /usr/bin/start &&\
     ln -s $APP/scripts/build-linux.sh /usr/bin/build &&\
     ln -s $APP/scripts/build-image.sh /usr/bin/image
WORKDIR $APP/scripts
CMD build

讓我們快速看一下Dockerfile的主要命令:
  • The FROM command是最重要的,它指定了我們的Linux構建映象將基於哪個系統映象。在本例,我們是Ubuntu 22.10。

  • The RUN command在建立的映象中執行命令。也就是說,RUN之後的命令將像執行Ubuntu 22. 10一樣在命令列中執行。執行該命令後,系統映象將發生變化,因為這些命令更改了其中的檔案系統。

  • The COPY command將檔案從作業系統的檔案系統複製到建立的映象中。與RUN類似,它也會修改映象中的檔案系統。

  • The ARG and ENV commands比較令人困惑,不知道我是否會說清楚,ARG是在建立映象時使用的變數,而ENV是在基於該映象建立容器時使用的變數,同時此變數在容器中是可見的。

  • The WORKDIR command指定啟動基於我們的映象建立的容器時,在哪個目錄下工作。

  • The CMD command命令指定了容器啟動時預設執行的命令。


 11 
使用Docker執行和構建極簡Linux


我們可以試驗一下這個專案。在Windows上,最好在PowerShell中執行Docker。

建立一個Docker映象。

$ git clone 
cd simple-linux
$ docker build --build-arg APP -t simple-linux .

啟動極簡Linux。

$ mkdir linux
cd linux
$ docker run -v ${pwd}:/app/linux --rm -it simple-linux build

建立的Linux目錄將包含構建Llinux核心檔案和初始的根系統映象檔案。

為隨身碟建立一個引導映象。

注意,在Docker中需要使用--privileged選項,因為映象指令碼使用迴環裝置。

$ docker run -v ${pwd}:/app/linux –-privileged --rm -it simple-linux image

如果使用Linux下的Docker Desktop,Docker將需要使用sudo執行,並且需要使用${pwd}而不是$(pwd)。

 12 
製作一個引導映象U


為隨身碟(linux-5.15.79-busybox-1.35.0-disk.img)建立的映象檔案可以使用Win32DiskImager寫入隨身碟。

需要注意的是,在寫入時,隨身碟上現有的資料會丟失!

在將映象寫入隨身碟後,重新啟動計算機並選擇從USB-HDD啟動,即可以從剛建立的隨身碟啟動。如果遇到問題,最可能的情況是,在此之前,需要在BIOS中選擇“Legacy Boot”並禁用“Secure Boot”。


 13 
在筆記本上執行Linux專案


安裝Docker Desktop for Windows後,並在PowerShell中使用一個簡單的命令來構建一個極簡的Linux作業系統。

docker run -v ${pwd}:/app/linux --rm -it artyomsoft/simple-linux build

我們可以使用極簡的Linux命令列,當退出時,我們可以在當前目錄中看到一個Linux核心檔案和一個initrd檔案。


 14 
結論


在這篇文章中,我儘量提供詳細的說明,告訴大家如何從原始碼獲得一個可以工作的Linux系統。

對一些人來說,這篇文章可能看起來很簡單,不值得關注。但是,為了不被細節嚇到,我沒有深入討論諸如BIOS、UEFI、檔案系統、bootloader、glibc庫、詳細的作業系統引導過程、各種規範、動態和靜態連結、Linux核心模組……我只是給出了讓大家能夠接受和理解的最基本的理論,事實上,這能讓大家更快速地理解這個主題,而不是像我之前那也一點一點地收集所有資訊。

我們其實幾乎不會在未來使用這樣的作業系統,這篇文章也只是希望大家能將Linux的抽象知識轉化為對Linux更深的理解和一些小技能。

這篇文章有用嗎?你是否嘗試了從原始碼構建一個極簡的Linux作業系統?歡迎大家在評論中分享你的經驗!

相關連結:

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024420/viewspace-2936625/,如需轉載,請註明出處,否則將追究法律責任。

相關文章