x86实模式保护模式

windows intel 8086

版权所有:

  • 《x86 汇编语言 从实模式到保护模式——李忠 王晓波 余洁》

加载器 用户程序

两者需要遵从一致的协议

用户程序内部的某个固定位置,包含有对该程序的描述信息

加载器在该固定位置进行读取

这个位置就是用户程序的开头

头部在源程序中以一个段的形式出现,且是程序中第一个被定义的段

该段包含以下信息:

  • 用户程序的尺寸,以字节为单位

    • 加载器需要根据该尺寸来决定读取多少个逻辑扇区
    • program_length dd program_end,32位,以避免程序长度值超过16bit表示范围
    • 在程序的尾部定义一个标号program_end,然后通过取得该标号的偏移地址来确定程序的尺寸
  • 应用程序的入口点,段地址+偏移地址

    • EntryPotin

    • 理想情况下,第一条指令就是代码段的第一条指令

    • 也就是说,程序入口点是代码段中偏移地址为0的那条指令

    • 但是,如果程序中包含有多个代码段,就需要明确指定出第一条指令的确切位置

    • ;用户程序入口点
      code_entry      dw start                ;偏移地址[0x04]
                      dd section.code_1.start ;段地址[0x06] 
      
  • 段重定位表

    • 程序在加载到内存之后,每个段的地址必须重新确定

    • 加载器负责对段进行重定位,他需要知道每个段在用户程序中的位置

    • 在用户程序头程建立一张重定位表,用于记录段的位置

    • 段重定位表行数的计算

    • realloc_tbl_len dw (header_end-code_1_segment)/4
                                                  ;段重定位表项个数[0x0a]
          
              ;段重定位表           
              code_1_segment  dd section.code_1.start ;[0x0c]
              code_2_segment  dd section.code_2.start ;[0x10]
              data_1_segment  dd section.data_1.start ;[0x14]
              data_2_segment  dd section.data_2.start ;[0x18]
              stack_segment   dd section.stack.start  ;[0x1c]
          
              header_end:  
      
    • 可以看到,是由段的数量来决定的,每一行占4个字节,用尾部标号header_end减去头部标号code_1_segment即可得出行数

    • 然后声明每一行的内容即可,地址是一个双字(4字节)

加载器工作流程

记载器在加载程序之前需要确定两件事:

  • 哪些内存是空闲的,从哪个内存地址加载用户程序
  • 用户程序位于硬盘上的哪一个位置

确定要加载到的内存地址

加载器为用户程序确定内存物理地址

phy_base dd 0x10000             ;用户程序被加载的物理起始地址

该地址必须是16的整数倍,也就是说,如果使用16进制进行表示,最后一位一定是0

就像10进制中,能够被10整除的数的最后一位一定是0一样

后面需要将该物理地址转换为段地址,因为该地址是16位对齐的,因此直接除以16即可获得对应的段地址

mov ax,[cs:phy_base]            ;计算用于加载用户程序的逻辑段地址 
mov dx,[cs:phy_base+0x02]
mov bx,16        
div bx            
mov ds,ax                       ;令DS和ES指向该段以进行操作
mov es,ax           

由于定义物理起始地址的时候使用的是dd,所以这里需要两个16位的寄存器来存放

ax存低位,dx存高位

由于存储格式位小端序,所以高位在高址,低位在低址

mov ax,[cs:phy_base]            ;计算用于加载用户程序的逻辑段地址 
mov dx,[cs:phy_base+0x02]

除以16,商就是段地址,获得的商会自动存储在AX寄存器中,将该值赋予DS和ES段寄存器

读取硬盘数据

然后加载器需要从硬盘读取用户程序

I/O Controller Hub (ICH)芯片——南桥

硬件操作

独立编址

端口

0~65535

操作指令:in out

in al, dx

in指令的目的操作数只能是al或者ax,分别代表8位宽端口和16位宽端口

源操作数只能是dx寄存器

