《操作系统真象还原》第五章 —- 轻取物理内存容量 启用分页畅游虚拟空间 力斧直斩内核先劈一角 闲庭信步摸谈特权级

文章目录

    • 专栏博客链接
    • 相关查阅博客链接
    • 本书中错误勘误
    • 部分缩写熟知 + 小建议
    • 修改代码前的小闲聊
    • 修改loader.S(读取内存大小)
      • 检验是否成功读取内存大小
    • 开始分页新篇章的分页理解
      • 一级页表
      • 二级页表
    • 修改loader.S(开启分页)
      • 检验是否完成分页 进入虚拟内存时代
    • 修改Loader.S(加载内核)
      • 1、编写内核程序(main.c)
      • 2、从磁盘读入内核区代码
      • 3、修改Loader.S(转移内核代码 跳转至内核)
        • 1、新函数rd_disk_m_32
        • 2、处理文件头elf
          • GCC降级处理办法(最推荐的处理办法 方便后面的程序编写)
          • GCC不降级处理办法(最啥比的处理办法 非常不推荐)
    • Loader.S(全代码)+boot.inc(代码修改)
      • GCC 4.4 最终Loader.S代码(可一直使用)
      • GCC 9.4 苦手版本(不兼容后面章节代码)
      • boot.inc代码
    • 结束语(第五章完结撒花~)

专栏博客链接


《操作系统真象还原》从零开始自制操作系统 全章节博客链接


相关查阅博客链接


汇编中关于EQU指令的问题
Bochs常用调试命令
CLD汇编指令
Ubuntu高版本如何安装低版本GCC 以Ubuntu 20安装GCC5为例)
ubuntu 16.04 gcc高低版本切换


本书中错误勘误


同样 这个错误 是我认为可能书上没有写好的地方
可能也是我的理解错误 我没有在否认书的质量 只是希望写这个
能给调试数据出不来的你一些启发

要是这本书 刚哥写的不好 我也就不会三天看到第五章了哈哈哈

1、这个是不是这一章的错误 我认为是没有计算进去的
就是我们把内存大小放在了 0Xb00 位置
我仔细算过 确实数据区域是512字节
可是我们的jmp 指令同样也占3字节
就导致 如果你是按照书上的格式来设计的 0x900来作为loader.S开始位置
那么你存放入的total_mem_bytes就应该是在 0xb03的位置

但是感谢这个错误 导致我用了部分 反汇编 查看数据
调试指令出来 看看每一位的数据 每一条指令


2、同样也是加载完内存的不完全Loader.S
我们那个时候还没有写error_hlt错误处理函数
所以要自己写一个标号 里面先放个jmp $
然后我们就可以跑去看看内存读入调用成功没有


3、这个也不算错误勘误 但是务必务必后面一点点慢慢跟着
下面的博客一点点走 因为对于elf头文件处理 必须要用gcc 4.x.x的版本
如果用的是4.6.1才能跟书上的elf头文件处理方式一样
主要如果现在用的是Ubuntu 20版本的话 gcc 我现在的版本是9.4
编辑出来完全不一样 就导致我后面第五章的头文件处理 第六章的链接
都有很大很大的问题 所以在处理头文件时 分了两部分来写 现在这段话是我已经写到了第六章中间后面补的 因为第六章的时候 我又把gcc版本降级到4.4 Loader.S重写了一部分 真的很烦很烦
所以想一次性弄好的话 可以直接跳转到处理文件头部分 给了链接


部分缩写熟知 + 小建议


nr number
buf buffer
ptr pointer
dir directory


小建议放在这里说确实不是很合适 因为都已经写到第五章了
人有点懒 但这些东西也都是自己能够想到办法解决的
相信聪明的读者遇到这些情况的时候 可以自己解决

第一个的就是 Vmware Station无响应
其实我经常半天就会出现一次无响应
第一次出现的时候我就直接把虚拟机用任务管理器关了
结果好了 Ubuntu显示永久被占用 再也打不开了
所以建议就是 等待其自己恢复相应 期间不进行任何操作

第二个就是 即使做完一部分 把Ubuntu上面的bochs文件夹
及时拷贝一份到Windows你的主机上
因为万一哪一次你的Vmware Station真的10分钟都无响应
你一气之下直接给关了
好了 重新装Ubuntu几分钟就解决了 那你自己之前写的操作系统
不直接从零开始了吗
相信这会彻底性摧毁你刚刚萌发出的热情火焰
所以这边还是建议拷贝一份 利用Vmware tool 拖动一下即可


修改代码前的小闲聊


当我看到这部分内容的时候
我就知道这部分肯定是块硬骨头 我们必须要攻克的
在Lab中 至少之前对后面的进程与线程是有所触及的
而这边对于具体的代码实现是真的没有怎么了解过

但又抑或是 每次我遇到这些 自己都感觉
要花很大力气或者思考很久才能做出来的事情
有所退却时
我总是会想到

这些事情也都必须要去做 之后过半年 一年或者毕业
等自己刚刚入职不久 又或是已经成为老程序员的时候
再不妨看看年轻的时候 自己所做的这些事情
对于那时的自己 何有不是一种纪念和回忆

所以还是需要迎难而上
就像刚开始学习的数据结构 自己咬了咬牙一口气
一个月多一点的时间 把浙大数据结构课后的36道题目
全部一口气拿下的时候

那个时候自己从做一道题要花一天多的时间 到后面轻车熟路
做一道也许花个一两个小时 再到现在力扣已经刷了400多道的时候
再回想起以前 不是说真的那36道题帮助我奠定了多少基础
而是给予了我对于后面困难 迎难而上的勇气与不退却

我会想 前面既然这么难的题目我都能够做下来
后面还会有什么题目是我做不出来的
出去吃个饭今天尽量给把第五章实现了


修改loader.S(读取内存大小)


这里就先进入到内存读取的了
当然 上面写的错误勘误栏里面也有对于这里
我调试半天找到的问题 这里就看书直接放代码吧

注意 这个专栏博客里面 我的loader.S起始地址都设置的是
0x600 因为博客名字是Love 6

然后对于total_mem_bytes经过修改后
也是一样在0x600后面的0x200 = 0x800位成功读入
内存大小 而我自己设置的内存大小比较大 设置的是512M

这个在后面检测部分会详细写到

%include "boot.inc"
SECTION loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR 		   ;是个程序都需要有栈区 我设置的0x600以下的区域到0x500区域都是可用空间 况且也用不到
jmp loader_start                     		   	   ;下面存放数据段 构建gdt 跳跃到下面的代码区 ;对汇编再复习 db define byte,dw define word,dd define dwordGDT_BASE        : dd 0x00000000          		   ;刚开始的段选择子0不能使用 故用两个双字 来填充dd 0x00000000 CODE_DESC       : dd 0x0000FFFF         		   ;FFFF是与其他的几部分相连接 形成0XFFFFF段界限dd DESC_CODE_HIGH4DATA_STACK_DESC : dd 0x0000FFFFdd DESC_DATA_HIGH4VIDEO_DESC      : dd 0x80000007         		   ;0xB80000xBFFFF为文字模式显示内存 B只能在boot.inc中出现定义了 此处不够空间了 8000刚好够dd DESC_VIDEO_HIGH4     	   ;0x0007 bFFFF-b8000)/4k = 0x7GDT_SIZE              equ $ - GDT_BASE               ;当前位置减去GDT_BASE的地址 等于GDT的大小GDT_LIMIT       	   equ GDT_SIZE - 1   	           ;SIZE - 1即为最大偏移量times 59 dq 0                             	   ;预留59个 define double四字型 8字描述符times 5 db 0                                         ;为了凑整数 0x800 导致前面少了三个total_mem_bytes  dd 0;在此前经过计算程序内偏移量为0x200 我算了算 60*8+4*8=512 刚好是 0x200 说这里的之后还会用到;我们刚开始程序设置的地址位置为 0x600 那这就是0x800gdt_ptr           dw GDT_LIMIT			   ;gdt指针 2字gdt界限放在前面 4字gdt地址放在后面 lgdt 48位格式 低位16位界限 高位32位起始地址dd GDT_BASEards_buf times 244 db 0                              ;buf  记录内存大小的缓冲区ards_nr dw 0					   ;nr 记录20字节结构体个数  计算了一下 4+2+4+244+2=256 刚好256字节;书籍作者有强迫症 哈哈 这里244的buf用不到那么多的 实属强迫症使然 哈哈SELECTOR_CODE        equ 0X0001<<3) + TI_GDT + RPL0    ;16位寄存器 4位TI RPL状态 GDT剩下的选择子SELECTOR_DATA	  equ 0X0002<<3) + TI_GDT + RPL0SELECTOR_VIDEO       equ 0X0003<<3) + TI_GDT + RPL0   loader_start:mov sp,LOADER_BASE_ADDR                                   ;先初始化了栈指针xor ebx,ebx                                               ;异或自己 即等于0mov ax,0                                       mov es,ax                                                 ;心有不安 还是把es给初始化一下mov di,ards_buf                                           ;di指向缓冲区位置
.e820_mem_get_loop:mov eax,0x0000E820                                            ;每次都需要初始化mov ecx,0x14mov edx,0x534d4150int 0x15                                                  ;调用了0x15中断jc  .e820_failed_so_try_e801                              ;这时候回去看了看jc跳转条件 就是CF位=1 carry flag = 1 中途失败了即跳转add di,cx							;把di的数值增加20 为了下一次作准备inc word [ards_nr]cmp ebx,0jne .e820_mem_get_loop                                    ;直至读取完全结束 则进入下面的处理时间mov cx,[ards_nr]                                          ;反正也就是5 cx足以mov ebx,ards_bufxor edx,edx
.find_max_mem_area:mov eax,[ebx]						 ;我也不是很清楚为什么用内存上限来表示操作系统可用部分add eax,[ebx+8]                                            ;既然作者这样用了 我们就这样用add ebx,20    						 ;简单的排序cmp edx,eaxjge .next_ardsmov edx,eax.next_ards:loop .find_max_mem_areajmp .mem_get_ok.e820_failed_so_try_e801:                                       ;地址段名字取的真的简单易懂 哈哈哈哈 mov ax,0xe801int 0x15jc .e801_failed_so_try_88;1 先算出来低15MB的内存    mov cx,0x400mul cx                                                      ;低位放在ax 高位放在了dxshl edx,16                                                  ;dx把低位的16位以上的书往上面抬 变成正常的数and eax,0x0000FFFF                                          ;把除了16位以下的 16位以上的数清零 防止影响or edx,eax                                                  ;15MB以下的数 暂时放到了edx中add edx,0x100000                                            ;加了1MB 内存空缺 mov esi,edx;2 接着算16MB以上的内存 字节为单位xor eax,eaxmov ax,bxmov ecx,0x10000                                              ;0x1000064KB  64*1024  mul ecx                                                      ;32位为0 因为低32位即有4GB 故只用加eaxmov edx,esiadd edx,eaxjmp .mem_get_ok.e801_failed_so_try_88:mov ah,0x88int 0x15jc .error_hltand eax,0x0000FFFFmov cx,0x400                                                 ;1024mul cxshl edx,16or edx,eax add edx,0x100000.error_hlt:jmp $
.mem_get_ok:mov [total_mem_bytes],edx
; --------------------------------- 设置进入保护模式 -----------------------------
; 1 打开A20 gate
; 2 加载gdt
; 3 将cr0 的 pe位置1in al,0x92                 ;端口号0x92 中 第1位变成1即可or al,0000_0010bout 0x92,allgdt [gdt_ptr]mov eax,cr0                ;cr0寄存器第0位设置位1or  eax,0x00000001              mov cr0,eax;-------------------------------- 已经打开保护模式 ---------------------------------------jmp dword SELECTOR_CODE:p_mode_start                       ;刷新流水线[bits 32]p_mode_start: mov ax,SELECTOR_DATAmov ds,axmov es,axmov ss,axmov esp,LOADER_STACK_TOPmov ax,SELECTOR_VIDEOmov gs,axmov byte [gs:160],'P'jmp $

检验是否成功读取内存大小


上面的代码已经写完了 此时我们不妨来检验一下是否成功读取
内存大小

一样我们先进入我们之前设置的bochsrc.disk
模拟硬件信息的设置信息看看

我这里设置的是512MB内存大小 相比书中的32MB是不是略显阔气 宽裕很多 哈哈哈哈哈
在这里插入图片描述


老规矩 先把我们的三条指令放到随便一个文档
然后先看看我们重新编辑的loader.S大小有多大
ls -l 一看 1012KB 相比我们之前设置的
count=2更改两个盘和在MBR.S中读取了四块扇区
都还是在范围内 所以先暂时不用更改

nasm -I include/ -o boot/loader.bin boot/loader.S
dd if=/home/cooiboi/bochs/boot/loader.bin of=/home/cooiboi/bochs/hd60M.img bs=512 count=2 seek=2 conv=notrunc
bin/bochs -f bochsrc.disk

运行后老样子
此时再调用xp 0x800
书上设置的loader.S开始位置为0x900 则为0xb00)

ok 看到显示我们的内存大小为0x20000000
书上的32M0x02000000
这里其实就验证正确了 因为16进制下左移一位
即乘上16 刚好等于我们设置的512M 验证成功

在这里插入图片描述


开始分页新篇章的分页理解


一级页表


我很清楚 之前在学习哈工大操作系统和现代操作系统的时候
对分段分页都还是懵懵懂懂的
现在 刚刚看了书 忽然一下子对整体构建设想已经有思路了

首先我们还是段选择子CS
在我们的GDT表中找到自己的段起始位置
并作一些检测对自己的段做一些保护检测 看让不让访问

ok 然后根据我们得到的线性地址
但其实在平坦模式下 段起始位置都是0 哈哈哈
就看32位的ESP 段偏移了
然后我们根据 段起始位置+段内偏移 得到线性地址
到了关键一步如何在段表里面查找了
我们可以不妨这样想

例如现在CPU就设置的页面一页4KB 4096字节
那我们的32位 一共需要多少4kb的物理地址描述符呢
也就是页表中我们需要得到的页表项 做一个计算

需要1048576个页表项
ok
那我们得到了线性地址如何得到相对应的映射
通过我们的页表项 找到在页表中的第几个呢
哈哈 这就简单了 不是有1048576个页表项吗
我们就通过 把32位的前20位 也就是把4kb 12位砍去的
前20位高位 从而得到了在页表中的索引

举个例子 0x10100123
这是我们的线性地址 32位对吧
我们一看就能得到 页表中对应的页表项是第0x10100个
而在页面的相对应的偏移位置是 0x123

那直接访问其地址可以通过 第0x10100*4字节
通过cr3中的页表地址 + 0x10100)乘上4 即可得到
物理地址 终于理清楚了 此时此刻真的想马上跑到广场 在广场上热舞10分钟
一团迷雾 在学习了那么久的操作系统的今天 终于搞清楚了
不容易啊 不容易


二级页表


看完书 又感觉感觉到领悟的感觉 哎呀
现在真的恨不得 马上跳一支热情桑巴

且容我激动的心 得以释放

为什么需要我们需要二级分页
书上是这样写的
确实 按照我刚开始的设想也是这个样子的
一级分页确实必须要提前设好 不然的话我们是按照前20位索引定位
是按照4kb的字节来寻找的 必须每一个就要对应一个
如果没有填充那些4kb 那么这对于定位物理地址将是毁灭性的缓慢
时间上是绝对不能接受的

但是一级分页 说少 1024的平方项这么多 算了一下 光页表就需要4kb
但是每一个进程属于自己的段中 必须要有一个独立的页表
至于为什么 我也不是很清楚 而且我看到的部分暂时没有介绍
但是就站在这点而言 如果有200多个进程
岂不是 光页表就1GB了
这是绝对绝对 不能接受的

既然我们能想到一级分页 我们肯定就能想到二级分页
我这个时候就忽然想起来了 李治军老师哈工大操作系统课上说的
如同一本书 之前每一个小节都单独写了出来
现在根据小节的内容又出现了大章
我们可以先搜索大章 从而再去定位小节 此时搜索效率就相当高了
而且空间也得以节约了

对于一些进程 那么多一级页表项我用不到 我不可能把你每个
页表项都放到我自己的进程区域 那简直是 大大的浪费!

这个时候我也忽然领会到了 知识的融汇贯通 和前面所学的照应
也许有的时候 前面学了很久都没有学懂 而在后面又学习了 忽然领悟了
并与前面的知识串在了一起 那种感觉 简直是太好了 哈哈


这里再说一下 二级页表中 我们怎么重新回到一级页表并且定位
当然 相同的原理 相同的道理

这里我们对于二级页表的表述 就不再是页表了
是页目录项 其实从名字方面看起来 当然就很直接
从现在了解了原理的我 也终于明白了这个名字真的取的妙哉啊 哈哈

1024个页目录项 1024个一级页表项
恰好 20 = 10位+10位
大家平均分摊了这10位 跟一级页表的原理是一样的
前10位定位页目录项的索引 中间10位定位一级页表项的索引
最后12位定位页内偏移 哈哈 心情确实好啊!

终于理解了这个原理了! 哈哈!
在一头雾水的情况下 强迫自己看了六章的《现代操作系统》
抱着复习 实操的态度做完了8个Lab的 哈工大操作系统
直到现在 抽丝拨解终于解开了 好久好久 一直没有搞懂的问题

相信你一定有过这样的时刻 能够理解我现在的心情!


修改loader.S(开启分页)


我知道这是块硬骨头
啃它就完事了!

再经过了调试和查阅一些资料后
也是终于把分页给搞定了 但是这部分还没有进入到
动态分配页面那部分 我先给出完成二级页表的代码

你看到这段文字的时候
已经是我开始写这个博客的第二天下午了
因为已经调试了一天左右了 终于调试好了
总是因为各种莫名其妙的小问题
有些地方我弄到我最后看源码 才得以解决
这部分就需要注意一下就好了

我建议写代码的时候 不要照抄书上的代码
尽量自己复原 出现BUG 你调试的时候 找错误的时候
你如果原理 分页机制是怎么开启的都不知道
你是根本找不出来错误的
有的时候出现错误也是个好事

setup_page:mov ecx,0x1000                                             ;循环4096次 将页目录项清空 内存清0mov esi,0                                                   .clear_page_dir_mem:                                          ;dir directory 把页目录项清空mov byte [PAGE_DIR_TABLE_POS+esi],0inc esiloop .clear_page_dir_mem.create_pde: mov eax,PAGE_DIR_TABLE_POS				  ;页目录项 起始位置add eax,0x1000                                              ;页目录项刚好4k字节 add eax即得第一个页表项的地址;接下来我们要做的是 把虚拟地址1M下和3G+1M 两部分的1M内存在页目录项中都映射到物理地址0-0XFFFFFor  eax, PG_P | PG_RW_W | PG_US_U                           ;哦 悟了 哈哈哈 这里设置为PG_US_U 是因为init在用户进程 如果这里设置成US_S 这样子连进内核都进不去了mov [PAGE_DIR_TABLE_POS+0x0],eax                             ;页目录项偏移0字节与偏移0xc00 对应0x 一条页目录项对应2^224MB 偏移由前10*4字节得到 可自己推算一下mov [PAGE_DIR_TABLE_POS+0xc00],eax                        sub eax,0x1000      mov [PAGE_DIR_TABLE_POS+4092],eax                           ;虚拟内存最后一个目录项 指向页目录表自身 书上写的是为了动态操纵页表 我也不是很清楚 反正有用 先放放;这里就创建了一页页表    mov eax,PAGE_DIR_TABLE_POSadd eax,0x1000mov ecx,256mov esi,0mov ebx,PG_P | PG_RW_W | PG_US_U .create_kernel_pte:           mov [eax+esi*4],ebxinc esiadd ebx,0x1000loop .create_kernel_pte ;这里对于我们这里填写的目录项所对应的页表 页表中我们还没填写的值
;为了实现 真正意义上的 内核空间被用户进程完全共享
;只是把页目录与页表的映射做出来了 mov eax,PAGE_DIR_TABLE_POSadd eax,0x2000       					   ;eax此时处于第二个页表or  eax,PG_P | PG_RW_W | PG_US_U
;这里循环254次可以来分析一下 我们这里做的是 0xc0 以上部分的映射    0xc0 对应的是第768个页表项 页表项中一共有 2^10=1024;1023项我们已经设置成 映射到页目录项本身位置了 即1022 - 769 +1 = 254mov ebx,PAGE_DIR_TABLE_POSmov ecx,254						  mov esi,769.create_kernel_pde:mov [ebx+esi*4],eaxinc esiadd eax,0x1000loop .create_kernel_pde ret

检验是否完成分页 进入虚拟内存时代


这里我们老套路
还是先用文档 把三条指令给保存下来
但是在此之前 我们先看一下loader.S的大小

cd boot ls -l 一看1100多kb
这下子 我们就需要改动 之前的dd指令了

这里就是之后我写的三条指令

nasm -I include/ -o boot/loader.bin boot/loader.S
dd if=/home/cooiboi/bochs/boot/loader.bin of=/home/cooiboi/bochs/hd60M.img bs=512 count=3 seek=2 conv=notrunc
bin/bochs -f bochsrc.disk

进去c一下

先再看看映射关系 是否如书上的五条
info tab

然后再看一下gdt表是否加载成功
V字符是否出现

这里本来我预想的base地址 应该是0xc0000600 结果多了三
之后我就找到了问题 也不算问题
因为Loader.S最刚开始的跳转指令占了3字节
没有办法缩减 那就0xc0000603
反正只是强迫症 看到这个数字估计想抓狂 哈哈
我没有强迫症 所以我哈哈一笑 继续往下面看咯

在这里插入图片描述

在这里插入图片描述


修改Loader.S(加载内核)


同样当我开始写这段话时 这个博客的编辑时长已经来到了第三天了
哈哈哈哈 因为调试Bug 追寻数据 找到错误点 修正
一个一个字节的改

现在已经是在完成分页的第二天下午了 刚刚我也才
终于把内核加载完成了
如果抛开我上面说的那些心酸史 回到加载内核区域
我们应该不难的想到 是有这几步的
1、编写内核程序

2、将内核程序dd 复制到磁盘上
3、Loader.S 读取磁盘把内核转移到内存中 并跳转到内核区


1、编写内核程序(main.c)


我们先来说第一步1、编写内核程序
这个是我们后面用c语言编写的重头戏 这里我们就用最简单的一个循环
因为后面我们主要专攻的地方就是内核区域的程序了
我们具有历史使命的MBR Loader.S
完成了中断描述符 完成了保护模式的开启 完成了分页模式的开启
并且即将要完成真正的内核代码的加载 跳转

我们已经不能再要求更多了!

下面就是我们 大气的内核区域代码 哈哈 修改完善是之后几章的事情了 我们可以先不管 毕竟现在我们Loader.S的功能都还没完成完整呢

int main)
{while1);return 0;
}

2、从磁盘读入内核区代码


上面我们才把代码编写好
关于编译 elf文件头 链接等知识 就请诸位详详细细的看看书吧
这些东西 我就不再复述了
我刚开始并不想仔细研究 Elf文件头
但是发现 按照书上的代码敲出来
我的操作系统卡死出错 我研究了很久 发现就是文件头的问题!
所以不得不重新仔细看了看 并且一点点分析 这部分后面会再说的

我们先编译 链接定位 复制到磁盘上 代码如下
一次性复制200块磁盘 200*512=100KB 为了后面方便
不需要一直修改count 一码用到底

并且注意 相对文件位置是你自己设定的

gcc -c -o kernel/main.bin kernel/main.c
ld kernel/main.o -Ttext 0xc0001500 -e main -o kernel/kernel.bin
dd if=/home/cooiboi/bochs/kernel/kernel.bin of=/home/cooiboi/bochs/hd60M.img bs=512 count=200 seek=9 conv=notrunc

3、修改Loader.S(转移内核代码 跳转至内核)


这部分就真的是重头戏了
内容真的很多 我觉得一部分一部分讲太繁琐了
而且主要我想写的是 对于elf文件头部分
可能是版本更新或者gcc编译器版本不同
完全按照书上的代码 是不可能跳转成功 读取内容成功的
所以下面会写很多注意部分


1、新函数rd_disk_m_32


1、新函数rd_disk_m_32
其实就是rd_disk_m_16的增强版
只是由于第一次读取磁盘我们只有16位 而这时我们需要32位的寄存器
注意 我们读取部分要到0x70000 明显0x7000016位是不能满足需求的

我们只需要在函数的最后一部分 bx改成ebx即可

至于为什么不用修改其他的
因为传值模式一直没变 ax保存两字节
只是我们需要的寄存器保存内存地址的需求变大了而已
对于计算次数的cx 200*512/一次两字节 = 51200
cx最大储值范围为2^16 = 65536 是足够的


2、处理文件头elf


为什么忽然写到这里多了一个处理文件头这部分 因为这已经是我第五次再次编辑这个博客了
而为什么又重新编辑 明明我之前这部分已经写好了 程序运行也正确了

请听我细细讲来原因 节省各位读者的时间
如果不想看原因的话 总而言之 还是推荐大家还是选择后面的
GCC降级处理办法 如果你现在gcc -V 看到你的gcc版本在5及以上

那头文件编辑出来多多少少有问题 防止后面出现更多的问题
我们不妨把我们的编译器版本变成书上的gcc 4.x.x 书上第一章写着的是gcc 4.6.1

如果你还是不想降级 按照我后面不降级处理办法 大概率你只能处理好这一章节的内容 到了第六章各种 链接 版本处理问题就会接踵而至 到最后你还是只能降级才能解决

好了 这部分就先写到这里 各位读者自己往下看吧


GCC降级处理办法(最推荐的处理办法 方便后面的程序编写)

ubuntu 16.04 gcc高低版本切换

大家根据我给出的博客链接 大家安装时就安装g++ 4.4就行了
至于原因 尽管最后的elf文件头部分 还是跟书上的有一点点的差别
但是差别真的很小很小 约等于没有 而且完全匹配书上的代码
而且经过我尝试一晚上 发现我安装的gcc 4.6.4差别更大 而且有些地方还要自己再进行处理 gcc 9.4差别简直不说了 下午我写了大部分篇幅都是 当时一个苦手 一点点慢慢修改 慢慢推敲最后才勉强得出来的代码

至于为什么最推荐 因为Ubuntu 64位链接后面不能和32位程序链接
这就是原因 还有gcc 4.x.x是书上的gcc编译版本

在更换完编译器版本后 可以gcc -V 看看版本号 是不是gcc 4.4

后面的编译指令都必须要改成如下的
Loader.S编译不变

nasm -I include/ -o boot/loader.bin boot/loader.S
dd if=/home/cooiboi/bochs/boot/loader.bin of=/home/cooiboi/bochs/hd60M.img bs=512 count=3 seek=2 conv=notrunc

main.c编译变成32位编译

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

在文章的最后再给出 在这个后面我一直用的最终Loader.S的版本
和给一个原来的gcc 9.4编译下的版本


GCC不降级处理办法(最啥比的处理办法 非常不推荐)

这部分 纯纯的就当作 我在苦海里面寻找出路的寻找之路吧
可以省略不看 最后还给了boot.inc全代码 可以直接跳转到那部分

在我们把内核代码放在了0x70000时
我们需要再把我们的核心代码区域放到0xc0001500
但不用担心 虚拟内存对应的物理地址也就是0x1500
在我们内存映射的1MB以下

为什么需要先把内核代码先放到0x70000后转移到0x1500
因为我们用c语言编译的 出来的是不是一个纯二进制执行命令文件
也就是其实是 包含信息头+代码 的一个文件

而我们之前为什么直接编译完了就可以放进去了
是因为之前我们直接编写的是汇编 汇编编译后 一条语句对应一条机械码
在某种意义上 我们就是用机械语言编写的
c语言需要先转化成汇编 后才变成机械码

总而言之 我们通过 elf文件头 就可以得到 我们真正需要的
而我们只需要把我们所需要的段 复制到我们想要复制到的内存空间
跳转过去 第一条指令也就是我们在通过c语言编译得到的最终执行指令


如果我按照书上写的代码 直接写上去 直接能够跑起来
并且成功进入 内核区 跳转到0xc0001500(虚拟空间)
我肯定就不会写那么多 而会直接给代码了

那中间遇到什么问题了呢
哈哈 那肯定就是 bochs虚拟机卡死了呗 死活运行不出来
之后直接给我整怒了 我一条一条代码 比对着书上的
把我之前自己写的全删了 全部一条一条按着书上抄
卡死 卡死 还是卡死


哈哈 那没有办法 只能耐心点 一点一点的慢慢寻找问题
由于现在处在的是 纯二进制文件 而不是elf文件格式
所以在书前面出现的断点查找错误 应该是不能用的 我猜的
我比较习惯用 在文件中添加jmp $ 不断地保存执行
如果当前还可以运行 那么我就把这个语句继续往下移动
如果卡住了 就把语句往上移动 直到定位到出错误的语句

其实在我定位之前 我先检测了一下是不是之前分页的问题
其实在写到这里之前 我在开始断点调试之前发现我的分页出现了问题
在调试完分页的问题后 还发现了内存移动有问题
在整完了 内存复制移动的问题之后 最后发现处理ELF头文件有问题
真的是无语啊

既然我们已经把问题定位到了了ELF头文件的问题
那我们就废话少说直接开始 那免不了重新看看elf文件头格式了
看完之后 我们输入指令xxd kernel.bin
即出现这样子的图 这个图就是 我们的elf文件的二进制文件格式

在这里插入图片描述


既然已经出现了 二进制字节文件 我们一步一回头 先检测一下内存0x70000 是否存储的是我们的字节
xp /100 0x70000就先看看 嗯 是这些字节
做操作系统就必须要走一步 看一步 每一步走的稳稳当当才能继续往下走

在这里插入图片描述


我们再根据书上写的readelf -e kernel.bin
根据系统直接分析出来的elf文件头信息 来每个字节每个字节比对
得到下面的图

有程序段表的入口 段个数 每个段的详细信息 段表中每项描述字节数
以及其他的信息 有了这个武器 我们的调试信心倍增!

在这里插入图片描述
在这里插入图片描述


按照这些字节 我们再根据书上的字节对比 很幸运的发现 read给出来的信息表 是按照字节顺序相同的顺序给的 太好了 方便我们进行的对比
在对比途中 就发现了大问题

我们可以仔细观察 我们的程序头表在文件中的偏移量 竟然出现在0x2032字节)上面
而我们根据格式 和 书上给出的应该是在
16字节+2字节+2字节+4字节+4字节 = 28字节的偏移量上
这让我非常的诧异并且深感疑惑 是我眼花了还是是我系统出错了
我连续试了两三次 都是这样的结果

那个时候我就觉得应该是系统定义结构体不同的原因
于是我进入了源码 就在书上给出的usr/include/elf.h

同样也定位到了 相同的结构体定义
结果发现是一模一样 并且每一个字都是相同的
去看了看是不是Elf32_HalfElf32_Word出现了偏差
发现也并没有 真的太奇怪了

在这里插入图片描述


但是我们不得不承认一些事情 有些东西不是由我们操控的
编译器不是我们编的 每个字节也不是由我们的手一个字一个字敲上去的
通电也不是由我们太阳能发电 把自己身上的电通到电脑让它工作的
既然出现了这样的情况 我们也仔细分析了 但是结果就是 偏移出现了问题

我其实有个猜想 可能是编译器的版本问题?还是系统不相同 Ubuntu 和 CentOs 的问题 不清楚
但无论如何 既然这样 我们只能承认这个事实 并且修改我们的代码 从某种方面来看 我们也找到了问题

就这样修改 我们需要修改好几处
段入口 段描述符字节 段个数计数都在与书上不同的位置

在不进入mem_cpy前定点jmp $ 查看寄存器的信息
这样子核对我们是否信息正确了
在修改完这些 我原本以为终于结束了
然后把jmp $定位在了memcpyret movsb后 结果发现出错了
然后我又修改在ret movsb前面 就发现可以运行 说明就出错在这里


为什么又出错了 本来想一拳给电脑锤了 开玩笑开玩笑
电脑属实是我的小心肝 看到电脑出问题了 心都要震一下

做操作系统本来就是一个 粗心不得的问题
我估计在这后面之后的调试之路还远着呢 这就当练练手了

分析完源文件源码后 发现还是没有什么问题
当我处于绝望时 我想起来了 会不会还是因为数据储存的问题
然后我就转向到了 程序头的问题

在这里插入图片描述


我们先进入0x40相对程序段表的入口 开始分析 发现怎么又是这个问题
偏移位置又发生了差错 发生了差错

我又去研究了下elf.h的源码 发现段描述的结构体还是跟书上一样
那没有办法 我们还是一个字节一个字节的分析 把我们写的错误的偏移地址
一点一点慢慢扭正过来

详细的分析过程就不写了 后面会给出代码的


在修正之后 我们直接开冲 就不相信你才能出错
结果 真的又出错了 还是ret movsb位置

那个时候已经麻了 在麻了和很麻的地方不断徘徊

还是不能放弃 走到这一步了 在做操作系统之前就应该抱有耐心的调试心态的态度来的
这个时候在经过研究 发现半小时后 我们发现一个问题
看下图
在这里插入图片描述

我们仔细观察第一个段的LOAD 有没有发现一个问题
Virtaddr的地址有点熟悉 仔细的在做的读者已经发现了
0x400000似乎我们还没有做映射

那个时候我心想 天哪 怎么你编译到这个地方来的
到后面又转念一想 既然我们只需要0xc0001500的代码区域
其他区域我们不要即可 ok 问题终于解决了


Loader.S(全代码)+boot.inc(代码修改)


gcc 4.4版本下 我一直在用的代码
这部分就是我再次修改过的版本了 也是我到后面章节一直没改过的代码
再下面的部分就是 gcc 9.4苦手版本 还是给出来 大家参考一下 但是不推荐使用


GCC 4.4 最终Loader.S代码(可一直使用)


