当前位置: 首页 > news >正文

《操作系统真象还原》第五章(3)——载入内核

文章目录

    • 前言
    • 一个报错的解决
    • 用 C 语言写内核
      • GCC版本问题
      • 编译连接,得到可执行文件
    • 将内核载入内存
      • 写两个脚本
      • 修改loader.S
        • 加载内核
        • 初始化内核
        • 跳转到内核区
      • 运行Bochs
    • 目前的完整源码
      • loader.S
      • boot.int
    • 结语

前言

这是第五章第三部分,主要完成内核相关的内容。

前两部分链接:

《操作系统真象还原》第五章(1)——获取内存容量-CSDN博客

《操作系统真象还原》第五章(2)——启用内存分页机制-CSDN博客

这篇博客也是第五章最后一篇博客,最后会对第五章进行一个总结。

全文25000字+,请耐心阅读。


一个报错的解决

第五章第二部分结尾出现了一个报错,昨天时间不够暂且搁置,今天把它解决。对照书上的截图和代码后发现源代码有问题,下面贴出修改后的代码。

;将页目录0和0x300项都指向第一个页表的地址,都对应0-4mb的物理地址。
	mov [PAGE_DIR_TABLE_POS+0x0],eax	;0项
	mov [PAGE_DIR_TABLE_POS+0xc00],eax	;0x300项转化为10进制是3*256=768 
										;错误代码是mov [PAGE_DIR_TABLE_POS+0x300],eax,错误原因是一个项4字节忘了×4
	
	sub eax,0x1000
	mov [PAGE_DIR_TABLE_POS+4092],eax	;在页目录最后一项1023项写入页目录本身的地址,所以是4096-4

编译源代码、写入硬盘、运行bochs

nasm -I include/ -o loader.bin loader.S
dd if=/home/hongbai/bochs/loader.bin of=/home/hongbai/bochs/bin/c.img bs=512 count=4 seek=2 conv=notrunc
cd bin
./bochs -f bochsrc.disk

6,c,ctrl+c,然后输入info gdt查看gdtr寄存器情况,结果如下

可以看到原来的报错已经消失,成功显示了四个段提示符(0,代码段,数据段,显存段)

输入info tab查看虚拟地址和物理地址的映射情况,结果如下

和理想中的情况一致。


用 C 语言写内核

这部分需要注意的是我们写操作系统必须避开依赖,所以只支持c原生语法结构,不能用c标准库。

在对应路径下(对我而言就是~/bochs)新建kernel文件夹,在kernel文件夹下新建文本文件,命名为main.c,代码如下:

int main(void){
	while(1);
	return 0;
}

这个简单粗暴的死循环仅仅是为了演示 elf 文件解析以及加载内核的作用。

GCC版本问题

参考博客链接:关于Ubutun20及以上安装gcc-4.4 gcc-4.4-multilib的方法_gcc-multilib安装-CSDN博客

输入gcc -v查看gcc版本,结果如下,我的版本是13.3.0,需要把它降级为4.4,便于兼容。

我的ubuntu版本是24.4,下面添加添加14.4的更新源

sudo add-apt-repository 'deb http://archive.ubuntu.com/ubuntu/ 	trusty main'
sudo add-apt-repository 'deb http://archive.ubuntu.com/ubuntu/ trusty universe'
sudo apt update

报错

W: GPG 错误:http://archive.ubuntu.com/ubuntu trusty Release: 由于没有公钥,无法验证下列签名: NO_PUBKEY 40976EAF437D05B5 NO_PUBKEY 3B4FE6ACC0B21F32
E: 仓库 “http://archive.ubuntu.com/ubuntu trusty Release” 没有数字签名。
N: 无法安全地用该源进行更新,所以默认禁用该源。
N: 参见 apt-secure(8) 手册以了解仓库创建和用户配置方面的细节。

因为 Ubuntu 的软件仓库使用 GPG 密钥进行签名验证,而较旧版本的密钥可能已从新系统中移除。所以我们需要先导入密钥。下面三行代码是下载密钥,导入密钥到文件,再次更新更新源的代码。

gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 40976EAF437D05B5 3B4FE6ACC0B21F32
gpg --export 40976EAF437D05B5 | sudo tee /etc/apt/trusted.gpg.d/ubuntu-trusty-main.gpg > /dev/null
gpg --export 3B4FE6ACC0B21F32 | sudo tee /etc/apt/trusted.gpg.d/ubuntu-trusty-universe.gpg > /dev/null
sudo apt update

结果如下图

输入sudo apt-get install -y gcc-4.4 g++-4.4安装4.4版本的gcc和g++,然后输入dpkg -l | grep gcc查看gcc版本,结果如下

目前已经安装了4.4gcc,输入下面两行代码,把4.4设置为高优先级并查看。

sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.4 50
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 40
sudo update-alternatives --config gcc

结果如下,可以看到4.4已经是默认版本。

编译连接,得到可执行文件

输入gcc -c -o kernel/main.o kernel/main.c,编译main.c,得到main.o,这是一个待重定位文件,还是个半成品。

输入ld kernel/main.o -Ttext 0xc0001500 -e main -o kernel/kernel.bin

-Ttext是指定起始虚拟地址,我们设定为0xc0001500,这个数字后面有解释。-e是指定程序从哪个起始地址开始运行,可以用标号,如果不指定,默认是从_start符号处开始运行。

最后得到kernel.bin,这是个可执行文件,最终结果如下

我们输入以下两行代码,查看main.c最终转化为的汇编代码是什么样的。

gcc -S -o /tmp/main.S main.c
cat -n /tmp/main.S

结果如下,和设想的一样,转化为类似jmp $的代码

输入readelf -e kernel.bin,查看elf重点信息,结果如下

ELF 头:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  类别:                              ELF64
  数据:                              2 补码,小端序 (little endian)
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI 版本:                          0
  类型:                              EXEC (可执行文件)
  系统架构:                          Advanced Micro Devices X86-64
  版本:                              0x1
  入口点地址:               0xc0001500
  程序头起点:          64 (bytes into file)
  Start of section headers:          4432 (bytes into file)
  标志:             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         4
  Size of section headers:           64 (bytes)
  Number of section headers:         7
  Section header string table index: 6

节头:
  [号] 名称              类型             地址              偏移量
       大小              全体大小          旗标   链接   信息   对齐
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         00000000c0001500  00000500
       0000000000000006  0000000000000000  AX       0     0     1
  [ 2] .eh_frame         PROGBITS         00000000c0002000  00001000
       0000000000000034  0000000000000000   A       0     0     8
  [ 3] .comment          PROGBITS         0000000000000000  00001034
       000000000000002a  0000000000000001  MS       0     0     1
  [ 4] .symtab           SYMTAB           0000000000000000  00001060
       0000000000000090  0000000000000018           5     2     8
  [ 5] .strtab           STRTAB           0000000000000000  000010f0
       0000000000000025  0000000000000000           0     0     1
  [ 6] .shstrtab         STRTAB           0000000000000000  00001115
       0000000000000034  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  D (mbind), l (large), p (processor specific)

程序头:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x00000000c0001000 0x00000000c0000000
                 0x0000000000000120 0x0000000000000120  R      0x1000
  LOAD           0x0000000000000500 0x00000000c0001500 0x00000000c0001500
                 0x0000000000000006 0x0000000000000006  R E    0x1000
  LOAD           0x0000000000001000 0x00000000c0002000 0x00000000c0002000
                 0x0000000000000034 0x0000000000000034  R      0x1000
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x10

 Section to Segment mapping:
  段节...
   00     
   01     .text 
   02     .eh_frame 
   03     

将内核载入内存

内核文件是kernel.bin,loader把它从硬盘载入内存,完成接力棒最后一棒。先把kernel写入硬盘,代码如下

dd if=/home/hongbai/bochs/kernel/kernel.bin of=/home/hongbai/bochs/bin/c.img bs=512 count=200 seek=10 conv=notrunc

写入虚拟硬盘的第10扇区,设置为200个扇区大小,200*512B=100KB,写大一点,方便我们后续扩展。

gcc -c -o main.o main.c
ldmain.o -Ttext 0xc0001500 -e main -o kernel.bin
dd if=/home/hongbai/bochs/kernel/kernel.bin of=/home/hongbai/bochs/bin/c.img bs=512 count=200 seek=10 conv=notrunc

写两个脚本

考虑到后续我们会多次修改kernel,干脆把编译、连接、写入硬盘三部分工作集成起来,写一个脚本。

在kernel文件夹下新建build_kernel.sh,代码如下

#!/bin/bash
# 编译链接main.c并将可执行文件写入硬盘的脚本

# 1. 编译main.c生成main.o
echo "正在编译main.c..."
gcc -c -o main.o main.c
if [ $? -ne 0 ]; then
    echo "编译失败!"
    exit 1
fi

# 2. 链接main.o生成kernel.bin
echo "正在链接目标文件..."
ld main.o -Ttext 0xc0001500 -e main -o kernel.bin
if [ $? -ne 0 ]; then
    echo "链接失败!"
    exit 1
fi

# 3. 将kernel.bin写入磁盘镜像
echo "正在写入磁盘镜像..."
dd if=/home/hongbai/bochs/kernel/kernel.bin of=/home/hongbai/bochs/bin/c.img bs=512 count=200 seek=10 conv=notrunc
if [ $? -ne 0 ]; then
    echo "写入磁盘失败!"
    exit 1
fi

echo "操作成功完成!"

保存,然后输入chmod +x build_kernel.sh给予执行权限。

同理我们再写一个用于构建loader的脚本

#!/bin/bash
# 编译链接写入loader的脚本

# 定义路径变量(方便修改)
LOADER_SOURCE="loader.S"
LOADER_BIN="loader.bin"
BOCHS_DIR="/home/hongbai/bochs"
OUTPUT_IMG="$BOCHS_DIR/bin/c.img"
INCLUDE_DIR="include/"

# 1. 使用nasm编译loader.S
echo "正在编译loader.S..."
nasm -I $INCLUDE_DIR -o $LOADER_BIN $LOADER_SOURCE
if [ $? -ne 0 ]; then
    echo "编译失败!"
    exit 1
fi

# 2. 将loader.bin写入磁盘镜像
echo "正在写入磁盘镜像..."
dd if=$BOCHS_DIR/$LOADER_BIN of=$OUTPUT_IMG bs=512 count=4 seek=2 conv=notrunc
if [ $? -ne 0 ]; then
    echo "写入磁盘失败!"
    exit 1
fi

echo "loader编译并写入成功完成!"

放在bochs文件夹。给权限chmod +x build_loader.sh

运行指令是./build_loader.sh(加载器)和./build_kernel.sh(内核)

修改loader.S

总共要修改两个部分:1.加载内核,即把内核从硬盘加载到内存。2.初始化内核,作用是在分页后,把加载到内存的elf文件放到相应的虚拟内存地址,并jmp到那里,去执行内核。

加载内核

这部分我们放到分页前。

内核要写到内存的哪里?低1MB中,0-4ff存了中断向量表和bios数据,9fc00-9ffff也存了bios数据,除了这些地方都可用,范围是0x500-0x9fbff。我们要先把kernel.bin加载到内存,再把这个bin解析后展开成内核映像,这个内核映像才是实际运行的操作系统。我们把bin放到高地址,内核映像放到低地址,bin加载完成后可以被覆盖,最终选择了0x70000,这个地址到高地址边界还有200k左右空间,足够我们载入bin了,后续被覆盖也无所谓。

这部分代码类似mbr加载loader的代码。

先修改boot.inc,加入以下代码

;-------------------- 内核kernel --------------------
KERNEL_START_SECTOR	equ 0xa		;硬盘扇区号
KERNEL_BIN_BASE_ADDR	equ 0x70000	;内存地址
KERNEL_ENTRY_POINT	equ 0xc0001500	;虚拟地址入口

然后就是加载kernel部分

	mov eax,KERNEL_START_SECTOR	;kernel.bin所在的扇区号
	mov ebx,KERNEL_BIN_BASE_ADDR	;从磁盘读出后,写入到 ebx指定的地址
	mov ecx,200			;读入的扇区数
	call rd_disk_m_32		;写入内存

关于rd_disk_m_32这部分代码,整体和mbr.S中的rd_disk_m_16一致,只需要把最后循环读入字节的地址寄存器bx改为ebx(我们是从0x70000开始读,bx显然不够大)。这里我就不再贴出来了。

初始化内核

初始化的含义是,根据 elf 规范将内核文件中的段(segment)展开到(复制到)内存中的相应位置。接着给出在哪段物理内存存放内存映像,以及内存映像的虚拟地址入口。

先看物理地址。loader.bin加载到内存0x900处,最终大小在2000字节以内,因为这里面定义了gdt,我们不要覆盖它,所以地址要在0x900+2000后,同时地址要尽可能的低一点,干脆选择0x1500这个地址作为内存映像的起始地址,那么对应的虚拟地址就是0xc0001500,这也是我们之前连接时指定的虚拟地址位置。

整体思路是从elf头中获取程序头数量、每个程序头尺寸、第一个程序头的文件偏移地址,循环程序头数量次,每次先检验这个程序头是否被使用,如果被使用就跳过,否则通过程序头获取段文件偏移地址、段要放到的虚拟地址位置、段字节数,然后利用一个memset函数进行复制,这样最终就展开了所有的segment。

代码如下:

;----------将kernel.bin中的segment展开 -----------
kernel_init:
	xor eax,eax
	xor ebx,ebx		;记录程序头表的地址,即e_phoff,也就是第一个程序头的偏移地址
	xor ecx,ecx		;记录程序头表中,程序头的数量e_phnum,也就是segment数量
	xor edx,edx		;记录每个程序头的尺寸,即e_phentsize
	
	mov dx,[KERNEL_BIN_BASE_ADDR+42]	;程序头大小
	mov ebx,[KERNEL_BIN_BASE_ADDR+28]
	add ebx,KERNEL_BIN_BASE_ADDR		;程序头地址
	mov cx,[KERNEL_BIN_BASE_ADDR+44]	;程序头数量
	
.each_segment:
	cmp byte [ebx+0],PT_NULL;若相等,说明这个程序头未使用
	je .PTNULL				;未使用则跳转
	;模拟c语言里的mem_cpy,输入三个参数:源地址,目标地址,拷贝内容字节数
	push dword [ebx+16]	;获取段字节数p_filesz
	mov eax,[ebx+4]		;距程序头偏移量为4字节的位置是p_offset
	add eax,KERNEL_BIN_BASE_ADDR
	push eax			;段偏移地址,也就是源地址
	push dword [ebx+8]	;目标地址p_vaddr
	call mem_cpy
	add esp,12			;一次是向栈段压入3*4=12字节的数据
	
.PTNULL:
	add ebx,edx			;指向下一个程序头
	
	loop .each_segment		
	ret
	
