唠叨一下
没想到第一课的信息量就这么大,如果是没有接触过汇编的同学可能会有些难度,这里不仅要求对系统有一定的了解,还得会通过调试的方法来查看整个程序的运行过程。
我的环境还没有搭建好,看了一些大佬的在环境搭建的时所到的坑,多有点耐心一定可以的。
不知道你们有没有看PPT,第一课的教案内容也十分丰富,人家的绪论一点都不水反而是整体的总结了一下系统的各个层次和设计难点。作业可以看成是实践教学了,美国的这种教育体系很和我的胃口不会觉得无聊,有挑战性!
作业笔记
第一步很人性化的教你如何建立一个git上的仓库,这一步有兴趣可以看看
第二步搭建环境,老师写了一个小的内核程序,JOS,我们需要完善里面的一些代码。程序运行在qemu上面,属于模拟器毕竟操作系统可能被玩坏。
Part 1: PC BootstrapGetting
Started with x86 assembly:熟悉一下指令集,老师也知道单独看汇编那些指令也记不住,所以只是告诉我们在之后可能会用得上这些资料。汇编有好几种不同的指令集,这次所用的是NASM Intel风格的汇编指令。
Simulating the x86:配置搭建运行环境,这里参加一些大佬的环境部署,会有很多坑
The PC’s Physical Address Space:这张图信息量爆炸!不过也不难理解,讲的很详细
个人理解:
- 最早的时候PC电脑只有16位(Intel 8088),因此可以寻址空间很少,因为总线只有16位。所以最早只能寻址640K (0x00000000 ~ 0x0000FFFF),所以这一段被称为是low memory,如果在16bit下只能随机的访问这么多内存。
- 屏幕的缓存和固件驱动都需要占用一定的存储空间,这个部分就是空出来的,一般来说我们的视频缓存内容都会用voliate修饰并且放在这个区域里面。
- 最早的BIOS程序都是放在ROM上面的,近几年都放在了flash上面,程序也不大,就是一个初始化硬件引导系统的一个装置,有了它无论是在u盘里面还是硬盘,系统都能被加载到内存里面运行起来。
- JOS使用的是前256M因此就只学习32位系统的设计
- 学习GDB调试方法,由于已经提供了.gdbinit文件,
[f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b
这一个是GDB第一个执行的指令的位置,其中可以分析得到几点
PC开始执行的位置需要两个寄存器,在括号的里面,前面的f000是CS(code segment)存储的地址,再加上IP 存储的位置是0xfff0。physical address = 16 * segment + offset。CS存储的是段的地址,IP是offset。
ljmp是一个长跳转指令,可以不改变系统状态寄存器的值。这条指令的意思是jumps to the segmented address CS = 0xf000 and IP = 0xe05b。在下面的boot.S文件里面也用到了这个指令。
Boot.S文件内容分析
其实boot的整个的过程需要两个文件,其中一个是.S结尾的汇编文件,另一个是.C结尾的c文件,因为在最初的时候需要经历实模式到特权模式的转换,16bit到32bit的转换,所以最开始的步骤是在汇编文件内部完成初始化的,之后切换到32bit的模式下,最后进入main函数内部开启bootloader程序的过程(系统引导等)
#include <inc/mmu.h>
#用于给一些寄存器做配置
.set PROT_MODE_CSEG, 0x8
.set PROT_MODE_DSEG, 0x10
.set CR0_PE_ON, 0x1
#.globl代表代码start可以被其他的文件所调用
.globl start
start:
.code16 # Assemble for 16-bit mode
cli # Disable interrupts
cld # String operations increment 主要是为了main函数复制内核程序使用
# Set up the important data segment registers (DS, ES, SS). 清零所有寄存器
xorw %ax,%ax # Segment number zero 异或操作指令
movw %ax,%ds # -> Data Segment
movw %ax,%es # -> Extra Segment
movw %ax,%ss # -> Stack Segment
# Enable A20: 使能A20总线,可以提升访问空间的容量
# For backwards compatibility with the earliest PCs, physical
# address line 20 is tied low, so that addresses higher than
# 1MB wrap around to zero by default. This code undoes this.
seta20.1:
inb $0x64,%al # Wait for not busy
testb $0x2,%al
jnz seta20.1
movb $0xd1,%al # 0xd1 -> port 0x64
outb %al,$0x64
seta20.2:
inb $0x64,%al # Wait for not busy
testb $0x2,%al
jnz seta20.2
movb $0xdf,%al # 0xdf -> port 0x60
outb %al,$0x60
# Switch from real to protected mode, using a bootstrap GDT
# and segment translation that makes virtual addresses
# identical to their physical addresses, so that the
# effective memory map does not change during the switch.
# 加载全局描述符(GDT),标号gdtsec处存放了6字节有关GDT位置的信息
lgdt gdtdesc
# 将cr0最后一位置为1,正式切换到保护模式
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0
# Jump to next instruction, but in 32-bit code segment.
# Switches processor into 32-bit mode.
# ljmp长跳转,在加载GDT以后,由于内部硬件设计的原因,必须要重设所有段寄存器的值,但没有直接改变指令寄存器cs的指令,因此使用一个长跳转指令改变cs的值。
ljmp $PROT_MODE_CSEG, $protcseg
.code32 # Assemble for 32-bit mode
protcseg:
# Set up the protected-mode data segment registers
movw $PROT_MODE_DSEG, %ax # Our data segment selector
movw %ax, %ds # -> DS: Data Segment
movw %ax, %es # -> ES: Extra Segment
movw %ax, %fs # -> FS
movw %ax, %gs # -> GS
movw %ax, %ss # -> SS: Stack Segment
# Set up the stack pointer and call into C.
movl $start, %esp
call bootmain #进入bootmain c文件内
# If bootmain returns (it shouldn't), loop.
#这里是一个死循环,如果bootmain函数发生错误返回,计算机就会卡在这儿,就操作系统而言,最好在这里设置一些提示信息
spin:
jmp spin
介绍一下gdt
# Bootstrap GDT
.p2align 2 # force 4 byte alignment
gdt:
SEG_NULL # null seg
SEG(STA_X|STA_R, 0x0, 0xffffffff) # code seg 代码段
SEG(STA_W, 0x0, 0xffffffff) # data seg 数据段
gdtdesc:
.word 0x17 # sizeof(gdt) - 1
.long gdt # address gdt
标号gdt处设计了有3个表项的GDT表,每个表项长度为8字节,第一个表项必须设置为空的
标号gdtdesc是要往GDTR寄存器里加载的6字节信息,GDTR寄存器的低2字节储存GDT表的长度,高4字节储存GDT表在内存中的首地址。
在保护模式下需要gdt来完成寻址等操作,下面来分析一下GDT的作用
参考GDT详解
实模式下的内存寻址
内存绝对地址 Address = Segament:Offset
16-bit段寄存器CS(Code Segment),DS(Data Segment),SS(Stack Segment)来指定Segment,CPU将段积存器中的数值向左偏移4-bit,放到20-bit的地址线上就成为20-bit的Base Address。
- Segment是一个段的Base Address。最大长度是64 KB,这是16-bit系统所能表示的最大长度
- Offset是相对于此Segment Base Address的偏移量
保护模式下的内存寻址
段模式 && 页模式
页模式基于段模式,因此实际上是分为纯段模式和段页模式两种
段模式下的寻址
基本思想仍旧是
Address = segment + offset
但是由于运行在32位系统上面,所以两个指标都是32位,因此需要记录段的最大长度limit段的base address 以及在保护模式下对于某一段所设定的保护权限。这三个指标就构成了64位的数据,这个数据就叫做段描述符。
段描述符格式 [Base Address ,Limit, Access]
- IA-32允许将一个段的Base Address设为32-bit所能表示的任何值
- Limit则可以被设为32-bit所能表示的,以2^12为倍数的任何指
- Real Mode下,一个段的Base Address只能是16的倍数(因为其低4-bit是通过左移运算得来的,只能为0,从而达到使用16-bit段寄存器表示20-bit Base Address的目的
- 而一个段的Limit只能为固定值64 KB。
GDT的出场
虽然需要64bit的数据结构去存储一个段的描述符,但是由于intel需要向后兼容所以,将段积存器仍然规定为16-bit。
16bit是不够用的,所以需要把这些长度为64-bit的段描述符放入一个数组中,而将段寄存器中的值作为下标索引来间接引用。(事实上,是将段寄存器中的高13 -bit的内容作为索引)
GDT可以被放在内存的任何位置,那么当程序员通过段寄存器来引用一个段描述符时,CPU必须知道GDT的入口,也就是基地址放在哪里。所以 Intel的设计者门提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此积存器。
除了GDT之外,IA-32还允许程序员构建与GDT类似的数据结构,它们被称作LDT(Local Descriptor Table),但与GDT不同的是,LDT在系统中可以存在多个,并且从LDT的名字可以得知,LDT不是全局可见的,它们只对引用它们的任务可见,每个任务最多可以拥有一个LDT。另外,每一个LDT自身作为一个段存在,它们的段描述符被放在GDT中。