Busybox构建最小Linux系统

 

前言说明

编译程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# 安装所需程序
sudo apt install build-essential libncurses5-dev

# 获取源码包及解压
wget https://busybox.net/downloads/busybox-1.37.0.tar.bz2
tar xvf busybox-1.37.0.tar.bz2 && cd busybox-1.37.0

make defconfig # 生成默认的配置,最大配置:allyesconfig,最小配置:allnoconfig
make menuconfig # 进入图形化配置界面
# make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- # 其他架构也可更改Makefile中 ARCH ?= aarch64 和 CROSS_COMPILE ? 来编译
make -j$(nproc) # 编译后的程序为源码目录下的 busybox
make install # 默认部署在源码目录下的 _install 中
# make CONFIG_PREFIX=/home/leux/rootfs install # 也可自定义根文件系统的部署路径

# 默认 make install 安装后的目录结构
leux@B650I:~/busybox-1.37.0$ tree _install/
_install/
├── bin
│   ├── arch -> busybox
│   ├── ......
│   └── zcip -> ../bin/busybox
├── linuxrc -> bin/busybox
├── sbin
│   ├── acpid -> ../bin/busybox
│   ├── ......
│   └── zcip -> ../bin/busybox
└── usr
├── bin
│   ├── [ -> ../../bin/busybox
│   ├── [[ -> ../../bin/busybox
│   ├── ascii -> ../../bin/busybox
│   ├── ......
│   └── yes -> ../../bin/busybox
└── sbin
├── addgroup -> ../../bin/busybox
   ├── ......
└── udhcpd -> ../../bin/busybox

6 directories, 404 files
leux@B650I:~/busybox-1.37.0$

# make menuconfig 中的部分选项介绍:
# 使用静态编译(必选,因为内核启动时程序无法加载动态库)
Settings --->
--- Build Options
[*] Build static binary (no shared libs)

# 添加Unicode宽字符支持
Settings --->
--- Library Tuning
[*] Support Unicode # 默认已支持Unicode
[*] Allow wide Unicode characters on output

编译内核

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 安装编译内核所需程序
sudo apt install build-essential flex bison bc libncurses5-dev libelf-dev libssl-dev

# 下载内核源码及解压
wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v6.x/linux-6.12.17.tar.xz
tar -xJvf linux-6.12.17.tar.xz && cd linux-6.12.17

# 使用默认配置编译完成后拷贝内核 bzImage 待用
make x86_64_defconfig
make menuconfig
make -j$(nproc)
cp arch/x86/boot/bzImage /mnt/d/

# make menuconfig 中可能需要调整的选项:
General setup --->
----> [*] Initial RAM filesystem and RAM disk (initramfs/initrd) support # 内核初始化支持initramfs和initrd
Device Drivers --->
[*] Block devices --->
<*> RAM block device support # 内核支持ram disk块设备驱动
(16) Default number of RAM disks # 注册的/dev/ram0块设备数量,默认是16则会有/dev/ram0~ram15
(65536) Default RAM disk size (kbytes) # RamDisk占用的最大内存,这里我写的是64M

# 启动后如果硬盘或分区找不到可把 SCSI device 和 ext4 等驱动打包到内核中

文件系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# 默认从内核启动到系统的执行顺序如下:
# Bootloader => bzimage => /init (/sbin/init) => /etc/inittab => /etc/init.d/rcS => /etc/fstab ...

# 进入busybox根目录
cd busybox-1.37.0/_install/

# 依照嵌入式Linux根文件系统结构,在根目录 _install 中创建其他目录
mkdir -p dev proc sys etc/init.d/ tmp lib var


# 执行 /init 时会读取 /etc/inittab 中的配置
cat > etc/inittab << EOF
::sysinit:/etc/init.d/rcS
::respawn:-/bin/sh
::askfirst:-/bin/sh
::ctrlaltdel:/bin/umount -a -r
EOF


# 配置文件 /etc/inittab 中首行指定系统启动后执行 /etc/init.d/rcS 脚本
cat > etc/init.d/rcS << EOF
#! /bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mdev -s

/bin/mount -a
exec /sbin/init
EOF


# 脚本 /etc/init.d/rcS 要有执行权限,而 inittab 配置文件要可读
chmod a+x etc/init.d/rcS


# 在 rcS 脚本中的 /bin/mount -a 则会读取 /etc/fstab 配置来挂载系统分区
cat > etc/fstab << EOF
tmpfs /tmp tmpfs defaults 0 0
EOF


# 内核启动Initrd时默认会执行 /init 则不需要 linuxrc,最后将其打包压缩待用
# ln -s sbin/init init && rm linuxrc
# find . | cpio -o -H newc | gzip -9c > ../initrd.gz
# 这样制作的镜像虽然能够启动,但因为是运行在内存中的,所以全部更改重启后会丢失
# qemu-system-x86_64 -kernel bzImage -initrd initrd.gz

# 下面来制作根文件系统硬盘文件,这样运行后重启就不会丢失更改
sudo dd if=/dev/zero of=rootfs.ext4 bs=1M count=64
sudo mkfs.ext4 rootfs.ext4
sudo mount -t ext4 rootfs.ext4 /mnt/rootfs/ -o loop
# 将上面 busybox 安装目录中的文件全部复制到制作的硬盘中
sudo cp -r busybox-1.37.0/_install/* /mnt/rootfs/
sudo umount /mnt/rootfs/
# 使用QEMU启动内核和根文件系统,这样制作的虚拟硬盘在QEMU中Linux下为 /dev/sda 设备
qemu-system-x86_64 -kernel bzImage -append "root=/dev/sda rw" -hda rootfs.ext4

启动系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 内核既可以自行编译也可用Debian的内核包linux-image-amd64中的:\data.tar\boot\vmlinuz-6.1.0-0.deb11.31-cloud-amd64
https://mirrors.aliyun.com/debian-security/pool/main/l/linux-signed-6.1-amd64/linux-image-6.1.0-0.deb11.31-cloud-amd64_6.1.128-1~deb11u1_amd64.deb

# 将编译好的 bzImage 与打包压缩好的 initrd内存镜像 或 根文件系统 用 QEMU 启动
qemu-system-x86_64 -m 512M -smp 4 -kernel vmlinuz-6.1.0-31-amd64 -initrd initrd.gz --accel whpx,kernel-irqchip=off
qemu-system-x86_64 -kernel bzImage -append "init=/linuxrc root=/dev/sda rw console=ttyS0" -nographic -drive format=raw,file=*.ext4 -net nic -net tap,ifname=tap0

-kernel # 指定获取到的内核
-initrd # 指定制作的initrd
-nographic # 取消图形输出窗口
-append "console=ttyS0" # 将输出重定向到console,与-nographic搭配使用,将会显示在标准输出stdio
-append "init=/linuxrc" # 内核启动完成后首先执行的,默认执行 /sbin/init
-append "root=/dev/sda rw" # 内核启动完成后用的根系统,默认只读 ro,可读可写 rw
--accel whpx,kernel-irqchip=off # 启用hyper-v加速,加 kernel-irqchip=off 可解决 whpx: injection failed, 问题
-drive format=raw,file=*.ext4 # 用 -hda *.ext4 也可以,但会报警告:WARNING: Image format was not specified for......
-net nic -net tap,ifname=tap0 # 使用TAP虚拟网卡与宿主机通信(Win32下需安装TAP-Windows驱动并将TAP网卡名改为tap0)

设置网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 在Windows中安装tap-windows驱动后,将上网的卡与TAP-Windows网卡桥接或将上网的卡共享给TAP-Windows网卡
# 桥接网卡下虚拟机中获取的地址与局域网IP在同一网段,可与当前局域网设备互相访问
# 共享网卡下虚拟机中获取的地址默认为192.168.137.*,只能单向访问局域网中设备而不能从其他设备直接访问虚拟机,需要做转发
qemu-system-x86_64 -kernel bzImage -append "root=/dev/sda rw" -drive format=raw,file=rootfs.ext4 -net nic -net tap,ifname=tap0

# 启动后手动设置网络的方法(以桥接两网卡为例)
ip link set dev eth0 up # 开启默认关闭的网卡
ip addr add 192.168.1.108/24 dev eth0 # 为网卡设置静态地址
ip route add default via 192.168.1.1 # 添加默认路由
echo "nameserver 223.5.5.5" >> /etc/resolv.conf # 设置域名解析

# 通过DHCP自动获取地址与域名解析
cp busybox-1.37.0/examples/udhcp/simple.script usr/share/udhcpc/default.script # 从源码中获取脚本
chmod +x usr/share/udhcpc/default.script # 为其添加执行权限,没有该脚本 udhcpc 命令将失效
udhcpc eth0 # 自动获取动态地址和配置网络

# 在 /etc/inittab 中加入下面一行语句即可在开机时自动获取地址与设置域名解析:
# 因为 udhcpc 不会循环执行所以不能使用 respawn,必须为once 即执行一次,否则将一直出现udhcpc重启的log信息
::once:/sbin/udhcpc

换Alpine

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 创建个硬盘来存储Alpine系统
sudo dd if=/dev/zero of=rootfs.ext4 bs=1M count=64
sudo mkfs.ext4 rootfs.ext4
sudo mount -t ext4 rootfs.ext4 /mnt/rootfs -o loop

# 获取Alpine官方根文件系统
wget https://mirrors.ustc.edu.cn/alpine/v3.21/releases/x86_64/alpine-minirootfs-3.21.3-x86_64.tar.gz
tar -xzvf alpine-minirootfs-3.21.3-x86_64.tar.gz -C /mnt/rootfs

# 进入获取的Alpine根目录可直接修改修改设置
cd /mnt/rootfs
echo 'Alpine' > etc/hostname
sed -i 's/localhost /Alpine /g' etc/hosts
echo 'nameserver 223.5.5.5' > etc/resolv.conf # 设置临时用的DNS服务器,启动后会被 networking 服务重新设置
sed -i 's/root:\*::0:::::/root:::0:::::/g' etc/shadow # 设置密码为空,!! 或 * 代表没有密码不能登录,! 或 x 使密码失效不能登录
# 可选:在 /etc/inittab 中其他多余的 tty2 ~ tty6 可以删除,但如下必须存在且未被注释
# tty1::respawn:/sbin/getty 38400 tty1 # 必须存在,否则在本地显示中无法直接操作
# ttyS0::respawn:/sbin/getty -L 115200 ttyS0 vt100 # 可选:通过 -append "console=ttyS0" -nographic 在Sehll中直接操作

# 而如下需要切换根目录后再更改的
chroot /mnt/rootfs /bin/sh
apk update && apk add openrc
rc-update add devfs boot
rc-update add procfs boot
rc-update add sysfs boot
rc-update add networking boot

cat > /etc/network/interfaces << EOF
auto eth0
iface eth0 inet dhcp
EOF

cat > /etc/fstab << EOF
tmpfs /tmp tmpfs defaults 0 0
EOF

apk update && apk add openssh-server
echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config # 允许root登录
echo 'PermitEmptyPasswords yes' >> /etc/ssh/sshd_config # 允许无密登录
echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config # 允许密码登录
rc-update add sshd default

相关问题

  1. 从内存虚拟磁盘映像切换到硬盘中的系统
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 创建Initrd中切换根所需目录,再将上面静态编译好的busybox命令拷贝到bin目录
mkdir bin dev proc newroot && cp /src/busybox-static bin/busybox

# 进入 bin 目录后执行如下一行命令来将busybox中所有命令软链接到其中
for cmd in $(./busybox --list); do ln -sf busybox $cmd; done

# 以下为最简的initrd.gz中 /init 启动脚本示例,内核启动加载initrd后会首先执行它来挂载系统资源和/dev/sda,然后再卸载系统资源
# 最后将根文件系统从 initrd.gz 切换到 /dev/sda 后会重新执行 /dev/sda中的/sbin/init 加载 /etc/inittab 配置来挂载系统资源
# 创建或修改如下脚本后还需要为其添加执行权限:chmod +x /initrd.gz/init 否则将无法执行从而导致启动失败
---------------------------------------
#!/bin/busybox sh
mount -t proc none /proc
mount -t devtmpfs none /dev

mount /dev/sda /newroot

umount /proc
umount /dev

exec switch_root /newroot /sbin/init
---------------------------------------

# 最后将其GZ压缩:find . | cpio -o -H newc | gzip -9c > ../initrd.gz
# 如果需要GZ解压:zcat ../initrd.gz | cpio -idmv

# 如上示例中系统启动用的命令,参考教程:https://wiki.gentoo.org/wiki/Custom_Initramfs
qemu-system-x86_64 -kernel bzImage -initrd initrd.gz -drive format=raw,file=x86-rootfs.ext4

  1. 挂载配置文件 /etc/fstab 的加载流程
1
2
3
4
5
6
7
8
9
10
# /init -> /sbin/init	# 会读取 /etc/inittab
# /etc/inittab # 会执行 /etc/init.d/rcS
# /etc/init.d/rcS # 会读取 /etc/fstab
# cat /etc/fstab # rcS中执行 /bin/mount -a 时会读取该配置来挂载
tmpfs /tmp tmpfs defaults 0 0
proc /proc proc defaults 0 0
mdev /dev ramfs defaults 0 0
sysfs /sys sysfs defaults 0 0
devpts /dev/pts devpts defaults 0 0

  1. 路径 /dev/ 下没有相关节点,添加 mdev 命令及内核热插拔支持
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 不需要制作根文件系统时拷贝本机的了,因为后面执行 mdev -s 时会扫描 /sys 自动生成
#cp -a /dev/{null,console,tty,tty1,tty2,tty3,tty4} dev/

# 在启动脚本 init 中添加命令 mdev -s 来扫描 /sys 自动生成
cat > init << EOF
#!/bin/busybox sh
mount -t proc none /proc
mount -t sysfs none /sys
echo /sbin/mdev > /proc/sys/kernel/hotplug # 热插拔设备时内核就会执行 /sbin/mdev 为这个设备在 /dev 下创建设备节点
mdev -s

exec /sbin/init
EOF

# 当有热插拔事件产生时,内核会调用 /proc/sys/kernel/hotplug 中的程序来处理热插拔事件
# mdev的作用就是在系统启动和热插拔或动态加载驱动程序时,自动产生驱动程序所需要的节点文件
# mdev无参数时就是利用hotplug机制来决定创建什么样的设备文件,mdev -s 则通过扫描 /sys/class 和 /sys/block 中所有的类设备目录信息在 /dev 下创建设备节点

# 配置busybox支持mdev命令(默认已包含)
BusyBox 1.37.0 Configuration
Linux System Utilities --->
[*] mdev (20 kb)
[*] Support /etc/mdev.conf
[*] Support subdirs/symlinks
[*] Support regular expressions substitutions when renaming device
[*] Support command execution at device addition/removal
[*] Support loading of firmware
[*] Support daemon mode

# 配置内核支持热插拔,可解决:can't create /proc/sys/kernel/hotplug: nonexistent directory
> Device Drivers > Generic Driver Options > [*] Support for uevent helper
CONFIG_UEVENT_HELPER=y # 必须,/proc/sys/kernel/hotplug 存在的关键
CONFIG_UEVENT_HELPER_PATH="/sbin/mdev" # 可选,也可 echo /sbin/mdev > /proc/sys/kernel/hotplug

  1. 如何修改 default.script 文件地址
1
2
3
4
# 该文件在 busybox-1.37.0/networking/udhcp/Config.src 中定义,用来给udhcpc使用的
config UDHCPC_DEFAULT_SCRIPT default "/usr/share/udhcpc/default.script"
config UDHCPC6_DEFAULT_SCRIPT default "/usr/share/udhcpc/default6.script"

  1. 启动后若反复提示:can't open /dev/tty2: No such file or directory
1
2
3
# 在 /etc/inittab 中有【tty2::askfirst:-/bin/sh】这句导致的,删除这句前面的 tty2 
::askfirst:-/bin/sh