mem_cpy:
	cld				;将DF标志位设置为0,确保指针递增
	push ebp		;我们用ebp指针配合sp,而不是esp,干脆在这里存进栈备份一下
	mov ebp,esp
	push ecx		;ecx保存着程序头数,后续会改为段字节数,这里备份一下
	
	mov edi,[ebp+8]		;源地址
	mov esi,[ebp+12]	;目标地址
	mov ecx,[ebp+16]	;我们是逐字节复制,字节数也就是要复制的次数
	rep movsb		;rep是重复,次数是ecx,movsb是按字节从ds:ei复制内容到es:di处,和cld配合使用,让ei和di自动自增
	
	pop ecx			
	pop ebp			;恢复这两个参数,不影响上面循环的进行
	ret
跳转到内核区

这部分代码非常短,一是让cpu跳转到内核区,二是给出虚拟地址入口,起到一个衔接作用。

;-------------------- 跳转到内核区 --------------------------------
	jmp SELECTOR_CODE:enter_kernel	;刷新流水线
enter_kernel:
	call kernel_init
	mov esp,0xc009f000		;设立栈段的起始地址,这个数字是最接近高地址的和4KB对齐的数
	jmp KERNEL_ENTRY_POINT		;写在宏里面,0xc0001500

运行Bochs

编译写入loader,编译连接写入kernel,启动bochs并调试

gcc -m32 -c -o main.o main.c
ld -m elf_i386 main.o -Ttext 0xc0001500 -e main -o kernel.bin
dd if=/home/hongbai/bochs/kernel/kernel.bin of=/home/hongbai/bochs/bin/c.img bs=512 count=200 seek=10 conv=notrunc

nasm -I include/ -o loader.bin loader.S
dd if=/home/hongbai/bochs/loader.bin of=/home/hongbai/bochs/bin/c.img bs=512 count=4 seek=2 conv=notrunc

cd bin
./bochs -f bochsrc.disk

这部分有个要点:我们在切换gcc版本为4.4后,默认编译方式仍然是64位,要在编译main时加上参数(所以之前的脚本还需要修改),指定以32位编码,cpu架构是80386,编译连接后输入readelf -e kernel.bin,看是否如下图所示

最终结果如下

关于这个9是怎么来的,可以看下面代码中“跳转到内核区”部分的注释。


目前的完整源码

贴一下目前完整的源码,包括loader.S和boot.inc,mbr和kernel比较简短简单就不贴了。

loader.S

%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
	jmp loader_start
;-------------------- 数据段 --------------------------------

;这部分完成GDT和4个段描述符的构建
;编译后程序的地址是越来越高的,所以代码要先开辟低地址再开辟高地址。8字节64位是一个段描述符。dd命令开辟出一个4字节32位空间。
GDT_BASE:
	dd 0x00000000
	dd 0x00000000		;第0个段描述符无法使用,开辟出来空着
CODE_DESC:
	dd 0x0000ffff		;前四位是16位段基址,设置为0,后四位是16位段界限,设置为1。高位在前低位在后
	dd DESC_CODE_HIGH4	;提前在配置文件中写好的高32位
DATA_STACK_DESC:
	dd 0x0000ffff
	dd DESC_DATA_HIGH4	;数据和栈用相同的段描述符
VIDEO_DESC:
	dd 0x80000007		;这部分请看博客里的说明
	dd DESC_VIDEO_HIGH4
;计算gdt大小,同时预留出60个段描述符的空间 
;这部分为加载gdt做准备。dq定义4字8字节64位。times是nasm提供的伪指令,作用是循环。以后我们还要向gdt里添加别的表符,这里提前留出位置。
GDT_SIZE 	equ $-GDT_BASE
GDT_LIMIT 	equ GDT_SIZE-1
	times 59 dq 0
	times 5 db 0
;构建选择子
;选择子放在段寄存器里,16位大小,高13位是gdt的索引,第2位是ti位,指示索引是gdt的还是ldt的,0、1两位是特权级位。
SELECTOR_CODE 	equ (0x0001<<3)+TI_GDT+RPL0	;0001就是下标1,左移3位相当于*8,因为一个表项是8字节
SELECTOR_DATA 	equ (0x0002<<3)+TI_GDT+RPL0
SELECTOR_VIDEO 	equ (0x0003<<3)+TI_GDT+RPL0
;设置保存内存容量的标号
;关于这个标号的地址,loader.S的起始地址是0x900,这行前面有64个8字节的段描述符,所以这里是0x900+0x200=0xb00
	total_mem_bytes dd 0	;初始为0,最终变成总内存容量
;定义GDT指针
gdt_ptr:
	dw GDT_LIMIT		;前2字节是gdt的界限
	dd GDT_BASE		;后4字节是gdt的起始位置
;ards缓冲区地址和数量
	ards_buf times 244 db 0	;ards缓冲区,存放ards
	ards_nr dw 0		;用于记录ards结构体的数量
	
;-------------------- 实现三种获取内存容量的方法 --------------------------------

loader_start:
	mov dword [GDT_BASE],0		
	mov dword [GDT_BASE+4],0;显式清零
	mov sp,LOADER_BASE_ADDR	;先初始化了栈指针
	
