一、源码编译
Linux kernel
- vmlinux:原始未经压缩的内核可执行(ELF)文件,即 kernel 编译出来的原始文件
- vmlinuz:由 vmlinux 经过 OBJCOPY 后再经过压缩后的文件
- zImage:由 vmlinuz 经过压缩后的文件
- bzImage:由 vmlinuz 经过压缩后的文件
wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v6.x/linux-6.5.7.tar.xz
sudo apt install -y tar xz-utils && tar -xf linux-6.5.7.tar.xz
# 配置,查看 make 目标:make help
make distclean && make x86_64_defconfig && make menuconfig
# 编译,会提示缺少一些组件。例如 debian 12 需要 sudo apt install -y make gcc flex bison libncurses-dev libelf-dev bc libssl-dev
# 默认生成 bzImage:linux-6.5.7/arch/x86_64/boot/bzImage
make -j64
busybox
wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2
sudo apt install -y tar bzip2 && tar -xjf busybox-1.36.1.tar.bz2
# 配置静态编译:Settings -> Build static binary (no shared libs)
make distclean && make defconfig && make menuconfig
# 编译好后的程序:busybox-1.36.1/busybox
make -j64
二、运行
需要安装 QEMU,安装文档:https://www.qemu.org/download,例如 debian 使用 sudo apt install -y qemu-system
使用 QEMU 启动系统的几种方式:https://www.qemu.org/docs/master/system/invocation.html#hxtool-8
计算机启动大概流程:主板 BIOS/UEFI -> 引导程序(Bootloader),例如 GRUB -> OS
启动一个 Bootloader 程序,由主板 firmware 加载,这时候还没有 OS
当使用 Legacy BIOS 启动时,Legacy BIOS 把第一个可引导设备的第一个 512 字节加载到物理内存的 7c00 位置,此时处理器处于 16-bit 模式
#define SECT_SIZE 512
.code16 // 16-bit assembly
// Entry of the code
.globl _start
_start:
lea (msg), %si // R[si] = &msg;
again:
movb (%si), %al // R[al] = *R[si]; <---+
incw %si // R[si]++; |
orb %al, %al // if (!R[al]); |
jz done // goto done; ---+ |
movb $0x0e, %ah // R[ah] = 0x0e; | |
movb $0x00, %bh // R[bh] = 0x00; | |
int $0x10 // bios_call(); | | // firmware
jmp again // goto again; --+-----+
// |
done: // |
jmp . // goto done; <--+
// Data: const char msg[] = " ... ";
msg:
.asciz "This is a baby step towards operating systems!\r\n"
// Magic number for bootable device
.org SECT_SIZE - 2
.byte 0x55, 0xAA
编译运行
gcc -ggdb -c mbr.S
ld mbr.o -Ttext 0x7c00
objcopy -S -O binary -j .text a.out mbr.img
qemu-system-x86_64 mbr.img # SeaBIOS
也可以用 gdb 调试运行
qemu-system-x86_64 -s -S mbr.img & # Run QEMU in background
gdb -x init.gdb # RTFM: gdb (1)
# Kill process (QEMU) on gdb exits
define hook-quit
kill
end
# Connect to remote
target remote localhost:1234
file a.out
wa *0x7c00
break *0x7c00
layout src
continue
启动一个 OS 程序,由 Bootloader 加载,可以是任意程序(操作系统也是一个程序)
asm(".long 0x1badb002, 0, (-(0x1badb002 + 0))");
unsigned char *videobuf = (unsigned char *) 0xb8000;
const char *str = "Hello, World !! ";
int start_entry(void) {
int i;
for (i = 0; str[i]; i++) {
videobuf[i * 2 + 0] = str[i];
videobuf[i * 2 + 1] = 0x17;
}
for (; i < 80 * 25; i++) {
videobuf[i * 2 + 0] = ' ';
videobuf[i * 2 + 1] = 0x17;
}
while (1) {}
return 0;
}
编译运行
gcc -c -fno-builtin -ffreestanding -nostdlib -m32 miniboot.c -o miniboot.o
ld -e start_entry -m elf_i386 -Ttext-seg=0x100000 miniboot.o -o miniboot.elf
qemu-system-i386 -kernel miniboot.elf
启动 Linux kernel,和上面启动 OS 一样,只是换了一个程序
通常有两个阶段,kernel 启动后会加载 initramfs,再跳转到 rootfs,这些可以通过参数指定:https://docs.kernel.org/admin-guide/kernel-parameters.html
1、可以先让 kernel 启动后在 initramfs 下执行一个小程序测试下。这个程序不依赖 libc(此时没有 libc 环境),直接执行系统调用
#include <sys/syscall.h>
.globl _start
_start:
movq $SYS_write, %rax // write(
movq $1, %rdi // fd=1,
movq $st, %rsi // buf=st,
movq $(ed - st), %rdx // count=ed-st
syscall // );
movq $SYS_exit, %rax // exit(
movq $1, %rdi // status=1
syscall // );
st:
.ascii "\033[01;31mHello, OS World\033[0m\n"
ed:
编译运行,这里使用 Makefile,make clean && make initramfs && make run
# Reguires statically linked busybox
INIT := /minimal
initramfs:
# Copy kernel and busybox from the host system
@mkdir -p build/initramfs/bin
sudo bash -c "cp ../linux-6.5.7/arch/x86/boot/bzImage build/vmlinuz && chmod 666 build/vmlinuz"
gcc -c minimal.S && ld minimal.o -o build/initramfs/minimal
# Pack build/initramfs as gzipped cpio archive
cd build/initramfs && \
find . -print0 \
| cpio --null -ov --format=newc \
| gzip -9 > ../initramfs.cpio.gz
run:
# Run QEMU with the installed kernel and generated initramfs
sudo qemu-system-x86_64 \
-serial mon:stdio \
-kernel build/vmlinuz \
-initrd build/initramfs.cpio.gz \
-machine accel=kvm:tcg \
-append "console=ttyS0 quiet rdinit=$(INIT)" \
-nographic \
-nodefaults
.PHONY: initramfs run clean
clean:
rm -rf build *.o
2、把上面的测试程序换成 init 脚本,作用是在 initramfs 下创建 shell,https://zhuanlan.zhihu.com/p/619237809
#!/bin/busybox sh
# initrd, only busybox and /init
BB=/bin/busybox
# (1) Print something and exit
$BB echo -e "\033[31minitramfs\033[0m"
#$BB poweroff -f
# (2) Run a shell on the init console
#$BB sh
# (3) ROCK'n Roll!
for cmd in $($BB --list); do
$BB ln -s $BB /bin/$cmd
done
mkdir -p /tmp
mkdir -p /proc && mount -t proc none /proc
mkdir -p /sys && mount -t sysfs none /sys
mknod /dev/null c 1 3
mknod /dev/tty c 4 1
setsid /bin/sh </dev/tty >/dev/tty 2>&1
编译运行
# Reguires statically linked busybox
INIT := /init
initramfs:
# Copy kernel and busybox from the host system
@mkdir -p build/initramfs/bin
sudo bash -c "cp ../linux-6.5.7/arch/x86/boot/bzImage build/vmlinuz && chmod 666 build/vmlinuz"
cp init build/initramfs/
cp ../busybox-1.36.1/busybox build/initramfs/bin/
# Pack build/initramfs as gzipped cpio archive
cd build/initramfs && \
find . -print0 \
| cpio --null -ov --format=newc \
| gzip -9 > ../initramfs.cpio.gz
run:
# Run QEMU with the installed kernel and generated initramfs
sudo qemu-system-x86_64 \
-serial mon:stdio \
-kernel build/vmlinuz \
-initrd build/initramfs.cpio.gz \
-machine accel=kvm:tcg \
-append "console=ttyS0 quiet rdinit=$(INIT)"
.PHONY: initramfs run clean
clean:
rm -rf build
3、修改上面 initramfs 下的 init 脚本,作用是从 initramfs 跳转到 rootfs
#!/bin/busybox sh
echo -e "\033[31minitramfs\033[0m"
busybox mknod /dev/sda b 8 0
busybox mkdir -p /newroot
busybox mount -t ext4 /dev/sda /newroot
# https://man7.org/linux/man-pages/man2/pivot_root.2.html
exec busybox switch_root /newroot/ /sbin/init
跳转到 rootfs 需要准备一块 img 磁盘文件给 qemu
dd if=/dev/zero of=disk.img bs=1G count=1
mkfs -t ext4 disk.img
sudo mkdir /mnt/disk && sudo mount disk.img /mnt/disk/
# 磁盘中只有两个文件
cd /mnt/disk/
sudo mkdir -p tmp proc sys dev bin sbin usr/bin usr/sbin
sudo cp ~/busybox-1.36.1/busybox /mnt/disk/bin/
sudo vim /mnt/disk/sbin/init && sudo chmod +x /mnt/disk/sbin/init
跳转后会执行磁盘上的程序。这里是直接创建 shell,在发行版 Linux 中一般是执行 systemd。rootfs 下的 init 脚本如下
#!/bin/busybox sh
/bin/busybox --install -s
export PATH=/bin:/sbin:/usr/bin
echo -e "\033[31mrootfs\033[0m"
busybox mount -t proc none /proc
busybox mount -t sysfs none /sys
busybox mknod /dev/null c 1 3
busybox mknod /dev/zero c 1 5
busybox mknod /dev/random c 1 8
busybox mknod /dev/urandom c 1 9
# busybox modprobe e1000
busybox mknod /dev/tty c 4 1
# busybox ln -s /bin/busybox /bin/sh
busybox setsid /bin/sh </dev/tty >/dev/tty 2>&1
编译运行
# Reguires statically linked busybox
INIT := /init
initramfs:
# Copy kernel and busybox from the host system
@mkdir -p build/initramfs/bin
sudo bash -c "cp ../linux-6.5.7/arch/x86/boot/bzImage build/vmlinuz && chmod 666 build/vmlinuz"
cp init build/initramfs/
cp ../busybox-1.36.1/busybox build/initramfs/bin/
# Pack build/initramfs as gzipped cpio archive
cd build/initramfs && \
find . -print0 \
| cpio --null -ov --format=newc \
| gzip -9 > ../initramfs.cpio.gz
run:
# Run QEMU with the installed kernel and generated initramfs
sudo qemu-system-x86_64 \
-serial mon:stdio \
-kernel build/vmlinuz \
-initrd build/initramfs.cpio.gz \
-drive file=disk.img,format=raw \
-machine accel=kvm:tcg \
-append "console=ttyS0 quiet rdinit=$(INIT)"
.PHONY: initramfs run clean
clean:
rm -rf build
三、手动安装系统
上面都是指定了内核,这里直接指定磁盘,事先把系统安装到磁盘上由 qemu 启动
1、创建磁盘镜像文件
qemu-img create -f raw disk.img 1G
2、创建分区,安装 GRUB 引导。通常有两种:Legacy + MBR 和 UEFI + GPT
- MBR 分区,在 QEMU 中 Legacy BIOS 是 SeaBIOS(/usr/share/seabios/)
# 分区
fdisk disk.img
fdisk -l disk.img
# 创建磁盘分区映射,默认 /dev/mapper/loopXp1(X 是循环设备号,p1 表示第一个分区)
sudo apt install -y kpartx
sudo kpartx -av disk.img
# 格式化分区
sudo mkfs -t ext4 /dev/mapper/loop0p1
# 挂载
sudo mount /dev/mapper/loop0p1 /mnt/disk/
sudo blkid
# 在磁盘镜像文件中安装 grub 引导
sudo apt install -y grub2
# /mnt/disk/ 是磁盘镜像文件某个分区的挂载目录,/dev/loop0 是磁盘镜像文件的映射目录
sudo grub-install --boot-directory=/mnt/disk/boot/ /dev/loop0
# 卸载磁盘镜像文件
sudo umount /mnt/disk/
sudo kpartx -d disk.img
启动:qemu-system-x86_64 ~/disk.img -serial stdio,会进入 grub 引导界面的命令行
- GPT 分区,在 QEMU 中 UEFI Firmware 是 TianoCore(/usr/share/OVMF/)
# 分区,这里需要分两个区
sudo apt install -y gdisk
gdisk disk.img
gdisk -l disk.img
# 创建磁盘分区映射,默认 /dev/mapper/loopXp1(X 是循环设备号,p1 表示第一个分区)
sudo apt install -y kpartx
sudo kpartx -av disk.img
# 格式化分区
sudo apt install -y dosfstools
sudo mkfs -t vfat -F 32 /dev/mapper/loop0p1 # EFI 引导分区
sudo mkfs -t ext4 /dev/mapper/loop0p2 # 系统分区
# 挂载
sudo mount /dev/mapper/loop0p1 /mnt/disk/
sudo blkid
# 在磁盘镜像文件的引导分区中安装 grub 引导
sudo apt install -y grub-efi
# /mnt/disk/ 是磁盘镜像文件某个分区的挂载目录
sudo grub-install --target=x86_64-efi --efi-directory=/mnt/disk --bootloader-id=GRUB
# 卸载磁盘镜像文件
sudo umount /mnt/disk/
sudo kpartx -d disk.img
启动:qemu-system-x86_64 -drive file=/usr/share/qemu/OVMF.fd,format=raw,if=pflash -drive format=raw,file=/home/my/disk.img -serial stdio -m 1G,会进入 UEFI 引导界面的命令行
qemu-system-x86_64 \
-blockdev node-name=code,driver=file,filename=/usr/share/OVMF/OVMF_CODE.fd,read-only=on \
-blockdev node-name=vars,driver=file,filename=/usr/share/OVMF/OVMF_VARS.fd \
-machine pflash0=code,pflash1=vars \
-drive format=raw,file=/home/my/disk.img \
-serial stdio -m 1G
qemu-system-x86_64 \
-drive format=raw,if=pflash,file=/usr/share/OVMF/OVMF_CODE.fd,read-only=on \
-drive format=raw,if=pflash,file=/usr/share/OVMF/OVMF_VARS.fd,snapshot=on \
-drive format=raw,file=/home/my/disk.img \
-serial stdio -m 1G
qemu-system-x86_64 \
-pflash /usr/share/OVMF/OVMF_CODE.fd \
-pflash /usr/share/OVMF/OVMF_VARS.fd \
-drive format=raw,file=/home/my/disk.img \
-serial stdio -m 1G
View Code
这里如果使用 -bios /usr/share/qemu/OVMF.fd 会无法保存 UEFI 设置。执行下面命令设置引导,也可输入 fs0:\EFI\GRUB\grubx64.efi 直接进入 grub 引导界面的命令行
bcfg boot dump # 查看引导
bcfg boot rm 0 # 删除引导 0
bcfg boot add 0 fs0:\EFI\GRUB\grubx64.efi "GRUB Bootloader" # 添加引导
bcfg boot dump
reset #重启
3、配置 grub 引导启动 linux 内核。在 MBR 中通常 grub 配置和 linux 内核在一个分区,在 GPT 中通常 grub 配置在 EFI 分区中,linux 内核在系统分区中。以 GPT 为例:
# 配置 grub 到引导(EFI)分区
sudo mount /dev/mapper/loop0p1 /mnt/disk/
sudo vim /mnt/disk/EFI/GRUB/grub.cfg # /mnt/disk/boot/grub/grub.cfg
set default="0"
set timeout=3
menuentry "Linux rootfs" {
set root=(hd0,gpt2) # 设置 vmlinuz 所在磁盘分区
linux /boot/vmlinuz root=/dev/sda2 console=ttyS0 rw # sda2 为系统所在分区
}
linux 内核默认启动 root 参数指定分区中的 /sbin/init 脚本,也可通过内核的 init 选项指定。Linux FHS:https://www.ruanyifeng.com/blog/2012/02/a_history_of_unix_directory_structure.html
# 复制内核等程序到系统分区
sudo mount /dev/mapper/loop0p2 /mnt/disk/ && cd /mnt/disk/
sudo mkdir -p tmp proc sys dev bin sbin usr/bin usr/sbin usr/share/udhcpc etc boot
sudo cp ~/linux-6.5.7/arch/x86_64/boot/bzImage /mnt/disk/boot/vmlinuz && sudo chmod +x /mnt/disk/boot/vmlinuz
sudo cp ~/busybox-1.36.1/busybox /mnt/disk/bin/
sudo cp ~/busybox-1.36.1/examples/udhcp/simple.script /mnt/disk/usr/share/udhcpc/default.script
sudo vim /mnt/disk/sbin/init && sudo chmod +x /mnt/disk/sbin/init
rootfs 下的 init 脚本如下
#!/bin/busybox sh
export PATH=/bin:/sbin:/usr/bin
echo -e "\033[31mrootfs\033[0m"
# busybox mount -o remount,rw /
busybox mount -n -t proc none /proc
busybox mount -n -t sysfs none /sys
busybox mount -n -t tmpfs none /dev
busybox mknod /dev/null c 1 3
busybox mknod /dev/zero c 1 5
busybox mknod /dev/random c 1 8
busybox mknod /dev/urandom c 1 9
/bin/busybox --install -s
# busybox modprobe e1000
busybox mknod /dev/tty c 4 1
busybox setsid /bin/sh </dev/tty >/dev/tty 2>&1
再启动就可以进入操作系统了。使用网络: