[linux_内核相关] 自制启动x86_64架构下的linux(qemu+grub2+busybox+vmlinuz+登录配置+网络配置)

一、名词说明

qemu:虚拟机,用于模拟某种机器的物理架构,这样我们可以模拟从开机加电到最后的操作系统的启动

grub2:bootloader,用于加载linux内核(vmlinuz)和initrd(ram disk,内存文件系统)

busybox:用于制作最终的根文件系统(rootfs),也就是我们在ubuntu等发行版操作系统下的根目录/

ram disk(initrd, initram):其中initrd和initram都是ram disk的一种文件格式而已,上面说到的busybox是最终的根文件系统(rootfs),那ram disk就是linux操作系统用于过渡到rootfs根文件系统的中间根文件系统。其会作为一个临时的根文件系统先被linux操作系统挂载使用,然后再最终切换到rootfs文件系统(上面busybox生成的)

注:linux需要ram disk的原因是,虽然bootloader程序能够识别磁盘(如grub能够识别ext类型的文件系统),但是这些代码是bootloader的,而当bootloader将控制权交由linux操作系统后,linux操作系统是无法预知当前的文件系统格式是哪一种,因此有两种方式来完成,第一种是将所有的文件系统类型的所有驱动代码都静态编译到linux操作系统中,但是这样的话不单是linux内核代码量加大,而且要是迁移或者有升级的文件系统类型,那么需要重新编译linux操作系统。那么第二种就是现在linux现在使用的,bootloader会预先将initrd预先加载到内存中,然后将起始地址和结束地址告知linux操作系统,而linux天生是能操作内存的和initrd是固定文件系统类型的,因此linux能知道该内存文件系统中存放了什么类型的数据。因此也就可以以此作为根目录,并加载其他的文件系统类型的驱动,这样便可以读取到实际磁盘的rootfs,并最终完成切换

vmlinuz:linux实际内核文件,该文件主要由三部分组成:

第一部分(代码模式切换):16位实代码+32位保护模式代码+64位长模式代码

第二部分:解压内核代码

第三部分:被压缩的内核代码,被解压后为elf文件格式,最终内核运行的核心代码,前面第一、二部分都可以说是环境准备阶段

二、内核启动过程

1.开机加电

2.bios程序执行

3.检测各个磁盘的mbr(主引导记录)状态位

4.对于活跃的mbr磁盘(512 byte,末尾为0xAA55),加载磁盘前512byte数据作为bootloader代码(这里就是grub2的代码)

5.bootloader程序运行,即grub2 bootloader程序执行,其会检测分区表(分区表也在512 byte mbr中,其也在mbr的末尾附近,如下图所示:因为grub2 bootloader能够识别ext类型的文件系统,而通过mbr中的分区表也能知道各个分区的位置信息和分区的文件系统类型,这样grub2 bootloader便能够读取到该分区下路径为/boot/grub/grub.cfg的文件信息。这样bootloader便通过该文件所配置的vmlinuz和initrd的位置来进行加载到内存中,然后将控制权切换给vmlinuz内核代码

6.vmlinuz由开头开始执行代码,代码大致执行流程

1)arch/x86/boot/header.S 进入16位实模式代码(物理地址=线性地址=段寄存器<<4+偏移)

2)跳转到arch/x86/boot/main.c 安装idt和gdt(全局描述表,因为后面切换到32位保护模式下时,段寄存器保存的值叫段选择子,其用于和gdtr的地址值一起计算得到该段寄存器实际的起始地址,地址大小和访问等权限,这时物理地址=线程地址=起始地址+偏移)

3)跳转到arch/x86/boot/pmjump.S,cr0控制寄存器的PE位置1开启32位保护模式,然后跳转到保护模式代码,即现在开始寄存器可以使用32位且段寄存器存储为段选择子

4)跳转到arch/x86/boot/compressed/head_64.S,为64位长模式切换做好准备,重新配置gdtr的值,初始化话cr3的值(分页准备),开启PAE,开启EFER.LME,最后设置cr0开启分页模式,这样进入64位长模式。

5)然后调用extract_kernel解压linux得到elf格式的内核代码,然后根据elf文件格式将内核放置到正确的地址空间位置,然后跳转到start_kernel函数,该函数会将bootloader传入的initrd相关参数作为依据来挂载该initrd作为根文件系统,然后根据该initrd读取实际的rootfs根文件系统,然后完成切换,最终调用该rootfs根文件系统下的init(默认/sbin/init)第一个用户程序,以切换到用户态程序和初始化相关的系统环境