;方法1:利用0xe820获取内存
;以下部分,通过0xe820获取所有的ards
	xor ebx,ebx		;用异或置零,初始ebx设置为0,后续我们不需要再处理
	mov edx,0x534d4150	;固定值
	mov di,ards_buf		;es:di指向缓冲区,es在mbr设置,这里修改di即可
	
.e820_mem_get_loop:
	mov eax,0x0000e820	;因为执行完int 0x15后eax,ebx,ecx会变化,所以每次循环都要重新设置
	mov ecx,20		;返回的字节数,固定为20
	
	int 0x15
	jc .e820_failed_so_try_e801 ;如果e820失败,尝试e801
	add di,cx		;+20字节指向下一个ards
	inc word [ards_nr]	;记录ards数量
	cmp ebx,0		;如果ebx=0且cf=0,所有ards全部返回
	jnz .e820_mem_get_loop 	;如果ebx!=0,继续循环
	
;遍历ards,找到最大的32位基地址+内存长度,即为最大内存容量
	mov cx,[ards_nr]
	mov ebx,ards_buf
	xor edx,edx		;edx保存最大内存容量,初始置0
.find_max_mem_area:
	mov eax,[ebx]		;32位基地址
	add eax,[ebx+8]		;内存长度
	add ebx,20		;指向下一个ards
	cmp edx,eax
	jge .next_ards		;如果edx>=eax,跳转到下一个ards,否则让edx=eax,最终效果是找到最大的ards
	mov edx,eax
	
.next_ards:
	loop .find_max_mem_area
	jmp .mem_get_ok
	
;方法2:利用0xe801获取内存 
;返回后,ax=cx单位是1kb,里面是小于16mb的单位数,bx=dx单位是64kb,里面是大于16mb的单位数。最终需要转化为字节数。
.e820_failed_so_try_e801:
	mov ax,0x0000e801
	int 0x15
	jc .e801_failed_so_try88	;如果e801失败,尝试88方法
;以下计算出低于16mb的内存容量大小
	mov cx,0x400		;1024
	mul cx			;16位乘法,结果是32位,低16在ax,高16在dx
	shl edx,16		;左移16位
	
	and eax,0x0000ffff	;保留低16位
	or edx,eax		;拼接edx的高16位和eax低16位,放到edx中
	add edx,0x100000	;+1mb,原因是获取的内存比实际内存少1mb
	mov esi,edx		;备份edx
;以下计算16mb以上内存容量大小
	xor eax,eax
	mov ax,bx		;大于16mb的单位数存在bx、dx里
	mov ecx,0x10000		;单位是64kb
	mul ecx			;32位乘法,结果是64位,低32位在eax,高32位在edx
	add esi,eax		;这种方法的上限就是4gb,所以不必理会高32位,只需要把低32位加进结果即可
	
	mov edx,esi
	jmp .mem_get_ok
	
;方法3:利用0x88获取内存 
;这部分是方法2的简化版,代码参考2,不再写注释
.e801_failed_so_try88:
	mov ah,0x88
	int 0x15
	jc .error_hlt
	and eax,0x0000ffff
	
	mov cx,0x400
	mul cx
	shl edx,16
	or edx,eax
	add edx,0x100000
	
;-------------------- 记录内存容量 --------------------------------

;如果三种方法都失败,跳转到这里,进行一个死循环
.error_hlt:
	jmp $
;不管使用了哪种方法,只要成功,都要跳转到这里记录,单位是1字节
.mem_get_ok:
	mov [total_mem_bytes],edx	;在total_mem_bytes地址记录总内存容量
	
;-------------------- 完成进入保护模式的三个步骤 --------------------------------

	in al,0x92 
	or al,0000_0010B 
	out 0x92,al 		;打开 A20

	lgdt [gdt_ptr] 		;加载 GDT

	mov eax, cr0 
	or eax, 0x00000001 
	mov cr0, eax		;cr0 第 0 位置 1
	
;-------------------- 进入保护模式 --------------------------------

	jmp dword SELECTOR_CODE:p_mode_start	;刷新流水线
[bits 32] 
p_mode_start: 
	mov ax,SELECTOR_DATA 
	mov ds,ax 
	mov es,ax 
	mov ss,ax 
	mov esp,LOADER_STACK_TOP
	
	mov ax,SELECTOR_VIDEO
	mov gs,ax 
	mov byte [gs:160], 'P' 		;通过显卡打印一个字符,验证是否进入保护模式,后续被覆盖

;-------------------- 加载内核到缓冲区 --------------------------------

	mov eax,KERNEL_START_SECTOR	;kernel.bin所在的扇区号
	mov ebx,KERNEL_BIN_BASE_ADDR	;从磁盘读出后,写入到 ebx指定的地址
	mov ecx,200			;读入的扇区数
	call rd_disk_m_32		;写入内存

