第03章 cpu实模式

1 实模式

实模式是指8086 cpu的寻址方式,寄存器大小,指令用法的工作环境,工作状态这一整套内容.

1.1 cpu

cpu分为:

  1. 控制单元:

是cpu的控制中心包括:指令寄存器IR,指令译码器ID,操作控制器OC.

控制单元根据ip寄存器的只想,将对应的指令装载到指令寄存器中,然后指令译码器将指令解码.

  1. 存储单元:

存储单元是指cpu内部的L1,L2缓存,以及寄存器.

和指令相关的处理cs:ip寄存器就是控制单元,而和数据有关的就是存储单元.存储单元使用的是DRAM.

  1. 运算单元

从控制单元哪里接受命令,负责算术运算和逻辑运算.只是个执行部件

寄存器是一种物理存储部件.cpu中的寄存器大致分为两大类:

  1. 内部使用,对程序员不可见.一般指的是能否直接使用mov命令修改.一般可以通过push,修改,pop的方式修改:flags,cr0~3或是使用专门的指令修改:GDTR,cs,ip
  2. 程序员可见.例如通用寄存器和段寄存器

1.2 寄存器

部分寄存器讲解:

flags寄存器:是cou内部各项设置,指标.任何一个指令的执行,器质性过程的谢姐,对计算机造成了哪些影响,都早flags寄存器中通过一些标志位反映出来.某些指令需要满足某些条件才能执行.他的条件就是判断上次指令执行的过程.这个过程或是结果存储在flags寄存器的某些标志位中.

通用寄存器:

  1. ax:累加器,
  2. bx:基址寄存器,常用来存储内存地址.用来作为基址
  3. cx:计数器:用于循环指令中的循环次数
  4. dx:数据寄存器,用于存放数据,保存外设的端口号
  5. si:源变址寄存器:用于字符串操作中的原地址存放
  6. di:目的变址寄存器.同上,用于目的地址存放
  7. sp:栈指针寄存器,起段基址是ss
  8. bp机制指针:用于访问栈中数据的

flags寄存器,实模式下是16位的.

从低往高以此位:

  1. 第0位:CF位:Carry Flag ,进位.运算中,数值的最高位可能是进位,也可能是借位.当发生进位或是借位是CF位1
  2. 第2位:PF位:Parity Flag, 奇偶位,用于标记结果第8位中1的个数,如果是偶数,则PF位1.奇偶校验通常用于数据传输开始和结束后的对比,判断是否产生错误
  3. 第4位:AF位,Auxiliary carry Flag, 辅助进位标志,用来记录运算结果的低4位的进位,借位情况,也就是低4位出现进位,借位,则AF位1
  4. 第6位:ZF位,Zero Flag,0标志位,当计算结果位0,则ZF位1
  5. 第7位:SF位,Sign Flag,符号标志位,若运算结果位负,则SF位1
  6. 第8位:TF位,Trap Flag,陷阱标志位,若TF位1,则让cpu进入但不运行方式,否则位连续工作模式.当debug,但不调试的时候,原理上是让TF为1
  7. 第9位:IF位,Interrupt Flag,中断标志位,如果IF位1,标识中断开启,cou可以相应外部可屏蔽中断,当IF为0,表示cpu不响应可屏蔽中断.
  8. 第10位:DF位,Direction Flag,方向标志位,用于字符串操作指令中,DF为1,则指令操作数地址自动减少1个单位,为0则增加.
  9. 第11位:OF位,Overflow Flag,溢出标志位,用于标识计算结果是否超出了数据类型可标识的范围
  10. 第12~13位:IOPL:Input output Privilege Level,标识0~3这四个特权级.
  11. 第14位:NT位,Nest Task,任务嵌套标志.8088支持多任务,一个任务就是一个进程,当一个任务重又嵌套了另一个任务是,NT为1

还有其他的,跳过

1.3 实模式下寻址方式