%include "boot.inc"
SECTION loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR 		   ;是个程序都需要有栈区 我设置的0x600以下的区域到0x500区域都是可用空间 况且也用不到
jmp loader_start                     		   	   ;下面存放数据段 构建gdt 跳跃到下面的代码区 ;对汇编再复习 db define byte,dw define word,dd define dwordGDT_BASE        : dd 0x00000000          		   ;刚开始的段选择子0不能使用 故用两个双字 来填充dd 0x00000000 CODE_DESC       : dd 0x0000FFFF         		   ;FFFF是与其他的几部分相连接 形成0XFFFFF段界限dd DESC_CODE_HIGH4DATA_STACK_DESC : dd 0x0000FFFFdd DESC_DATA_HIGH4VIDEO_DESC      : dd 0x80000007         		   ;0xB80000xBFFFF为文字模式显示内存 B只能在boot.inc中出现定义了 此处不够空间了 8000刚好够dd DESC_VIDEO_HIGH4     	   ;0x0007 bFFFF-b8000)/4k = 0x7GDT_SIZE              equ $ - GDT_BASE               ;当前位置减去GDT_BASE的地址 等于GDT的大小GDT_LIMIT       	   equ GDT_SIZE - 1   	           ;SIZE - 1即为最大偏移量times 59 dq 0                             	   ;预留59个 define double四字型 8字描述符times 5 db 0                                         ;为了凑整数 0x800 导致前面少了三个total_mem_bytes  dd 0;在此前经过计算程序内偏移量为0x200 我算了算 60*8+4*8=512 刚好是 0x200 说这里的之后还会用到;我们刚开始程序设置的地址位置为 0x600 那这就是0x800gdt_ptr           dw GDT_LIMIT			   ;gdt指针 2字gdt界限放在前面 4字gdt地址放在后面 lgdt 48位格式 低位16位界限 高位32位起始地址dd GDT_BASEards_buf times 244 db 0                              ;buf  记录内存大小的缓冲区ards_nr dw 0					   ;nr 记录20字节结构体个数  计算了一下 4+2+4+244+2=256 刚好256字节;书籍作者有强迫症 哈哈 这里244的buf用不到那么多的 实属强迫症使然 哈哈SELECTOR_CODE        equ 0X0001<<3) + TI_GDT + RPL0    ;16位寄存器 4位TI RPL状态 GDT剩下的选择子SELECTOR_DATA	  equ 0X0002<<3) + TI_GDT + RPL0SELECTOR_VIDEO       equ 0X0003<<3) + TI_GDT + RPL0   loader_start:mov sp,LOADER_BASE_ADDR                                   ;先初始化了栈指针xor ebx,ebx                                               ;异或自己 即等于0mov ax,0                                       mov es,ax                                                 ;心有不安 还是把es给初始化一下mov di,ards_buf                                           ;di指向缓冲区位置
.e820_mem_get_loop:mov eax,0x0000E820                                            ;每次都需要初始化mov ecx,0x14mov edx,0x534d4150int 0x15                                                  ;调用了0x15中断jc  .e820_failed_so_try_e801                              ;这时候回去看了看jc跳转条件 就是CF位=1 carry flag = 1 中途失败了即跳转add di,cx							;把di的数值增加20 为了下一次作准备inc word [ards_nr]cmp ebx,0jne .e820_mem_get_loop                                    ;直至读取完全结束 则进入下面的处理时间mov cx,[ards_nr]                                          ;反正也就是5 cx足以mov ebx,ards_bufxor edx,edx
.find_max_mem_area:mov eax,[ebx]						 ;我也不是很清楚为什么用内存上限来表示操作系统可用部分add eax,[ebx+8]                                            ;既然作者这样用了 我们就这样用add ebx,20    						 ;简单的排序cmp edx,eaxjge .next_ardsmov edx,eax.next_ards:loop .find_max_mem_areajmp .mem_get_ok.e820_failed_so_try_e801:                                       ;地址段名字取的真的简单易懂 哈哈哈哈 mov ax,0xe801int 0x15jc .e801_failed_so_try_88;1 先算出来低15MB的内存    mov cx,0x400mul cx                                                      ;低位放在ax 高位放在了dxshl edx,16                                                  ;dx把低位的16位以上的书往上面抬 变成正常的数and eax,0x0000FFFF                                          ;把除了16位以下的 16位以上的数清零 防止影响or edx,eax                                                  ;15MB以下的数 暂时放到了edx中add edx,0x100000                                            ;加了1MB 内存空缺 mov esi,edx;2 接着算16MB以上的内存 字节为单位xor eax,eaxmov ax,bxmov ecx,0x10000                                              ;0x1000064KB  64*1024  mul ecx                                                      ;32位为0 因为低32位即有4GB 故只用加eaxmov edx,esiadd edx,eaxjmp .mem_get_ok.e801_failed_so_try_88:mov ah,0x88int 0x15jc .error_hltand eax,0x0000FFFFmov cx,0x400                                                 ;1024mul cxshl edx,16or edx,eax add edx,0x100000.error_hlt:jmp $
.mem_get_ok:mov [total_mem_bytes],edx
; --------------------------------- 设置进入保护模式 -----------------------------
; 1 打开A20 gate
; 2 加载gdt
; 3 将cr0 的 pe位置1in al,0x92                 ;端口号0x92 中 第1位变成1即可or al,0000_0010bout 0x92,allgdt [gdt_ptr]mov eax,cr0                ;cr0寄存器第0位设置位1or  eax,0x00000001              mov cr0,eax;-------------------------------- 已经打开保护模式 ---------------------------------------jmp dword SELECTOR_CODE:p_mode_start                       ;刷新流水线[bits 32]p_mode_start: mov ax,SELECTOR_DATAmov ds,axmov es,axmov ss,axmov esp,LOADER_STACK_TOP;------------------------------- 加载内核到缓冲区 -------------------------------------------------mov eax, KERNEL_BIN_SECTORmov ebx, KERNEL_BIN_BASE_ADDRmov ecx,200call rd_disk_m_32;------------------------------- 启动分页 ---------------------------------------------------call setup_page;这里我再把gdtr的格式写一下 0-15位界限 16-47位起始地址sgdt [gdt_ptr]                                             ;将gdt寄存器中的指 还是放到gdt_ptr内存中 我们修改相对应的 段描述符mov ebx,[gdt_ptr+2]                                        ;32位内存先倒出来 为的就是先把显存区域描述法的值改了 可以点开boot.inc 和 翻翻之前的段描述符;段基址的最高位在高4字节 故or dword [ebx+0x18+4],0xc0000000add dword [gdt_ptr+2],0xc0000000                            ;gdt起始地址增加 分页机制开启的前奏add esp,0xc0000000                                         ;栈指针也进入高1GB虚拟内存区mov eax,PAGE_DIR_TABLE_POSmov cr3,eaxmov eax,cr0or eax,0x80000000mov cr0,eaxlgdt [gdt_ptr]mov eax,SELECTOR_VIDEOmov gs,eaxmov byte [gs:160],'V'jmp SELECTOR_CODE:enter_kernel;------------------------------ 跳转到内核区    enter_kernel:call kernel_init					          ;根据我们的1M以下的内存分布区 综合考虑出的数据mov  esp,0xc009f000jmp  KERNEL_ENTER_ADDR;------------------------------- 创建页表 ------------------------------------------------    
setup_page:mov ecx,0x1000                                             ;循环4096次 将页目录项清空 内存清0mov esi,0                                                   .clear_page_dir_mem:                                          ;dir directory 把页目录项清空mov byte [PAGE_DIR_TABLE_POS+esi],0inc esiloop .clear_page_dir_mem.create_pde: mov eax,PAGE_DIR_TABLE_POS				  ;页目录项 起始位置add eax,0x1000                                              ;页目录项刚好4k字节 add eax即得第一个页表项的地址;接下来我们要做的是 把虚拟地址1M下和3G+1M 两部分的1M内存在页目录项中都映射到物理地址0-0XFFFFFor  eax, PG_P | PG_RW_W | PG_US_U                           ;哦 悟了 哈哈哈 这里设置为PG_US_U 是因为init在用户进程 如果这里设置成US_S 这样子连进内核都进不去了mov [PAGE_DIR_TABLE_POS+0x0],eax                             ;页目录项偏移0字节与偏移0xc00 对应0x 一条页目录项对应2^224MB 偏移由前10*4字节得到 可自己推算一下mov [PAGE_DIR_TABLE_POS+0xc00],eax                        sub eax,0x1000      mov [PAGE_DIR_TABLE_POS+4092],eax                           ;虚拟内存最后一个目录项 指向页目录表自身 书上写的是为了动态操纵页表 我也不是很清楚 反正有用 先放放;这里就创建了一页页表    mov eax,PAGE_DIR_TABLE_POSadd eax,0x1000mov ecx,256mov esi,0mov ebx,PG_P | PG_RW_W | PG_US_U .create_kernel_pte:           mov [eax+esi*4],ebxinc esiadd ebx,0x1000loop .create_kernel_pte ;这里对于我们这里填写的目录项所对应的页表 页表中我们还没填写的值
;为了实现 真正意义上的 内核空间被用户进程完全共享
;只是把页目录与页表的映射做出来了 mov eax,PAGE_DIR_TABLE_POSadd eax,0x2000       					   ;eax此时处于第二个页表or  eax,PG_P | PG_RW_W | PG_US_U
;这里循环254次可以来分析一下 我们这里做的是 0xc0 以上部分的映射    0xc0 对应的是第768个页表项 页表项中一共有 2^10=1024;1023项我们已经设置成 映射到页目录项本身位置了 即1022 - 769 +1 = 254mov ebx,PAGE_DIR_TABLE_POSmov ecx,254						  mov esi,769.create_kernel_pde:mov [ebx+esi*4],eaxinc esiadd eax,0x1000loop .create_kernel_pde ret            ;----------------------- 初始化内核 把缓冲区的内核代码放到0x1500区域 ------------------------------------------
;这个地方主要对elf文件头部分用的很多
;可以参照着书上给的格式 来比较对比
kernel_init:xor eax,eax   ;全部清零xor ebx,ebxxor ecx,ecxxor edx,edx;这里稍微解释一下 因为0x7000064kb*7=448kb 而我们的内核映射区域是4MB 而在虚拟地址4MB以内的都可以当作1:1映射mov ebx,[KERNEL_BIN_BASE_ADDR+28]add ebx,KERNEL_BIN_BASE_ADDR                               ;ebx当前位置为程序段表mov dx,[KERNEL_BIN_BASE_ADDR+42]		         ;获取程序段表每个条目描述符字节大小mov cx,[KERNEL_BIN_BASE_ADDR+44]                         ;一共有几个段.get_each_segment:cmp dword [ebx+0],PT_NULLje .PTNULL                                                 ;空即跳转即可 不进行mem_cpymov eax,[ebx+8]cmp eax,0xc0001500jb .PTNULLpush dword [ebx+16]                                        ;ebx+16在存储的数是filesz  可以翻到Loader刚开始mov eax,[ebx+4]                                            add eax,KERNEL_BIN_BASE_ADDRpush eax                                                   ;p_offset 在文件中的偏移位置    源位置         push dword [ebx+8]                                         ;目标位置call mem_cpyadd esp,12                                                 ;把三个参数把栈扔出去 等于恢复栈指针.PTNULL:add  ebx,edx                                               ;edx是一个描述符字节大小loop .get_each_segment                                     ;继续进行外层循环    retmem_cpy:cld                                                        ;向高地址自动加数字 cld std 向低地址自动移动push ebp                                                   ;保存ebp 因为访问的时候通过ebp 良好的编程习惯保存相关寄存器mov  ebp,esp push ecx                                                   ;外层循环还要用 必须保存 外层eax存储着还有几个段;分析一下为什么是 8 因为进入的时候又重新push了ebp 所以相对应的都需要+4;并且进入函数时 还Push了函数返回地址 所以就那么多了mov edi,[ebp+8]                                            ;目的指针 edi存储的是目的位置 4+4mov esi,[ebp+12]                                           ;源指针   源位置             8+4mov ecx,[ebp+16]                                           ;与Movsb好兄弟 互相搭配      12+4rep movsb                                                  ;一个一个字节复制pop ecx pop ebpret;------------------------ rd_disk_m_32  在mbr.S复制粘贴过来的 修改了点代码 ----------------------
rd_disk_m_32:
;1 写入待操作磁盘数
;2 写入LBA 低24位寄存器 确认扇区
;3 device 寄存器 第4位主次盘 第6位LBA模式 改为1
;4 command 写指令
;5 读取status状态寄存器 判断是否完成工作
;6 完成工作 取出数据;;;;;;;;;;;;;;;;;;;;;;1 写入待操作磁盘数;;;;;;;;;;;;;;;;;;;;;mov esi,eax   ; !!! 备份eaxmov di,cx     ; !!! 备份cxmov dx,0x1F2  ; 0x1F2为Sector Count 端口号 送到dx寄存器中mov al,cl     ; !!! 忘了只能由ax al传递数据out dx,al     ; !!! 这里修改了 原out dx,clmov eax,esi   ; !!!袄无! 原来备份是这个用 前面需要ax来传递数据 麻了;;;;;;;;;;;;;;;;;;;;;
;2 写入LBA 24位寄存器 确认扇区
;;;;;;;;;;;;;;;;;;;;;mov cl,0x8    ; shr 右移8位 把24位给送到 LBA low mid high 寄存器中mov dx,0x1F3  ; LBA lowout dx,al mov dx,0x1F4  ; LBA midshr eax,cl    ; eax为32位 ax为16位 eax的低位字节 右移8位即8~15out dx,almov dx,0x1F5shr eax,clout dx,al;;;;;;;;;;;;;;;;;;;;;
;3 device 寄存器 第4位主次盘 第6位LBA模式 改为1
;;;;;;;;;;;;;;;;;;;;;; 24 25 26 27位 尽管我们知道ax只有2 但还是需要按规矩办事 ; 把除了最后四位的其他位置设置成0shr eax,cland al,0x0f or al,0xe0   ;!!! 把第四-七位设置成0111 转换为LBA模式mov dx,0x1F6 ; 参照硬盘控制器端口表 Device out dx,al;;;;;;;;;;;;;;;;;;;;;
;4 向Command写操作 Status和Command一个寄存器
;;;;;;;;;;;;;;;;;;;;;mov dx,0x1F7 ; Status寄存器端口号mov ax,0x20  ; 0x20是读命令out dx,al;;;;;;;;;;;;;;;;;;;;;
;5 向Status查看是否准备好惹 
;;;;;;;;;;;;;;;;;;;;;;设置不断读取重复 如果不为1则一直循环.not_ready:     nop           ; !!! 空跳转指令 在循环中达到延时目的in al,dx      ; 把寄存器中的信息返还出来and al,0x88   ; !!! 0100 0100 0x88cmp al,0x08jne .not_ready ; !!! jump not equal == 0;;;;;;;;;;;;;;;;;;;;;
;6 读取数据
;;;;;;;;;;;;;;;;;;;;;mov ax,di      ;把 di 储存的cx 取出来mov dx,256mul dx        ;与di 与 ax 做乘法 计算一共需要读多少次 方便作循环 低16位放ax 高16位放dxmov cx,ax      ;loop 与 cx相匹配 cx-- 当cx == 0即跳出循环mov dx,0x1F0.go_read_loop:in ax,dx      ;两字节dx 一次读两字mov [ebx],axadd ebx,2loop .go_read_loopret ;与call 配对返回原来的位置 跳转到call下一条指令

GCC 9.4 苦手版本(不兼容后面章节代码)


下面是 gcc 9.4的苦手版本 非常不建议使用 看一乐即可
注意这里我给的全代码 是在gcc 9.4版本下才能运行正确的
如果还想完成之后的章节代码的话 转向gcc4.4那部分代码


%include "boot.inc"
SECTION loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR 		   ;是个程序都需要有栈区 我设置的0x600以下的区域到0x500区域都是可用空间 况且也用不到
jmp loader_start                     		   	   ;下面存放数据段 构建gdt 跳跃到下面的代码区 ;对汇编再复习 db define byte,dw define word,dd define dwordGDT_BASE        : dd 0x00000000          		   ;刚开始的段选择子0不能使用 故用两个双字 来填充dd 0x00000000 CODE_DESC       : dd 0x0000FFFF         		   ;FFFF是与其他的几部分相连接 形成0XFFFFF段界限dd DESC_CODE_HIGH4DATA_STACK_DESC : dd 0x0000FFFFdd DESC_DATA_HIGH4VIDEO_DESC      : dd 0x80000007         		   ;0xB80000xBFFFF为文字模式显示内存 B只能在boot.inc中出现定义了 此处不够空间了 8000刚好够dd DESC_VIDEO_HIGH4     	   ;0x0007 bFFFF-b8000)/4k = 0x7GDT_SIZE              equ $ - GDT_BASE               ;当前位置减去GDT_BASE的地址 等于GDT的大小GDT_LIMIT       	   equ GDT_SIZE - 1   	           ;SIZE - 1即为最大偏移量times 59 dq 0                             	   ;预留59个 define double四字型 8字描述符times 5 db 0                                         ;为了凑整数 0x800 导致前面少了三个total_mem_bytes  dd 0;在此前经过计算程序内偏移量为0x200 我算了算 60*8+4*8=512 刚好是 0x200 说这里的之后还会用到;我们刚开始程序设置的地址位置为 0x600 那这就是0x800gdt_ptr           dw GDT_LIMIT			   ;gdt指针 2字gdt界限放在前面 4字gdt地址放在后面 lgdt 48位格式 低位16位界限 高位32位起始地址dd GDT_BASEards_buf times 244 db 0                              ;buf  记录内存大小的缓冲区ards_nr dw 0					   ;nr 记录20字节结构体个数  计算了一下 4+2+4+244+2=256 刚好256字节;书籍作者有强迫症 哈哈 这里244的buf用不到那么多的 实属强迫症使然 哈哈SELECTOR_CODE        equ 0X0001<<3) + TI_GDT + RPL0    ;16位寄存器 4位TI RPL状态 GDT剩下的选择子SELECTOR_DATA	  equ 0X0002<<3) + TI_GDT + RPL0SELECTOR_VIDEO       equ 0X0003<<3) + TI_GDT + RPL0   loader_start:mov sp,LOADER_BASE_ADDR                                   ;先初始化了栈指针xor ebx,ebx                                               ;异或自己 即等于0mov ax,0                                       mov es,ax                                                 ;心有不安 还是把es给初始化一下mov di,ards_buf                                           ;di指向缓冲区位置
.e820_mem_get_loop:mov eax,0x0000E820                                            ;每次都需要初始化mov ecx,0x14mov edx,0x534d4150int 0x15                                                  ;调用了0x15中断jc  .e820_failed_so_try_e801                              ;这时候回去看了看jc跳转条件 就是CF位=1 carry flag = 1 中途失败了即跳转add di,cx							;把di的数值增加20 为了下一次作准备inc word [ards_nr]cmp ebx,0jne .e820_mem_get_loop                                    ;直至读取完全结束 则进入下面的处理时间mov cx,[ards_nr]                                          ;反正也就是5 cx足以mov ebx,ards_bufxor edx,edx
.find_max_mem_area:mov eax,[ebx]						 ;我也不是很清楚为什么用内存上限来表示操作系统可用部分add eax,[ebx+8]                                            ;既然作者这样用了 我们就这样用add ebx,20    						 ;简单的排序cmp edx,eaxjge .next_ardsmov edx,eax.next_ards:loop .find_max_mem_areajmp .mem_get_ok.e820_failed_so_try_e801:                                       ;地址段名字取的真的简单易懂 哈哈哈哈 mov ax,0xe801int 0x15jc .e801_failed_so_try_88;1 先算出来低15MB的内存    mov cx,0x400mul cx                                                      ;低位放在ax 高位放在了dxshl edx,16                                                  ;dx把低位的16位以上的书往上面抬 变成正常的数and eax,0x0000FFFF                                          ;把除了16位以下的 16位以上的数清零 防止影响or edx,eax                                                  ;15MB以下的数 暂时放到了edx中add edx,0x100000                                            ;加了1MB 内存空缺 mov esi,edx;2 接着算16MB以上的内存 字节为单位xor eax,eaxmov ax,bxmov ecx,0x10000                                              ;0x1000064KB  64*1024  mul ecx                                                      ;32位为0 因为低32位即有4GB 故只用加eaxmov edx,esiadd edx,eaxjmp .mem_get_ok.e801_failed_so_try_88:mov ah,0x88int 0x15jc .error_hltand eax,0x0000FFFFmov cx,0x400                                                 ;1024mul cxshl edx,16or edx,eax add edx,0x100000.error_hlt:jmp $
.mem_get_ok:mov [total_mem_bytes],edx
; --------------------------------- 设置进入保护模式 -----------------------------
; 1 打开A20 gate
; 2 加载gdt
; 3 将cr0 的 pe位置1in al,0x92                 ;端口号0x92 中 第1位变成1即可or al,0000_0010bout 0x92,allgdt [gdt_ptr]mov eax,cr0                ;cr0寄存器第0位设置位1or  eax,0x00000001              mov cr0,eax;-------------------------------- 已经打开保护模式 ---------------------------------------jmp dword SELECTOR_CODE:p_mode_start                       ;刷新流水线[bits 32]p_mode_start: mov ax,SELECTOR_DATAmov ds,axmov es,axmov ss,axmov esp,LOADER_STACK_TOP;------------------------------- 加载内核到缓冲区 -------------------------------------------------mov eax, KERNEL_BIN_SECTORmov ebx, KERNEL_BIN_BASE_ADDRmov ecx,200call rd_disk_m_32;------------------------------- 启动分页 ---------------------------------------------------call setup_page;这里我再把gdtr的格式写一下 0-15位界限 16-47位起始地址sgdt [gdt_ptr]                                             ;将gdt寄存器中的指 还是放到gdt_ptr内存中 我们修改相对应的 段描述符mov ebx,[gdt_ptr+2]                                        ;32位内存先倒出来 为的就是先把显存区域描述法的值改了 可以点开boot.inc 和 翻翻之前的段描述符;段基址的最高位在高4字节 故or dword [ebx+0x18+4],0xc0000000add dword [gdt_ptr+2],0xc0000000                            ;gdt起始地址增加 分页机制开启的前奏add esp,0xc0000000                                         ;栈指针也进入高1GB虚拟内存区mov eax,PAGE_DIR_TABLE_POSmov cr3,eaxmov eax,cr0or eax,0x80000000mov cr0,eaxlgdt [gdt_ptr]mov eax,SELECTOR_VIDEOmov gs,eaxmov byte [gs:160],'V'jmp SELECTOR_CODE:enter_kernel;------------------------------ 跳转到内核区    enter_kernel:call kernel_init					          ;根据我们的1M以下的内存分布区 综合考虑出的数据mov  esp,0xc009f000jmp  KERNEL_ENTER_ADDR;------------------------------- 创建页表 ------------------------------------------------    
setup_page:mov ecx,0x1000                                             ;循环4096次 将页目录项清空 内存清0mov esi,0                                                   .clear_page_dir_mem:                                          ;dir directory 把页目录项清空mov byte [PAGE_DIR_TABLE_POS+esi],0inc esiloop .clear_page_dir_mem.create_pde: mov eax,PAGE_DIR_TABLE_POS				  ;页目录项 起始位置add eax,0x1000                                              ;页目录项刚好4k字节 add eax即得第一个页表项的地址;接下来我们要做的是 把虚拟地址1M下和3G+1M 两部分的1M内存在页目录项中都映射到物理地址0-0XFFFFFor  eax, PG_P | PG_RW_W | PG_US_U                           ;哦 悟了 哈哈哈 这里设置为PG_US_U 是因为init在用户进程 如果这里设置成US_S 这样子连进内核都进不去了mov [PAGE_DIR_TABLE_POS+0x0],eax                             ;页目录项偏移0字节与偏移0xc00 对应0x 一条页目录项对应2^224MB 偏移由前10*4字节得到 可自己推算一下mov [PAGE_DIR_TABLE_POS+0xc00],eax                        sub eax,0x1000      mov [PAGE_DIR_TABLE_POS+4092],eax                           ;虚拟内存最后一个目录项 指向页目录表自身 书上写的是为了动态操纵页表 我也不是很清楚 反正有用 先放放;这里就创建了一页页表    mov eax,PAGE_DIR_TABLE_POSadd eax,0x1000mov ecx,256mov esi,0mov ebx,PG_P | PG_RW_W | PG_US_U .create_kernel_pte:           mov [eax+esi*4],ebxinc esiadd ebx,0x1000loop .create_kernel_pte ;这里对于我们这里填写的目录项所对应的页表 页表中我们还没填写的值
;为了实现 真正意义上的 内核空间被用户进程完全共享
;只是把页目录与页表的映射做出来了 mov eax,PAGE_DIR_TABLE_POSadd eax,0x2000       					   ;eax此时处于第二个页表or  eax,PG_P | PG_RW_W | PG_US_U
;这里循环254次可以来分析一下 我们这里做的是 0xc0 以上部分的映射    0xc0 对应的是第768个页表项 页表项中一共有 2^10=1024;1023项我们已经设置成 映射到页目录项本身位置了 即1022 - 769 +1 = 254mov ebx,PAGE_DIR_TABLE_POSmov ecx,254						  mov esi,769.create_kernel_pde:mov [ebx+esi*4],eaxinc esiadd eax,0x1000loop .create_kernel_pde ret            ;----------------------- 初始化内核 把缓冲区的内核代码放到0x1500区域 ------------------------------------------
;这个地方主要对elf文件头部分用的很多
;可以参照着书上给的格式 来比较对比
kernel_init:xor eax,eax   ;全部清零xor ebx,ebxxor ecx,ecxxor edx,edx;这里稍微解释一下 因为0x7000064kb*7=448kb 而我们的内核映射区域是4MB 而在虚拟地址4MB以内的都可以当作1:1映射mov ebx,[KERNEL_BIN_BASE_ADDR+0x20]add ebx,KERNEL_BIN_BASE_ADDR                               ;ebx当前位置为程序段表mov dx,[KERNEL_BIN_BASE_ADDR+0x36]		         ;获取程序段表每个条目描述符字节大小mov cx,[KERNEL_BIN_BASE_ADDR+0x38]                         ;一共有几个段.get_each_segment:cmp dword [ebx+0],PT_NULLje .PTNULL                                                 ;空即跳转即可 不进行mem_cpymov eax,[ebx+0x10]mov esi,0xc0001500cmp eax,esijb .PTNULLpush dword [ebx+0x20]                                        ;ebx+16在存储的数是filesz  可以翻到Loader刚开始mov eax,[ebx+8]                                            add eax,KERNEL_BIN_BASE_ADDRpush eax                                                   ;p_offset 在文件中的偏移位置    源位置         push dword [ebx+0x10]                                         ;目标位置call mem_cpyadd esp,12                                                 ;把三个参数把栈扔出去 等于恢复栈指针.PTNULL:add  ebx,edx                                               ;edx是一个描述符字节大小loop .get_each_segment                                     ;继续进行外层循环    retmem_cpy:cld                                                        ;向高地址自动加数字 cld std 向低地址自动移动push ebp                                                   ;保存ebp 因为访问的时候通过ebp 良好的编程习惯保存相关寄存器mov  ebp,esp push ecx                                                   ;外层循环还要用 必须保存 外层eax存储着还有几个段;分析一下为什么是 8 因为进入的时候又重新push了ebp 所以相对应的都需要+4;并且进入函数时 还Push了函数返回地址 所以就那么多了mov edi,[ebp+8]                                            ;目的指针 edi存储的是目的位置 4+4mov esi,[ebp+12]                                           ;源指针   源位置             8+4mov ecx,[ebp+16]                                           ;与Movsb好兄弟 互相搭配      12+4rep movsb                                                  ;一个一个字节复制pop ecx pop ebpret;------------------------ rd_disk_m_32  在mbr.S复制粘贴过来的 修改了点代码 ----------------------
rd_disk_m_32:
;1 写入待操作磁盘数
;2 写入LBA 低24位寄存器 确认扇区
;3 device 寄存器 第4位主次盘 第6位LBA模式 改为1
;4 command 写指令
;5 读取status状态寄存器 判断是否完成工作
;6 完成工作 取出数据;;;;;;;;;;;;;;;;;;;;;;1 写入待操作磁盘数;;;;;;;;;;;;;;;;;;;;;mov esi,eax   ; !!! 备份eaxmov di,cx     ; !!! 备份cxmov dx,0x1F2  ; 0x1F2为Sector Count 端口号 送到dx寄存器中mov al,cl     ; !!! 忘了只能由ax al传递数据out dx,al     ; !!! 这里修改了 原out dx,clmov eax,esi   ; !!!袄无! 原来备份是这个用 前面需要ax来传递数据 麻了;;;;;;;;;;;;;;;;;;;;;
;2 写入LBA 24位寄存器 确认扇区
;;;;;;;;;;;;;;;;;;;;;mov cl,0x8    ; shr 右移8位 把24位给送到 LBA low mid high 寄存器中mov dx,0x1F3  ; LBA lowout dx,al mov dx,0x1F4  ; LBA midshr eax,cl    ; eax为32位 ax为16位 eax的低位字节 右移8位即8~15out dx,almov dx,0x1F5shr eax,clout dx,al;;;;;;;;;;;;;;;;;;;;;
;3 device 寄存器 第4位主次盘 第6位LBA模式 改为1
;;;;;;;;;;;;;;;;;;;;;; 24 25 26 27位 尽管我们知道ax只有2 但还是需要按规矩办事 ; 把除了最后四位的其他位置设置成0shr eax,cland al,0x0f or al,0xe0   ;!!! 把第四-七位设置成0111 转换为LBA模式mov dx,0x1F6 ; 参照硬盘控制器端口表 Device out dx,al;;;;;;;;;;;;;;;;;;;;;
;4 向Command写操作 Status和Command一个寄存器
;;;;;;;;;;;;;;;;;;;;;mov dx,0x1F7 ; Status寄存器端口号mov ax,0x20  ; 0x20是读命令out dx,al;;;;;;;;;;;;;;;;;;;;;
;5 向Status查看是否准备好惹 
;;;;;;;;;;;;;;;;;;;;;;设置不断读取重复 如果不为1则一直循环.not_ready:     nop           ; !!! 空跳转指令 在循环中达到延时目的in al,dx      ; 把寄存器中的信息返还出来and al,0x88   ; !!! 0100 0100 0x88cmp al,0x08jne .not_ready ; !!! jump not equal == 0;;;;;;;;;;;;;;;;;;;;;
;6 读取数据
;;;;;;;;;;;;;;;;;;;;;mov ax,di      ;把 di 储存的cx 取出来mov dx,256mul dx        ;与di 与 ax 做乘法 计算一共需要读多少次 方便作循环 低16位放ax 高16位放dxmov cx,ax      ;loop 与 cx相匹配 cx-- 当cx == 0即跳出循环mov dx,0x1F0.go_read_loop:in ax,dx      ;两字节dx 一次读两字mov [ebx],axadd ebx,2loop .go_read_loopret ;与call 配对返回原来的位置 跳转到call下一条指令

boot.inc代码


boot.inc目前全代码 方便各位读者自己调试

;------------------- 进入loader所需要的宏 --------------------------LOADER_START_SECTOR equ 2
LOADER_BASE_ADDR equ 0x600 ;博客名字是Love 6 干脆就把Loader设置加载到0x600;-------------------- 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少打了一位 这里的全是0为高位 低位即可表示段基址DESC_P            equ 	1_000000000000000b	  ;15位  P present判断段是否存在于内存  
DESC_DPL_0        equ  00_0000000000000b         ;13-14位 这两位更是重量级 Privilege Level 0-3
DESC_DPL_1        equ  01_0000000000000b	  ;0为操作系统 权力最高 3为用户段 用于保护
DESC_DPL_2        equ  10_0000000000000b
DESC_DPL_3        equ  11_0000000000000bDESC_S_sys        equ  0_000000000000b           ;12位为0 则表示系统段 为1则表示数据段
DESC_S_CODE       equ  1_000000000000b           ;12位与type字段结合 判断是否为系统段还是数据段
DESC_S_DATA       equ  DESC_S_CODEDESC_TYPE_CODE    equ  1000_00000000b            ;9-11位表示该段状态 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字节初始化 0x008<<2432位初始化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  ;整挺好 我看书上写的0x00 结果我自己推算出来这里末尾是B ;-------------------- 选择子属性 --------------------------------
;0-1位 RPL 特权级比较是否允许访问  第2位TI 0表示GDT 1表示LDT    第3-15位索引值
RPL0    equ 00b
RPL1    equ 01b
RPL2    equ 10b
RPL3    equ 11b
TI_GDT  equ 000b
TI_LDT  equ 100b;------------------ 开启页表所需要的宏 ---------------------------PAGE_DIR_TABLE_POS  equ 0x100000                          ;这里设置了页目录项的起始位置;------------------ 页表相关属性 ---------------------------------PG_P 	 equ  1b                                            ;PG目录项的属性 Present存在于当前物理内存
PG_RW_R  equ 00b                                            ;只可读不可写
PG_RW_W  equ 10b                                            ;可写可读
PG_US_S  equ 000b                                           ;Supervisor  超级用户
PG_US_U  equ 100b                                           ;User        普通用户
;不是很清楚 Global位为什么宏先不定义 但是剩下的PWT PCD 我们用不到即设置为0 A位是cpu操控的 页表项就算是弄完了;-----------------  加载内核宏定义 -------------------------------KERNEL_BIN_SECTOR    equ  0x9
KERNEL_BIN_BASE_ADDR equ  0x70000
KERNEL_ENTER_ADDR    equ  0xc0001500PT_NULL              equ  0x0

结束语(第五章完结撒花~)


终于结束了 为期三天的调试+学习第五章落下了帷幕
真的还是很感谢这本书 我真的说这三天把我之前很多的疑惑扫荡了干净
也让我在这次分段分页手写中 提升了太多太多
真的也让我深感到 如果我在大学期间没有自己写一个操作系统出来
那么操作系统这门课 我学的再久再多 终将是一场梦和无限的疑惑

也还是很感谢哈工大的李治军老师说的一句话 我时常能够在完成很多东西后想起
纸上得来终觉浅 觉知此事要躬行
实属让我受益匪浅 感恩

Published by

风君子

独自遨游何稽首 揭天掀地慰生平

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注