x86从实模式到保护模式
上一节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