寻址方式,指的是访问数据的方式.从大方向看可以分为三大类:

  1. 寄存器寻址
  2. 立即数寻址
  3. 内存寻址
  4. 直接寻址
  5. 基址寻址
  6. 变址寻址
  7. 基址变址寻址

寄存器寻址:最简单的寻址方式,他的数据直接在寄存器中了.mov ss,ax,而这之前的ax中的数据,可能是立即数寻址得到的

立即数寻址:立即数就是常熟,立即能用的意思.mov ax,0x7c00或是mov cs,msg_end-msg_start

内存寻址:寄存器寻址和立即数寻址的数据实在指令中直接给出,不是在内存中.

操作书在内存中的寻址方式成为内存寻址.

直接寻址:将操作数作为内存地址,告诉cpu取此处的地址作为操作数:mov ax,[0x7c00],mov bx,[gs:0x1]直接使用数字,那么他的段寄存器默认是ds,可以显示的给出段寄存器是哪个

基址寻址:在操作数中,使用bxbp寄存器中的值作为物理地址去取数据,其中,bx寄存器默认段寄存器是ds,bp默认段寄存器是ss:mov ax,[bx]

变址寻址:将sidi寄存器作为"数组的起始",可以在此基础上加减一个偏移量.默认的段寄存器是ds:mov ax,[si+0x11]

基址变址寻址:是基址和变址寻址的叠加:mov ax,[dx+si+0x1]

1.4 栈

栈是一种线性表.

栈是一个种很伟大的发明,可以结局很多难题:

  1. 表达式计算,如中缀表达式和后缀表达是的转换
  2. 函数调用,无论是嵌套调用或是地柜调用,用来维护返回地址
  3. 深度有效搜索算法

栈段寄存器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个字节,替换ipcs寄存器中的值.

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与外部设备的逻辑控制部件.接口的作用是链接处理器和外部设备:

  1. 设置数据缓冲区,解决cpu与外设的速度不匹配问题
  2. 设置信号电平转换电路:cpu和外设的信号点评不同,cpu采用TTL电平,二外设大多数是几点设备.不能使用TTL电平驱动.
  3. 设置数据格式转换
  4. 设置时序控制电路来同步cpu和外部设备.接口电路协调cpu和外设的时间计法.也就是如何通信.
  5. 提供地址译

同一时刻cpu只能和一个IO接口通信,为了解决很多IO接口同时和cpu对话的情景,在cpu和IO接口之间又加了一层:输入输出控制中心ICH.南桥.用于仲裁IO接口的竞争.

IO接口是链接cpu和外设的桥梁,每个端口有自己的用途,端口也是寄存器,有自己的宽度,不同厂商的设备有不同的宽度.

访问端口使用专门的inout指令

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的出现,主板支持多少硬盘取决于主板的能力.

这部分的端口很多.书上也只列举了部分,跳过吧

总体顺序是:

  1. 选择通道,
  2. 想该通道的三个LBA寄存器中写入扇区起始地址的低24位
  3. 想device寄存器中写入LBA的24~27位
  4. 向该通道的command寄存器吸入操作命令
  5. 读取该通道上status寄存器判断是否完成共组
  6. 将硬盘数据读出

常用的数据传送方式:

  1. 无条件传输:数据源设备随时准备好了了数据,cpu随时拿都没问题.比如寄存器内存这样的设备
  2. 查询传输方式:也称为程序IO,在传输之前,由程序去检测设备的状态.数据远设备在一定条件下才能传送数据.这类设备通常是低速设备.
  3. 中断传送方式,也称为中断驱动IO,如果数据源设备将数据准备号后通知cpu来取,cpu需要压栈保护现场
  4. 直接存储器存取方式DMA:数据源直接和内存交互,不需要cpu的参与,需要DMA控制器
  5. 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个字节一组

可以发现除了大小端不同以外,内容是相同的

猜你喜欢

转载自www.cnblogs.com/perfy576/p/9023942.html