x86从实际模式到保护模式(2):外围设备、用户程序和保护模式初探

上一节x86从实际模式到保护模式(1):汇编语言程序与设计总结

硬盘和显卡的访问和控制

给汇编程序分段

;数据段
section data1 align=16;表示段以16B对齐,如果编译过程中缺失则填充0
;数据段
section data2
;代码段
section code
;栈段
section stack

段的最大长度是64KB

section data1 align=16 vstart=0;段内汇编地址都为相对于本段的偏移地址

加载器和用户程序头部段

section header vstart=0
	program_llength dd program_end;程序总长度
	
	;用户程序入口点
	code_entry dw start;偏移地址
			   dd section.code.start;段地址
     readlloc_tbl_lwn (segtbl_end-segtbl_bgein)/4;段重定位表个数
     
     ;段重定位表
     segtbl_begin;
     code_segment dd section.code.start
     data_segment dd section.data.start
     stack_segment dd section.stack.start
     segtbl_end:
 ...
 section end;此处没有vstart,所以取到program_end可知是非段内偏移地址
 	program_end:
 		...

加载器的工作流程:

  • 读取用户程序的起始扇区;
  • 把整个用户程序都读入内存;
  • 计算段的物理地址和逻辑段地址(段的重定位);
  • 转移到用户程序执行(将处理器的控制权交给用户程序)。
     app_lba_start equ 100           ;声明常数(用户程序起始逻辑扇区号)
                                     ;常数的声明不会占用汇编地址
     
     phy_base dd 0x10000             ;用户程序被加载的物理起始地址
SECTION mbr align=16 vstart=0x7c00                           
     ;设置堆栈段和栈指针 
     mov ax,0      
     mov ss,ax
     mov sp,ax

     mov ax,[cs:phy_base]       ;高16位     
     mov dx,[cs:phy_base+0x02]	;低16位
     
     mov bx,16        
     div bx           ;ax/16等于段地址 
     mov ds,ax                       
     mov es,ax 
 times 510-($-$$) db 0
                  db 0x55,0xaa

输入输出端口的访问

al:从端口读数据到处理器(目的寄存器源操作数)

in al, dx;8位端口
in ax, dx;16位端口

out:从端口向外围设备发送数据(目的操作数:源操作数)

in dx, al;8位端口
in dx, ax;16位端口

过程和过程调用

使用call指令调用过程,执行ret指令,过程返回

     ;以下读取程序的起始部分 
     xor di,di
     mov si,app_lba_start            ;程序在硬盘上的起始逻辑扇区号 
     xor bx,bx                       ;加载到DS:0x0000处 
     call read_hard_disk_0
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

过程调用和返回的原理

16位相对近调用:当执行call时,IP会修改成call下一条指令的地址(偏移地址)并入栈,将来弹出IP并返回IP的地址

;call 标号

加载整个用户程序

     ;以下判断整个程序有多大
     mov dx,[2]                      ;曾经把dx写成了ds,花了二十分钟排错 
     mov ax,[0]
     mov bx,512                      ;512字节每扇区
     div bx
     cmp dx,0
     jnz @1                          ;未除尽,因此结果比实际扇区数少1 
     dec ax                          ;已经读了一个扇区,扇区总数减1 
@1:
     cmp ax,0                        ;考虑实际长度小于等于512个字节的情况 
     jz direct

     ;读取剩余的扇区
     push ds                         ;以下要用到并改变DS寄存器 

     mov cx,ax                       ;循环次数(剩余扇区数)
@2:
     mov ax,ds
     add ax,0x20                     ;得到下一个以512字节为边界的段地址
     mov ds,ax  

     xor bx,bx                       ;每次读时,偏移地址始终为0x0000 
     inc si                          ;下一个逻辑扇区 
     call read_hard_disk_0
     loop @2                         ;循环读,直到读完整个功能程序 

     pop ds                          ;恢复数据段基址到用户程序头部段 

用户程序的重定位

在把用户程序加载进内存之后,接下来就是对重定位表进行赋值

     ;计算入口点代码段基址 
direct:
     mov dx,[0x08]
     mov ax,[0x06]
     call calc_segment_base
     mov [0x06],ax                   ;回填修正后的入口点代码段基址
calc_segment_base:                       ;计算16位段地址
                                         ;输入:DX:AX=32位物理地址
                                         ;返回:AX=16位段基地址 
     push dx                          

     add ax,[cs:phy_base]
     adc dx,[cs:phy_base+0x02]
     shr ax,4
     ror dx,4
     and dx,0xf000
     or ax,dx

     pop dx

     ret

转到用户程序内执行

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

realloc:
     mov dx,[bx+0x02]                ;32位地址的高16位 
     mov ax,[bx]
     call calc_segment_base
     mov [bx],ax                     ;回填段的基址
     add bx,4                        ;下一个重定位项(每项占4个字节) 
     loop realloc 

     jmp far [0x04]                  ;转移到用户程序

16间接绝对远转移

段间转移

jmp far m;必须包含两个字,前一个字是偏移地址,后一个字是段地址

;如下
jmp far 2002; 

用户程序的执行过程

start:
     ;初始执行时,DS和ES指向用户程序头部段
     mov ax,[stack_segment]           ;设置到用户程序自己的堆栈 
     mov ss,ax
     mov sp,stack_end

     mov ax,[data_1_segment]          ;设置到用户程序自己的数据段
     mov ds,ax

res

resb;声明多少字节的空间,也可以用times
resw
resd

过程调用

16位直接绝对远调用

call cs:ip

call far m;先把cs和ip压栈,然后跳到m处的段地址和偏移地址

用call far调用过程,压入的是CS和IP;在过程内部用retf返回

32位x86处理器编程架构

寄存器的扩展和扩充

eax, ebx, ecx, edx, esi, edi, ebp, esp, eip, eflags
;低16位和原来的寄存器是一样的,高16位是加入了一些其他数据

内存的访问方式

;段寄存器,还是16位
cs,es,ds,fs,gs,ss

段地址范围[FFFF],偏移地址范围[FFFF],总范围大小[10FFFF],80386有32根地址线,因此,可以访问10FFFF的空间,而8086只有20根地址线,因此只能访问到[FFFF]。

传统表达上,把[FFFF]~[10FFFF]叫做高位内存区HMA(high memory area)

保护模式下,程序的每个段的信息必须放在描述符表中,段寄存器去描述符表中寻址段的基址,然后加上通用寄存器给出的偏移地址,从而寻找到段的最终地址

流水线技术

为了提高处理器的执行效率和执行速度,可以把执行的过程分成若干个细小的步骤,并分配和响应的单元来完成,因此,这种指令的执行就会重叠起来,这便是流水线技术。

其实就是用不同的单元来接收不同的结果,当这个单元没事做了,就进行下一个操作,保证单元不空闲

打个比方,当单元1是来取指令的(IP),单元2是来进行译码的(译码器),当指令1进入单元1的时候,指令2就进行等待队列等待,此时单元1是忙碌的,当单元1处理完指令1后,指令1进入到单元2,此时单元1是空闲的,因此就从等待队列中取出指令2进行取指令,依次递推,这便是流水线技术。

高速缓存技术

根据局部性原理取数据,当处理器取数据的时候直接从告诉缓存中寻找数据,而非内存

乱序执行技术

为了实现流水线技术,当一个指令有多个微操作时,就要将操作分解为多个微操作进行执行,保证总线不空闲。

分支目标预测

当处理器遇到转移指令时,需要清空(flush)流水线

转移是否会发生?

在处理器中有一个分支目标缓存器BTB,存放转移指令后的地址,下一次遇到转移指令,查看是否有转移地址,如果有直接跳转

进入保护模式

GDT和DGTR

GDTR48位寄存器执行GDT全局描述符表

GDTR:32位全局描述符表线性基地址 + 16位全局描述符表边界

一个描述符占8个字节,64b

在进入保护之前,必须创建全局描述符表GDT

      ;设置堆栈段和栈指针 
     mov ax,cs      
     mov ss,ax
     mov sp,0x7c00

     ;计算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                          ;段内起始偏移地址 
              ;创建0#描述符,它是空描述符,这是处理器的要求
     mov dword [bx+0x00],0x00
     mov dword [bx+0x04],0x00  

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

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

     ;创建#3描述符,保护模式下的堆栈段描述符
     mov dword [bx+0x18],0x00007a00
     mov dword [bx+0x1c],0x00409600

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

     lgdt [cs: gdt_size+0x7c00]

     gdt_size         dw 0
     gdt_base         dd 0x00007e00     ;GDT的物理地址 

