编译 Linux kernel 并用 QEMU 运行
一、源码编译
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:https://www.busybox.net/FAQ.html#getting_started
curl -LOJ 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 LDFLAGS="--static" make -j 64
二、运行
需要安装 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
这里如果使用 -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
再启动就可以进入操作系统了。使用网络:https://www.cnblogs.com/jhxxb/p/13522902.html
https://www.linuxfromscratch.org
https://refspecs.linuxfoundation.org

浙公网安备 33010602011771号