;-------------------- 启动分页 --------------------------------

	call setup_page		;创建页目录和页表
	sgdt [gdt_ptr]		;要将描述符表地址及偏移量写入内存 gdt_ptr,一会儿用新地址重新加载
	mov ebx,[gdt_ptr+2]	;先获取gdt基址,基址存在后四字节
	add dword [gdt_ptr+2],0xc0000000	;将 gdt 的基址加上 0xc0000000 使其成为内核所在的高地址
	or dword [ebx+0x18+4],0xc0000000	;将 gdt描述符中视频段描述符中的段基址+0xc0000000
						;视频段是第 3 个段描述符,每个描述符是 8 字节,24=0x18 
						;段描述符的高 4 字节的最高位是段基址的第 31~24 位
	add esp,0xc0000000	;将栈指针同样映射到内核地址
	
	mov eax,PAGE_DIR_TABLE_POS 
	mov cr3,eax		;把页目录地址赋给 cr3

	mov eax,cr0 
	or eax,0x80000000
	mov cr0,eax		;打开 cr0的 pg位(第 31 位)
	
	lgdt [gdt_ptr]		;在开启分页后,用 gdt 新的地址重新加载
	
	mov eax,SELECTOR_VIDEO
	mov gs,eax
	mov byte [gs:160],'V'	;通过显卡打印一个字符,验证是否开启分页,后续被覆盖

	jmp SELECTOR_CODE:enter_kernel
	
;-------------------- 跳转到内核区 --------------------------------

enter_kernel:
	call kernel_init
	mov esp,0xc009f000		;设立栈段的起始地址,这个数字是最接近高地址的和4KB对齐的数
	mov eax,SELECTOR_VIDEO
	mov gs,eax
	mov [gs:160], byte '9'		;验证内核是否加载成功,这也是最后屏幕打印出的字符
	jmp KERNEL_ENTRY_POINT		;写在宏里面,0xc0001500
	
;-------------------- 创建页目录和页表 --------------------------------

;首先先明确一点,那就是我们这里只处理低1mb的页表,目标是让0~1mb和3gb~3gb+1mb都映射低1mb
setup_page:
;先逐字节清0
	mov ecx,4096			;循环4096次
	mov esi,0
.clear_page_dir:
	mov byte [PAGE_DIR_TABLE_POS+esi],0
	inc esi
	loop .clear_page_dir		;清理出4KB的空间
	
;创建两个页目录项
.create_pde:
	mov eax,PAGE_DIR_TABLE_POS
	add eax,0x1000			;起始位置+4KB,指向第一个页表,为后面的创建页表项作准备
	mov ebx,eax
	
	or eax,PG_US_U|PG_RW_W|PG_P	;将后12位和前20位地址拼接在一起,组成一个表项
					;后面三个或后=0x7,代表这个目录项允许任意特权级访问,可写,后续我们会用到init用户级进程。
	
	;将页目录0和0x300项都指向第一个页表的地址,都对应0-4mb的物理地址。
	mov [PAGE_DIR_TABLE_POS+0x0],eax	;0项
	mov [PAGE_DIR_TABLE_POS+0xc00],eax	;0xc00项转化为10进制是4*768,对应768项,每个项4字节
						;错误代码是mov [PAGE_DIR_TABLE_POS+0x300],eax,错误原因是一个项4字节忘了×4
	
	sub eax,0x1000
	mov [PAGE_DIR_TABLE_POS+4092],eax	;在页目录最后一项1023项写入页目录本身的地址,所以是4096-4
	
;创建页表项
	mov ecx,256			;目前我们先安排低1mb内存,1M/4k=256项
	mov esi,0
	mov edx,PG_US_U|PG_RW_W|PG_P	;edx存每个表项的相对地址,初始为0+0x7
.create_pte:
	mov [ebx+esi*4],edx		;每过4字节有一项,每项存着一页,一页是4k
	add edx,4096
	inc esi
	loop .create_pte		;循环256次
	
;创建内核其他页表的PDE
;这部分是3gb后的部分,共享给所有用户线程的,所以只关注页目录768项后的项,1023项用来存页目录本身的地址,不需要处理
;这里创建剩下的769~1022=254项
	mov eax,PAGE_DIR_TABLE_POS
	add eax,0x2000		;从第二个页表开始,映射到目录里
	or eax,PG_US_U|PG_RW_W|PG_P
	mov ebx, PAGE_DIR_TABLE_POS 
	mov ecx,254 		;范围为第 769~1022 的所有目录项数量
	mov esi,769 
.create_kernel_pde: 
	mov [ebx+esi*4],eax
	inc esi 
	add eax,0x1000 
	loop .create_kernel_pde 
	ret

;-------------------- 初始化内核 --------------------------------

kernel_init:
	xor eax,eax
	xor ebx,ebx		;记录程序头表的地址,即e_phoff,也就是第一个程序头的偏移地址
	xor ecx,ecx		;记录程序头表中,程序头的数量e_phnum,也就是segment数量
	xor edx,edx		;记录每个程序头的尺寸,即e_phentsize
	
	mov dx,[KERNEL_BIN_BASE_ADDR+42]	;程序头大小
	mov ebx,[KERNEL_BIN_BASE_ADDR+28]
	add ebx,KERNEL_BIN_BASE_ADDR		;程序头地址
	mov cx,[KERNEL_BIN_BASE_ADDR+44]	;程序头数量
	
