这一章节的主要任务是,完善MBR,文件一mbr.S任务为加载到内存0x7c00位置,文件二loader.S任务为完成内核初始化和加载硬盘上的内核文件到内存。因此主要核心功能为硬盘读取操作。
整理了硬盘读取操作过程:
1.硬盘读取操作接口寄存器地址表
2.确当硬盘LBA/CHS地址,配置接口寄存器
3.确认硬盘状态,放入数据
程序加载器负责将根据编译后的程序地址加载到内存中,mbr 用 vstart=0x7c00
来修饰的原因,是开发人员知道 mbr 要被加载器(BIOS)加载到物理地址 0x7c00,mbr 中后续的物理地址都是 0x7c00+。
当下处于实模式下,利用汇编指令,控制编译器编译的起始地址vstart,让编译器从vstart开始为mbr section指定一个虚拟的起始地址,此起始地址为虚拟内存地址,通过该地址访存是找不到section的。
源码 | 地址(byte) | 反汇编代码 |
---|---|---|
section code vstart=0x7c00 | ||
mov ax,$$ | 00000000 | mov ax,0x7c00 |
mov ax,section.code.start | 00000003 | mov ax,0x10 |
mov ax,section.data.start | 00000006 | mov ax,0x14 |
mov ax,$ | 00000009 | mov ax,0x7c09 |
mov ax,[var1] | 0000000C | mov ax,[0x900] |
mov ax,[var2] | 0000000F | mov ax,[0x904] |
jmp $ | 00000012 | jmp -2 |
section data vstart=0x900 | ||
var1 dd 0x4 | 00000014 | |
00000016 | ||
var2 dw 0x99 | 00000018 | |
00000019 |
补充:$$指向程序的编译起始地址,$指向当前指令的偏移地址。
实模式下,操作系统将内存中的用户数据根据段来划分,那么编译器在编译时也是根据section来划分汇编文件的数据块。也可以视为C语言的函数。
寄存器寻址:mov ax,cs
立即数寻址:mov ax,0x10
内存变址寻址:mov ax,[cs:0x10]
本质是更改了CS:IP指向的代码段,跳转到待执行的内存地址
ret是返回原程序,既然有返回,那么一定有转移,assembly language转移包括jmp和call,jmp是无返回的直接冲,call是需要有ret的,因此ret和call搭配使用。
与中断的保护现场一样,assembly process的call也是需要进行保护现场
的,借助栈的先入后出原理,在调用call时将指令信息保存到栈中,实现多层call调用时,能正确ret。
call分为近调用和远调用,对应ret和retf(return far)。具体原理为:ret指令将栈顶指针[ss:sp]的两个字节取出,赋值给IP寄存器,不需要改变CS寄存器值;retf将栈顶的4个字节取出,前两个字节赋值给IP寄存器,后两个字节赋值给CS寄存器。具体的选择需要程序员根据应用需求自动调整。
转移指令 | 条 件 | 意 义 | 英文助记 |
---|---|---|---|
jz/je | ZF=1 | 相减结果等于0/相等时转移 | Jump if Zero/Equal |
jnz/jne | ZF=0 | 不等于0/不相等时转移 | Jump if Not Zero/Not Equal |
JS | SF=1 | 负数时转移 | Jump if Sign |
jns | SF=0 | 正数时转移 | Jump if Not Sign |
jo | OF=1 | 溢出时转移 | Jump if Overflow |
jno | OF=0 | 未溢出时转移 | Jump if Not Overflow |
jp/jpe | PF=1 | 低字节中有偶数个1时转移 | Jump if Parity/Parity Even |
jnp/jpo | PF=0 | 低字节中有奇数个1时转移 | Jump if Not Parity/Parity Odd |
jbe/jna | CF=1或 ZF=1 | 小于等于/不大于时转移 | Jump if Below or Equal/Above |
jnbe/ja | CF=ZF=0 | 不小于等于/大于时转移 | Jump if Not Below or Equal/Above |
jc/jb/jnae | CF=1 | 进位/小于/小于等于时转移 | Jump if Carry/Below/Not Above Equal |
jnc/jnb/jae | CF=0 | 未进位/不小于/大于等于时转移 | Jump if Not Carry/Not Below/Above Equal |
jl/jinge | SF!=OF | 小于/不大于等于时转移 | Jump Less/Not Great Equal |
jnl/jge | SF=OF | 不小于/大于等于时转移 | Jump if Not Less/Great Equal |
jle/jng | ZF!=OF 或 ZF=1 | 小于等于/不大于 | Jump if Less or Equal/Not Great |
jnle/jg | SF=OF 且 ZF=O | 不小于等于/大于时转移 | Jump Not Less Equal/Great |
Jcxz | CX寄存器值=0 | CX 寄存器值为0时转移 | Jump if register CX’s vaJue is Zero |
a | b | c | e | g | j | l | n | o | p |
---|---|---|---|---|---|---|---|---|---|
表示 above | 表示 below | 表示 carry | 表示 equal | 表示 great | 表示 jmp | 表示 less | 表示not | 表示 overflow | 表示 parity |
实模式最终会被保护模式替代掉,本质是安全问题。
上述过程均在实模式下完成,实模式将所有的内存空间暴露给用户,用户通过ds<<0x1+偏移地址
可以访问内存任意位置,可能会影响操作系统的稳定。因此衍生出了保护模式,保护模式对用户的访存操作加入了特权判断,那谁来分配特权呢???
用户编写程序时,只允许有两种特权分配,内核态的特权0或用户态的特权3,用户程序通过系统调用进入内核态,访问系统硬件和内核。
IO 接口是连接 CPU 与外部设备的逻辑控制部件 ,分为硬件和软件两部分:
(1)据缓冲问题。CPU数据处理速率较外设快很多,如果直接将CPU与外设相连,CPU阻塞等待导致系统性能降低,因此通过建立I/O接口建立缓冲区,当缓冲区满了才中断CPU响应。
(2)据格式不一致问题。CPU只处理数字信号,而外设信号包括数字信号、模拟信号等,I/O接口搭载有A/D转换电路和D/A转换电路,完成CPU的数字信号到外设的模拟信号转换、外设的模拟信号到CPU的数字信号转换。
(3)信号电平不一致问题。CPU信号为TTL电平,外设大多是机电设备,采用CMOS电平,两个接口电平不一致,直接对接可能会烧坏器件,因此I/O接口设置有信号电平转换电路。
TTL电平与CMOS电平的区别
(一)TTL高电平3.6~5V,低电平0V~2.4V
CMOS电平Vcc可达到12V
CMOS电路输出高电平约为0.9Vcc,而输出低电平约为0.1Vcc。
CMOS电路不使用的输入端不能悬空,会造成逻辑混乱。
TTL电路不使用的输入端悬空为高电平**
另外,CMOS集成电路电源电压可以在较大范围内变化,因而对电源的要求不像TTL集成电路那样严格。
用TTL电平他们就可以兼容
(二)TTL电平是5V,CMOS电平一般是12V。
因为TTL电路电源电压是5V,CMOS电路电源电压一般是12V。
5V的电平不能触发CMOS电路,12V的电平会损坏TTL电路,因此不能互相兼容匹配。
(三)TTL电平标准
输出 L: <0.4V ; H:>2.4V。
输入 L: <0.8V ; H:>2.0V
TTL器件输出低电平要小于0.4V,高电平要大于2.4V。输入,低于0.8V就认为是0,高于2.0就认为是1。
CMOS电平:
输出 L: <0.1Vcc ; H:>0.9Vcc。
输入 L: <0.3Vcc ; H:>0.7Vcc.
(4)信号时序不一致问题。一些外设拥有自己的晶振时序,直接接收或发送CPU的数据,会导致数据丢失或数据污染。因此需要I/O接口设计时序转换电路。
(5)支持多个外设地址译码。由于多个外设公用一个接口,因此CPU需要明确数据来源,同时,I/O接口需要确定CPU的数据转发地址。
南桥用于连接低速外设,北桥用于连接内存等高速外设,为了提高访存速率,某些厂商将北桥集成在CPU内部。
CPU通过专门的in,out指令完成对接口数据的读取。按照Intel指令规范,操作码 目的操作数,源操作数的格式。则in指令(input)从接口读取数据,格式为:
in al,dx ;当源操作数dx为8bit时
in ax,dx ;当源操作数dx为16bit时
out指令向接口中写入数据,格式为:
out dx,al
out dx,ax
out 立即数,al
out 立即数,ax
实模式存在中断向量表,而保护模式没有中断向量表,因此保护模式无法通过BIOS中断实现打印输出功能。但是系统不是从刚开始就进入保护模式,首先要进入实模式执行BIOS初始化,再开启保护模式。
CLI关中断,禁止中断发生;STI开中断,允许中断发生。
从磁盘读写数据,首先需要确定硬盘访问地址,与CHS(柱面-磁头-扇区,Cylinder Head Sector)地址需要确定几盘几道几扇区的访问方式不同,LBA地址将硬盘视为一个整体,从0开始编址。然后根据in,out命令向LBA地址执行读写操作。
具体过程如下:
写入前,需要确定磁盘的起始地址、写入地址、待写入的扇区数。
mov eax,LOADER_START_SECTOR ;起始扇区LBA地址,0x900,这里对应的是loader.S文件位置,也是硬盘读写操作
mov bx,LOADER_BASE_ADDR ;写入的地址,0x2
mov cx,1 ;待写入的扇区数
1.向磁盘sector count端口0x1f2传入待写入的磁盘数
执行的操作:out dx,[待写入的扇区数]
mov dx,0x1f2
mov al,cl
out dx,al
2.向磁盘的LBA地址端口写入地址
;将LBA地址(逻辑地址)存入0x1f3-0x1f6,小端存储
;LBA地址7-0位写入端口0x1f3
mov dx,0x1f3
out dx,al
;LBA地址15-8位写入端口0x1f4
mov cl,8
shr eax,cl
mov dx,0x1f4
out dx,al
;LBA地址23-16位写入端口0x1f6
shr eax,cl
mov dx,0x1f5
out dx,al
;设置LBA地址模式
shr eax,cl
;取LBA 24-27位
and al,0x0f
;设置7-4位为1110,表示LBA模式
or al,0xe0
mov dx,0x1f6
out dx,al
3.向0x1f7端口写入status寄存器配置信息,确定磁盘状态是否满足当下读写要求。
status寄存器控制命令主要有三个:
(1)硬盘识别:0xEC
(2)读数据:接口写入0x20
(3)写数据:接口写入0x30
mov dx,0x1f7
mov ax,0x20 ;写数据0x20
out dx,ax
4.当硬盘稳定后,执行读数据命令
.not_ready:
nop
in al,dx ;将端口中的信息读到al中,注意此时dx=0x1f7不变,此时是status寄存器,也就是状态端口
and al,0x88
cmp al,0x08 ;第7位为1,表示占用;第8位为1,表示空闲
jnz .not_ready ;如果被占取了,就循环 jnz=jmp not equal
mov ax,di ;di=1
mov dx,256
mul dx ;dx=ax*dx 每次读取1个字,也就是两字节,一共512字节,所以需要256次
mov cx,ax ;cx指定循环的次数
mov dx,0x1f0 ;数据端口,终于开始读取数据了
.go_on_read:
in ax,dx ;将端口中指定的数据,也就是指定的扇区的数据读入到ax中
mov [bx],ax ;bx寄存器存储的就是0x900也就是loader的内存地址
add bx,2 ;每次读两字节
loop .go_on_read
ret ;返回后就会执行jmp跳转到0x900去了,此时机会执行loader.bin
.go_on_read:
in ax,dx ;将端口中指定的数据,也就是指定的扇区的数据读入到ax中
mov [bx],ax ;bx寄存器存储的就是0x900也就是loader的内存地址
add bx,2 ;每次读两字节
loop .go_on_read
ret ;返回后就会执行jmp跳转到0x900去了,此时机会执行loader.bin`