in指令相关的汇编代码对应的操作码是0xEC和0xED,只有一个字节,因为源和目的操作数只有ax和al的区别

0xEC对应al,0xED对应ax

与之对应的是out指令,不再赘述

通过硬盘控制器端口读取扇区数据

硬盘读写的基本单位就是扇区

硬盘读写的最小单位是扇区,原子操作,只要读或者写就必须读取或者写入n个扇区

因此硬盘是块设备(数据交换是成块的——扇区)

最早的逻辑扇区编址方法是LBA28,使用28位进行逻辑扇区的编码

2^28=268,435,456个扇区,512 bytes/扇区

268,435,456 * 512 bytes = 137,438,953,472.0 Bytes (B) = 128.0 Gigabytes (GB)

也就是说这种逻辑扇区的组织方式最大能管理128GB的硬盘

但是已经无法满足使用了,还是太小了,现在普遍流行的是LBA48编址方式,可以管理131072 TB的硬盘

使用LBA28来访问硬盘

从硬盘读取逻辑扇区的步骤如下:

  • 设置要读取的扇区数量

    • mov dx, 0x1f2
      mov al, 0x01
      out dx, al
      
    • 此处设置要读取的扇区数量为1,如果设置为0,则表示要读取256个扇区(因为是使用的是al,表示该端口为8位宽)

  • 设置起始LBA扇区编号

    • 只需要给出第一个扇区的编号即可(因为是连续读取)

    • 扇区号长度为28,因此需要分在4个8位宽的端口中分别写入(0x1f3, 0x1f4, 0x1f5, 0x1f6)

    • 依次写入0~23位

    • 如果需要设置扇区编号为0x02,则需要执行如下指令

    • mov dx, 0x1f3
      mov al, 0x02
      out dx, al
      
      inc dx	
      mov al, 0x00
      out dx, al
      
      inc dx
      out dx, al
      
      inc dx
      mov al, 0xe0
      out dx, al
      
    • 倒数第二行代码,低4位为0,表示扇区编号的第24~27,高4位为e,即1110

    • 高3位111,表示LBA模式,第4位01分别表示主副硬盘

    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RlQCUwUb-1651485615325)(https://i.imgur.com/Z8atDF5.png)]

  • 请求硬盘读

    • mov dx, 0x1f7	; 向0x1f7端口写入
      mov al, 0x20	; 读命令
      out dx, al
      
  • 等待读写操作完成(之前未完成的读写操作)

    • 0x1f7端口即是命令端口又是状态端口

    • 在硬盘操作期间,会将0x1f7端口的第7位置为1,表示处于“忙”状态

    • 如果可以进行操作,则该bit位会清零并将第3位设置为1,表示ready

    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QasbX3Kl-1651485615326)(https://i.imgur.com/s0irXfb.png)]

    • mov dx, 0x1f7
      .waits:
      	in al, dx
      	and al, 0x88
      	cmp al, 0x08
      	jnz .waits		; 不忙且硬盘已准备好进行数据传输
      
    • and al, 0x88指令对al和0x88进行与操作

    • 0x88的二进制形式为10001000,也就是我们关心的第3位和第7位

    • 如果第7位为0,且第3位为1,也就是00001000(0x08)

    • 那么就说明硬盘已经可以进行操作了

  • 连续读取数据

    • 0x1f0为硬盘的数据端口,16位的宽端口

    • 我们可以通过这个端口读取或者写入数据

    • mov cx, 256
      mov dx, 0x1f0
      .readw:
          in ax, dx
          mov [bx], ax
          add bx, 2
          lopp .readw
      
    • 上述指令会读取256字(512bytes)的数据出来,正好是一个扇区的长度

最后还有一个0x1f1端口,该端口中存储的是硬盘驱动器最后一次执行命令后的状态

过程调用

像上面这种操作硬盘的工作,对于操作系统来说,会很频繁的执行

每次都写这么一堆代码太费事了,不如封装成过程(Procedure),又称作例程、子程序等

其实就相当于高级语言中的函数(方法)

处理器通过过程调用指令跳转到这段代码执行,然后通过过程返回指令返回到调用处的下一条指令继续执行

如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gvfo8j5W-1651485615327)(https://i.imgur.com/DHCeSBw.png)]

在本例中,过程调用为磁盘读取程序

read_hard_disk_0:                        ;从硬盘读取一个逻辑扇区
                                         ;输入:DI:SI=起始逻辑扇区号
                                         ;      DS:BX=目标缓冲区地址
         push ax
         push bx
         push cx
         push dx
      
         mov dx,0x1f2
         mov al,1
         out dx,al                       ;读取的扇区数

         inc dx                          ;0x1f3
         mov ax,si
         out dx,al                       ;LBA地址7~0

         inc dx                          ;0x1f4
         mov al,ah
         out dx,al                       ;LBA地址15~8

         inc dx                          ;0x1f5
         mov ax,di
         out dx,al                       ;LBA地址23~16

         inc dx                          ;0x1f6
         mov al,0xe0                     ;LBA28模式,主盘
         or al,ah                        ;LBA地址27~24
         out dx,al

         inc dx                          ;0x1f7
         mov al,0x20                     ;读命令
         out dx,al

  .waits:
         in al,dx
         and al,0x88
         cmp al,0x08
         jnz .waits                      ;不忙,且硬盘已准备好数据传输 

         mov cx,256                      ;总共要读取的字数
         mov dx,0x1f0
  .readw:
         in ax,dx
         mov [bx],ax
         add bx,2
         loop .readw

         pop dx
         pop cx
         pop bx
         pop ax
      
         ret

在过程代码的开始,需要执行一系列压栈操作来保护寄存器的值

只要是在过程中会被修改值的寄存器,都要进行压栈操作,然后在返回前POP

通过DI和SI寄存器来传递参数——逻辑扇区编号

由于逻辑扇区编号为28bit,因此需要两个16bit的通用寄存器

DI中存储高12bit,且需要将DI的高4bit置0,SI存放低16bit

约定将读出的数据存放到由段寄存器DS指向的数据段中,使用BX作为段偏移量,这个就是第二个参数,读缓冲区的地址

 xor di,di
 mov si,app_lba_start            ;程序在硬盘上的起始逻辑扇区号 
 xor bx,bx                       ;加载到DS:0x0000处 
 call read_hard_disk_0

可以看到上面的代码,就是过程被调用的代码,DI和SI代表参数:扇区编号,BX代表缓冲区偏移量

8086支持四中调用方式:

  • 16bit相对近调用 call near

    • 近调用的含义就是被调用的过程位于当前代码段,只需要拿到代码段的偏移地址就行了,因此只需要16bit
    • 相对近调是3字节指令,操作码0xE8占一个字节,操作数为16bit偏移地址,两字节
    • 偏移地址的计算方式为过程标号代表的会变地址-call指令所在的汇编地址-3(指令长度)
    • 由于过程可能位于call指令的前方,也就是说汇编地址可能要比call小,那么此时偏移量就是一个负数,因此近调用的操作数是一个16bit的有符号数,因此只能在正之间浮动
    • 也就是调用指令的-32768和32767字节之间
    • 超过了这个范围,就不能使用call near了
  • 16bit间接绝对近调用 call near

    • 和上面一样,被调用的过程也存在于当前代码段,只不过指令的操作数由偏移量变成了过程的真实偏移地址(相对于代码段段首)

    • 形式如下:

    • call cx
      call [0x3000]
      call [bx]
      call [bx+si+0x02]
      
    • 这种调用方式相较于上一中没有距离限制,可以调用到当前代码段中的任意一处

  • 16bit直接绝对远调用 call far

    • 这种调用方式属于段间调用
    • 即被调用的过程和call指令位于不同的代码段中
    • 远调用既需要调用过程所在的段地址,又需要偏移地址
    • 不明白为什么这里还叫做16bit,明明需要32bit来表明过程的地址
    • 段地址和偏移地址会直接在call指令中给出
  • 16bit间接绝对远调用 call far

    • 形式如下:

    • call far [0x2000]
      call far [proc_1]
      call far [bx]
      call far [bx+si]
      
    • 上面的代码中给出的是DS段的偏移地址

    • 处理器根据该地址找到两个字节的数据,第一个字节存储的是代码段偏移量,第二个字节存储的是代码段地址

    • 如下所示:

    • proc_1 dw 0x0102, 0x2000

与call相对的就是ret和retf返回指令

ret相对于call near,retf相对于call far

ret指令执行时,处理器会执行POP IP

retf指令执行师,处理器会执行POP IP, POP CS

加载用户程序

用户程序头部示意图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v2so02uD-1651485615327)(https://i.imgur.com/HSR5RYR.png)]

通过程序长度确定扇区数量

mov dx,[2]
mov ax,[0]
mov bx,512                      ;512字节每扇区
div bx

由于小端序的原因,长度的高16bit存储在高址[2],低16bit存储在低址[0]

然后除以512bytes获取扇区个数

除不尽的话,那么AX(商)中存储的值是比实际要读取的扇区数小一的(按块操作,不足512字节也会占用一个扇区)

但是由于我们已经读取了一个扇区(用于读取程序头部),因此我们无需对结果进行任何操作

但是在这种情况下,我们还需要判断AX(商)是否为0(实际长度小于等于一个扇区的大小)

如果为0,则说明程序只占用一个扇区,我们也就无需再读取后续的扇区了

如果除尽了(DX——余数值为0),那么DX中存储的值就正好是扇区数,但是我们需要减一,因为已经读取了一个扇区

由于我们需要DS和BX来指定读取数据存放的缓冲区,我们每次读取一个扇区,也就是512bytes

我们需要对段地址进行修改,因为我们每次都会把bx重置为0

因此每读完一个扇区,段地址要增加512bytes = 0x200 bytes

由于是段地址,需要除以16,也就是段地址每次需要增加0x20

mov ax,ds
add ax,0x20                     ;得到下一个以512字节为边界的段地址
mov ds,ax  

用户程序重定位

这里有一个疑问,根据之前的知识,程序入口地址存储在程序头部的[0x06]和[0x04],不知道这里书上为什么写的是[0x08]和[0x06]

示意图中明显比之前介绍的程序头部多出了一部分
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H6viwEq1-1651485615327)(https://i.imgur.com/zAdJ9cm.png)]

从[0x08]和[0x06]获取程序入口地址的高16bit(其中只有低4bit有效)和低16bit

然后使用cs:phy_base获取用户程序在内存中的基址,本例中为双字0x10000

上面从程序头部获取的双字的高低字分别被存储到了DX和AX中

分别和phy_base的高低字进行相加

add ax,[cs:phy_base]             ; 内存的低址 + 用户程序中定义的程序入口点地址的低址
adc dx,[cs:phy_base+0x02]        ; 内存的高址 + 用户程序中定义的程序入口点地址的高址
                                 ; 进位加法,避免由于上次加法操作(add  ax,[cs:phy_base])产生的进位丢失,使用CF获取进位
shr ax,4                        
ror dx,4
and dx,0xf000
or ax,dx

然后后面通过右移4bit ax,并滚动右移4bit dx,再给dx与上0xf000来清空除了高4bit的其余bit位,最后对两个寄存器进行或操作拼接出段地址

滚动右(左)移之前已经介绍过:https://hackmd.io/wMbWVcYjRd62aMbxz740Fg

下面还需要重定向用户程序的所有段地址

这个需要根据段重定位表来完成

; 开始处理段重定位表
mov cx,[0x0a]                   ;需要重定位的项目数量
mov bx,0x0c                     ;重定位表首地址

这个计算过程跟上面计算段地址是一样的

mov dx,[bx+0x02]                ;32位地址的高16位 
mov ax,[bx]                    	;32位地址的低16位 
call calc_segment_base

根据cx进行循环即可

每次循环都会将重定位之后的段地址存储到段重定位表的对应地址

将控制权移交给用户程序

现在用户程序已经被加载到内存,并准备就绪

直接一个跳转指令jmp far调到用户程序即可

这里也解答了我上面的疑问,就是用户程序头部的0x04的两个字

[0x04] [0x06]

前者为偏移地址,后者为根据[0x08]和[0x06]这两个字计算出来的段地址

处理器会根据该地址跳转到用户程序

jmp far [0x04]

现在处理器已经去用户程序那里了,我们需要对用户程序进行跟踪

但是在此之前我们需要了解一下无条件转移指令

好吧,这个无条件转移指令没什么好了解的,跟上面的call near(far)差不多

用户程序的工作流程

在刚进入用户程序的时候,DS和ES段寄存器仍然指向程序头部

段寄存器SS还在指向加载器的栈空间

代码:
https://blog.csdn.net/ma_de_hao_mei_le/article/details/124462602

中断和动态时钟显示

中断向量表

8086

256项

每一项都存储了中断编号对应的中断程序的入口地址

每个中断在中断向量表中占2bytes

分别是中断处理程序所在的偏移地址和段地址

可屏蔽中断和非屏蔽中断

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lVUyYyt1-1651485615327)(https://i.imgur.com/TEnXq0B.png)]

NMI引脚过来的中断都是非屏蔽中断,必须处理,不可忽略

且该中断大概率都是致命错误

可屏蔽中断通过INTR引脚进入处理器内部

处理器每次只能处理一个中断

需要通过一个代理来接受外部设备发出的中断信号,且该代理设备需要对多个同时到达的中断信号进行仲裁,决定谁可以将中断信号发送给CPU

这个代理设备就是8259芯片——中断控制器

在Intel允许的256个中断中,8259负责提供其中的15个,但并不是固定的15个,8259可以进行编程来改变中断号

8259分为主从两片

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0me7ycOp-1651485615328)(https://i.imgur.com/AsX9ofq.png)]

不知道为啥就形成了15个中断信号,这明明不是有16个引脚吗

应该是由于级联的原因,从片的8个输入的输出连到了主片的IR2引脚上,因此就变成了7+8=15

主片引脚IR0->系统定时器

从片引脚IR0->实时时钟芯片RTC

在8259内部有一个屏蔽寄存器IMR 8bit

分别对应8个输入引脚 0->允许 1->屏蔽

主片端口号-> 0x20 0x21

从片端口号-> 0xa0 0xa1

可以通过这些端口访问8259芯片,设置他的工作模式以及IMR(屏蔽寄存器)的内容

是否处理中断,还要看CPU中的IF标志寄存器,IF的I就是Interrupt,只有当该标志为1的时候CPU才会处理中断

可以使用cli和sti指令来清除和置位IF

中断的优先级和引脚相关,主片的IR0引脚是最高的优先级,从片也如此

如果在8259处理一个中断的时候,来了一个优先级更高的中断,那么当前中断的处理会被打断,这个叫做中断嵌套

实模式下的中断向量表

中断向量表起始于0x00000,终止于0x003ff 1KB

处理器在处理中断的时候,需要做以下几件事:

  • 保护断点现场
    • 压栈,标志寄存器全部压栈,重置IF和TF,TF为陷阱标志位
    • 然后将CS和IP压栈
  • 执行中断处理程序
    • 将拿到的中断号*4,即可得到中断向量在表中的偏移量
    • 因为每一个中断向量在表格中占用4bytes
    • 得到该偏移量之后,即可获取到中断处理程序的段地址和偏移地址
    • 分别传送给CS和IP,即可开始执行中断程序
    • 此时由于IF被重置,CPU将不再响应硬件中断,如果你希望更高优先级的中断被处理,则可以在编写中断处理程序时,在合适的位置开放中断(置位IF)
  • 返回到断点继续执行
    • 所有中断处理程序的最后一条指令一定是iret(中断返回指令)
    • 该指令会导致IP、CS、标志寄存器依次出栈,此时由于IF的值恢复,CPU可以重新处理中断

NMI(非屏蔽中断),对于这种类型的中断,CPU不需要获取中断号,会自动生成中断号2

中断向量表是由BIOS进行维护的

但是在一开始,BIOS会将所有的中断向量入口程序地址设置为同一个值,且该中断处理程序只有一个iret指令

在计算机启动后,操作系统和用户程序会根据自己的需要来改写中断向量表中的入口地址

实时时钟、CMOS RAM和BCD编码

CMOS——互补金属氧化物

CMOS RAM即由互补金属氧化物材料组成的静态存储器

实时时钟负责计时,日期和时间的数值被存储在这块存储器中

RTC芯片由一个振荡频率为32.768kHz的石英晶体振荡器驱动(不知道是啥玩意儿)

可以通过端口访问CMOS RAM

0x70 0x74 是索引端口,和前面介绍的光标寄存器一样

用于指定CMOS RAM内的单元

0x71 0x75是数据端口,用于读写相应单元里的内容

mov al, 0x06
out 0x70, al
in al, 0x71

上面这段代码可以用于读取今天是星期几

CMOS RAM中的时间信息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZgzH8I7U-1651485615328)(https://i.imgur.com/urS8XG6.png)]

另外需要注意一点的是,索引端口0x70的值的最高bit位用于决定NMI是否阻断

第7个bit位为1时,则阻断NMI(不可屏蔽中断)信号,为0时,则允许NMI中断到达处理器

剩余的7bit(0~6)才是真正用于指定CMOS RAM单元的索引号

BCD——Binary Coded Decimal

BCD编码会将一个字节分为高4bit和低4bit,分别用于表示一个十进制数的十位和个位

比如25->00100101 10进制最大值为9,因此任何一个4bit的值超过了1001,都是非法的

在CMOS RAM中的0x0a~0x0d这四个单元,并不是普通的单元,而是4个寄存器的索引号

也是通过0x70和0x71这两个端口进行访问

这四个寄存器用于设置实时时钟电路的参数和工作状态

寄存器A的功能说明

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WTsolFKp-1651485615329)(https://i.imgur.com/UXgLUxl.png)]

寄存器B的功能说明

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uwuc1vQA-1651485615329)(https://i.imgur.com/Q96sGkZ.png)]

C

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z6XOjxfh-1651485615329)(https://i.imgur.com/APYgrYZ.png)]

D

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3xqQ7MbB-1651485615329)(https://i.imgur.com/OQC4KSs.png)]

可以使用c08_mbr.asm 程序加载器来加载本章的用户程序

在修改栈段SS的时候,我们还需要修改栈指针SP

这两个寄存器的修改总是先后紧挨着发生的

因此在这两条指令执行期间,CPU不会处理任何中断,因为中断也是需要依靠栈的

如果在SS修改完之后,CPU立马去处理中断,此时SP还未修改,那么就会导致栈段错误

因此CPU规定,在SS修改指令的下一条指令完成之前,不会处理任何中断

因此我们应该把SP的修改紧跟在SS的修改之后

BIOS会把8259的主片IR0设置为0x08,从片的IR0设置为0x70

RTC的中断号默认为0x70

中断号*4即可得到中断向量在表中的偏移

通过设置RTC更新周期结束中断,可以每一秒发送一个中断

mov al,0x0b                        ;RTC寄存器B
or al,0x80                         ;阻断NMI 
out 0x70,al
mov al,0x12                        ;设置寄存器B,禁止周期性中断,开放更 
out 0x71,al                        ;新结束后中断,BCD码,24小时制 

对照寄存器B的功能表格

0x12 -> 00010010

即第1和第4bit位置位,表示每个更新周期结束时产生一个中断,使用24小时制

通过将0x70端口的值的最高位置1(or al, 0x80),阻断NMI中断。。。。(对照表格)

可以通过读取寄存器C的内容来获取中断发生的原因

如果在中断之后没有读取C寄存器的内容,那么同样的中断将不再产生

mov al,0x0c
out 0x70,al
in al,0x71                         ;读RTC寄存器C,复位未决的中断状态

另外还需要修改8259的屏蔽寄存器的值,来开放IR0引脚,不然来自CMOS RAM的中断将不会被处理

in al,0xa1                         ;读8259从片的IMR寄存器 
and al,0xfe                        ;清除bit 0(此位连接RTC)
out 0xa1,al                        ;写回此寄存器 
使处理器进入地宫

软中断

int指令

int3 断点中断指令

32位保护模式

偏移地址叫做有效地址

内存分页

段部件在分段模式下,产生的是物理内存的绝对地址

在分页模式下,段部件产生的是线性地址

线性地址需要经过页部件转换,才能形成物理地址

现代处理器的结构和特点

流水线工作模式。一边取指令,一边执行,一边译码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4THkix4C-1651485615330)(https://i.imgur.com/VJ6gjQd.png)]

高速缓存

内存和CPU响应速度的差异

高速缓存是处理器与内存之间的一个静态存储器(造价高)静态存储器SRAM,响应速度位纳秒级

为了实现流水线技术,需要将指令拆分成更小的可独立执行部分——微操作

当遇上跳转指令的时候,流水线工作模式下的后续指令将全部失效,需要flush流水线

另外,对于分支结构,流水线模型也不能很好的适应

因为流水线无法预测下一条指令是哪个分支的

解决办法是在Pentinum Pro处理器中引入的分支预测技术

指令前缀

相同的机器指令,在16位和32位模式下的解释和执行效果是不同的

指令前缀0x66,可以反转当前默认操作数大小

32->16 16->32

可以使用bits指令来指示程序默认运行在16还是32位模式下

16位是默认模式

保护模式

段描述符,一个段需要8个字节来描述

SD segment descriptor

需要在内存中专门开辟出一片空间来保存这些描述符,构成一张描述符表

全局描述符表(Global Descriptor Table – GDT)

在进入保护模式之前,必须要定义GDT

CPU中有一个48bit的寄存器,用于跟踪GDT,叫做GDTR

全局描述符表寄存器

GDTR分为两部分,32bit的线性地址和16bit的边界

32bit的线性地址保存的是GDT在内存中的起始地址,边界保存的是GDT的边界,其值是GDT大小(bytes)-1,其实就是GDT的长度-1,因为起始位置占1bytes

界限值就是GDT中的最后一个字节的偏移量

最大是65536字节 64KB

一个SD占用8bytes,因此GDT中最多包含65536/8 = 8192项

用户程序通常情况下没有修改GDT的权限,GDT是由操作系统来维护的

段描述符格式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pMDJn6cw-1651485615330)(https://i.imgur.com/QraS1z5.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RBM3Q1C4-1651485615331)(https://i.imgur.com/kOSNpiJ.png)]

安装存储器的段描述符并加载GDTR

在实模式下,需要将线性地址转换成段地址+偏移地址的形式

;计算GDT所在的逻辑段地址 
mov ax,[cs:gdt_base+0x7c00]        ;低16位 
mov dx,[cs:gdt_base+0x7c00+0x02]   ;高16位 
mov bx,16        
div bx            
mov ds,ax                          ;令DS指向该段以进行操作
mov bx,dx                          ;段内起始偏移地址 

除以16,商就是段地址,余数就是偏移地址

处理器规定GDT的第一项是一个空描述符

;创建0#描述符,它是空描述符,这是处理器的要求
mov dword [bx+0x00],0x00
mov dword [bx+0x04],0x00  

0x7c0001ff

根据图11-4进行解析

解析代码已完成:
https://blog.csdn.net/ma_de_hao_mei_le/article/details/124532960?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22124532960%22%2C%22source%22%3A%22ma_de_hao_mei_le%22%7D&ctrtid=QZ9N6

处理器规定GDT中的第一个表项必须为NULL

因此前两个双字全为0

;创建0#描述符,它是空描述符,这是处理器的要求
mov dword [bx+0x00],0x00
mov dword [bx+0x04],0x00  

第二项就是代码段描述符

;创建#1描述符,保护模式下的代码段描述符
mov dword [bx+0x08],0x7c0001ff     
mov dword [bx+0x0c],0x00409800     

解析脚本上面已经给出了连接,此处不再赘述

基址是7c00,正好就是主引导程序所在的区域

低双字在低地址段,高字节在高地址端

;创建#2描述符,保护模式下的数据段描述符(文本模式下的显示缓冲区) 
mov dword [bx+0x10],0x8000ffff     
mov dword [bx+0x14],0x0040920b 

然后安装数据段描述符

经过解析之后可以看到段地址是显存的地址0x000b8000

然后安装栈段

然后将描述符表的线性地址和界限到GDTR寄存器中

使用lgdt指令

操作数为一个48bit,低16bit为GDT界限值,高32bit为GDT线性基地址

;初始化描述符表寄存器GDTR
         mov word [cs: gdt_size+0x7c00],31  ;描述符表的界限(总字节数减一)   
                                             
         lgdt [cs: gdt_size+0x7c00]

将31这个数字写入内存中,占用两个字节,起始地址是[cs:gdt_size+0x7c00]

gdt_size是一个标号,因为代码是从0x7c00处开始的,因此需要加上0x7c00

gdt_size就是该标号相对于代码起始位置的偏移量,这个位置定义了一个word,值为0

现在把31挪到了这里

不知道为啥不用ds,非要用cs,明明也没啥区别呀,哦,ds段没有被初始化

从这个位置cs: gdt_size+0x7c00往后读取6个字节放到GDTR中,在代码中gdt_size和gdt_base是连着定义的,因此正好可以将界限值和线性基地址都写进去

历史遗留问题,第21根地址线

CR0 处理器内部的控制寄存器

32bit

包含一系列用于控制处理器操作模式和运行状态的标志位

0bit 保护模式允许bit Protection Enable PE

this bit is set to 1 to indicate that we’re entering into Protection Mode, and we run with PM’s rules

x86实模式保护模式_第1张图片

the Interrupt Machanism between PM and TM is different

so the original interrupt vector table is no longer usable

进入保护模式之后BIOS中断就不能再被使用了,因为BIOS中断是实模式下的代码

在重设保护模式下的中断环境之前,需要先关闭中断

     cli                                ;保护模式下中断机制尚未建立,应 
                                        ;禁止中断 

mov eax,cr0
or eax,1
mov cr0,eax ;设置PE位

上述代码执行完成后,处理器就会进入保护模式

32bit保护模式下的数据访问
x86实模式保护模式_第2张图片

段选择子依次占用13bit、1bit、2bit

格式为

mov cx,00000000000_10_000B ;加载数据段选择子(0x10)
mov ds,cx

我也不知道为什么要这样写

上面的索引号为10b,即2

就是数据段

实际偏移地址,索引号*8 因为每一个表项占用8bytes

x86实模式保护模式_第3张图片

     mov byte [0x00],'P'  

该指令在内存中的执行示意图
x86实模式保护模式_第4张图片

清空流水线并串行化处理器

在实模式中,段寄存器的描述符高速缓存中只有低20位有效

这些内容在进入保护模式后,还会残留,并且有可能会影响到后面进入保护模式之后的程序的使用

因此最好的做法就是在进入保护模式之后,及时刷新各个段寄存器

另外还需要清空流水线,避免因为对操作数和指令的解释不通而引起程序运行错误

使用jmp和call可以让处理器清空流水线并串行化执行指令(之前有乱序执行的代码)

在设置了CR0之后,立即使用cmp或者call转移到当前指令刘的下一条指令上即可

然后加载显存选择子

在保护模式下,不允许直接使用mov指令操作cs段寄存器

保护模式下的栈

栈段描述符的界限值

栈段位于3号描述符(第四个)

你可能感兴趣的:(安全,x86,汇编)