描述符的分类

描述符:

  • 存储器的段描述符:
    • 数据段描述符
    • 代码段描述符
    • 栈段描述符
  • 系统描述符
    • 系统的段描述符
    • 门描述符

由于描述符的S位指定系统描述符(0)还是存储器描述符(1),type字段再继续划分

存储器的段描述符

低32位:

  • 0~15:段界限
  • 16~31:段基地址

高32位:

  • 0~7:段基地址
  • 8~11:TYPE,此下位结尾存储器描述符位
    • X:1为代码段,0为数据段
    • C:是否依从,依从则直接读,否则低特权级也可进入
    • E:段的扩展方向
    • R:可读(1)
    • W:是否科协
    • A:是否已访问
  • 12:S,指定描述符种类
  • 13~14:描述符的特权级位
  • 15:P,段存在位
  • 16~19:段界限
  • 20:AVL,可自由使用的保留位
  • 21:L,长模式,64位代码段标志
  • 22:D/B,指定是按16位操作还是32位操作
  • 23:G,段界限的单位,0-B,1-4K
  • 24~31:段基地址

实际使用的段界限=描述符中的段界限× 0x1000+ OxFFF

开启处理器的第21根地址线

通过设置CR0进入保护模式

CR0:控制寄存器

标志位:

  • 0:PE
     cli                                ;保护模式下中断机制尚未建立,应 
                                        ;禁止中断 
     mov eax,cr0
     or eax,1
     mov cr0,eax                        ;设置PE位

     ;以下进入保护模式... ...
     jmp dword 0x0008:flush             ;16位的描述符选择子:32位偏移
                                        ;清流水线并串行化处理器

描述符高速缓存器

前面说到在32位寄存器中,后16位和16位寄存器的作用相同(段选择器,是描述符的段选择子),前16位是用来描述符高速缓存器,用来记录段的线性基地址、界限和属性。

当第一个访问到段时,把该段的信息放入高速缓存器,当第二次访问的时,直接访问高速缓存器即可

段选择子

  • 0~1:RPL,请求特权级
  • 2:TI,描述符表的指示器(0-GDT,1-LDT)
  • 3~15:描述符索引

指令的格式及其操作尺寸

16位处理器的指令操作尺寸

所谓指令的操作尺寸,是指指令中操作数的长度以及有效地址(偏移地址、偏移量)的长度。

在16位处理器中,操作数的尺寸可以是8位的,也可以是16位的;有效地址的尺寸始终是16位的。

x86的指令格式-操作码和立即数部分

操作码:1到3个字节

立即数:1、2、4个字节

不做概述

bits

指定操作尺寸

[bits 32]

描述符高速缓存器的D位

当第一次请求段描述符的时候,就可以请求到D/B位,高速缓存器的D位也改为D/B位的尺寸,此后处理器都使用这个尺寸

实模式下:jmp 逻辑段地址段内偏移地址

保护模式下:jmp 描述符选择子段内偏移量

存储器的保护

通过别名实现段的共用和共享

     ;创建1#描述符,这是一个数据段,对应0~4GB的线性地址空间
     mov dword [ebx+0x08],0x0000ffff    ;基地址为0,段界限为0xfffff
     mov dword [ebx+0x0c],0x00cf9200    ;粒度为4KB,存储器段描述符 

     ;创建保护模式下初始代码段描述符
     mov dword [ebx+0x10],0x7c0001ff    ;基地址为0x00007c00,512字节 
     mov dword [ebx+0x14],0x00409800    ;粒度为1个字节,代码段描述符 

进入内核执行

jmp far [edi+0x10]

即跳到

 core_entry       dd start          ;核心代码段入口点#10
                  dw core_code_seg_sel

edi指向的是内核加载位置,0x10值得是内核入口点的段内偏移地址

显示文本信息

 mov ebx,message_1
 call sys_routine_seg_sel:put_string

显示处理器品牌信息

     ;显示处理器品牌信息 
     mov eax,0x80000002
     cpuid
     mov [cpu_brand + 0x00],eax
     mov [cpu_brand + 0x04],ebx
     mov [cpu_brand + 0x08],ecx
     mov [cpu_brand + 0x0c],edx

     mov eax,0x80000003
     cpuid
     mov [cpu_brand + 0x10],eax
     mov [cpu_brand + 0x14],ebx
     mov [cpu_brand + 0x18],ecx
     mov [cpu_brand + 0x1c],edx

     mov eax,0x80000004
     cpuid
     mov [cpu_brand + 0x20],eax
     mov [cpu_brand + 0x24],ebx
     mov [cpu_brand + 0x28],ecx
     mov [cpu_brand + 0x2c],edx

     mov ebx,cpu_brnd0
     call sys_routine_seg_sel:put_string
     mov ebx,cpu_brand
     call sys_routine_seg_sel:put_string
     mov ebx,cpu_brnd1
     call sys_routine_seg_sel:put_string

准备加载用户程序

     mov ebx,message_5
     call sys_routine_seg_sel:put_string
     mov esi,50                          ;用户程序位于逻辑50扇区 
     call load_relocate_program;加载内存并创建描述符
    mov ebx,do_status
    call sys_routine_seg_sel:put_string
    mov [esp_pointer],esp               ;临时保存堆栈指针

    mov ds,ax

    jmp far [0x10]                      ;控制权交给用户程序(入口点)
                             ;堆栈可能切换

cmovne

cmp eax, edx;影响zf位
cmovne eax, edx;如果zf位0,则将edx的值传给eax,为1不传送

内存分配的基本策略和方法

allocate_memory:                            ;分配内存
                                            ;输入:ECX=希望分配的字节数
                                            ;输出:ECX=起始线性地址 
         push ds
         push eax
         push ebx
      
         mov eax,core_data_seg_sel
         mov ds,eax
      
         mov eax,[ram_alloc]
         add eax,ecx                        ;下一次分配时的起始地址
      
         ;这里应当有检测可用内存数量的指令
          
         mov ecx,[ram_alloc]                ;返回分配的起始地址

         mov ebx,eax
         and ebx,0xfffffffc
         add ebx,4                          ;强制对齐 
         test eax,0x00000003                ;下次分配的起始地址最好是4字节对齐
         cmovnz eax,ebx                     ;如果没有对齐,则强制对齐 
         mov [ram_alloc],eax                ;下次从该地址分配内存
                                            ;cmovcc指令可以避免控制转移 
         pop ebx
         pop eax
         pop ds

         retf

动态页面分配

逻辑上的分页,并非物理上的分页

传统的段式存储管理中,段内的汇编地址都是根据该段的偏移地址,当在实模式下,对段进行重定位,注意此时是虚拟内存,当需要某些段的某些数据或代码,则该地址通过页部件分配空闲的页进行加载数据或代码

根据线性地址的高20位来决定用到那些页目录项,最后12位为页的偏移地址

     ;准备打开分页机制

     ;创建系统内核的页目录表PDT
     ;页目录表清零 
     mov ecx,1024                       ;1024个目录项
     mov ebx,0x00020000                 ;页目录的物理地址
     xor esi,esi
.b1:
     mov dword [es:ebx+esi],0x00000000  ;页目录表项清零 
     add esi,4
     loop .b1

页表和页目录表

  • 21~31:页表物理基地址
  • 0:P,页表和页目录表是否在内存中
  • 1:RW,页表和页目录表是否可读写
  • 2:US,用户或管理位,特权级位
  • 3:PWT,页级通写位,是否写入高速缓存
  • 4:PCD,页级高速缓存位
  • 5:A,是否被访问
  • 6:D,是否写过数据
  • 7:O(页目录表);PAT(页表项):页属性表支持位
  • 8:G,全局位,该表是否为全局位
  • 9~11:程序是否可使用

由于是先加载内核再加载页,所以内核的段地址直接等于页地址

     ;令CR3寄存器指向页目录,并正式开启页功能 
     mov eax,0x00020000                 ;PCD=PWT=0
     mov cr3,eax

猜你喜欢

转载自blog.csdn.net/qq_48322523/article/details/121055269
今日推荐