三、需要环境

1.ubuntu (快速得到vmlinuz和initrd)

2.mac os x(qemu的安装机子,理论linux和windows安装的qemu都应该效果一样的,这里指出只是因为自己是在mac os x操作系统上测试的)

四、制作启动文件

下面操作在ubuntu下完成

1.获取vmlinuz

为了快速串联起来看到linux启动效果,可直接拿ubuntu下的/boot/vmlinuz-x.x.x-xxx-generic来使用,编译的情况网上已经有很多讲解的文章,等效果能串联起来后,你再尝试编译自己的内核也是可以

2.获取initrd-内存文件系统

同样可以从ubuntu下的/boot/initrd.img-x.x.x-xxx-generic得到,如果你想查看该内存文件系统到底存放了什么,可以通过下面指令来得到,其实其既然是第一个根文件系统,那么也就和我们在ubuntu等发行版的操作系统所看到的根文件系统是类似的。

mkdir initrd
cd initrd
cp /boot/initrd.img-x.x.x-xxx-generic ./initrd.img.gz
gunzip initrd.img.gz
cpio -ivmd < initrd.img

可看到下面类似文件结构:

3.制作启动文件(start.img=mbr+rootfs)

1)创建100M启动文件,用来模拟后续的物理磁盘

touch start.img
dd if=/dev/zero of=./start.img bs=1M count=100

2)磁盘分区

fdisk start.img

输入n创建分区,用于制作rootfs根文件系统,可以看到创建的第一个分区是start.img1,大小为99M(记得输入w保存修改):

 然后通过命令hd(hexdump)命令可以查看前512字节(后面作为mbr结构)的内容:

hd start.img -n 512

可以看到start.img的前512字节末尾为0x55 0xAA(存储时低位在前,高位在后,因此也就是0xAA55,表明为mbr,且为活跃状态),另从00 20 21 00 83 be 32 0c 00 08 00 00 00 00 18 03 00为第一个磁盘分区项,用于描述该分区的相关信息,其也在mbr中:

 3)制作rootfs根文件系统

将start.img的第一个分区格式化为ext4文件系统:

sudo losetup /dev/loop0 start.img // 将start.img关联到/dev/loop0设备,这样linux操作系统可以将start.img当作块设备(磁盘)来进行操作
sudo kpartx -av /dev/loop0 // 将start.img的分区生成设备到/dev/mapper中,这里第一个分区即为/dev/mapper/loop0p1
sudo mkfs.ext4 /dev/mapper/loop0p1 // 将start.img的第一个分区格式化为ext4文件系统

挂载第一分区,开始制作rootfs根文件系统:

mkdir rootfs
sudo mount /dev/mapper/loop0p1 rootfs // 这样便可以对第一个分区的文件系统进行文件目录等操作了

编译busybox(这里选择动态编译,后续的网络ping静态编译会出bug):

mkdir busybox
cd busybox
wget https://busybox.net/downloads/busybox-1.29.3.tar.bz2
tar -xjf busybox-1.29.3.tar.bz2
cd busybox-1.29.3
make menuconfig // 生成busybox的配置文件,因为需要动态编译,默认这个版本就是动态编译,所以直接退出保存就可以

make
make install //会将文件到安装到当前目录的_install下

 

因为是动态编译,需要将查看bin和sbin下的命令需要什么动态.so文件,使用ldd指令查看即可:

ldd bin/ping

 会发现主要依赖的是/lib/x86_64-linux-gnu和/lib64目录下的文件,因此拷贝过来就可以了:

sudo mkdir lib
sudo cp -r /lib/x86_64-linux-gnu/ ./lib/
sudo cp -r /lib64 ./

准备工作准备得差不多了,开始制作rootfs根文件系统了:

