x86实模式保护模式

加载器 用户程序
两者需要遵从一致的协议
用户程序内部的某个固定位置,包含有对该程序的描述信息
加载器在该固定位置进行读取
这个位置就是用户程序的开头
头部在源程序中以一个段的形式出现,且是程序中第一个被定义的段
该段包含以下信息:
应用程序的入口点,段地址+偏移地址
段重定位表
加载器工作流程
记载器在加载程序之前需要确定两件事:
确定要加载到的内存地址
加载器为用户程序确定内存物理地址
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,16div bxmov 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/OHub (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(GB)
也就是说这种逻辑扇区的组织方式最大能管理128GB的硬盘
但是已经无法满足使用了,还是太小了,现在普遍流行的是LBA48编址方式,可以管理 TB的硬盘
使用LBA28来访问硬盘
从硬盘读取逻辑扇区的步骤如下:
设置起始LBA扇区编号
请求硬盘读
等待读写操作完成(之前未完成的读写操作)
连续读取数据
最后还有一个0x1f1端口,该端口中存储的是硬盘驱动器最后一次执行命令后的状态
过程调用
像上面这种操作硬盘的工作,对于操作系统来说,会很频繁的执行
每次都写这么一堆代码太费事了,不如封装成过程(),又称作例程、子程序等
其实就相当于高级语言中的函数(方法)
处理器通过过程调用指令跳转到这段代码执行,然后通过过程返回指令返回到调用处的下一条指令继续执行
如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img--27)()]
在本例中,过程调用为磁盘读取程序
read_hard_disk_0:;从硬盘读取一个逻辑扇区;输入:DI:SI=起始逻辑扇区号;DS:BX=目标缓冲区地址push axpush bxpush cxpush dxmov dx,0x1f2mov al,1out dx,al;读取的扇区数inc dx;0x1f3mov ax,siout dx,al;LBA地址7~0inc dx;0x1f4mov al,ahout dx,al;LBA地址15~8inc dx;0x1f5mov ax,diout dx,al;LBA地址23~16inc dx;0x1f6mov al,0xe0;LBA28模式,主盘or al,ah;LBA地址27~24out dx,alinc dx;0x1f7mov al,0x20;读命令out dx,al.waits:in al,dxand al,0x88cmp al,0x08jnz .waits;不忙,且硬盘已准备好数据传输 mov cx,256;总共要读取的字数mov dx,0x1f0.readw:in ax,dxmov [bx],axadd bx,2loop .readwpop dxpop cxpop bxpop axret
在过程代码的开始,需要执行一系列压栈操作来保护寄存器的值
只要是在过程中会被修改值的寄存器,都要进行压栈操作,然后在返回前POP
通过DI和SI寄存器来传递参数——逻辑扇区编号
由于逻辑扇区编号为28bit,因此需要两个16bit的通用寄存器
DI中存储高12bit,且需要将DI的高4bit置0,SI存放低16bit
约定将读出的数据存放到由段寄存器DS指向的数据段中,使用BX作为段偏移量,这个就是第二个参数,读缓冲区的地址
xor di,dimov si,app_lba_start;程序在硬盘上的起始逻辑扇区号 xor bx,bx;加载到DS:0x0000处 call read_hard_disk_0
可以看到上面的代码,就是过程被调用的代码,DI和SI代表参数:扇区编号,BX代表缓冲区偏移量
8086支持四中调用方式:
16bit间接绝对近调用 call near
16bit直接绝对远调用 call far
16bit间接绝对远调用 call far
与call相对的就是ret和retf返回指令
ret相对于call near,retf相对于call far
ret指令执行时,处理器会执行POP IP
retf指令执行师,处理器会执行POP IP, POP CS

x86实模式保护模式

文章插图
加载用户程序
用户程序头部示意图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img--27)()]
通过程序长度确定扇区数量
mov dx,[2]mov ax,[0]mov bx,512;512字节每扇区div bx
由于小端序的原因,长度的高16bit存储在高址[2],低16bit存储在低址[0]
然后除以获取扇区个数
除不尽的话,那么AX(商)中存储的值是比实际要读取的扇区数小一的(按块操作,不足512字节也会占用一个扇区)
但是由于我们已经读取了一个扇区(用于读取程序头部),因此我们无需对结果进行任何操作
但是在这种情况下,我们还需要判断AX(商)是否为0(实际长度小于等于一个扇区的大小)
如果为0,则说明程序只占用一个扇区,我们也就无需再读取后续的扇区了
如果除尽了(DX——余数值为0),那么DX中存储的值就正好是扇区数,但是我们需要减一,因为已经读取了一个扇区
由于我们需要DS和BX来指定读取数据存放的缓冲区,我们每次读取一个扇区,也就是
我们需要对段地址进行修改,因为我们每次都会把bx重置为0
因此每读完一个扇区,段地址要增加 = 0x200 bytes
由于是段地址,需要除以16,也就是段地址每次需要增加0x20
mov ax,dsadd ax,0x20;得到下一个以512字节为边界的段地址mov ds,ax
用户程序重定位
这里有一个疑问,根据之前的知识,程序入口地址存储在程序头部的[0x06]和[0x04],不知道这里书上为什么写的是[0x08]和[0x06]
示意图中明显比之前介绍的程序头部多出了一部分
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img--27)()]
从[0x08]和[0x06]获取程序入口地址的高16bit(其中只有低4bit有效)和低16bit
然后使用cs:获取用户程序在内存中的基址,本例中为双字
上面从程序头部获取的双字的高低字分别被存储到了DX和AX中
分别和的高低字进行相加
add ax,[cs:phy_base]; 内存的低址 + 用户程序中定义的程序入口点地址的低址adc dx,[cs:phy_base+0x02]; 内存的高址 + 用户程序中定义的程序入口点地址的高址; 进位加法,避免由于上次加法操作(addax,[cs:phy_base])产生的进位丢失,使用CF获取进位shr ax,4ror dx,4and dx,0xf000or ax,dx
然后后面通过右移4bit ax,并滚动右移4bit dx,再给dx与上来清空除了高4bit的其余bit位,最后对两个寄存器进行或操作拼接出段地址
滚动右(左)移之前已经介绍过:
下面还需要重定向用户程序的所有段地址
这个需要根据段重定位表来完成
; 开始处理段重定位表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还在指向加载器的栈空间
代码:
中断和动态时钟显示
中断向量表
8086
【x86实模式保护模式】256项
每一项都存储了中断编号对应的中断程序的入口地址
每个中断在中断向量表中占
分别是中断处理程序所在的偏移地址和段地址
可屏蔽中断和非屏蔽中断
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img--27)()]
NMI引脚过来的中断都是非屏蔽中断,必须处理,不可忽略
且该中断大概率都是致命错误
可屏蔽中断通过INTR引脚进入处理器内部
处理器每次只能处理一个中断
需要通过一个代理来接受外部设备发出的中断信号,且该代理设备需要对多个同时到达的中断信号进行仲裁,决定谁可以将中断信号发送给CPU
这个代理设备就是8259芯片——中断控制器
在Intel允许的256个中断中,8259负责提供其中的15个,但并不是固定的15个,8259可以进行编程来改变中断号
8259分为主从两片
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img--28)()]
不知道为啥就形成了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就是,只有当该标志为1的时候CPU才会处理中断
x86实模式保护模式

文章插图
可以使用cli和sti指令来清除和置位IF
中断的优先级和引脚相关,主片的IR0引脚是最高的优先级,从片也如此
如果在8259处理一个中断的时候,来了一个优先级更高的中断,那么当前中断的处理会被打断,这个叫做中断嵌套
实模式下的中断向量表
中断向量表起始于,终止于 1KB
处理器在处理中断的时候,需要做以下几件事:
执行中断处理程序返回到断点继续执行
NMI(非屏蔽中断),对于这种类型的中断,CPU不需要获取中断号,会自动生成中断号2
中断向量表是由BIOS进行维护的
但是在一开始,BIOS会将所有的中断向量入口程序地址设置为同一个值,且该中断处理程序只有一个iret指令
在计算机启动后,操作系统和用户程序会根据自己的需要来改写中断向量表中的入口地址
实时时钟、CMOS RAM和BCD编码
CMOS——互补金属氧化物
CMOS RAM即由互补金属氧化物材料组成的静态存储器
实时时钟负责计时,日期和时间的数值被存储在这块存储器中
RTC芯片由一个振荡频率为32.的石英晶体振荡器驱动(不知道是啥玩意儿)
可以通过端口访问CMOS RAM
0x70 0x74 是索引端口,和前面介绍的光标寄存器一样
用于指定CMOS RAM内的单元
0x71 0x75是数据端口,用于读写相应单元里的内容
mov al, 0x06out 0x70, alin al, 0x71
上面这段代码可以用于读取今天是星期几
CMOS RAM中的时间信息
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img--28)()]
另外需要注意一点的是,索引端口0x70的值的最高bit位用于决定NMI是否阻断
第7个bit位为1时,则阻断NMI(不可屏蔽中断)信号,为0时,则允许NMI中断到达处理器
剩余的7bit(0~6)才是真正用于指定CMOS RAM单元的索引号
BCD—— Coded
BCD编码会将一个字节分为高4bit和低4bit,分别用于表示一个十进制数的十位和个位
比如25-> 10进制最大值为9,因此任何一个4bit的值超过了1001,都是非法的
在CMOS RAM中的0x0a~0x0d这四个单元,并不是普通的单元,而是4个寄存器的索引号
也是通过0x70和0x71这两个端口进行访问
这四个寄存器用于设置实时时钟电路的参数和工作状态
寄存器A的功能说明
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img--29)()]
寄存器B的功能说明
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img--29)()]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img--29)()]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3xMbB-29)()]
可以使用.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寄存器Bor al,0x80;阻断NMI out 0x70,almov al,0x12;设置寄存器B,禁止周期性中断,开放更 out 0x71,al;新结束后中断,BCD码,24小时制
对照寄存器B的功能表格
0x12 ->
即第1和第4bit位置位,表示每个更新周期结束时产生一个中断,使用24小时制
通过将0x70端口的值的最高位置1(or al, 0x80),阻断NMI中断 。。。。(对照表格)
可以通过读取寄存器C的内容来获取中断发生的原因
如果在中断之后没有读取C寄存器的内容,那么同样的中断将不再产生
mov al,0x0cout 0x70,alin 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--30)()]
高速缓存
内存和CPU响应速度的差异
高速缓存是处理器与内存之间的一个静态存储器(造价高)静态存储器SRAM,响应速度位纳秒级
为了实现流水线技术,需要将指令拆分成更小的可独立执行部分——微操作
当遇上跳转指令的时候,流水线工作模式下的后续指令将全部失效,需要flush流水线
另外,对于分支结构,流水线模型也不能很好的适应