1 实模式
实模式是指8086 cpu的寻址方式,寄存器大小,指令用法的工作环境,工作状态这一整套内容.
1.1 cpu
cpu分为:
- 控制单元:
是cpu的控制中心包括:指令寄存器IR,指令译码器ID,操作控制器OC.
控制单元根据ip寄存器的只想,将对应的指令装载到指令寄存器中,然后指令译码器将指令解码.
- 存储单元:
存储单元是指cpu内部的L1,L2缓存,以及寄存器.
和指令相关的处理cs:ip寄存器就是控制单元,而和数据有关的就是存储单元.存储单元使用的是DRAM.
- 运算单元
从控制单元哪里接受命令,负责算术运算和逻辑运算.只是个执行部件
寄存器是一种物理存储部件.cpu中的寄存器大致分为两大类:
- 内部使用,对程序员不可见.一般指的是能否直接使用mov命令修改.一般可以通过push,修改,pop的方式修改:flags,cr0~3或是使用专门的指令修改:GDTR,cs,ip
- 程序员可见.例如通用寄存器和段寄存器
1.2 寄存器
部分寄存器讲解:
flags寄存器:是cou内部各项设置,指标.任何一个指令的执行,器质性过程的谢姐,对计算机造成了哪些影响,都早flags寄存器中通过一些标志位反映出来.某些指令需要满足某些条件才能执行.他的条件就是判断上次指令执行的过程.这个过程或是结果存储在flags寄存器的某些标志位中.
通用寄存器:
- ax:累加器,
- bx:基址寄存器,常用来存储内存地址.用来作为基址
- cx:计数器:用于循环指令中的循环次数
- dx:数据寄存器,用于存放数据,保存外设的端口号
- si:源变址寄存器:用于字符串操作中的原地址存放
- di:目的变址寄存器.同上,用于目的地址存放
- sp:栈指针寄存器,起段基址是ss
- bp机制指针:用于访问栈中数据的
flags寄存器,实模式下是16位的.
从低往高以此位:
- 第0位:CF位:Carry Flag ,进位.运算中,数值的最高位可能是进位,也可能是借位.当发生进位或是借位是CF位1
- 第2位:PF位:Parity Flag, 奇偶位,用于标记结果第8位中1的个数,如果是偶数,则PF位1.奇偶校验通常用于数据传输开始和结束后的对比,判断是否产生错误
- 第4位:AF位,Auxiliary carry Flag, 辅助进位标志,用来记录运算结果的低4位的进位,借位情况,也就是低4位出现进位,借位,则AF位1
- 第6位:ZF位,Zero Flag,0标志位,当计算结果位0,则ZF位1
- 第7位:SF位,Sign Flag,符号标志位,若运算结果位负,则SF位1
- 第8位:TF位,Trap Flag,陷阱标志位,若TF位1,则让cpu进入但不运行方式,否则位连续工作模式.当debug,但不调试的时候,原理上是让TF为1
- 第9位:IF位,Interrupt Flag,中断标志位,如果IF位1,标识中断开启,cou可以相应外部可屏蔽中断,当IF为0,表示cpu不响应可屏蔽中断.
- 第10位:DF位,Direction Flag,方向标志位,用于字符串操作指令中,DF为1,则指令操作数地址自动减少1个单位,为0则增加.
- 第11位:OF位,Overflow Flag,溢出标志位,用于标识计算结果是否超出了数据类型可标识的范围
- 第12~13位:IOPL:Input output Privilege Level,标识0~3这四个特权级.
- 第14位:NT位,Nest Task,任务嵌套标志.8088支持多任务,一个任务就是一个进程,当一个任务重又嵌套了另一个任务是,NT为1
还有其他的,跳过
1.3 实模式下寻址方式
寻址方式,指的是访问数据的方式.从大方向看可以分为三大类:
- 寄存器寻址
- 立即数寻址
- 内存寻址
- 直接寻址
- 基址寻址
- 变址寻址
- 基址变址寻址
寄存器寻址:最简单的寻址方式,他的数据直接在寄存器中了.mov ss,ax
,而这之前的ax
中的数据,可能是立即数寻址得到的
立即数寻址:立即数就是常熟,立即能用的意思.mov ax,0x7c00
或是mov cs,msg_end-msg_start
内存寻址:寄存器寻址和立即数寻址的数据实在指令中直接给出,不是在内存中.
操作书在内存中的寻址方式成为内存寻址.
直接寻址:将操作数作为内存地址,告诉cpu取此处的地址作为操作数:mov ax,[0x7c00]
,mov bx,[gs:0x1]
直接使用数字,那么他的段寄存器默认是ds
,可以显示的给出段寄存器是哪个
基址寻址:在操作数中,使用bx
或bp
寄存器中的值作为物理地址去取数据,其中,bx
寄存器默认段寄存器是ds
,bp
默认段寄存器是ss
:mov ax,[bx]
变址寻址:将si
和di
寄存器作为"数组的起始",可以在此基础上加减一个偏移量.默认的段寄存器是ds
:mov ax,[si+0x11]
基址变址寻址:是基址和变址寻址的叠加:mov ax,[dx+si+0x1]
1.4 栈
栈是一种线性表.
栈是一个种很伟大的发明,可以结局很多难题:
- 表达式计算,如中缀表达式和后缀表达是的转换
- 函数调用,无论是嵌套调用或是地柜调用,用来维护返回地址
- 深度有效搜索算法
栈段寄存器ss
和栈指针sp
分别指向栈底和栈顶.
2 cs:ip
指令都是存在内存中,所以cpu也要访问内存才能拿到要执行的指令.对于指令来说,cs
寄存器是代码段的段基址,ip
寄存器是代码段的段内偏移地址,经过段寄存器cs
\(\times 16 +\)ip
寄存器的值,就是指令存放的内存地址.cpu的前进方向永远是cs:ip
寄存器.
call
指令用于执行一段新的代码,改变cs:ip
寄存器的值.ret
用于返回到调用本段代码的地方.
call
指令将程序计数器PC,x86中是cs:ip
寄存器的值(可能只是ip
)压入栈中.
ret
指令用于在栈顶,弹出两个字节的内容,来替换ip
寄存器的值.,
retf
指令用于在栈顶,弹出4个字节,替换ip
和cs
寄存器中的值.
ret
用于近返回与call是一对
,retf
用于远返回与call far
是一对.
2.1 实模式下的call
8086处理器中由三各系列指令用于改变程序流程:jmp
,call
,ret
jmp
:属于一区不回头的去执行新的代码,适用于程序的交接.
16位实模式相对近调用:
近表示在同一段内,不需要切换段,也就不需要提供段基址.使用near
修饰,默认省略
指令格式:call near 立即数
其中这个立即数,在编程的时候,可以写成标签的名,但是在编译后,其值是跳转的指令相对于call的偏移地址,然后再减去call所占的3个字节.然后在cpu执行的执行的时候又将这个偏移地址转换位物理地址去取指令.
call
相对近调用发生时,cpu将当前ip寄存起的值压入栈中,在通过call
的操作数与自己的所在的地址相加+3,计算出物理地址.
16位实模式间接绝对近调用:
间接是值目标函数的地址没有直接给出.
绝对是指最后拿到的地址是绝对地址(而不是相对于call
指令的偏移地址)
指令格式:call 寄存器寻址
或是call 内存寻址
16位实模式直接绝对远调用:
直接:不需要经过寄存器或是内存,操作书以立即数的形式直接给出
远:跨段
指令格式:call far 段基址:偏移地址
,都是立即数
16位实模式间接绝对远调用:
指令格式:call far 内存寻址
2.2 实模式下的jmp
无条件跳转,值得是,无条件的直接跳转
jmp转移指令是值更新cs:ip
寄存器.不压栈
16位实模式相对短转移:
相对的意思同上,值得是一个偏移地址,是由编译器处理的.
短的意思是,立即数的值只能是-128~127
指令格式:jmp short 立即数
其中关键字short
限制了立即数的大小
16位实模式间接绝对近转移:
同上,只是没有了short
关键字,因此限制立即数的值:-32768~32767
16位实模式直接绝对远转移:
直接:操作数不仅是立即数,而且cpu直接拿来就用,不用在转换
绝对:给出的地址,是直接的地址不再是一个偏移地址
远:跨段,需要给出段基址
指令格式:jmp 段基址:段内偏移地址
,都是立即数
16位实模式间接绝对远转移:
直接变间接,也就是段基址和段内偏移地址是以寄存器或是内存寻址的形式给出.
2.3 条件跳转
有条件转移是一个指令族,如果满足条件就跳转到指定位置,如果不满足,则跳过跳转命令,继续向下执行.主要是判断flags寄存器中的位
3 控制外设
cpu和外设之间有一层叫做IO接口.IO接口是链接cpu与外部设备的逻辑控制部件.接口的作用是链接处理器和外部设备:
- 设置数据缓冲区,解决cpu与外设的速度不匹配问题
- 设置信号电平转换电路:cpu和外设的信号点评不同,cpu采用TTL电平,二外设大多数是几点设备.不能使用TTL电平驱动.
- 设置数据格式转换
- 设置时序控制电路来同步cpu和外部设备.接口电路协调cpu和外设的时间计法.也就是如何通信.
- 提供地址译
同一时刻cpu只能和一个IO接口通信,为了解决很多IO接口同时和cpu对话的情景,在cpu和IO接口之间又加了一层:输入输出控制中心ICH.南桥.用于仲裁IO接口的竞争.
IO接口是链接cpu和外设的桥梁,每个端口有自己的用途,端口也是寄存器,有自己的宽度,不同厂商的设备有不同的宽度.
访问端口使用专门的in
和out
指令
in
指令用于从端口中读取数据:in al,dx
out
指令用于往端口中写数据:out dx,al
3.1 控制显卡
控制内存的方式:
实模式下,内存的\(0xB8000\dots 0xBFFFF\)是用于文本模式显示适配器区,一共\(32KB\).直接向该部分内存中写入数据,就可以在屏幕上显示.(这里应该还设计到一个显示模式的问题)
文本模式下,一个字符占两个字节,低位8表示字符,高位8表示字符的属性,属性中,低4位标识前景色,高4位表示背景色.
一屏幕一共25行,每行可现实80字符,因此一行占据160字节.
因此:
mov ax,0xb800 ;立即数寻址
mov gs,ax ;寄存器寻址
;从第三行开始输出一个3
mov byte [gs:320],'3' ;直接寻址
mov byte [gs:321],00001111b
3.2 控制硬盘
温切斯特技术:磁盘片在封装的空间中告诉自传,磁头炫富在磁盘膳房,固定在磁头壁上沿磁盘径向移动.
磁盘控制器和硬盘在一起(物理上被封装在一起)称之为,集成设备电路IDE,
以前的主机只支持4块并口硬盘,随着硬盘串行接口SATA的出现,主板支持多少硬盘取决于主板的能力.
这部分的端口很多.书上也只列举了部分,跳过吧
总体顺序是:
- 选择通道,
- 想该通道的三个LBA寄存器中写入扇区起始地址的低24位
- 想device寄存器中写入LBA的24~27位
- 向该通道的command寄存器吸入操作命令
- 读取该通道上status寄存器判断是否完成共组
- 将硬盘数据读出
常用的数据传送方式:
- 无条件传输:数据源设备随时准备好了了数据,cpu随时拿都没问题.比如寄存器内存这样的设备
- 查询传输方式:也称为程序IO,在传输之前,由程序去检测设备的状态.数据远设备在一定条件下才能传送数据.这类设备通常是低速设备.
- 中断传送方式,也称为中断驱动IO,如果数据源设备将数据准备号后通知cpu来取,cpu需要压栈保护现场
- 直接存储器存取方式DMA:数据源直接和内存交互,不需要cpu的参与,需要DMA控制器
- IO处理机传送方式:
从硬盘中读取数据的代码是:
; 功能:读取硬盘n个扇区
; 参数:
; eax:开始读取的磁盘扇区
; cx:读取的扇区个数
; bx:数据送到内存中的起始位置
rd_disk_m_16:
; 这里要保存eax 的原因在与,下面section count 寄存器需要一个8位的寄存器
; 只有acbd这四个寄存器能够拆分为高低8位来使用,而dx作为寄存器号,被占用了
; 因此需要个abc三个寄存器中一个来用,这里选择了 ax
mov esi,eax
mov di,cx
; 0x1f2 寄存器:sector count ,读写的时候都表示要读写的扇区数目
; 该寄存器是8位的,因此送入的数据位 cl
mov dx,0x1f2
mov al,cl
out dx,al
; 恢复eax
mov eax,esi
; eax中存放的是要读取的扇区开始标号,是一个32位的值,因此 al 是低8位
; 0x1f3 存放0~7位的LBA地址,该寄存器是一个8位的
mov dx,0x1f3
out dx,al
; 下面的 0x1f4 和5 分别是8~15,16~23位LBA地址,这俩寄存器都是8位的
; 因此是用shr,将eax右移8位,然后每次都用al取eax中的低8位
mov cl,8
shr eax,cl
mov dx,0x1f4
out dx,al
shr eax,cl
mov dx,0x1f5
out dx,al
; 0x1f6 寄存器低4位存放 24~27位LBA地址,
; 0x1f6 寄存器是一个杂项,其第六位,1标识LBA地址模式,0标识CHS模式
; 上面使用的是LBA地址,因此第六位位1
shr eax,cl
and al,0x0f
or al,0xe0
mov dx,0x1f6
out dx,al
; 0x1f7 寄存器,读取该寄存器的时候,其中的数据是磁盘的状态
; 写到该寄存器的时候,写入的僵尸要执行的命令,写入以后,直接开始执行命令
; 因此需要在写该寄存器的时候,将所有参数设置号
; 0x20 表示读扇区,0x30写扇区
mov dx,0x1f7
mov al,0x20
out dx,al
.not_ready:
; 读 0x1f7 判断数据是否就绪,没就绪就循环等待.
nop
in al,dx
and al,0x88
cmp al,0x08
jnz .not_ready
; 到这一步表示数据就绪,设置各项数据,开始读取
; 一个扇区512字节,每次读2字节,因此读一个扇区需要256次从寄存器中读取数据
; di 中是最开始的cx也就是要读取的扇区数
; mul dx 是ax=ax * dx ,因此最终ax 中是要读取的次数
mov ax,di
mov dx,256
mul dx
mov cx,ax
; 0x1f0 寄存器是一个16位寄存器,读写的时候,都是数据.
mov dx,0x1f0
.go_on_read:
in ax,dx
mov [bx],ax
add bx,2 ;ax 是 16位寄存器,读出的也是2字节,因此读一次 dx+2
loop .go_on_read
ret
上面代码涉及到了和磁盘相关的一些寄存器.
4 代码
目录结构:
├── bochs
├── boot.inc
├── hd60m.img
├── loader.asm
├── mbr.asm
└── start.sh
4.1 宏文件mbr.asm
因为一些变量共用,因此使用一个额外的文件来保存.和c++等的#include
一样,汇编使用%include
引入文件.
equ
相当于define
语句了
在bochs
目录下新建一个boot.inc
文件,里面存放一些宏定义:
LOADER_IN_MEM equ 0x900 ;内核加载器加载到内存的位置
LOADER_IN_DISK equ 2 ;内核加载器在硬盘中的位置
4.2 内核加载器 loader.asm
内核加载器,由mbr从硬盘中读取到内存,然后跳转到对应的地址去执行.
新建一个bochs/loader.asm
是内核加载器的代码:
因为已知这段代码会被加载到LOADER_IN_MEM
所在的位置,所以,编译的时候以LOADER_IN_MEM
为起始地址
%include "boot.inc"
SECTION loader vstart=LOADER_IN_MEM
mov ax,0xb800
mov gs,ax
mov byte [gs:1120],'l'
mov byte [gs:1121],00000111b
mov byte [gs:1122],'o'
mov byte [gs:1123],00000111b
mov byte [gs:1124],'a'
mov byte [gs:1125],00000111b
mov byte [gs:1126],'d'
mov byte [gs:1127],00000111b
mov byte [gs:1128],'e'
mov byte [gs:1129],00000111b
mov byte [gs:1130],'r'
mov byte [gs:1131],00000111b
mov byte [gs:1132],'!'
mov byte [gs:1133],00000111b
jmp $
4.3 mbr.asm
mar.asm文件中,加入读取磁盘文件的函数代码,以及读取内核加载器,然后跳转过去执行的代码
然后,bochs/mbr.asm
文件,加上,读取硬盘中数据,并且,跳转到对应代码执行的代码:
; mbr.asm 主引导程序
%include "boot.inc"
SECTION MBR vstart=0x7c00
mov ax,cs
mov ds,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
; 清除屏幕
; INT 0x10 功能号0x06,AH中存功能号,AL中存上卷行数,BH中存上卷行属性
; ah:子功能号,al:0表示0x06子功能号,向上卷屏幕的行数,0表示全部
; bh:空白区域缺省属性
; cx,dx:上卷时候认为屏幕的大小,也就是只上卷cx,dx标识的矩形区域内的字符.
mov ax,0x600
mov bx,0x0700
mov cx,0x0
mov dx,0x184f
int 0x10
; 设置光标位置
; INT 0x10 子功能号0x02,AH中存子功能号
; bh:显示页码,dh:y,dl:x
mov ax,0x02
mov dx,0x01
int 0x10
; 显示字符串,在光标出打印,其中是dx中是光标位置
; int 0x10 子功能号0x13
; es:bp:字符串起始地址,bh:页码,cx:字符串长度,al输出模式
mov ax,msg_start
mov bp,ax
mov cx,msg_end-msg_start
mov bx,0x0002
mov ax,0x1301
int 0x10
; 直接读写显卡映射内存
mov ax,0xb800
mov gs,ax
mov byte [gs:320],' '
mov byte [gs:321],00001111b
mov byte [gs:322],'d'
mov byte [gs:323],00001111b
mov byte [gs:324],'i'
mov byte [gs:325],00001111b
mov byte [gs:326],'r'
mov byte [gs:327],00001111b
mov byte [gs:328],'e'
mov byte [gs:329],00001111b
mov byte [gs:330],'c'
mov byte [gs:331],00001111b
mov byte [gs:332],'t'
mov byte [gs:333],00001111b
; -------------------------------------新加代码-------------------------------------
; 设置调用rd_disk_m_16时候的3个参数.
; eax 是要读取磁盘的LBA地址
; bx 是读取出来的数据,加载到内存的位置
; cx 是要读取的扇区数.
mov eax,LOADER_IN_DISK
mov bx,LOADER_IN_MEM
mov cx,1
mov byte [gs:640],'r'
mov byte [gs:641],00001111b
mov byte [gs:642],'e'
mov byte [gs:643],00001111b
mov byte [gs:644],'a'
mov byte [gs:645],00001111b
mov byte [gs:646],'d'
mov byte [gs:647],00001111b
mov byte [gs:648],' '
mov byte [gs:649],00001111b
mov byte [gs:650],'d'
mov byte [gs:651],00001111b
mov byte [gs:652],'i'
mov byte [gs:653],00001111b
mov byte [gs:654],'s'
mov byte [gs:655],00001111b
mov byte [gs:656],'k'
mov byte [gs:657],00001111b
; 开始读取
call rd_disk_m_16
mov byte [gs:800],'j'
mov byte [gs:801],00001111b
mov byte [gs:802],'m'
mov byte [gs:803],00001111b
mov byte [gs:804],'p'
mov byte [gs:805],00001111b
; 因为读取的是 内核加载器loader ,因此读取完成以后,直接跳转过去执行
jmp LOADER_IN_MEM
; 功能:读取硬盘n个扇区
; 参数:
; eax:开始读取的磁盘扇区
; cx:读取的扇区个数
; bx:数据送到内存中的起始位置
rd_disk_m_16:
; 这里要保存eax 的原因在与,下面section count 寄存器需要一个8位的寄存器
; 只有acbd这四个寄存器能够拆分为高低8位来使用,而dx作为寄存器号,被占用了
; 因此需要个abc三个寄存器中一个来用,这里选择了 ax
mov esi,eax
mov di,cx
; 0x1f2 寄存器:sector count ,读写的时候都表示要读写的扇区数目
; 该寄存器是8位的,因此送入的数据位 cl
mov dx,0x1f2
mov al,cl
out dx,al
; 恢复eax
mov eax,esi
; eax中存放的是要读取的扇区开始标号,是一个32位的值,因此 al 是低8位
; 0x1f3 存放0~7位的LBA地址,该寄存器是一个8位的
mov dx,0x1f3
out dx,al
; 下面的 0x1f4 和5 分别是8~15,16~23位LBA地址,这俩寄存器都是8位的
; 因此是用shr,将eax右移8位,然后每次都用al取eax中的低8位
mov cl,8
shr eax,cl
mov dx,0x1f4
out dx,al
shr eax,cl
mov dx,0x1f5
out dx,al
; 0x1f6 寄存器低4位存放 24~27位LBA地址,
; 0x1f6 寄存器是一个杂项,其第六位,1标识LBA地址模式,0标识CHS模式
; 上面使用的是LBA地址,因此第六位位1
shr eax,cl
and al,0x0f
or al,0xe0
mov dx,0x1f6
out dx,al
; 0x1f7 寄存器,读取该寄存器的时候,其中的数据是磁盘的状态
; 写到该寄存器的时候,写入的僵尸要执行的命令,写入以后,直接开始执行命令
; 因此需要在写该寄存器的时候,将所有参数设置号
; 0x20 表示读扇区,0x30写扇区
mov dx,0x1f7
mov al,0x20
out dx,al
.not_ready:
; 读 0x1f7 判断数据是否就绪,没就绪就循环等待.
nop
in al,dx
and al,0x88
cmp al,0x08
jnz .not_ready
; 到这一步表示数据就绪,设置各项数据,开始读取
; 一个扇区512字节,每次读2字节,因此读一个扇区需要256次从寄存器中读取数据
; di 中是最开始的cx也就是要读取的扇区数
; mul dx 是ax=ax * dx ,因此最终ax 中是要读取的次数
mov ax,di
mov dx,256
mul dx
mov cx,ax
; 0x1f0 寄存器是一个16位寄存器,读写的时候,都是数据.
mov dx,0x1f0
.go_on_read:
in ax,dx
mov [bx],ax
add bx,2 ;ax 是 16位寄存器,读出的也是2字节,因此读一次 dx+2
loop .go_on_read
ret
; -------------------------------------新加代码-------------------------------------
msg_start db "2 mbr start!!!!!"
msg_end db 0
times 510-($-$$) db 0
db 0x55,0xaa
4.4 start,sh
脚本文件start.sh
中加入编译loader.asm
和刻录的语句,同时,在编写代码的时候,可能出错,因此,加上判断是否出错的处理.当编译或是刻录不成功,直接中断脚本文件的执行:
#! /bin/bash
# 编译mbr.asm
echo "----------nasm starts----------"
if !(nasm -o mbr.bin mbr.asm);then
echo "nasm error"
exit
fi
echo "----------nasm end ----------"
# 刻录mbr.bin
echo "----------dd starts ----------"
if !(dd if=./mbr.bin of=./hd60m.img bs=512 count=1 conv=notrunc);then
echo "dd error"
exit
fi
echo "----------dd end ----------"
# 编译 loader.asm
echo "----------nasm starts----------"
if !(nasm -o loader.bin loader.asm -I include/);then
echo "nasm error"
exit
fi
echo "----------nasm end ----------"
# 刻录loader.bin
echo "----------dd starts ----------"
if !(dd if=./loader.bin of=./hd60m.img bs=512 count=1 seek=2 conv=notrunc);then
echo "dd error"
exit
fi
echo "----------dd end ----------"
# 删除临时文件
sleep 1s
rm -rf mbr.bin
rm -rf loader.bin
# 运行bochs
bochs
运行的以后结果是:
使用xp
调试命令,查看一下0x900
处的内存内容:xp/100 0x800
.
xxd
命令查看loader.bin
文件的内容,-g 4
命令表示4个字节一组
可以发现除了大小端不同以外,内容是相同的