.each_segment:
	cmp byte [ebx+0],PT_NULL;若相等,说明这个程序头未使用
	je .PTNULL		;未使用则跳转
	
	mov eax,[ebx+8]
	cmp eax,0xc0001500
	jb .PTNULL
	
;模拟c语言里的mem_cpy,输入三个参数:源地址,目标地址,拷贝内容字节数
	push dword [ebx+16]	;获取段字节数p_filesz
	mov eax,[ebx+4]		;距程序头偏移量为4字节的位置是p_offset
	add eax,KERNEL_BIN_BASE_ADDR
	push eax		;段偏移地址,也就是源地址
	push dword [ebx+8]	;目标地址p_vaddr
	call mem_cpy
	add esp,12		;一次是向栈段压入3*4=12字节的数据
	
.PTNULL:
	add ebx,edx		;指向下一个程序头
		
	
	loop .each_segment		
	ret
	
mem_cpy:
	cld			;将DF标志位设置为0,确保指针递增
	push ebp		;我们用ebp指针配合sp,而不是esp,干脆在这里存进栈备份一下
	mov ebp,esp
	push ecx		;ecx保存着程序头数,后续会改为段字节数,这里备份一下
	
	mov edi,[ebp+8]		;源地址
	mov esi,[ebp+12]	;目标地址
	mov ecx,[ebp+16]	;我们是逐字节复制,字节数也就是要复制的次数
	rep movsb		;rep是重复,次数是ecx,movsb是按字节从ds:ei复制内容到es:di处,和cld配合使用,让ei和di自动自增
	
	pop ecx			
	pop ebp			;恢复这两个参数,不影响上面循环的进行
	ret
;-------------------- 从硬盘读取内核的函数 --------------------------------

rd_disk_m_32:
;以下部分备份关键数据
	mov esi,eax		;备份eax
	mov di,cx		;备份cx
;以下部分写入要读取的扇区数
	mov dx,0x1f2
	mov al,cl
	out dx,al
	mov eax,esi		;写入扇区数
;以下部分写入LBA编号和参数
	mov dx,0x1f3
	out dx,al
	
	mov cl,0x8
	shr eax,cl
	mov dx,0x1f4
	out dx,al
	
	shr eax,cl
	mov dx,0x1f5
	out dx,al		;写入LBA低24位的地址
	
	shr eax,cl
	and al,0x0f		;00001111,作用是保留0-3位,4-7位变为0
	or al,0xe0		;01110000,作用是把al的7-4位设置为0111,即设置lba模式
	mov dx,0x1f6
	out dx,al		;向0x1F6,device寄存器写LBA最高4位数字和LBA参数
;以下部分写入要进行的硬盘操作的操作码	
	mov dx,0x1f7
	mov ax,0x20
	out dx,al		;向0x1F7,command寄存器写读入命令0x20
;以下部分检验硬盘状态
.not_ready:
	nop			;在这里设置了一定的时间
	in al,dx
	and al,0x88 		;10001000,第 4 位为 1 表示硬盘控制器已准备好数据传输,第 7 位为 1 表示硬盘忙
	cmp al,0x08		;比较,改变标志寄存器状态
	jne .not_ready		;jump_not_equal,检验硬盘状态,如果失败就继续检验
;以下部分从硬盘读入loader程序
	mov ax,di
	mov dx,256
	mul dx 			;计算读取的字数,mul的结果默认存在ax里,相当于ax=ax*256,即扇区数*512(字节数)/2(1字=2字节)
	mov cx,ax 		;cx寄存器存循环次数
	mov dx,0x1f0
.go_on_ready:
	in ax,dx
	mov [ebx],ax
	add ebx,2
	loop .go_on_ready	;从0x1F0,data寄存器读取数据

	ret

boot.int

;-------------------- 加载器loader --------------------
LOADER_START_SECTOR equ 0x2		;硬盘扇区号
LOADER_BASE_ADDR equ 0x900		;内存地址
;-------------------- 内核kernel --------------------
KERNEL_START_SECTOR	equ 0xa		;硬盘扇区号
KERNEL_BIN_BASE_ADDR	equ 0x70000	;内存地址
KERNEL_ENTRY_POINT	equ 0xc0001500	;虚拟地址入口
;-------------------- 页表 --------------------
PAGE_DIR_TABLE_POS equ 0x100000		;内存地址,在低1MB之外
;-------------------- gdt段描述符属性 --------------------------------
;上面的第多少位都是针对的高32位而言的 参照博客的图 

DESC_G_4K equ 1_00000000000000000000000b ;第23位G段,设置粒度,表示4K或者1MB,段界限的单位值,此时为1则为4k 
DESC_D_32 equ 1_0000000000000000000000b  ;第22位D/B位 表示地址值用32位EIP寄存器 操作数与指令码32位
DESC_L    equ 0_000000000000000000000b   ;第21位 设置成0表示不设置成64位代码段 忽略
DESC_AVL  equ 0_00000000000000000000b    ;第20位 是软件可用的 操作系统额外提供的 可不设置
                                         