cd rootfs // 到达之前挂载第一分区的rootfs目录下
sudo cp -r busybox/busybox-1.29.3/_install/* ./ // 将之前编译的busybox的_install目录下的所有文件都拷贝过来

根据上面的操作,我们已经有个rootfs根文件系统了,该rootfs根文件系统安装在第一分区

现在我们已经有了mbr,且mbr中已经有第一个分区的描述,而第一个分区已经制作成rootfs根文件系统了,那么还缺少bios程序所跳转到mbr中的grub2 bootloader程序和grub2查找到vmlinuz和initrd.img的路径位置。

4)安装grub2 bootloader

cd rootfs
sudo mkdir -p boot/grub
sudo grub-install --root-directory=/tmp/rootfs_learn/rootfs /dev/loop0 // 意思是将grub2 bootloader程序安装到start.img的mbr中(前512字节的代码内容),以及将grub启动过程中需要的一些驱动文件安装到根目录下的boot/grub目录下

 可以通过查看hd start.img -n 512,可以看到前面的内容已经不再全是0,而且也能通过右边字符看到一些grub的描述,可以知道grub将bootloader的代码安装到了start.img的mbr中:

另可以查看ls boot/grub/ 目录:

这样grub2 bootloader便算安装成功(后面会说一下grub.cfg的配置)

5.告知vmlinuz和initrd.img的位置信息给grub2:

在这里我们首先回顾一下我们现在有哪些了,我们有mbr的bootloader程序,我们也已经为磁盘start.img的第一个分区划分了存储容量,并且文件系统格式为ext4和制作成了rootfs根文件系统。而grub2 bootloader程序是能够识别ext4文件系统,因此其能够知道第一个分区到底存储了什么内容,因此我们只需要将vmlinuz和initrd.img存放到第一分区,grub2 bootloader是有能力读取到这两个文件的。

注:由于笔误,下面的initrd.img和initrd是指同一个文件

cd rootfs
sudp cp -r /boot/vmlinuz-x.x.x-xxx-generic ./boot/
sudo cp -r /boot/initrd.img-x.x.x-xxx-generic ./boot/
sudo ln -s ./boot/vmlinuz-x.x.x-xxx-generic ./vmlinuz
sudo ln -s ./boot/initrd.img-x.x.x-xxx-generic ./initrd

现在的rootfs目录便是如下情况: 

5.1 因为使用的是ubuntu的initrd.img,其被作为根文件系统时会执行其/init脚本,会挂载一些目录如/dev, /proc, /run等,因此还需要创建一些额外的目录:

cd rootfs
sudo mkdir dev proc run etc root sys

 6.一切准备就绪,执行该启动文件测试效果:

cd ../ // 到达与rootfs目录同级
sudo umount /dev/mapper/loop0p1 // 卸载start.img的第一分区
sudo dmsetup remove /dev/mapper/loop0p1 // 删除start.img第一分区所生成的设备
sudo losetup -d /dev/loop0 // 取消start.img关联到/dev/loop0设备

这样start.img启动文件便制作完成,现在将start.img拷贝到装有qemu的机器上(我自己的是macbook):

qemu-system-x86_64 -m 1024M -drive format=raw,file=start.img

启动界面会到达grub命令行模式,虽然grub能够识别ext4文件系统,但是我们并没有告诉它具体的vmlinuz和initrd.img的路径,因此我们通过手动方式来告知grub这两个位置(其中set root是告知grub路径/vmlinuz和/initrd所在的磁盘分区,这里hd0,msdos1表示第一块磁盘的第一个分区即start.img的第一个分区,set prefix是告知grub后续的命令和驱动的目录位置在哪,如这里的linux命令和initrd命令),最后执行启动boot即可:

出现下面这个,便表示linux内核被成功启动起来了:

五、附加操作-优化

1.第一个程序init程序(ubuntu现在的第一个程序已经被systemd替代)

当linux启动后会启动第一个init用户程序,该程序会先读取/etc/inittab文件,然后根据/etc/inittab文件的配置信息来初始化系统环境

所以/etc/inittab可以作为我们开机启动项配置:

cd rootfs
cd etc
sudo touch inittab

# 创建第一个初始化脚本
sudo mkdir init.d
cd init.d
sudo touch rcS
sudo chmod +x rcS

/etc/initiab,意思是linux初始化工程中调用/etc/init.d/rcS脚本程序,然后会再执行/bin/sh进入shell命令模式:

::sysinit:/etc/init.d/rcS
::respawn:-/bin/sh

/etc/init.d/rcS,细心的你会发现默认情况下,linux挂载的根文件系统时read-only file system(只读文件系统),因此可以通过下面两个指令使根文件系统可以写:

#!/bin/sh

mount -o remount,rw /
mount -o remount,rw

这样配置后执行过程->/sbin/init->读取/etc/inittab->然后调用/etc/init.d/rcS初始化->/bin/sh进入shell命令模式

注:上面的操作都是在ubuntu下重新编辑start.img的第一分区rootfs,因为一开始根文件系统不可写

注:下面操作均可以在qemu启动的rootfs中执行

2.配置用户登录信息

我们这里用/bin/login,/bin/login的简要执行过程:

1.其会首先获取当前机子的hostname,然后展示 hostname login:

2.当用户输入用户名和密码后,其会读取/etc/passwd和/etc/shadow得到正确的用户名和密码

3.用户登录成功后,会默认调用/etc/passwd末尾所配置的执行程序如/bin/sh或者/bin/bash

因此首先配置hostname:

我们将hostname文件配置到/etc/hostname

cd /etc
echo "busybox" > hostname
hostname -F /etc/hostname

为了每次开机启动都能加载该hostname,我们将hostname -F指令写到rcS文件内

#!/bin/sh

mount -o remount,rw /
mount -o remount,rw

hostname -F /etc/hostname

这样便配置好hostname了

然后我们可以将ubuntu下的/etc/passwd和/etc/shadow文件拷贝过来(即是用该root/密码进行登录),然后只保留root用户的那一行

最后修改inittab文件即可:

::sysinit:/etc/init.d/rcS
::respawn:-/bin/login

3.改变/bin/sh的前缀展示

现在展示的是"/ #"

首先/bin/sh加载的配置文件顺序先读取/etc/profile的全局配置文件,然后再读取~/.bashrc下的配置文件

而/bin/sh前缀展示根据环境变量PS1来进行显示,因此我们可以通过将PS1配置在/etc/profile下

# 现在rootfs已经可以写了
cd /etc
touch profile
vi profile
PS1='[\u@\h \W]\#'
export PS1

意思是展示[用户名@hostname 路径]#,展示效果如下:

4.配置网络

默认qemu-system-x86_64,qemu内置的是user model network stack。

根据该图可以知道,我们要链接外网需要通过qemu默认配置的ip:10.0.2.2,因此将10.0.2.2作为我们的网关即可对外联网。

首先查看当前的网卡有哪些,ifconfig -a,这里可以看到有一个ens3和lo,ens3作为我们的外网网卡,而lo自然是我们的本地回环网卡:

因此我们可以配置网络到/etc/network/interfaces:

auto lo
iface lo inet loopback
auto ens3
iface ens3 inet static
address 10.0.2.15
netmask 255.255.255.0

然后执行ifup ens3 up启动该网卡配置:

ifdown ens3 down
ifup ens3 up

然后配置 路由,意思是除本机ip外,其他ip均从该网关出去:

route add -net 0.0.0.0 netmask 0.0.0.0 gw 10.0.2.2 dev ens3

配置域名解析服务/etc/resolv.conf:

nameserver 8.8.8.8
nameserver 8.8.4.4

可以测试ping www.baidu.com

现在网络已经通了,为了避免下次机器启动还是要重新配置,/etc/init.d/rcS添加如下配置:

#!/bin/sh

mount -o remount,rw /
mount -o remount,rw

hostname -F /etc/hostname



ifdown ens3 down
ifup ens3 up

route add -net 0.0.0.0 netmask 0.0.0.0 gw 10.0.2.2 dev ens3

另一种方式来设置网络,是通过dhcp来自动获取ip,流程和我们手动差不多:

0.先启动ens3网卡,ifconfig ens3 up

1.udhcpc -i ens3,获得到了可以分配给我们的ip(获取过程为udhcpc通过ens3网卡广播dhcp协议)

2.udhcpc会执行/etc/share/udhcpc/default.script脚本

3.脚本通过ifconfig ens3 ip netmask设置该网卡的ip和子网掩码

4.脚本继续调用route add default dev gw操作,设置外网网关路由

5.脚本最后会设置/etc/resolv.conf的域名解析服务地址配置

所以最终我们要做的就是:

1.拷贝busybox/examples/udhcp/simple.script到rootfs下的/usr/share/udhcpc/default.script

2.启动网卡,ifconfig ens3 up

3.调用udhcpc,udhcpc -i ens3即可

5.配置grub2菜单启动:

配置/boot/grub/grub.cfg即可,当选择了其中一个菜单后,grub默认会执行boot指令,故不需要再输入boot指令,UUID参数如何得到,可以通过blkid /dev/sda1得到该设备的UUID:

set default=0

menuentry 'Linux, busybox, root=/dev/sda1' {
    linux /vmlinuz root=/dev/sda1
    initrd /initrd
}

menuentry 'Linux, busybox, root=UUID' {
    linux /vmlinuz root=UUID=80f65534-ae79-4bcb-84db-b1b50c11ceb1
    initrd /initrd
}

menuentry 'Reboot' {
    reboot
}

menuentry 'Shutdown' {
    halt
}

效果如下:

发布了140 篇原创文章 · 获赞 28 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/qq_16097611/article/details/83278712