一、源码编译

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 引导界面的命令行

nohup shell启动python脚本 nohup 开机启动脚本_磁盘镜像

nohup shell启动python脚本 nohup 开机启动脚本_linux_02

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

再启动就可以进入操作系统了。使用网络: