《操作系统真相还原》第五章 保护模式进阶,向内核迈进 上

获得物理内存容量

Linux获得内存的基本方法

在linux2.6内核中,使用detect_memory函数来获取内存容量,其函数本质上是通过BIOS中断0x15实现的,分别是BIOS中断0x15的3个子功能,子功能号需要存放到寄存器EAX或AX中,下面介绍一下这三种模式。
EAX=0xE820:遍历主机上全部内存
AX=0xE801:分别检测低15MB和16MB~4GB内存,最大支持4GB
AH=0x88:最多检测64MB内存,实际内存超过此容量也按照64MB返回
这里我们并不是说要在保护模式下进行BIOS中断操作,我们先在实模式下检测物理内存再进入保护模式。这里我们将这三个方法依此介绍。

利用BIOS中断0x15子功能0xe820获取内存

据说这是最灵活的内存获取方式,说他灵活是因为他返回的信息比较丰富,而返回丰富的信息就表示我们需要用一种格式结构来组织这些数据。而内存信息的内容使用地址范围描述符来描述的,用于存储这种描述符的结构和称之为地址范围描述符,格式如下。
上述的字段从偏移也可以看出来每个占4字节,其中含义大家可以由表得知,这里详细介绍其中的TYPE字段,具体的意义如下:
而BIOS按照上述类型来返回内存细腻些是因为这段内存可能为以下几种情况:
1.系统ROM
2.ROM用到了这部分内存
3.设备内存映射到了这部分内存
4.由于某种原因,这段内存不适合标准设备使用
而由于我们是在32为环境下,所以我们只需要用到低32为属性,也就是BaseAddLow和LengthLow就可以了,当然我们在调用BIOS中断不仅仅使得EAX或AX里面有相应的功能号,我们还需要通过其他寄存器传入一系列参数。
这里值得注意的参数寄存器由EAX和ES:DI,其中ECX是指缓冲区大小,ES:DO是指缓冲区指针,被调用函数将所写入内容写入该缓冲区,然后记录写入内容大小然后记录在缓冲区大小寄存器中,注意这里调用者是传入的期待BIOS写入大小,而被调用者是往ECX写入实际大小,此中端的调用步骤如下:
1.填写好调用前函数寄存器
2.执行中断调用int 0x15
3.在CF为0的情况下,“返回后输出”的寄存器便会有相应的结果

利用BIOS中断0x15子功能0xe801获取内存

这个子功能不是很厉害,虽然他最多只能识别4GB内存,但是对我们32地址总线足够了,但是有点特殊的就是这种方法检测到的内存是分别放到两组寄存器中的。低于15MB的内存以1KB为单位来记录,单位数量在AX和CX中记录,其中AX和CX的值是一样的,所以在15MB空间一下的实际内存容量=AXx1024.AX,CX最大值为0x3c00,即0x3c00**** 1024=15MB,而16MB~4GB是以64KB为单位大小记录的,单位数量在BX,DX中存储,其中这俩内容一样,跟上述AX,CX类似。下面给出输入时寄存器以及输出时寄存器的功能和作用:
我们会注意到两个问题
1.为什么要分“前15MB”和“16MB~4GB”。
2.为什么要设两个内容相同的单位量寄存器,就是说AX=CX,BX=DX。
为了解释第一个问题,我们看表头,发现实际物理内存和检测到的内存大小总是相差1MB,这是为什么呢?
这实际上是遗留的问题,这是由于在80286版本由于有24位地址线,即表示16MB的内存空间,其中低15MB用来正常作为内存使用,而高1MB是留给一些ISA设备作为缓冲区使用的,到了现在由于为了向前兼容,这1MB被空了出来。所以当我们检查内存大小等于16MB时,其中AXx1024必然小于等于15MB,而BX×64K必然大于0,所以我们在这种情况下是可以检查出这个历史遗留的1MB的内存空洞,但若是我们检查内存小于16MB时,我们所检查的内容范围就是小于实际内存1MB.
至于第二个问题,为什么要用两个内容相同的问题,我们在上面的输入寄存器的图片中可以看到,AX与CX,BX和DX这两组寄存器中,都是一个充当Extended和Configured,但这里我们暂时不去区别它们之间的不同。
这里给出具体调用步骤:
1.将AX寄存器写入0xE801
2.执行中断调用
3.在CF为0的情况,“返回后输出”的寄存器便会有相应的结果

利用BIOS中断子功能0x88获取内存

这是最后一个子功能了,他使用简单,获取的东西也简单,他只能识别到最大64MB的内存,即使内存容量大于64MB,也只会显示63MB,这里为啥又少了1MB呢,这是因为此中断只能显示1MB之上的内存,所以我们在检测之后需要加上1MB。调用步骤如下
1.将AH寄存器写入0x88
2.执行中断调用int 0x15
3.在CF为0的情况下,“返回后输出”的寄存器便会有相应的结果

实战

;人工对齐:total_mem_bytes4+gdt_ptr6+ards_buf244+ards_nr2,256字节,0x100 
ards_buf times 244 db 0
ards_nr dw 0        ;用于记录ARDS结构体的数量

loader_start:
;------ int 15H eax = 0000E820,edx = 534D4150('SMAP') 获取内存布局-------
  xor ebx, ebx      ;第一次调用时,ebx置0
  mov edx, 0x534d4150 ;edx只赋值一次,循环体中不会改变
  mov di, ards_buf ;ards结构缓冲区,这里由于es我们在mbr.S中已经初始化,为0,所以这里我们不需要修改es,只需要对di赋值即可
.e820_mem_get_loop:
  mov eax, 0x0000e820   ;每次执行int 0x15之后,eax会变成0x534d4150,所以每次执行int之前都要更新为子功能号
  mov ecx, 20
  int 0x15
  jc .e820_failed_so_try_e801   ;若cf位为1则说明有错误发生,尝试下一个0xe801方法
  add di, cx            ;使di增加20字节指向缓冲区中新的ARDS结构位置
  inc word[ards_nr]     ;记录ARDS数量
  cmp ebx,0             ;若ebx为0且cf不为1,这说明ards全部返回
  jnz .e820_mem_get_loop
;在所有ards结构体中找出(base_add_low + length_low的最大值,即为内存容量
  mov cx, [ards_nr]     ;遍历每一个ards结构提,循环次数cx就是ards的数量
  mov ebx, ards_buf     ;将ebx中放入我们构造的缓冲区地址
  xor edx, edx          ;edx为最大的内存容量,在此先清0
.find_max_mem_area:     ;这里不需要判断type是否为1,最大的内存块一定是可被使用的
  mov eax, [ebx]
  add eax, [ebx+8]      ;这里ebx和ebx+8代表了BaseAddrLow 和 LengthLow
  add ebx, 20           ;ebx指向下一个ards结构体
  cmp edx, eax          ;冒泡排序,找出最大,edx寄存器始终是最大的内存容量
  jge .next_ards        ;大于或等于
  mov edx, eax          ;edx为总内存大小
.next_ards:
  loop .find_max_mem_area ;循环,以cx为循环次数
  jmp .mem_get_ok

;------ int 15H ax=E801h , 获取内存大小,最大支持4G------
;返回后,ax cx值一样,以KB为单位, bx dx 一样,以64KB为单位
;在ax和cx寄存器中为低16MB,在bx与dx寄存器中为16MB到4GB
.e820_failed_so_try_e801:
  mov ax, 0xe801
  int 0x15
  jc .e801_failed_so_try88  ;若cf位为1则说明有错误发生,尝试下一个88方法
;1 先算出低15MB的内存
; ax和cx中是以KB为单位的内存数量,因此我们将其转换为以byte为单位
  mov cx,0x400  ;这里由于cx和ax一样,所以我们将cx用作乘数,0x400即为1024
  mul cx        ;由于处于实模式,所以我们mul指令的含义是ax × cx,注意mul指令是16位乘法,生成乘数应该是32位,高16位在dx中,低16位存于ax中
  shl edx, 16   ;左移16,这里也就是将dx保存的高16位转移到edx的高16位上
  and eax, 0x0000FFFF   ;将eax高16位清0
  or edx, eax   ;或后得出乘积,保存至edx中
  add edx, 0x100000     ;最后将差的那1MB加上
  mov esi, edx      ;这里保存一下edx的值,因为在之后的计算过程中他会被破坏

;2 再将16MB以上的内存转换为byte为单位
  xor eax, eax
  mov ax, bx
  mov ecx, 0x10000  ;0x1000016进制的64K
  mul ecx           ;32位乘法,其高32位和低32位存放在edx和eax中
  add esi, eax      ;由于这里只能最大测出4GB,edx的值肯定为0,所以咱们只需要eax就可以了
  mov edx, esi      ;其中edx为总内存大小
  jmp .mem_get_ok

;----- int 15h ah=0x88 获取内存大小,只能获取64MB之内 -------
.e801_failed_so_try88:
  ;int 15h后,ax存入的是以KB为单位的内存容量
  mov ah, 0x88
  int 0x15
  jc .error_hlt
  and eax, 0x0000FFFF

  ;16位乘法
  mov cx, 0x400
  mul cx
  shl edx, 16
  or edx, eax
  add edx,0x100000  ;0x88子功能只会返回1MB以上的内存,所以最终我们还需要加上1MB

.error_hlt:
  jmp $

.mem_get_ok:
  mov [total_mem_bytes], edx            ;将内存换为bytes为单位然后存入total_mem_bytes中

因为我们loader是存放在内存0x900的地址,然而我们在这个地址上又加上了四个段描述符和60个段描述符预留空位,此时已经用了0x200,然后我们还需要划出一点来作为ards的存放缓冲,存放最大内存,还有gdt指针,这些统统加起来为了满足0x100的整数倍,我们在此选择缓冲区大小申请244字节,这里大家认真查看代码然后计算即可,
注意还有个需要修改的地方就是mbr.S,因为我们在loader.S上去掉了jmp loader_start(占3字节),而loader_start在loader中的偏移为我们精心准备好的0x300,所以我们在mbr跳转到loader_start时就要加上0x300.
修改部分如下:

 jmp LOADER_BASE_ADDR + 0x300         ;代码运行至此说明Loader已经加载完毕

这样在我们mbr运行完毕后就会直接跳转到loader_start开始内存检测了。这里我给出我们bochs的配置:

#第一步,首先设置 Bochs 在运行过程中能够使用的内存,本例为 32MB
#关键字为 me gs
megs :512
#第二步,设置对应真实机器的 BIOS VGA BIOS
#对应两个关键字为 romimage vgaromimage
romimage: file=/home/dawn/repos/OS_learning/bochs/share/bochs/BIOS-bochs-latest
vgaromimage: file=/home/dawn/repos/OS_learning/bochs/share/bochs/VGABIOS-lgpl-latest
#第三步,设置 Bochs 所使用的磁盘,软盘的关键字为 floppy
#若只有一个软盘,目IJ 使用 floppy 即可,若有多个,则为 floppya, floppyb… #floppya: 1_ 44=a.img, status=inserted
#第四步,选择启动盘符。
#boot: floppy #默认从软盘启动,将其注释
boot: disk #改为从硬盘启动。我们的任何代码都将直接写在硬盘上,所以不会再有读写软盘的操作。
#第五步,设置日志文件的输出。
log: bochs.out
#第六步,开启或关闭某些功能。
#下面是关闭鼠标,并打开键盘。
mouse: enabled=0
keyboard:keymap=/home/dawn/repos/OS_learning/bochs/share/bochs/keymaps/x11-pc-us.map
#硬盘设置
ata0: enabled=1, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14
#下面的是增加的 bochs gdb 的支持,这样 gdb 可以远程连接到此机器的 234 口调试了
#gdbstub : enabled=l, port=l234, text_base=O, data_base=O, bss_base=O
################### 配置文件结束 #####################
ata0-master: type=disk, path="hd60M.img", mode=flat

可以看到内存设置为512MB,我们在开启bochs查看是否如此。
发现果然如此,这个0x20000000大家用十进制表示会发现确实为512MB。

分页

虚拟地址

上面我们检测了内存地址,发现只有512MB,但是我们的操作系统是32位的,所以最大的寻址地址空间应该是4GB,难道说我们的程序若是放到高于512MB就无法运行了?事实上这个4GB寻址范围和内存地址512MB并不是一一对应的。也就是说我们的程序最开始是希望加载到一个4GB。但是加载到哪是由我们的连接器决定的,实际上是程序员自己选择的。但是真正的物理机上并不是4GB,所以我们将整片程序中的一部分称为页,然后哦我们按照自己的需要映射到真实的物理内存中,此时我们并不需要一次性全部的放到物理内存中。
现在的效果就是,咱们自以为的空间里面是连续的,而映射到物理内存中是由操作系统决定的,此时就并不一定连续,但我们程序进行的一系列操作都是基于我们自认为的虚拟空间,操作系统只需要负责映射就行。

一级页表

在我们没有使用分页机制的时候,我们采用的仍然是系统自带的分段的方式,也就是依靠段地址:段内偏移地址进行地址选择,且该地址仍然是物理地址,下面是分段的寻址过程
在这里插入图片描述
当我们开启分页机制之后,我们程序员所使用的地址就变为了虚拟地址,寻址过程就变了
在这里插入图片描述
我们使用4GB虚拟内存,首先会将其分为大小一致的一堆页,而这个页面大小一般定为4KB,也就是说在32位地址中,高20位为也地址,低12位位业内地址,在我们本来的程序中时进行了分段,但是载入物理内存的过程中就会进行让分页而打乱顺序,此时就需要页表,页表中保存的就是一个个映射,保证你按顺序访问虚拟地址,他会给出相对应的物理地址。
在这里插入图片描述
所以我们需要一个页表来建立这层映射关系,页表中的每个页表项就保存着一个真实物理地址,但是光有页表还不行,我们还需要找到并且得到他,所以我们还需要一个额外的寄存器来保存这个页表中在物理地址中的位置,这个寄存器就是CR3。
具体的寻址过程其实就是
1.首先我们拥有要访问的虚拟地址
2.此时我们取虚拟地址的高20位,这就是页表相对偏移
3.我们找到cr3寄存器中的页表首地址,然后加上我们刚刚取到的偏移再乘4,这是因为一个页表项占4字节,我们访问该物理地址就会得到另一个物理地址
4.刚刚从页表当中得到的物理地址是我们真正要访问的页地址,此时我们再加上虚拟地址的低12位,也就是页内地址,这样我们就得到了我们真正想访问的地址了。
在这里插入图片描述

二级页表

二级页表同一级页表类似,就是中间加了一层,这里提出二级页表的原因是由于最高级页表必须在内存,但是我们若只采用一级的话,常驻的内存页表会很大,所以我们需要再加上一级页表(这里应该称为页目录)用来减少内存消耗,我们在一级页表是采用了高20位来表示页表项的偏移,这里我们二级页表将其对半分开,高10位用作页目录偏移,剩下的10位用作页表偏移。
在这里插入图片描述
其中页目录项之于页目录,页表项之于页表,就如同段描述符之于全局描述表一样,下面给出这俩的具体结构
在这里插入图片描述
这里我们可以看到并不是说表项全是地址,他还有很多别的标志位,其中表项保存地址只用了20位,但为什么不是32位呢,因为咱们只需要高20位,也就是页的首地址,而页都是以0x1000为单位的,所以低12位肯定为0,就不需要保存啦,接下来介绍每个标志位的含义:

P位,Present,类似段描述符,表示是否存在,为1表示存在于物理内存
RW, Read/Write,读写位,为1则表示可读写
US, User/Supervisor,普通/超级用户位,若为1则表示处于用户级,任意级别(0,1,2,3)都可以使用此页,当为0的时候表示超级用户位,特权级别3不可访问,而(0,1,2)可以访问此页
PWT, Page-Level Write-Through,意为页级通写位,若为1表示采用通写方式,表示该页不仅在内存,还存在在高速缓存。我们在这里默认置0
PCD, Page-Level Cache-disable,意为页级高速缓存禁用位,1表示该页启用高速缓存,0为禁用,我们这里默认置0
A, Access,意为访问位,若为1则表示该页已经被CPU访问过了,这里是由CPU赋值的
D, Dirty,脏位,表示该页已经被修改。此项仅对于页表项有效,对目录项不发生改变
PAT, Page Attribute Table, 意为页属性表位,这位比较复杂,我们不涉及,直接置0
G, Global,全局位,用来指定该位是否为全局页,为1表示是,为0表示不是。若为全局页,则该页会在TLB中一直保存(快表)。顺便这里加个知识点:清空TLB有两种方式,一种是invlpg指令针对单独虚拟地址条目进行清理,还有一种是修改CR3寄存器,这将直接清空TLB
AVL, Available,可用位,这里咱们不需要管

若要启用分页机制,我们需要执行以下步骤:

1.准备好页目录表和页表
2.将页表地址写入CR3控制寄存器
3.将CR0的PG位置1

下面我们了解一下CR3寄存器的结构.
在这里插入图片描述
CR3寄存器被用来存放页表首地址,所以它还有个响亮的名字:页目录基址寄存器
执行开启页表的最后一步也就是将CR0的PG位置1,这里为1就表示真正意义上采用了采用了内存分页,而之前没设置的时候都是分段机制。

启用分页机制实战

;-------------  创建页目录以及页表 ------------
setup_page:
;先把页目录占用的空间逐字清0
  mov ecx, 4096     ;表示4K
  mov esi, 0
.clear_page_dir:
  mov byte [PAGE_DIR_TABLE_POS + esi], 0
  inc esi
  loop .clear_page_dir

;开始创建页目录项(PDE)
.create_pde:    ;创建Page Directory Entry
  mov eax, PAGE_DIR_TABLE_POS
  add eax, 0x1000   ;此时eax为第一个页表的位置以及属性
  mov ebx, eax      ;此处为ebx赋值, 是为.create_pte做准备, ebx为基址
;下面将页目录项00xc00都存为第一个页表的地址,每个页表表示4MB内存
;这样0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的页表
;这是为将地址映射为内核地址做准备
  or eax, PG_US_U | PG_RW_W | PG_P
  ;页目录项的属性RW和P位为1,US为1表示用户属性,所有特权级都可以访问
  mov [PAGE_DIR_TABLE_POS + 0x0], eax       ;第一个目录项
    ;在页目录表中的地一个目录项写入第一个页表的位置(0x101000)及属性
  mov [PAGE_DIR_TABLE_POS + 0xc00], eax     ;一个页表项占用4字节
  ;0xc00表示第768个页表占用的页表项,0xc00以上的目录项用于内核空间,76816进制表示为0x300,这个值再加就是刚好属于内核进程了
  ;也就是页表的0xc0000000~0xffffffff供给1G属于内核,0x0~0xbfffffff共计3G属于用户进程
  sub eax, 0x1000
  mov [PAGE_DIR_TABLE_POS + 4092], eax      ;使得最后一个目录项地址指向页目录表自己的地址

;开始创建页表项(PTE)
  mov ecx, 256                  ;1M低端内存/每页大小4K = 256
  mov esi, 0                    ;该页表用来分配0x0~0x3fffff的物理页,也就是虚拟地址0x00x3fffff和虚拟地址0xc0000000~0xc03fffff对应的物理页,我们现在只用了低1MB,所以此时虚拟地址是等于物理地址的
  mov edx, PG_US_U | PG_RW_W | PG_P     ;同上面类似
.create_pte:    ;创建Page Table Entry
  mov [ebx + esi*4], edx    ;此时ebx为第一个页表的首地址,这在上面咱们已经赋值了
  add edx, 4096
  inc esi
  loop .create_pte

;创建内核其他页面的PDE
  mov eax, PAGE_DIR_TABLE_POS
  add eax, 0x2000       ;此时eax为第二个页表的位置
  or eax, PG_US_U | PG_RW_W | PG_P  ;同上
  mov ebx, PAGE_DIR_TABLE_POS
  mov ecx, 254          ;范围为第7691022的所有页目录项数量
  mov esi, 769
.create_kernel_pde:
  mov [ebx+esi*4], eax
  inc esi
  add eax, 0x1000
  loop .create_kernel_pde
  ret

首先咱们是在0x100000的物理地址构建页目录,该页目录后面咱们就紧挨着存放页表,
在这里插入图片描述
而由于距离页目录偏移0xc00的地方之后,就属于了高1G,这里可以通过简单的计算得出来,0 + 0xc00/4 * 2^22 = 0xc0000000,这里刚好可以得出咱们内核存放的最低地址。
所以说我们首先将页目录偏移0,以及偏移0xc00的地方填入我们的地一个页表地址,由于咱们页目录和第i一个页表挨着存放,所以第0个页表地址应该为0x101000,如下图:
在这里插入图片描述
然后我们就构建页表项,此时我们虽然有一整页,但是我们只需要分配低1MB内存即可,因为咱们的内核就只需要用到这1MB而已,他并不是很大,此时我们沿着物理地址从0开始填入页表项,这里注意由于咱们的目录项第0位和第0xc00偏移的目录项都指向了这第0个页表,所以他俩这里的地址映射到的是同一快内存。
再之后我们就可以创建内核的其他目录项,这里的意义我么留做以后讲解,现在我们就是挨个填上页表地址而已。

以上就是页目录以及页表的初始构建,接下来我们使用来看看:
还记得我们上次loader运行到了jmp $么,我们接着往下来编写

;创建页目录及页表并初始化页内存位图
  call setup_page

;要将描述符表地址及偏移量写入内存gdt_ptr,一会儿用新地址重新加载
  sgdt [gdt_ptr]      ;存储到原来gdt所有的位置

;将gdt描述符中视频段描述符中的段基址+0xc0000000
  mov ebx, [gdt_ptr + 2]        ;加上2是因为gdt_ptr的低2字节是偏移量,高四字节才是GDT地址
  or dword [ebx + 0x18 + 4], 0xc0000000   ;视频段是第3个段描述符,每个描述符是8字节,故为0x18,这里再加上4是因为咱们要的是高4字节,这里或的含义就类似与加,因为目前最高位肯定为0
;段描述符高四字节的最高位是段基址的第3124;将gdt的基址加上0xc0000000使其成为内核所在的高地址
  add dword [gdt_ptr + 2], 0xc0000000

  add esp, 0xc0000000   ;将栈指针同样映射到内核地址

  ;把也目录地址附给cr3
  mov eax, PAGE_DIR_TABLE_POS
  mov cr3, eax

  ;打开cr0的pg位(第31位)
  mov eax, cr0
  or eax, 0x80000000
  mov cr0, eax

  ;在开启分页后,用gdt新的地址重新加载
  lgdt [gdt_ptr]    ;重新加载

  mov byte [gs:160], 'V'    ;视频段段基址已经被更新,用字符V表示vitual addr

  jmp $

接下来我们看结果
在这里插入图片描述
可以看到确实打印的是V了,这证明我们在修改视频段描述符后他正确通过虚拟地址转换为了物理地址然后实现了打印,这里我们可以再来看看gdt的内容是否变化:
在这里插入图片描述这里清楚的看到咱们的gdt初始地址已经存在与内核范围内了,VIDEO 描述符同样。

使用虚拟地址访问页表

由于在我们程序运行的过程中,免不了会进行内存申请,或者说因管理内存而选择释放块,所以我们的页表应该是一个动态的概念,我们的页表应该随着我们的要求来增加或者说删减,要实现这个功能我们首先就得使用虚拟地址访问到页表,在前面实现内存分页的代码中,我们将页目录的最后一项保存为页目录的首地址:

mov [PAGE_DIR_TABLE_POS + 4092], eax      ;使得最后一个目录项地址指向页目录表自己的地址

这里我们来先看看目前咱们程序的虚拟地址以及物理地址的映射关系:
在这里插入图片描述
这里有五对映射,是不是有点奇怪,难道说刚刚我们的代码建立了五对映射吗,我没注意到啊根本,实际上不是说我们自主构建的,而是由于访问机制的问题,让程序以为咱们构造了五对映射,这里我们依次讲解:

1.0x00000000~0x000fffff
这段虚拟内存映射到了咱们物理地址的首1MB位置,这与我们上面代码构造的一致

2.0xc0000000~0xc00fffff
同上,这是我们代码自己映射的,真真正在是咱们创造的

3.0xffc00000~0xffc00fff
这里就有点噶住了,为什么这里也会有个映射呢,不要急,我们取0xffc00000的高10位来查看发现为全1,这就说明我们访问此虚拟地址的时候访问的是最后一个页目录项,而最后一个页目录项咱们保存的并不是页表地址,而是咱们页目录的地址,这样我们机器就会将页目录看成一个页表来进行理解,而此时我们页目录最后一项保存的是0x101000,所以我们这段映射也会映射到0x101000~0x101fff

4.0xfff00000~0xfff00fff
这里的地址也比较特殊,取首地址0xfff00000来进行分析,首先高10位全1表示该虚拟地址在页目录中应对应的是最后一个页目录项,目前为止同上面是一致的,然后我们取中间10位发现其为0x300,这里是不是很眼熟,没错,在页目录项中第0x300表项也就是偏移为0xc00的地方,这里我们之前将其的地址也改为了第0个页表地址,所以此时这段映射会映射到0x101000~0x101fff

5.0xfffff000~0xffffffff
还是拿首地址来分析,0xfffff000这里我们可以观察到,高10位和中间10位都全为1,所以我们首先会查看页目录最后一项,然后将页目录表当作页表看待,然后我们再次访问页目录最后一项,我们以为他是页表最后一项,这里保存的仍然是0x100000,所以我们这里的映射是0x100000~0x100fff

这里我们可以得出结论,当我们若是虚拟地址高20位全为1时(也就是说上面的第五类映射)我们就能够访问到我们的页目录的物理地址。因此最后总结一下:

获取页目录地址:让虚拟地址高20位为0xFFFFF,低12位为0x000,也即0xfffff000,这也是页目录表中第0个页目录项自身的物理地址
访问页目录中的页目录项,也就是获取页表物理地址:要使虚拟地址为0xfffffxxx,其中xxx为偏移。
访问页表中的页表项:使得虚拟地址高10位为0x3ff,目的是获取页目录表的物理地址。中间10位为页表索引。低12位为页表内的偏移。

了解内核

接下来便是真正意义上的操作系统了–内核。
在当前目录创建一个kernel的文件夹,然后创建main.c文件。

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

然后我们进行编译‘

gcc -c -o kernel/main.o kernel/main.c

这里我们选择先生成目标文件而不是可执行文件,我们可以使用file命令来查看文件的属性,这里也可以看到是relocatable,表示可重定位文件。
在这里插入图片描述
而可重定位文件中的符号还没“定位”,我们可以通过nm命令来查看文件中的符号以及地址情况
在这里插入图片描述
可以看到该文件确实只包含一个符号,那就是main,且地址为0
这里我们不直接生成可执行文件是因为我们目前还需要自己来为其设置虚拟初始地址,这里我们使用linux自带的链接器ld进行链接

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

这里给大家解释一下各参数的含义:

-Ttext:指起始虚拟地址为0xc0001500
-o:指定输出文件
-e:指定程序的起始地址
这里的e参数需要特别解释一下,我们先去掉这个参数来看效果
在这里插入图片描述
这里报出一个错误就是找不到入口符号_start
一个程序总该有个入口地址,这个地址表示该程序从哪里开始执行,所以这个-e参数就是指定程序从哪儿开始执行,在这里由于我们的程序过于简单,没有_start符号,而链接器一般入口地址是给的_start所以这里会报错,因此我们在这儿将入口地址设置为main符号就可以了。(或者说你把main函数名换成_start也行,这样程序中就只有一个_start符号了)
我们用file命令来查看生成的kernel.bin文件发现成功生成了可执行文件(excutable)
在这里插入图片描述
这次的内核就到这里了。
对分段部分的源码的查看分段源码分析

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值