DESC_LIMIT_CODE2  equ  1111_0000000000000000b   ;第16-19位 段界限的最后四位 全部初始化为1 因为最大段界限*粒度必须等于0xffffffff
DESC_LIMIT_DATA2  equ  DESC_LIMIT_CODE2         ;相同的值  数据段与代码段段界限相同
DESC_LIMIT_VIDEO2 equ	0000_0000000000000000b	;第16-19位 显存区描述符VIDEO2 书上后面的0少打了一位
   
DESC_P            equ 	1_000000000000000b	;第15位 P段,判断段是否存在于内存 

DESC_DPL_0        equ  00_0000000000000b        ;第13-14位,设置特权级
DESC_DPL_1        equ  01_0000000000000b	;0为操作系统 权力最高 3为用户段 用于保护
DESC_DPL_2        equ  10_0000000000000b
DESC_DPL_3        equ  11_0000000000000b

DESC_S_sys        equ  0_000000000000b          ;第12位为0 则表示系统段 为1则表示数据段
DESC_S_CODE       equ  1_000000000000b          ;第12位与type字段结合 判断是否为系统段还是数据段
DESC_S_DATA       equ  DESC_S_CODE


DESC_TYPE_CODE    equ  1000_00000000b		;第9-11位type段 1000 可执行 不允许可读 已访问位0
						;x=1 e=0 w=0 a=0
DESC_TYPE_DATA    equ  0010_00000000b           ;第9-11位type段   0010  可写  
						;x=0 e=0 w=1 a=0
;-------------------- 段描述符初始化 --------------------------------

;代码段描述符高位4字节初始化 (0x00共8位 <<24 共32位初始化0) 
;4KB为单位 Data段32位操作数 初始化的部分段界限 最高权限操作系统代码段 P存在表示 状态 
DESC_CODE_HIGH4   equ  (0x00<<24) + DESC_G_4K + DESC_D_32 + \
DESC_L + DESC_AVL + DESC_LIMIT_CODE2 + \
DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE + 0x00

;数据段描述符高位4字节初始化
DESC_DATA_HIGH4   equ  (0x00<<24) + DESC_G_4K + DESC_D_32 + \
DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + \
DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x00

;显存段描述符高位4字节初始化
DESC_VIDEO_HIGH4   equ (0x00<<24) + DESC_G_4K + DESC_D_32 + \
DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + \
DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x0B  ;这里末尾是B 

;-------------------- 段描述符子属性 --------------------------------
;第0-1位 RPL特权级,决定是否允许访问  第2位TI,0表示GDT,1表示LDT
RPL0    	equ 00b
RPL1   		equ 01b
RPL2    	equ 10b
RPL3    	equ 11b
TI_GDT  	equ 000b
TI_LDT  	equ 100b
;-------------------- 页表子属性 --------------------
PG_P		equ 1b
PG_RW_R		equ 00b
PG_RW_W		equ 10b
PG_US_U		equ 000b
PG_US_S		equ 100b
;-------------------- elf段属性 --------------------
PT_NULL		equ 0x0

结语

三天多的时间,20多个小时的学习实践,几万字博客,几百行源码,终于完成了第五大章的实现,我们的操作系统算是有了个完整的框架。一路走来颇为不易,不过轻舟已过万重山,高楼的地基已经打好,后续就是添砖加瓦了,让我们共同期待大厦的建成吧!

相关文章:

  • JAVA学习-练习试用Java实现“实现一个Hadoop MapReduce任务,对大数据集中的数值进行排序和筛选”
  • 23种设计模式-行为型模式-中介者
  • 可以使用费曼学习法阅读重要的书籍
  • 【学Rust写CAD】34 精确 Alpha 混合函数(argb.rs补充方法)
  • 路由器的 WAN(广域网)口 和 LAN(局域网)口
  • 【微机及接口技术】- 第五章 输入输出与接口技术(下)
  • uniapp微信小程序引入vant组件库
  • docker部署rabbitmq
  • [刷题总结] 双指针 滑动窗口
  • 使用`sklearn`中的逻辑回归模型进行股票的情感分析,以及按日期统计积极和消极评论数量的功能
  • JavaScript箭头函数介绍(=>)(箭头函数不绑定自己的this,而是继承上下文的this;不能用于造函数)JavaScript =>
  • Linux网络应用层自定义协议与序列化
  • 导数的基本求导法则
  • 代码随想录算法训练营Day32| 完全背包问题(二维数组 滚动数组)、LeetCode 518 零钱兑换 II、377 组合总数 IV、爬楼梯(进阶)
  • 纠错:LLMs 并不是在预测下一个词
  • 【家政平台开发(21)】用户管理模块开发
  • 批量将 JSON 转换为 Excel/思维导入等其它格式
  • 人工智能起源:从图灵到ChatGPT
  • 4月6日随笔
  • 【注解小结】
  • 做gif有什么网站/百度博客收录提交入口
  • 企业官网网站建设/新手学seo
  • javascript怎么读/充电宝关键词优化
  • 程序员做任务的网站/在线网页制作系统搭建
  • 网站做的好的tkd/大数据培训班出来能就业吗
  • 网站建设外包费用/搜狗关键词优化软件