16位模式/32位模式下PUSH指令探究

最后更新于:2022-04-01 16:22:34

# 一、Intel 32 位处理器的工作模式 [![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fd6650b.jpg)](http://img.blog.csdn.net/20160128194403051) 如上图所示,Intel 32 位处理器有3种工作模式。 (1)实模式:工作方式相当于一个8086 (2)保护模式:提供支持多任务环境的工作方式,建立保护机制 (3)虚拟8086模式:这种方式可以使用户在保护模式下运行8086程序(比如cmd打开的console窗口,就是工作在虚拟8086模式) **有几点需要特别说明:** (1)保护模式可分为16位和32位的,由段描述符中的D标志指明。对于32位代码段和数据段,这个标志总是设为1;对于16位代码和数据段,这个标志被设置为0. D=1:默认使用32位地址和32位或8位的操作数。 D=0:默认使用16位地址和16位或8位的操作数。(主要是为了能够在32位处理器上运行16位保护模式的程序) 指令前缀0x66用来选择非默认值得操作数大小,0x67用来选择非默认值的地址大小。 (2)在实模式下,也可以使用32位的寄存器,比如 ~~~ mov eax,ecx mov ebx,0x12345678 ~~~ (3)在书中,把实模式和16位的保护模式统称为“16位模式”;把32位保护模式称为“32位模式”。我的博文也沿用这种叫法。 (4)32位处理器可以执行16位的程序,包括实模式和16位保护模式。 (5)当处理器在16位模式下运行时,可以使用32位的寄存器,执行32位运算。 (6)在16位模式下,数据的大小是8位或者16位的;控制转移和内存访问时,偏移量也是16位的。 (7)32位保护模式兼容80286的16位保护模式。 (8)在16位模式下,处理器把所有指令都看成是16位的。 结合(5)和(8),我们发现一个问题:当处理器运行16位模式下,既然把所有指令都看成16位的,那么怎么使用32位的寄存器,执行32位的运算呢?答案是利用指令前缀0x66和0x67.前面已经说过,**指令前缀0x66用来选择非默认值的操作数大小,0x67用来选择非默认值的地址大小。** 比如说,指令码0x40在16位模式下对应的指令是 ~~~ inc ax ~~~ 如果加上前缀0x66,也就是指令码66 40,当处理器在16位模式下运行,66 40对应的指令是 ~~~ inc eax ~~~ 同理,如果处理器运行在32位模式下,处理器认为指令是32位的,如果加了0x66,那么就表示指令的操作数是16位的。 在编写程序的时候,我们应该考虑指令的运行环境。为了指令默认的运行环境,NASM提供了伪指令bits,用于指明其后的指令是被编译成16位的还是32位的。比如: ~~~ [bits 16] mov cx,dx ;89 D1 mov eax,ebx ;66 89 D8 [bits 32] mov cx,dx ;66 89 D1 mov eax,ebx ;89 D8 ~~~ 注意,[bits 16]和[bits 32]的方括号是可以省略的。 # 二、PUSH指令探究 由于32位的处理器都拥有32位的寄存器和算术逻辑部件,而且同内存芯片之间的数据通路至少是32位的,因此,所有以寄存器或者内存单元为操作数的指令都被扩充,以适应32位的算术逻辑操作。而且,**这些扩展的操作即使是在16位模式下(实模式和16位保护模式)也是可用的。** 我在博文 [32位x86处理器编程导入——《x86汇编语言:从实模式到保护模式》读书笔记08](http://blog.csdn.net/longintchar/article/details/50486017) 中已经总结了一般指令的扩展,在这里,我仅对PUSH指令进行实验和总结。 实验目的就是测试在3种模式下,PUSH指令的工作行为(比如SP或ESP到底怎么变化,压入的数到底是多少)。所以,我列了一个单子,把所有能想到的形式都列出来了,其中有的我也不确定(或许这样写编译都会报错)。不管那么多,先写出来,然后让编译器筛选吧![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f8abd34.jpg) ~~~ 1 ;测试各种push 2 3 ;操作数是立即数,分为一字节、两字节、四字节 4 push 0x80 5 push byte 0x80 6 7 push 0x8000 8 push word 0x8000 9 10 push 0x87654321 11 push dword 0x87654321 12 13 ;操作数是寄存器,分为16位寄存器和32位寄存器 14 mov eax,0x86421357 15 push ax 16 push eax 17 18 ;操作数是内存单元,分为一字节、两字节、四字节 19 push [data] 20 push byte [data] 21 push word [data] 22 push dword [data] ~~~ 是不是有的写法明显就不对呢? 首先,第20行,肯定不对。因为如果是内存操作数的话,不能用byte修饰。剩下来的错误,我会在后文揭晓答案。 ### 1.在实模式下的实验 ### (1)实验代码 ~~~ 1 ;PUSH 指令实验 2 3 jmp near start 4 5 data db 0x12,0x34,0x56,0x78 6 message db 'Hello,PUSH!' 7 8 start: 9 mov ax,0x7c0 ;设置数据段的段基地址 10 mov ds,ax 11 12 mov ax,0xb800 ;设置附加段基址到显示缓冲区 13 mov es,ax 14 15 ;以下显示字符串 16 mov si,message 17 mov di,0 18 mov cx,start-message 19 @g: 20 mov al,[si] 21 mov [es:di],al 22 inc di 23 mov byte [es:di],0x02 24 inc di 25 inc si 26 loop @g 27 28 ;测试各种push 29 push 0x80 30 push byte 0x80 31 32 push 0x8000 33 push word 0x8000 34 35 push 0x87654321 36 push dword 0x87654321 37 38 mov eax,0x86421357 39 push ax 40 push eax 41 42 ;push [data] 43 push word [data] 44 push dword [data] 45 46 push ds 47 push gs 48 49 jmp near $ 50 51 52 times 510-($-$$) db 0 53 db 0x55,0xaa ~~~ 这段代码不是用的配书代码,是我自己写的。 第5行,定义了4字节的数据,这是为了后面验证“push + 内存操作数”这一情况。 第6行,定义了一个字符串,要把它显示在屏幕上。这样做是为了调试方便,让我们知道我们的程序已经RUN了。 第29行到47行,测试各种push,我会利用Bochs的调试功能,跟踪每条Push的执行情况,把结果总结出来。 好的,我们开始编译吧。 对于30行,有个警告: ~~~ push byte 0x80 ;warning: signed byte value exceeds bounds ~~~ 既然是警告,那么30行不必去掉。相反我们更加好奇了,看看执行时会发生什么。 对于35行,还是一个警告: ~~~ push 0x87654321 ;warning: word data exceeds bounds ~~~ 对于42行,呵呵,就是一个错误了。 ~~~ push [data] ; error: operation size not specified ~~~ 好吧,看来这样不指定操作数的大小是不行的,所以我们把42行注释掉。 然后再编译,好的,可以了。 调试的过程就是不断用n命令,反复用print-stack命令,还有reg命令等,仔细观察栈的变化和SP的变化。(此处省略2000字) ### (2)实验报告 小二,上实验报告! [![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fd98f9e.jpg)](http://img.blog.csdn.net/20160127220223687) 通过上面的实验,我们可以知道,如果CPU运行在实模式,如果用NASM编译,push指令可以这么用: [![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fdcdb5d.jpg)](http://img.blog.csdn.net/20160127220230578) ### 2.在16位保护模式下的实验 ### (1)关于16位保护模式 请参考我的博文 [关于80286——《x86汇编语言:从实模式到保护模式》读书笔记15](http://blog.csdn.net/longintchar/article/details/50575391) ### (2)实验代码 实验代码由配书代码(代码清单11-1 (文件名:c11_mbr.asm))修改而成。目的就是我们要从实模式进入16位的保护模式,然后测试16位保护模式下PUSH指令的行为。 ~~~ 1 ;test push (16位保护模式下) 2 3 ;设置堆栈段和栈指针 4 mov ax,cs 5 mov ss,ax 6 mov sp,0x7c00 7 8 ;计算GDT所在的逻辑段地址 9 mov ax,[cs:gdt_base+0x7c00] ;低16位 10 mov dx,[cs:gdt_base+0x7c00+0x02] ;高16位 11 mov bx,16 12 div bx 13 mov ds,ax ;令DS指向该段以进行操作 14 mov bx,dx ;段内起始偏移地址 15 16 ;创建0#描述符,它是空描述符,这是处理器的要求 17 mov dword [bx+0x00],0x00 18 mov dword [bx+0x04],0x00 19 20 ;创建#1描述符,保护模式下的代码段描述符 21 mov dword [bx+0x08],0x7c0001ff 22 mov dword [bx+0x0c],0x00009800 23 24 ;创建#2描述符,保护模式下的数据段描述符(文本模式下的显示缓冲区) 25 mov dword [bx+0x10],0x8000ffff 26 mov dword [bx+0x14],0x0000920b 27 28 ;创建#3描述符,保护模式下的堆栈段描述符 29 mov dword [bx+0x18],0x00007a00 30 mov dword [bx+0x1c],0x00009600 31 32 ;初始化描述符表寄存器GDTR 33 mov word [cs: gdt_size+0x7c00],31 ;描述符表的界限(总字节数减一) 34 35 lgdt [cs: gdt_size+0x7c00] 36 37 in al,0x92 ;南桥芯片内的端口 38 or al,0000_0010B 39 out 0x92,al ;打开A20 40 41 cli ;保护模式下中断机制尚未建立,应 42 ;禁止中断 43 mov eax,cr0 44 or eax,1 45 mov cr0,eax ;设置PE位 46 47 ;以下进入保护模式... ... 48 jmp 0x0008:flush ;描述符选择子:16位偏移 49 ;清流水线并串行化处理器 50 51 52 flush: 53 mov cx,00000000000_10_000B ;加载数据段选择子(0x10) 54 mov ds,cx 55 56 ;以下在屏幕上显示"ABCDEFGHIJK" 57 mov byte [0x00],'A' 58 mov byte [0x02],'B' 59 mov byte [0x04],'C' 60 mov byte [0x06],'D' 61 mov byte [0x08],'E' 62 mov byte [0x0a],'F' 63 mov byte [0x0c],'G' 64 mov byte [0x0e],'H' 65 mov byte [0x10],'I' 66 mov byte [0x12],'J' 67 mov byte [0x14],'K' 68 69 70 ;测试push 71 mov cx,00000000000_11_000B ;加载堆栈段选择子 72 mov ss,cx 73 mov sp,0x7c00 74 75 76 push 0x80 77 push byte 0x80 ; warning: signed byte value exceeds bounds 78 79 push 0x8000 80 push word 0x8000 81 82 push 0x87654321 83 ;warning: word data exceeds bounds 84 push dword 0x87654321 85 86 87 mov eax,0x86421357 88 push ax 89 push eax 90 91 ;push [0x00]error: operation size not specified 92 push byte [0x00] 93 push word [0x00] 94 95 push dword [0x00] 96 97 push ds 98 push gs 99 push es 100 push cs 101 102 ghalt: 103 hlt ;已经禁止中断,将不会被唤醒 104 105;------------------------------------------------------------------------------- 106 107 gdt_size dw 0 108 gdt_base dd 0x00007e00 ;GDT的物理地址 109 110 times 510-($-$$) db 0 111 db 0x55,0xaa ~~~ 对比32位保护模式的代码,就会发现16位保护模式的代码略有不同。 首先,比如说22行,段描述符的定义是 ~~~ 22 mov dword [bx+0x0c],0x00009800 ~~~ 因为80286中,段描述符的格式是 [![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fdee677.jpg)](http://img.blog.csdn.net/20160127220235237) 所以,高4字节的16~32位全部为0. 其次, ~~~ 47 ;以下进入保护模式... ... 48 jmp 0x0008:flush ;描述符选择子:16位偏移 49 ;清流水线并串行化处理器 ~~~ 这里,没有加伪指令[bits 32],而且,偏移flush没有用dword修饰。因为操作数和偏移是16位的。 好了,代码就说到这里,我们看实验报告吧。 ### (3)实验报告 [![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fe114df.jpg)](http://img.blog.csdn.net/20160127220238706) 通过和实模式的对比,可以发现,除了9、10两行中的指令码的偏移不一样(这和数据存放的位置有关系,和PUSH没有关系),PUSH指令的行为是惊人的相同。所以我们可以得出结论,16位保护模式下,PUSH的用法和实模式是一样的。我想,这也是在原书中,作者把实模式和16位的保护模式统称为“16位模式”,把32位保护模式称为“32位模式”的原因吧。 ### 3.在32位保护模式下的实验 ### (1)实验代码 实验代码由配书代码(代码清单11-1 (文件名:c11_mbr.asm))修改而成。 ~~~ 1 ;test push (32位保护模式) 2 3 ;设置堆栈段和栈指针 4 mov ax,cs 5 mov ss,ax 6 mov sp,0x7c00 7 8 ;计算GDT所在的逻辑段地址 9 mov ax,[cs:gdt_base+0x7c00] ;低16位 10 mov dx,[cs:gdt_base+0x7c00+0x02] ;高16位 11 mov bx,16 12 div bx 13 mov ds,ax ;令DS指向该段以进行操作 14 mov bx,dx ;段内起始偏移地址 15 16 ;创建0#描述符,它是空描述符,这是处理器的要求 17 mov dword [bx+0x00],0x00 18 mov dword [bx+0x04],0x00 19 20 ;创建#1描述符,保护模式下的代码段描述符 21 mov dword [bx+0x08],0x7c0001ff 22 mov dword [bx+0x0c],0x00409800 23 24 ;创建#2描述符,保护模式下的数据段描述符(文本模式下的显示缓冲区) 25 mov dword [bx+0x10],0x8000ffff 26 mov dword [bx+0x14],0x0040920b 27 28 ;创建#3描述符,保护模式下的堆栈段描述符 29 mov dword [bx+0x18],0x00007a00 30 mov dword [bx+0x1c],0x00409600 31 32 ;初始化描述符表寄存器GDTR 33 mov word [cs: gdt_size+0x7c00],31 ;描述符表的界限(总字节数减一) 34 35 lgdt [cs: gdt_size+0x7c00] 36 37 in al,0x92 ;南桥芯片内的端口 38 or al,0000_0010B 39 out 0x92,al ;打开A20 40 41 cli ;保护模式下中断机制尚未建立,应 42 ;禁止中断 43 mov eax,cr0 44 or eax,1 45 mov cr0,eax ;设置PE位 46 47 ;以下进入保护模式... ... 48 jmp dword 0x0008:flush ;16位的描述符选择子:32位偏移 49 ;清流水线并串行化处理器 50 [bits 32] 51 52 flush: 53 mov cx,00000000000_10_000B ;加载数据段选择子(0x10) 54 mov ds,cx 55 56 ;以下在屏幕上显示"ABCDEFGHIJK" 57 mov byte [0x00],'A' 58 mov byte [0x02],'B' 59 mov byte [0x04],'C' 60 mov byte [0x06],'D' 61 mov byte [0x08],'E' 62 mov byte [0x0a],'F' 63 mov byte [0x0c],'G' 64 mov byte [0x0e],'H' 65 mov byte [0x10],'I' 66 mov byte [0x12],'J' 67 mov byte [0x14],'K' 68 69 70 ;测试push 71 mov cx,00000000000_11_000B ;加载堆栈段选择子 72 mov ss,cx 73 mov esp,0x7c00 74 75 76 push 0x80 77 push byte 0x80 ;warning: signed byte value exceeds bounds 78 79 push 0x8000 80 push word 0x8000 81 82 push 0x87654321 83 push dword 0x87654321 84 85 mov eax,0x86421357 86 push ax 87 push eax 88 89 90 push word [0x00] 91 push dword [0x00] 92 93 push ds 94 push gs 95 push es 96 push cs 97 98 ghalt: 99 hlt ;已经禁止中断,将不会被唤醒 100 101;------------------------------------------------------------------------------- 102 103 gdt_size dw 0 104 gdt_base dd 0x00007e00 ;GDT的物理地址 105 106 times 510-($-$$) db 0 107 db 0x55,0xaa ~~~ 如果对上面的代码不熟悉的话,可以参考我的博文 [进入保护模式(一)——《x86汇编语言:从实模式到保护模式》读书笔记12](http://blog.csdn.net/longintchar/article/details/50513772) 等文章。 ### (2)实验报告 [![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fe2d5d7.jpg)](http://img.blog.csdn.net/20160128194409244) 根据测试报告,我们可以归纳出32位保护模式下,针对NASM编译器的push指令用法: [![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fe539bf.jpg)](http://img.blog.csdn.net/20160128194413598) (完)
';

关于80286

最后更新于:2022-04-01 16:22:32

# 一、80286的工作模式 80286首次提出了实模式和保护模式的概念。  实模式:和8086的工作方式相同;  保护模式:提供了存储器管理机制和保护机制,支持多任务。 # 二、80286的寄存器 ### (一)通用寄存器 80286的通用寄存器和8086一样,有AX,BX,CX,DX,BP,SP,SI,DI; ### (二)IP 80286的指令指针寄存器也和8086一样,没有什么特别的。 ### (三)标志寄存器 80286的标志寄存器同8086相比,增加了IOPL(特权级)和NT(子任务嵌套),这两个字段只在保护模式中使用。 ### (四)MSW MSW(机器状态字)是80286新设置的16位寄存器,在80386及其后续的CPU中,已经把MSW扩展为4个32位的控制寄存器CR0、CR1、CR2和CR3,原来的MSW功能由CR0的低16位来实现。  MSW的位0是启用保护模式(Protection Enable)标志,也就是我们常说的PE位。PE=1时,开启保护模式;PE=0时表示工作在实模式。 ### (五)段寄存器 8086中的段寄存器,在808286中称为段选择子或者段选择器。当进行逻辑地址到实地址的变换时,用它来选择描述符表中的一个描述符。段选择子的格式和含义同32位处理器的段选择子相同,这里不再赘述。 ### (六)段描述符 [![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fd4cb4e.jpg)](http://img.blog.csdn.net/20160124205743666) **字段说明** P位、DPL位、A位同32位处理器的段描述符含义相同。 【S】S=1代表代码段或者数据段描述符;S=0代表非段描述符; 【E】相当于32位处理器描述符的X位。E=1表示代码段描述符;E=0表示数据段描述符; 对于代码段描述符: 【C】相当于32位处理器段描述符的C(一致性)位。 【R】相当于32位处理器段描述符的R位。R=1时可读,可执行;R=0时仅执行; 对于数据段描述符: 【ED】相当于32位处理器段描述符的E(扩展方向)位。 【W】相当于32位处理器段描述符的W(是否可写)位。 ### (七)存储器寻址 物理地址由段描述符中的24位的段基址和16位的偏移量相加而成。
';

进入保护模式(二)

最后更新于:2022-04-01 16:22:29

首先来段题外话:之前我发现我贴出的代码都没有行号,给讲解带来不便。所以从现在起,我要给代码加上行号。我写博客用的这个插入代码的插件,确实不支持自动插入行号。我真的没有找到什么好方法,无奈之下,只能按照网友的说法,在VIM中给每行代码加上行号,然后再贴出来。 在VIM中每一行都添加上行号的方法是: :%s/^/\=line(".")/ 对,只要执行这个命令就可以了。至于为什么这样写,可以参考我的另一篇博文 《在VIM中添加行号的方法》[http://blog.csdn.net/longintchar/article/details/50569851](http://blog.csdn.net/longintchar/article/details/50569851)                     我们接着上篇博文 [进入保护模式(一)——《x86汇编语言:从实模式到保护模式》读书笔记12](http://blog.csdn.net/longintchar/article/details/50513772) 说。   ### (五)设置PE位 ~~~ 44 cli ;保护模式下中断机制尚未建立,应 45 ;禁止中断 46 mov eax,cr0 47 or eax,1 48 mov cr0,eax ;设置PE位 ~~~ 第44行,用于关中断。因为保护模式下的中断和实模式不同,所以原来的中断向量表不再适用,BIOS中断也不能再用,因为它们都是实模式下的代码。在重新配置保护模式下的中断环境之前,我们必须关中断。 CR0是处理器内部的一个控制寄存器,也是32位的(如下图,图片来自赵炯的《Linux内核完全剖析》)。 它的bit0是保护模式允许位(Protection Enable,PE)。当PE=1时,则处理器进入保护模式。 [![cr0](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fd0c09d.jpg "cr0")](http://img.blog.csdn.net/20160116204449463) 第46~48用于设置CR0的bit0为1. ### (六)关于段寄存器 我们知道,32位模式下,段寄存器有CS,DS,ES,SS,FS,GS. 这些段寄存器每个都分为2个部分,一个是16位的可见部分,一个是隐藏部分,称为描述符高速缓存器,用来存放段的线性基地址、段界限和段属性。如下图: [![New0002段寄存器的格式](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fd1fd04.jpg "New0002段寄存器的格式")](http://img.blog.csdn.net/20160116204457964) ### 1.实模式下的内存访问 在32位处理器上的实模式下,假如执行下面的代码。 ~~~ mov cx,0x2000 mov ds,cx mov [0xc0],al ~~~ CPU在把0x2000传送到DS的同时,还会把0x2000左移4位(0x20000),传送到DS描述符高速缓存寄存器(段基地址部分仅低20位有效,高12位全部是0)。此后,只要不改变DS的内容,那么每次访问内存都直接使用DS描述符高速缓存寄存器的内容作为段地址。 ### 2.保护模式下的内存访问 在保护模式下,实模式的6个段寄存器叫做“段选择器”。尽管在访问内存的时候也要指定一个段,但是传送到段选择器的内容不是逻辑段地址,而是**段选择子(也叫段选择符)**。 如下图(图片来自赵炯的《Linux内核完全剖析》)所示,段选择子由三部分组成。 - 请求特权级RPL(Requested Privilege Level):提供了段保护信息,我们以后会学习。现在只需设置为00即可。 - 表指示标志TI(Table Index):TI=0时,表示描述符在GDT中;TI=1时,表示描述符在LDT(我们以后会学习)中。 - 索引值(Index):描述符在GDT或者LDT中的索引项号。 [![段选择子](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fd3592b.jpg "段选择子")](http://img.blog.csdn.net/20160116204500249) 为了说明保护模式下的内存访问,我们回到代码。 ~~~ 56 mov cx,00000000000_10_000B ;加载数据段选择子(0x10) 57 mov ds,cx 58 59 ;以下在屏幕上显示"Protect mode OK." 60 mov byte [0x00],'P' 61 mov byte [0x02],'r' 62 mov byte [0x04],'o' 63 mov byte [0x06],'t' 64 mov byte [0x08],'e' 65 mov byte [0x0a],'c' 66 mov byte [0x0c],'t' ~~~ 第56、57行,将段选择子10000b传到段选择器DS中,从段选择子可以看出,RPL=0;TI=0(表示GDT);索引号为2; 当处理器执行任何改变段选择器的指令时(比如mov、jmp far、call far、iret、retf等),就将指令中提供的索引号*8作为偏移地址,同GDTR寄存器中的线性基地址相加,然后访问GDT。如果没有什么问题(比如超过了GDT的界限),就把找到的描述符加载到不可见的描述符高速缓存寄存器。此后,每当有访问内存的指令时,就不再访问GDT中的描述符,而是直接使用段寄存器的描述符高速缓存寄存器。 结合代码来说,第57行,处理器把2*8(=16)作为偏移地址,同GDTR的内容(内容为0x00007e00)相加,得到0x0000_7e16,根据这个地址找到描述符(就是我们之前创建的#2描述符) ~~~ 27 ;创建#2描述符,保护模式下的数据段描述符(文本模式下的显示缓冲区) 28 mov dword [bx+0x10],0x8000ffff 29 mov dword [bx+0x14],0x0040920b ~~~ 然后,把这个描述符加载到高速缓存寄存器(包括线性基地址0x000b8000,段界限,段属性)。 第60行,执行这条指令时,处理器用DS描述符高速缓存寄存器中的线性基地址(0x000b8000,文本模式的显存起始地址)加上指令中的偏移量0x00,形成32位的物理地址0x000b8000,并将字符‘P’写入该处。 不仅仅是访问数据段,处理器访问代码段取指令的时候,也是采用相同的方法。假设CS描述符高速缓存寄存器已经装载了正确的32位线性基地址,那么处理器取指令的时候,会使用CS描述符高速缓存寄存器中的32位线性基地址加上EIP中的偏移量,构成32位的物理地址,根据这个物理地址从内存中取得指令。 ### (七)清空流水线并串行化处理器 正如前文所述,即使在实模式下,段寄存器的高速缓存寄存器也被用于访问内存。当处理器进入保护模式后,高速缓存寄存器的内容依然残留,但是这些内容在保护模式下是无效的。因此,比较安全的做法是尽快刷新段选择器,包括描述符高速缓存寄存器。 另外,在进入保护模式之前,很多指令已经进入了流水线。因为处理器工作在实模式下,所以它们都是按照16位操作数和地址长度进行译码的,即使是那些用bits32编译的指令,为了防止执行结果不正确,所以必须清空流水线。还用,那些通过乱序执行得到的中间结果也是无效的,所以必须清理掉,让处理器串化执行。 为了达到上述目的,我们可以采用远转移指令jmp或者远过程调用指令call。遇到这类指令,处理器一般会清空流水线并且串化执行;另一方面,远转移会重新加载CS,并刷新描述符高速缓存寄存器的内容。所以,强烈建议在设置了PE位后,立刻用jmp或者call转移到当前指令流的下一条指令上。 于是代码中有: ~~~ 50 ;以下进入保护模式... ... 51 jmp dword 0x0008:flush ;16位的描述符选择子:32位偏移 52 ;清流水线并串行化处理器 53 [bits 32] 54 55 flush: 56 mov cx,00000000000_10_000B ;加载数据段选择子(0x10) 57 mov ds,cx ~~~ 第51行,是一条远转移指令。如果你忘记了jmp的用法,没有关系,可以参考我的另一篇博文[8086处理器的无条件转移指令——《x86汇编语言:从实模式到保护模式》读书笔记13](http://blog.csdn.net/longintchar/article/details/50529164)。 这条指令和位于它前面的指令一样,是默认用[bits 16]编译的。但是因为使用了关键字dword(注意:这里的dword是修饰偏移地址flush的),所以编译后的偏移地址是32位的。 如果51行这样写: ~~~ 51 jmp 0x0008:flush ;16位的描述符选择子:16位偏移 ~~~ 这样写是不严谨的。因为这样编译出来的目标地址是16位的。如果flush代表的地址是0x12345678,那么编译后会被截断成为0x5678,这显然是错的。所以这个跳转一定要加dword. 注意:因为设置了PE位,所以现在已经处于保护模式下了。所以处理器会把第一个操作数(0x0008)理解为段选择子,而不是是模式下的逻辑段基址。当51行的指令执行时,处理器会把选择子0x0008(索引号为1,TI=0,RPL=00)加载到CS,并把#1描述符(定义了一个代码段,基地址是0x7c00,段界限是0x1ff,长度为0x200)加载到CS描述符高速缓存寄存器中。所以程序会转移到基地址为0x0000_7c00的代码段内的某个位置执行。这个位置取决于偏移地址。偏移地址就是标号flush的汇编地址(因为指定了dword,所以编译后是32位的),处理器会用这个32位的数值来代替EIP的原有内容。于是,程序就转移到flush处了。 第53行,使用了伪指令[bits 32],从这以后,指令是按照32位编译的。因为指令执行到这里的时候,已经真真正正地进入了保护模式了。 ### (八)进入保护模式的主要步骤 我们总结一下进入保护模式的主要步骤: 1.安装段描述符,构造GDT 2.用lgdt指令加载GDTR 3.打开A20 4.设置CR0的PE位为1 5.跳转,真正进入保护保护模式。 ### (九)在屏幕上显示字符 ~~~ 55 flush: 56 mov cx,00000000000_10_000B ;加载数据段选择子(0x10) 57 mov ds,cx 58 59 ;以下在屏幕上显示"Protect mode OK." 60 mov byte [0x00],'P' 61 mov byte [0x02],'r' 62 mov byte [0x04],'o' 63 mov byte [0x06],'t' 64 mov byte [0x08],'e' 65 mov byte [0x0a],'c' 66 mov byte [0x0c],'t' 67 mov byte [0x0e],' ' 68 mov byte [0x10],'m' 69 mov byte [0x12],'o' 70 mov byte [0x14],'d' 71 mov byte [0x16],'e' 72 mov byte [0x18],' ' 73 mov byte [0x1a],'O' 74 mov byte [0x1c],'K' ~~~ 56、57行,前文已经说过,令DS指向文本模式的显示缓冲区。 60~74行,就是在屏幕左上角显示"Protect mode OK." 需要说明的是:不管是实模式还是保护模式,外围设备是不受影响的。 最后注意一点: 保护模式下,不允许使用mov指令改变段寄存器CS的内容。比如 mov cs,ax 这样写是不对的。这样做会导致处理器产生一个无效操作码的异常中断。
';

8086处理器的无条件转移指令

最后更新于:2022-04-01 16:22:27

本博文是对原书8.3.10的内容的总结。 ### 一、相对短转移 指令格式是: ~~~ jmp short 标号 ~~~ 标号也可以替换成具体的数值(标号和数值是等价的),例如 ~~~ jmp short 0x2000 ~~~ 说明: (1)该指令属于段内转移指令,而且只允许转移到距离当前指令-128~127字节的地方。 (2)指令的功能是:(IP) = (IP)+8位位移量(范围是-127~128,用补码表示) (3)short指明此处的位移为8位 (4)8位位移=标号处的地址-jmp指令后的第一个字节的地址;8位位移由编译器在编译时算出; ### 二、16位相对近转移 指令格式是: ~~~ jmp near 标号 ~~~ 同理,标号也可以是具体的数值,如 ~~~ jmp near 0x3200 ~~~ 说明: (1)该指令属于段内转移,转移范围是-32768~32767 (2)指令的功能是:(IP)= (IP)+16位位移量(范围是-32768~32767,用补码表示) (3)near指明此处的位移为16位 (4)16位位移=标号处的地址-jmp指令后的第一个字节的地址;16位位移由编译器在编译时算出;   注意:如果没有指定关键字是near或者short,那么NASM编译器会根据目标位置距离当前指令的位移量自动选择near(范围在-127~128内)或者short(范围在-127~128外,但是在-32768~32767内)。   ### 三、16位间接绝对近转移 指令格式为: ~~~ jmp (near) r16/m16 ~~~ 说明: (1)这种转移也是段内转移,但是转移的目标不是在指令中直接给出,而是由一个16位的通用寄存器或者内存地址间接给出 (2)near关键字可以省略 (3)执行时,处理器将用16位通用寄存器的值或内存中的那个字取代IP寄存器的内容。 ### 四、16位直接绝对远转移 指令格式为: ~~~ jmp 段地址:偏移地址 ~~~ 说明: (1)属于段间转移 (2)执行时,处理器用段地址的内容取代CS的值,用偏移地址(也可以是标号)取代IP的值 ### 五、16位直接绝对远转移 指令格式为: ~~~ jmp far m32 ~~~ 说明: (1)关键字far是必须的 (2)操作数是一个内存地址,内存地址处存放着2个字,低字是偏移地址,高字是段地址 (3)执行时,处理器根据内存地址找到偏移地址和段地址,分别用来代替IP和CS的内容 (完)
';

进入保护模式(一)

最后更新于:2022-04-01 16:22:25

之前已经做了一些理论上的铺垫,这次我们就可以看代码了。 # 一、代码清单 ~~~ ;代码清单11-1 ;文件名:c11_mbr.asm ;文件说明:硬盘主引导扇区代码 ;创建日期:2011-5-16 19:54 ;设置堆栈段和栈指针 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] in al,0x92 ;南桥芯片内的端口 or al,0000_0010B out 0x92,al ;打开A20 cli ;保护模式下中断机制尚未建立,应 ;禁止中断 mov eax,cr0 or eax,1 mov cr0,eax ;设置PE位 ;以下进入保护模式... ... jmp dword 0x0008:flush ;16位的描述符选择子:32位偏移 ;清流水线并串行化处理器 [bits 32] flush: mov cx,00000000000_10_000B ;加载数据段选择子(0x10) mov ds,cx ;以下在屏幕上显示"Protect mode OK." mov byte [0x00],'P' mov byte [0x02],'r' mov byte [0x04],'o' mov byte [0x06],'t' mov byte [0x08],'e' mov byte [0x0a],'c' mov byte [0x0c],'t' mov byte [0x0e],' ' mov byte [0x10],'m' mov byte [0x12],'o' mov byte [0x14],'d' mov byte [0x16],'e' mov byte [0x18],' ' mov byte [0x1a],'O' mov byte [0x1c],'K' ;以下用简单的示例来帮助阐述32位保护模式下的堆栈操作 mov cx,00000000000_11_000B ;加载堆栈段选择子 mov ss,cx mov esp,0x7c00 mov ebp,esp ;保存堆栈指针 push byte '.' ;压入立即数(字节) sub ebp,4 cmp ebp,esp ;判断压入立即数时,ESP是否减4 jnz ghalt pop eax mov [0x1e],al ;显示句点 ghalt: hlt ;已经禁止中断,将不会被唤醒 ;------------------------------------------------------------------------------- gdt_size dw 0 gdt_base dd 0x00007e00 ;GDT的物理地址 times 510-($-$$) db 0 db 0x55,0xaa ~~~ 上面就是配书源码。我们一点一点看。 # 二、源码分析 ### (一)设置堆栈和栈指针 ~~~ ;设置堆栈段和栈指针 mov ax,cs mov ss,ax mov sp,0x7c00 ~~~ 这个没有什么好说的,就是初始化栈。这三行执行后,SS=0; SP=0x7c00; 需要注意的是,这样设置后,栈的区域从0x0000_7c00向下扩展(不含0x0000_7c00这个字节),该区域包含了很多BIOS数据,包括实模式下的中断向量表,所以一定要小心。 ### (二)安装段描述符 ~~~ ;计算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 ;段内起始偏移地址 ~~~ 怎么理解这段代码呢? 首先,在代码清单的95、96行,有 ~~~ gdt_size dw 0 gdt_base dd 0x00007e00 ;GDT的物理地址 ~~~ 作者在这里声明了标号gdt_base,还初始化了一个双字——0x0000_7e00; 作者的意图是从这个地方开始建立全局描述符表GDT。我们的程序就是一个引导扇区,占用了512(=0x200)字节。程序加载的物理地址是0x7c00, 0x7c00+0x200 = 0x7e00. 可见,在物理地址的安排上,引导程序后面紧跟着就是GDT。 目前我们还是处在实模式下,所以要建立GDT,必须将GDT的线性地址(物理地址)转换成实模式下使用的“段地址:偏移地址”的形式。 mov ax,[cs:gdt_base+0x7c00] ; 这句使了段超越前缀“cs”,表明访问代码段中的数据;因为CS=0,所以就把物理地址(0x7c00+gdt_base)处的0x7e00传送给了ax; 同样地,将0x0000传送给dx; 为了把线性地址转换成逻辑地址,我们用DX:AX除以16,得到的商(AX)就是段地址,余数(DX)就是偏移地址。 mov bx,16                div bx                    mov ds,ax                          ;令DS指向该段以进行操作         mov bx,dx                          ;段内起始偏移地址 这几行执行之后,GDT的逻辑地址就是 DS:BX. ~~~ ;创建0#描述符,它是空描述符,这是处理器的要求 mov dword [bx+0x00],0x00 mov dword [bx+0x04],0x00 ~~~ **处理器规定,GDT中的第一个描述符必须是空描述符。**这是什么原因呢?因为很多时候,寄存器和内存单元的初始值都会为0,再加上程序设计有问题,就会在无意中用全0的索引来选择描述符,这当然是不好的。因此,处理器要求将第一个描述符定义成空描述符。所以,上面两行代码定义了一个空描述符。 ~~~ ;创建#1描述符,保护模式下的代码段描述符 mov dword [bx+0x08],0x7c0001ff mov dword [bx+0x0c],0x00409800 ~~~ 这两行用来创建第二个描述符。之前的博文我们已经掌握了数据段和代码段描述符的格式,所以对这个描述符就不难理解了。 还记得我上一篇博文中写了一个小程序吗?[http://blog.csdn.net/longintchar/article/details/50507218](http://blog.csdn.net/longintchar/article/details/50507218) 赶紧用它来分析一下吧: [![seg_des2](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fce537b.jpg "seg_des2")](http://img.blog.csdn.net/20160113223951465) 线性基地址:0x0000_7c00 段界限为0x001FF,因为G=0,所以该段的长度是512(2的9次方)字节; 特权级:0 其他字段就不逐个说明了,相信你一定能懂。很明显,这个描述符定义的段,就是主引导程序所在的区域。 接着看代码。 ~~~ ;创建#2描述符,保护模式下的数据段描述符(文本模式下的显示缓冲区) mov dword [bx+0x10],0x8000ffff mov dword [bx+0x14],0x0040920b ~~~ 程序分析的结果是: seg_base = 0XB8000 seg_limit = 0XFFFF S = 1 DPL = 0 G = 0 D/B = 1 TYPE = 2 数据段: 可读可写 看来这个段是指向显存的。 ~~~ ;创建#3描述符,保护模式下的堆栈段描述符 mov dword [bx+0x18],0x00007a00 mov dword [bx+0x1c],0x00409600 ~~~ 这是创建栈段的描述符。程序分析的结果是: ----------------------- seg_base = 0 seg_limit = 0X7A00 S = 1 DPL = 0 G = 0 D/B = 1 TYPE = 6 数据段: 向下扩展,可读可写 ------------------------ 正如作者所说:段界限的值0x7a00加上1(0x7a01),就是ESP寄存器所允许的最小值。当执行隐式的栈操作(如PUSH、CALL)时,处理器会检查ESP的值,一旦发现它小于0x7a01,就会引发异常中断。如果你还不理解,那么可以把书翻到215页。作者说在栈操作时,必须符合以下规则: **实际使用的段界限+1 <= (ESP的内容减操作数的长度) <= 0xFFFF_FFFF** 就拿这个例子来说,因为G=0,所以段界限就是0x7a00. 假设现在ESP的内容是0x7a04,此时执行下面的指令: push edx 因为压入的是双字,所以处理器会先将ESP的值减去4,于是ESP=0x7a00. 因为0x7a00小于0x7a01,因此会引发异常中断。 ### (三)LGDT指令 好了,现在描述符已经安装完毕,接下来的工作是加载描述符表的线性基地址和界限到GDTR寄存器。相关的指令是lgdt. 该指令的格式为: lgdt m48 也就是说,该指令的操作数内存操作数。注意,该指令在实模式和保护模式下都可以执行,也不影响任何标志位。 这个内存操作数指向一个6字节的内存区域,要求低16位是GDT的界限值(表的总字节数减去1),高32位是GDT的线性基地址。 ~~~ gdt_size dw 0 gdt_base dd 0x00007e00 ;GDT的物理地址 ~~~ 还记得吗,这是代码中事先定义了6字节的空间。前两个字节就是为了保存GDT的界限值。 ~~~ ;初始化描述符表寄存器GDTR mov word [cs: gdt_size+0x7c00],31 ;描述符表的界限(总字节数减一) lgdt [cs: gdt_size+0x7c00] ~~~ 第一句写入界限值,第二句把6字节加载到GDTR寄存器。 注意,到目前为止,我们依然在实模式下。 ### (四)关于A20 ### 1.A20 GATE 起源[1] 在8086/8088中,只有20根地址线,所以可以访问的地址是2^20=1M。但由于8086/8088是16位地址模式,能够表示的地址范围是0-64K,所以为了访问1M内存,Intel采取了分段的模式。 即:物理地址=16位段地址*16 + 16位偏移 但这种方式引起了新的问题,通过上述分段模式,能够表示的最大内存为:FFFFh:FFFFh=FFFF0h+FFFFh=10FFEFh 但8086/8088只有20位地址线,所以当访问100000h~10FFEFh之间的内存时,系统并不认为访问越界而产生异常,而是自动从重新0开始计算,也就是说系统计算实际地址的时候是按照对1M求模的方式进行的,这种技术被称为wrap-around(回绕)。 到了80286,系统的地址总线发展为24根,这样能够访问的内存可以达到2^24=16M。为了兼容,Intel在设计80286时提出的目标是:在实模式下,系统所表现的行为应该和8086/8088所表现的完全一样。但最终,80286芯片却存在一个BUG:如果程序员访问100000H~10FFEFH之间的内存,系统将实际访问这块内存,而不是象过去一样重新从0开始。 为了解决上述问题,IBM使用键盘控制器上剩余的一些输出线来管理第21根地址线(从0开始数是第20根),被称为A20Gate;如果A20 Gate打开,则当程序员给出100000H~10FFEFH之间的地址的时候,系统将真正访问这块内存区域;如果A20Gate被禁止,则当程序员给出100000H~10FFEFH之间的地址的时候,系统仍然使用8086/8088的方式。绝大多数IBM PC兼容机默认的A20Gate是被禁止的。由于在当时没有更好的方法来解决这个问题,所以IBM使用了键盘控制器来操作A20 Gate,但是这种操作太麻烦了,要使用一大堆指令。 ### 2.Alt_A20_GATE Alt_A20_GATE ,又称Fast A20. 通过端口0x92的bit1来打开A20,具体方法是:先从端口读出原数据,接着将bit1置1,然后再写入该端口,这样就打开了A20. 正如代码所示 ~~~ in al,0x92 ;南桥芯片内的端口 or al,0000_0010B out 0x92,al ;打开A20 ~~~   一次学太多会不会觉得累呢?我们就说到这里,下次继续…   【参考资料】 [1] 如烟海的专栏. [http://blog.csdn.net/ruyanhai/article/details/7181842](http://blog.csdn.net/ruyanhai/article/details/7181842 "http://blog.csdn.net/ruyanhai/article/details/7181842")
';

数据段描述符和代码段描述符(二)

最后更新于:2022-04-01 16:22:23

这篇博文,我们编写一个C语言的小程序,来解析数据段或者代码段描述符的各个字段。这样我们阅读原书的代码就会方便一点,只要运行这个小程序,就可以明白程序中定义的数据段或者代码段的描述符了。![眨眼](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f8abd34.jpg) 这段代码,我用了“位字段”的知识,这还是第一次使用C语言的这个特性呢,如果有不对的地方,欢迎博友斧正。 写代码之前,我们再复习一下数据段描述符和代码段描述符的格式。(图片选自赵炯老师的《Linux内核完全剖析》) ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fc93f76.jpg)   ~~~ #include <stdio.h> //定义描述符中的低32位 struct seg_des_low_word { unsigned int limit_0_15:16; unsigned int base_0_15 :16; }; ~~~ ~~~ //定义描述符中的高32位 struct seg_des_high_word { unsigned int base_16_23 :8; unsigned int type :4; unsigned int s :1; unsigned int dpl :2; unsigned int p :1; unsigned int limit_16_19:4; unsigned int avl :1; unsigned int l :1; unsigned int d_b :1; unsigned int g :1; unsigned int base_24_31 :8; }; ~~~ ~~~ //对TYPE字段进行解析 void parse_type(unsigned int t) { if(t<=7) printf("数据段: "); else printf("代码段: "); switch(t) { case 0: case 1: printf("只读\n"); break; case 2: case 3: printf("可读可写\n"); break; case 4: case 5: printf("向下扩展,只读\n"); break; case 6: case 7: printf("向下扩展,可读可写\n"); break; case 8: case 9: printf("仅执行\n"); break; case 10: case 11: printf("可读,可执行\n"); break; case 12: case 13: printf("一致性段,仅执行\n"); break; case 14: case 15: printf("一致性段,可读,可执行\n"); break; default: break; } } void parse_seg_des(struct seg_des_low_word* pl, struct seg_des_high_word* ph) { unsigned int seg_base; //拼接基地址字段 ~~~ ~~~ seg_base = (ph->base_24_31<<24)|(ph->base_16_23<<16)|pl->base_0_15; printf("seg_base = %#X\n",seg_base); unsigned int seg_limit; //拼接段限长字段 ~~~ ~~~ seg_limit = (ph->limit_16_19<<16)|pl->limit_0_15; printf("seg_limit = %#X\n",seg_limit); //下面的字段输出是不是很方便?这就是位字段的好处之一 ~~~ ~~~ printf("S = %d\n",ph->s); printf("DPL = %d\n",ph->dpl); printf("G = %d\n",ph->g); printf("D/B = %d\n",ph->d_b); printf("TYPE = %d\n",ph->type); ~~~ ~~~ //解析TYPE(目前只支持数据段描述符和代码段描述符,其他类型的,可以自己扩充) parse_type(ph->type); } int main(void) { printf("please input the segment descriptor, low= high=\n"); struct seg_des_high_word *high; struct seg_des_low_word *low; unsigned int l_word = 0; unsigned int h_word = 0; //请求用户输入描述符,先是低32位,再是高32位 ~~~ ~~~ scanf("%x" "%x",&l_word,&h_word); printf("-----------------------\n"); high =(struct seg_des_high_word*)&h_word; low =(struct seg_des_low_word*)&l_word; parse_seg_des(low,high); printf("------------------------\n"); return 0; } ~~~ 好了,代码就是这样。下面看看结果吧。编译后并运行。提示我们输入: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fcb1520.jpg) 我们就输入原书的配书代码c11_mbr.asm中 ~~~ ;创建#1描述符,保护模式下的代码段描述符 mov dword [bx+0x08],0x7c0001ff mov dword [bx+0x0c],0x00409800 ~~~ 注意,输入格式是16进制(不需要前导的0x),先输入低32位,也就是7c0001ff,然后空格,再输入高32位,也就是00409800,最后回车,就出结果了。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fcc6a50.jpg) 效果还不错吧。哈哈。 (完)
';

数据段描述符和代码段描述符(一)

最后更新于:2022-04-01 16:22:20

# 一、段描述符的分类 在上一篇博文中已经说过,为了使用段,我们必须要创建段描述符。80X86中有各种各样的段描述符,下图展示了它们的分类。 [![段描述符的分类](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fbe3f06.jpg "段描述符的分类")](http://img.blog.csdn.net/20160109212457811) 看了上图,你也许会说:天啊,怎么这么多段描述符啊!我可怎么记住呢?![哭泣的脸](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f857ea0.jpg) 别担心,我会在以后的博文中,跟随原书的作者,为您逐步介绍。我们的学习是循序渐进的,所以不要求一下子掌握所有东西。我们的原则是:用到什么学什么。我们今天的重点是“存储段描述符”。 # 二、段描述符的通用格式[1] 段描述符是GDT和LDT中的一个数据结构项,用于向处理器提供有关一个段的位置、大小以及访问控制的状态信息。每个段描述符的长度是8个字节,含有3个主要字段: - 段基地址 - 段限长 - 段属性 段描述符通常由编译器,链接器,加载器或者操作系统来创建,但绝不是应用程序。 下图给出了所有类型的段描述符的一般形式。 [![捕获](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fc12783.jpg "捕获")](http://img.blog.csdn.net/20160109212502690)   ### 1.段限长字段Limit 用于指定段的长度。处理器会把段描述符中两个段限长字段组合成一个20位的值,并根据颗粒度标志G来指定段限长Limit值的实际含义。 如果G=0,则Limit值的单位是B,也就是说Limit的范围可以是1B到1MB; 如果G=1,则Limit值的单位是4KB,也就是说Limit的范围可以是4KB到4GB。 根据段类型字段TYPE中的段扩展方向标志E,处理器可以以两种不同的方式使用Limit。 E=0:表示向上扩展的段(简称上扩段),逻辑地址中的偏移值范围可以从0到Limit; E=1:表示向下扩展的段(简称下扩段),逻辑地址中的偏移范围可以从Limit到0xFFFF(当B=0时)或者0xFFFF_FFFF(当B=1时)。关于B位,后面将解释。 ### 2.基地址字段Base 该字段定义在4GB线性地址空间中一个段的字节0所处的位置。也许你觉得这句话不好理解,我们换一种说法:对于一个逻辑地址,如果段内偏移为0,那么这个逻辑地址对应的线性地址就是Base;如果段内偏移为X,那么这个逻辑地址对应的线性地址就是Base+X; 段基地址可以是0~4GB范围内的任意地址(这同实模式不同,实模式下段基地址要求16字节对齐),但是,为了让程序具有最佳性能,还是建议段基地址对齐16字节边界。 ### 3.段类型字段TYPE 该字段用于指定段或者门(Gate)的类型、说明段的访问种类以及段的扩展方向。该字段的解释依赖于描述符类型标志S;TYPE字段的编码对代码段、数据段或者系统描述符都不同。 ### 4.描述符类型标志S S=0:表示存储段描述符。所谓“存储段”,就是存放可由程序直接进行访问的代码和数据的段。说白了,存储段就是代码段或者数据段。 S=1:表示系统描述符。 ### 5.描述符特权级字段DPL 用于指明描述符的特权级。特权级范围从0(最高)到3(最低)。DPL字段用于控制对段的访问。 ### 6.段存在标志P 用于指出一个段是在内存中(P=1)还是不在内存中(P=0). ### 7.D/B(默认操作数大小/默认栈指针大小和上界限) 对于代码段,此位称为“D”位;对于栈段,此位称为“B”位。我们在后文会说。 ### 8.颗粒度标志G 该字段用于确定段限长字段Limit值的单位。 如果G=0,则Limit值的单位是B; 如果G=1,则Limit值的单位是4KB; 注意:这个字段不影响段基地址的颗粒度,基地址的颗粒度总是以字节为单位。 ### 9.可用和保留位 L位(就是上图灰色的那个位):是64位代码段标志,保留此位给64位处理器使用。目前,我们将此位置“0”即可。 AVL:是软件可以使用的位,通常由操作系统来用,处理器并不使用它。   # 三、数据段描述符[1] 当S=1且TYPE字段的最高位(第2个双字的位11)为0时,表明是一个数据段描述符。 下图是数据段描述符的格式。 [![数据段描述符](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fc2de1e.jpg "数据段描述符")](http://img.blog.csdn.net/20160109212506980) ### 1.B位(默认栈指针大小和上界限) 对于栈段(由SS寄存器指向的数据段)来说,该位用来指明隐含堆栈操作(如PUSH、POP或CALL)时的栈指针大小。 B=0:使用SP寄存器 B=1:使用ESP寄存器 同时,B的值也决定了栈的上部边界。 B=0:栈段的上部边界(也就是SP寄存器的最大值)为0xFFFF; B=1:栈段的上部边界(也就是ESP寄存器的最大值)为0xFFFF_FFFF. ### 2.A位(已访问) 用于表示一个段最近是否被访问过(准确地说是指明从上次操作系统清零该位后一个段是否被访问过)。 当创建描述符的时候,应该把这位清零。之后,每当该段被访问时(准确地说是处理器把这个段的段选择符加载进段寄存器时,也许你不懂这句话,没有关系,现在忽略就可以了。)它就会将该位置“1”;对该位的清零是由操作系统负责的,通过定期监视该位的状态,就可以统计出该段的使用频率。当内存空间紧张时,可以把不经常使用的段退避到硬盘上,从而实现虚拟内存管理。 ### 3.W位(可写) 指示段的读写属性。 W=0:段不允许写入,否则会引发处理器异常中断; W=1:允许写入。 ### 4.E位(扩展方向) E=0:表示向上扩展的段(简称上扩段),逻辑地址中的偏移值范围可以从0到Limit; E=1:表示向下扩展的段(简称下扩段,通常是栈段),逻辑地址中的偏移范围可以从Limit到0xFFFF(当B=0时)或者0xFFFF_FFFF(当B=1时)。 # 四、代码段描述符[1] 当S=1且TYPE字段的最高位(第2个双字的位11)为1时,表明是一个代码段描述符。 下图是代码段描述符的格式。 [![代码段描述符](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fc4a479.jpg "代码段描述符")](http://img.blog.csdn.net/20160109212514896) ### 1.D位(默认操作数大小) 用于指出该段中的指令引用有效地址和操作数的默认长度。 D=0:默认值是16位的地址和16位或者8位的操作数; D=1:默认值是32位的地址和32位或者8位的操作数; 说明:指令前缀0x66可以用来选择非默认值的操作数大小,指令前缀0x67可以用来选择非默认值的地址大小。 ### 2.A位(已访问) 与数据段描述符中的A位相同。 ### 3.R位(可读) R=0:代码段不可读,只能执行。 R=1:代码段可读,可执行。 也许有人会问,当R=0时,既然代码段不可读,那处理器怎么从里面取指令执行呢?事实上,这里的R属性并非针对处理器,而是用来限制程序的行为。当常数或者静态数据被放在了一个ROM中时,就可以使用一个可读可执行的代码段,然后通过使用带CS前缀的指令,就可以读取代码段中的数据。 注意: - 在保护模式下,代码段是不可写的。 - 堆栈段必须是可读可写的数据段。 ### 4.C位(一致性) C=0:表示非一致性代码段。这样的代码段可以被同级代码段调用,或者通过门调用; C=1:表示一致性代码段。可以从低特权级的程序转移到该段执行(但是低特权级的程序仍然保持自身的特权级)。 注意:所有的数据段都是非一致性的,即意味着它们不能被低特权级的程序或过程访问。然而与代码段不同,数据段可以被更高特权级的程序或过程访问,而无需使用特殊的访问门。 有关特权级和特权级检查我们以后再讨论,这里对上面的概念了解即可,不用深究。 **** 最后,补充一张图表,也是引用自赵炯的《Linux内核完全剖析》。   [![表4-3](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fc6b0c4.jpg "表4-3")](http://img.blog.csdn.net/20160109212519317) **** **参考资料:** [1]:赵炯,《Linux内核完全剖析》,机械工业出版社. 4.3.4节
';

全局描述符表(GDT)

最后更新于:2022-04-01 16:22:18

在进入保护模式之前,我们先要学习一些基础知识。今天我们看一下全局描述符表(Global Descriptor Table, 简称GDT)。 同实模式一样,在保护模式下,对内存的访问仍然使用段地址加偏移地址。但是,在保护模式下,在每个段能够访问之前,必须先登记。这就好比像C语言中,“对变量的使用必须先定义”一样。 每个段在能够使用之前,都要为这个段建立一个描述符。每个描述符占8个字节,这些描述符集中存放在内存的某个区域,一个挨着一个,就构成了一张“表”。 80x86中有两种描述符表: - 全局描述符表(Global Descriptor Table, 简称GDT) - 局部描述符表(Local Descriptor Table,简称LDT) LDT我们以后再说,今天的重点是GDT. 在进入保护模式之前,必须要定义GDT,也就是说,我们要在内存中构建出一张表。 需要说明的是:在整个系统中,全局描述符表GDT只有一张(一个处理器对应一个GDT);GDT可以被放在内存的任何位置,但CPU必须知道GDT的入口。 你也许会问:CPU如何知道GDT的入口呢?别担心,在处理器内部,有一个48位的寄存器,名叫GDTR,也就是全局描述符表寄存器。其结构如下图:   [![GDTR](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fbd2ada.jpg "GDTR")](http://img.blog.csdn.net/20160109121106894)   该寄存器分为2部分: - 32位的线性基地址:GDT在内存中的起始线性地址(我们还没有涉及到分页,所以这里的线性地址等同于物理地址,下同,以后同); - 16位的表界限:在数值上等于表的大小(总字节数)减去1; 注意:在处理器刚上电的时候,基地址默认为0,表界限默认为0xFFFF; 在保护模式初始化过程中,必须给GDTR加载一个新值。 因为表界限是16位的,最大值是0xFFFF,也就是十进制的65535,那么表的大小就是65535+1=65536.又因为一个描述符占用8个字节,所以65536字节相当于8192个描述符(65536/8=8192).故理论上最多可以定义8192个描述符。实际上,不一定这么多,具体多少根据需要而定。 理论上,GDT可以放在内存中的任何地方。但是,我们必须在进入保护模式之前就定义GDT(不然就来不及了),所以GDT一般都定义在1MB以下的内存范围中。当然,允许在进入保护模式后换个位置重新定义GDT。
';

32位x86处理器编程导入

最后更新于:2022-04-01 16:22:16

在说正题之前,我们先看2个概念。 ### 1.指令集架构(ISA) ISA 的全称是 instruction set architecture,中文就是指令集架构,是指对程序员实际“可见”的指令集,包含了程序员编写一个能正确运行的二进制机器语言程序的所有信息,涉及到指令、 I/O 设备等。例如 Intel 的 IA-32、Intel 64、ARM 的 ARMv7、ARMv8 等等。 ### 2.微架构 微架构(Microarchitecture)又称为微体系结构/微处理器体系结构。是将一种给定的指令集架构在处理器中执行的方法。一种给定的指令集可以在不同的微架构中执行。 [1]   需要说明的是: 微架构与指令集是两个概念:指令集是CPU选择的语言,而微架构是具体的实现.[2] ARM公司将自己研发的指令集叫做ARM指令集,同时它还研发具体的微架构(如Cortex系列)并对外授权。但是,一款CPU使用了ARM指令集不等于它就使用了ARM研发的微架构。Intel、高通、苹果、Nvidia等厂商都自行开发了兼容ARM指令集的微架构,同时还有许多厂商使用ARM开发的微架构来制造CPU。通常,业界认为只有具备独立的微架构研发能力的企业才算具备了CPU研发能力,而是否使用自行研发的指令集则无关紧要。厂商研发CPU时并不需要获得指令集授权就可以获得指令集的相关资料与规范,指令集本身的技术含量并不是很高。获得授权主要是为了避免法律问题。然而微架构的设计细节是各家厂商绝对保密的,而且由于其技术复杂,即便获得相应文档也难以山寨。[2] 如前所述,仅仅从ARM购买微架构来组装芯片的厂商是不能被称作CPU研发企业的,这些芯片也不能被称为“xx厂商研发的CPU”.典型如华为的海思920、三星Exynos 5430,只能说是“使用ARM Cortex-A15核心的芯片”。但是如果一款兼容ARM指令集的芯片使用了厂商自主研发的微架构,情况就不同了。高通骁龙800、苹果A7就是这样的例子——它们分别使用了高通、苹果自主研发的CPU.[2] ### 3. 32位寄存器 (1)通用寄存器 在16位处理器内,有8个通用寄存器,分别是AX,BX,CX,DX,SI,DI,BP,SP. 其中,前4个还可以拆分成2个独立的寄存器来用。32处理器在16位处理器的基础上,扩展了这8个通用寄存器的长度,使之达到32位。它们的名字分别是EAX,EBX,ECX,EDX,ESI,EDI,ESP,EBP. 注意: - 在使用这些寄存器的时候,指令的源操作数和目的操作数必须具有相同的长度(个别特殊用途的指令除外)。如果目的操作数是32位寄存器,源操作数是立即数,那么立即数被视为是32位的。 - 32位通用寄存器的高16位是不能独立使用的,但是低16位保持同16位处理器的兼容性(可以拆成8位的来用)。 - 可以在32位的处理器上运行16位处理器上的软件。 (2)指令指针寄存器 在32位模式下,为了生成32位物理地址,处理器需要使用32位的指令指针寄存器,也就是说之前的16位的IP扩展成为32位的EIP。当处理器工作在16位模式下时,依然使用16位的IP;工作在32位模式下时,使用32位的EIP。 注意:和之前一样,EIP也只能由处理器自己使用,程序员无法直接访问。![哭泣的脸](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f857ea0.jpg) 对IP和EIP的修改通常是通过某些隐式的指令进行的,比如JMP,CALL,RET,IRET等等。 (3)标志寄存器 在32位处理器中,标志寄存器由之前16位的FLAGS扩展为32位的EFLAGS,低16位的每个字段和原先保持一致。 下图摘自《 The Intel Architecture Software Developer’s Manual——Volume 3 》 [![eflags](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fb7d741.jpg "eflags")](http://img.blog.csdn.net/20160109003529851) (4)段寄存器 在32位模式下,对内存的访问从理论上来说不需要分段,因为有32根地址线,可以直接寻址4G的内存。但是,IA-32结构的处理器是基于分段模型的,因此,32位处理器依然需要以段为单位访问内存,即使它工作在32位模式下。 不过,可以采取一个变通的方案,即只分一个段。也就是说段的基地址是0x0000_0000,段的长度是4GB。在这种情况下,相当于不分段,即平坦模型(Flat Mode)。 在32位模式下,处理器要求在加载程序时,先定义该程序拥有的段,然后才可以使用这些段。定义段时,除了起始地址外,还附加了段界限、特权级别、类型等属性。当程序访问一个段时,处理器将通过固件进行各种检查工作,以防止对内存的违规访问。 在32位模式下,传统的段寄存器,如CS,SS,DS,ES保存的不再是16位的段基地址,而是段选择子(到底什么是段选择子,我们以后再说)。另外,32位处理器还增加了两个额外的段寄存器,分别是FS和GS。 ### 4.实模式与保护模式 8086是16位的处理器,可以通过分段来访问1M的内存,段的最大长度是64KB。8086只有一种工作模式,就是我们现在所说的“实模式”。1985年,Intel公司推出了80386,获得了极大的成功。80386以及后续的处理器,都向前兼容,可以运行实模式下的8086程序。而且,在加电时,这些处理器都自动处于实模式下。只有在一番设置之后,才能运行在保护模式下。 ### 5.逻辑地址和线性地址 在8086,我们把“段地址:段内偏移地址”称为逻辑地址。8086CPU内部有一个地址加法器,用来把逻辑地址转换成物理地址。转换规则是:物理地址=段地址×10H+段内偏移量 但是在80386中,情况就不同了。80386的逻辑地址(也叫虚拟地址)构成是“段选择子:段内偏移量”。逻辑地址经过80386CPU内部的分段部件转换后成为线性地址。线性地址再经过分页部件转换就成为物理地址。如果禁用分页机制,那么线性地址就是物理地址。 ### 6.处理器的寻址方式 在16位处理器上,内存寻址方式为: [![16位内存寻址](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fb9cfe5.jpg "16位内存寻址")](http://img.blog.csdn.net/20160109003532770)   在32位处理器上,内存寻址方式为: [![32位的内存寻址方式](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fbafdc6.jpg "32位的内存寻址方式")](http://img.blog.csdn.net/20160109003534734) 也是就是说,在指定有效地址的时候,可以使用所有的32位通用寄存器作为基址寄存器。同时,还可以再加上一个除ESP之外的32位通用寄存器作为变址寄存器。另外,变址寄存器还允许乘以一个比例因子(1或2或4或8)。最后,还可以加上一个8位或者32位的偏移量。 举例: add eax,[0x2008] sub eax,[eax+0x04] mov ecx,[edx+esi*8+0x02] ### 7.汇编器指令 BITS 相同的机器指令,在16位模式和32位模式下的解释和执行效果是不同的。举例来说: 8B5022,这条机器码,在16位模式下,对应的汇编指令是:mov dx,[bx+si+0x02]; 但是,在32位模式下,对应的汇编指令却是 mov edx,[eax+0x02]; NASM汇编器中有一个伪指令——BITS. 'BITS'指令指定 NASM 产生的代码是被设计运行在 16 位模式的处理器上还是运行在32位模式的处理器上。语法是'BITS 16'或'BITS 32'. NASM以.bin格式输出时,默认是16位模式。如果我们需要编译成32位的,则需要加上[bits 32](方括号可以有,也可以没有。) ### 8.一般指令的扩展 (1)loop指令 在16位处理器上,loop指令的循环次数在寄存器CX中。在32位处理器上,如果当前模式是16位的,那么loop指令执行时,仍然使用CX寄存器;如果运行在32位模式下,则使用ECX; (2)mul指令 在16位处理器上,无符号数乘法指令mul的格式为 mul r/m8     ; AX <- AL * r/m8 mul r/m16  ; DX:AX <- AX * r/m16 说明:这里的r/m8表示8位的通用寄存器或内存单元, r/m16表示16位的通用寄存器或内存单元,下面的r/m32表示32位的通用寄存器或内存单元 在32位处理器上,除了依然支持上面的的操作,另外还支持以下扩展格式: mul r/m32  ;EDX:EAX <- EAX * r/m32 有符号数乘法指令imul与此相同。 (3)div指令 在16位处理器上,无符号除法指令div的格式为: div r/m8  ; AX  ÷  r/m8   = AL …… AH div r/m16 ; DX:AX ÷ r/m16  = AX ……DX 在32位处理器上,除了依然支持上面的操作,还支持以下扩展格式: div r/m32 ; EDX:EAX ÷ r/m32 = EAX……EDX 有符号数除法指令idiv于此相同; (4)push和pop指令 操作数是立即数的情况 32处理器的栈操作指令push和pop也有所扩展,允许压入双字操作数。特别是,它支持立即数的压栈操作。指令格式为(imm8/16/32表示8位或16位或32位立即数): push imm8  ;操作码为6A push imm16 ; 操作码为68 push imm32; 操作码也为68 还是举书上的例子吧: - 例1:压入一个字节 push byte 0x55;  这里的关键字byte是给编译器看的,告诉它压入的是字节(毕竟0x55可以解释为字0x0055或者双字0x0000_0055).这条指令的16位形式(用bits 16 编译)和32位形式(用bits 32 编译)是一样的,机器码都是 6A 55 但是,执行的时候就不同了。注意,无论什么时候,处理器都不会压入一个字节。它要么压入字,要么压入双字。 在16位模式下,默认的操作数是16位。于是处理器将0x55作符号扩展,扩展成16位的0x0055,然后压入栈。(压栈时用sp寄存器,且先将sp减去2); 在32位模式下,处理器将0x55扩展成32位的0x0000_0055,然后压入。(压栈时用esp寄存器,且先将esp减去4) - 例2:压入一个字 压入一个字,必须用word关键字来修饰,如 push word 0xfffb 在16位模式下,默认的操作数是16位的。处理器直接压入该字(压栈时用sp寄存器,且先将sp减去2); 在32位模式下,处理器将0xfffb扩展成32位的0xffff_fffb,然后压入。(压栈时用esp寄存器,且先将esp减去4) - 例3:压入一个双字 如果压入双字,则必须用关键字dword来修饰。如 push dword 0xfb 在16位模式下,压入的是0x0000_00fb(压栈时用sp寄存器,且先将sp减去4); 在32位模式下,压入的也是0x0000_00fb(压栈时用esp寄存器,且先将esp减去4). 操作数位于通用寄存器或者内存单元的情况 对于操作数位于通用寄存器或者内存单元的情况,只能压入字或者双字。指令格式为: push r/m16 push r/m32 比如: push ax push edx 如果操作数位于内存单元中,则必须用关键字word或者dword修饰,如: push word [0x2000] push dword [ecx+esi*2+0x02] 无论操作数位于寄存器还是内存单元, 在16位模式下,压入字的时候,将sp的内容减去2;压入双字的时候,将sp的内容减去4; 在32位模式下,压入字的时候,将esp的内容减去2;压入双字的时候,将esp的内容减去4. 操作数是段寄存器的情况 指令格式为: push cs/ds/es/fs/gs/ss 在16位模式下,将sp的内容减去2,然后直接压入段寄存器的内容; 在32位模式下,先将段寄存器的内容扩展为32位(高16位全为0),然后将esp的内容减去4,再压入扩展后的32位的值。   今天就说到这里,下次我们开启保护模式之旅![飞机](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fbc3148.jpg)     ### 参考资料: [1]百度百科:[http://baike.baidu.com/link?url=I1wsXUAGa541Pn8h1XVgSnR6GmUsfWK8VOjpALlzmE7vOccJVOpxkQfKjYYHODUe2BxqOw2q5KAB6pS4ZQjD9K](http://baike.baidu.com/link?url=I1wsXUAGa541Pn8h1XVgSnR6GmUsfWK8VOjpALlzmE7vOccJVOpxkQfKjYYHODUe2BxqOw2q5KAB6pS4ZQjD9K "http://baike.baidu.com/link?url=I1wsXUAGa541Pn8h1XVgSnR6GmUsfWK8VOjpALlzmE7vOccJVOpxkQfKjYYHODUe2BxqOw2q5KAB6pS4ZQjD9K") [2]知乎:王强,[http://zhuanlan.zhihu.com/xpenrynidea/19893066](http://zhuanlan.zhihu.com/xpenrynidea/19893066)
';

8086键盘输入实验

最后更新于:2022-04-01 16:22:13

# 1.BIOS中断 我们可以为所有中断类型自定义中断处理过程,包括内部中断、硬件中断和软中断。 BIOS中断,又称BIOS功能调用,主要是为了方便地使用最基本的硬件访问功能。通常,为了区分针对同一硬件的不同功能,使用寄存器AH来指定具体的功能编号。 比如说,以下的指令用于从键盘读取一个按键: ~~~ mov ah,0x00 ;0功能号对应从键盘读字符 int 0x16 ;键盘服务, int 0x16 ; 中断返回时,字符的ASCII在AL中 ~~~ 需要说明的是,BIOS可能会为一些简单地外围设备提供初始化代码和功能调用代码,并填写中断向量表,但是有一些BIOS中断是由外部设备接口自己建立的。 首先,每个外部设备接口,包括各种板卡,如网卡、显卡、键盘接口电路、硬件控制器等,都有自己的只读存储器(ROM),类似于BIOS芯片,这些ROM中提供了它们自己的功能调用例程,以及本设备的初始化代码。按照规范,前两个单元的内容是0x55和0xAA,第三个单元是本ROM中的代码长度(以512字节为单位);从第四个单元开始,就是实际的ROM代码。 其次,我们知道,从内存物理地址A0000开始,到FFFFF结束,有相当一部分空间是留给外围设备的。如果设备存在,那么它自带的ROM会映射到分配给它的地址范围内。 在计算机启动期间,BIOS会以2KB为单位搜索内存地址C0000~E0000之间的区域。当它发现某个区域的前两个字节是0x55和0xAA时,那意味着该区域有ROM代码的存在,是有效的。接着,它对该区域做累加和检查,看结果是否和第三个单元相符。如果相符,就从第四个单元进入。这时候,处理器执行的是硬件自带的程序指令,这些指令初始化外部设备的相关寄存器和工作状态。最后,填写相关的中断向量表,使其指向自带的中断处理过程。 # 2.键盘读字符并显示的实验 ### (1)代码清单 ~~~ ;代码清单9-2 ;文件名:c09_2.asm ;文件说明:用于演示BIOS中断的用户程序 ;创建日期:2012-3-28 20:35 ;=============================================================================== SECTION header vstart=0 ;定义用户程序头部段 program_length dd program_end ;程序总长度[0x00] ;用户程序入口点 code_entry dw start ;偏移地址[0x04] dd section.code.start ;段地址[0x06] realloc_tbl_len dw (header_end-realloc_begin)/4 ;段重定位表项个数[0x0a] realloc_begin: ;段重定位表 code_segment dd section.code.start ;[0x0c] data_segment dd section.data.start ;[0x14] stack_segment dd section.stack.start ;[0x1c] header_end: ;=============================================================================== SECTION code align=16 vstart=0 ;定义代码段(16字节对齐) start: mov ax,[stack_segment] mov ss,ax mov sp,ss_pointer mov ax,[data_segment] mov ds,ax mov cx,msg_end-message mov bx,message .putc: mov ah,0x0e mov al,[bx] int 0x10 inc bx loop .putc .reps: mov ah,0x00 int 0x16 mov ah,0x0e mov bl,0x07 int 0x10 jmp .reps ;=============================================================================== SECTION data align=16 vstart=0 message db 'Hello, friend!',0x0d,0x0a db 'This simple procedure used to demonstrate ' db 'the BIOS interrupt.',0x0d,0x0a db 'Please press the keys on the keyboard ->' msg_end: ;=============================================================================== SECTION stack align=16 vstart=0 resb 256 ss_pointer: ;=============================================================================== SECTION program_trail program_end: ~~~ ### (2)使用BIOS中断向屏幕写字符 关于代码,头部的部分和SS,DS的初始化自然不用多说,我们已经很熟悉了。 ~~~ mov cx,msg_end-message mov bx,message .putc: mov ah,0x0e mov al,[bx] int 0x10 inc bx loop .putc ~~~ 首先,把重复次数传入CX,然后让BX指向要显示的信息的首地址。 接下来,我们要利用0x10号中断的0x0e号功能。 **BIOS中断显示服务(Video Service——INT 10H)** 功能描述:在Teletype模式下显示字符,具体说就是在屏幕的光标处写一个字符,并推进光标的位置。 入口参数: AH=0EH AL=字符 BH=页码 BL=前景色(图形模式);注意,仅在图形模式下,设置BL才会改变前景色;在文本模式下,这个参数不起作用(我们的实验工作在文本模式下) 出口参数:无 ### (3)使用BIOS中断从键盘读取字符 ~~~ .reps: mov ah,0x00 int 0x16 mov ah,0x0e mov bl,0x07 ;我觉得这句可以不要 int 0x10 jmp .reps ~~~ 前面已经说了,0x16号中断的0x00号子功能是从键盘读字符。 然后,再次利用0x10号中断的0x0e号功能,把我们从键盘输入的字符显示出来。 **BIOS中断键盘服务(Keyboard Service——INT 16H)** 功能描述:从键盘读入字符 入口参数: AH=00H——读键盘 AH=10H——读扩展键盘(可根据0000:0496H单元的内容判断:扩展键盘是否有效 ) 出口参数: AH=键盘的扫描码 AL=字符的ASCII码 ### (4)实验结果截图 [![keyboard](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fb42d4d.jpg "keyboard")](http://img.blog.csdn.net/20160103233200929) 上图就是启动Bochs后,再按C之后的画面。接下来,我们就可以尝试按键,看看会发生什么![眨眼](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f8abd34.jpg)   下一次,我们就开始探索32位的x86了,你是否很期待呢?![大笑](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fa14f3c.jpg)
';

8086实时时钟实验(二)

最后更新于:2022-04-01 16:22:11

上次我们说了代码,这次我们说说怎样看到实验结果。 首先编译源文件(我的源文件就在当前路径下,a盘和c盘在上一级目录下): nasm -f bin c08_mbr.asm -o c08_mbr.bin nasm -f bin c09_1.asm -o c0901.bin 然后将bin文件写入a盘和c盘 dd if=c08_mbr.bin of=../a.img dd if=c0901.bin of=../c.img bs=512 seek=100 conv=notrunc 注意:也许你需要根据自身的情况修改路径   好了,运行Bochs 可是结果令人失望,不管我怎么尝试,都看到了如下的图 [![bochs_fail](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f8bfcfa.jpg "bochs_fail")](http://img.blog.csdn.net/20160103005657960) 只能看到屏幕中间的“@”在闪烁,可是根本看不到时间。 ![哭泣的脸](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f857ea0.jpg) ![哭泣的脸](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f857ea0.jpg) ![哭泣的脸](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f857ea0.jpg) 我也试图调试一下,在中断处理程序里面打了断点,可是中断似乎进不去。 无法运行出正确的结果,也许不是我们错了,或许是工具的问题。毕竟我用的是win7+vmware+ubuntu+bochs,而不是一个真实的环境。   好吧,既然这条路走不通,那我就换一条路吧。是不是vmware的问题呢?那我们用virtualBox试一试。 于是,我打造了win7+virtualBox+ubuntu+bochs,可是和上面的结果完全一样,还是失败了。看来不是vmware的问题。 真的是山穷水尽了吗?我又想了一个方法,就是作者在第四章推荐的方法,virtualBox+VHD文件; 说干就干,首先,我们需要在virtualBox上创建一台虚拟机。   第1步:启动VirtualBox,点击【新建】 [![vb-1](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f9000be.jpg "vb-1")](http://img.blog.csdn.net/20160103005704989) 图中的名称是自己起的,我的叫“hello_asm”; 类型和版本也不重要,很多人都会误解,以为VirtualBox会根据我们的选择来安装一个现成的操作系统。其实,并不是这样,让我们选择操作系统的唯一目的,就是根据我们的选择,在后面的步骤中为我们提供合理的硬件配置。实际上,我们不打算安装任何操作系统,所以,我选择了Other。好的,我们点击【下一步】;   第2步:分配内存大小 [![vb-2](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f92a269.jpg "vb-2")](http://img.blog.csdn.net/20160103005714944) 内存大小就根据自己主机的物理内存来配置吧,这里我选了512M,然后点击【下一步】;   第3步:创建虚拟硬盘(或者用作者提供的) [![vb-3](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f954b6f.jpg "vb-3")](http://img.blog.csdn.net/20160103005724790) 这里可以用作者提供的VHD文件,也可以自己创建。为了不失一般性,我们选择‘现在创建虚拟硬盘’,点击【创建】   第4步:选择虚拟硬盘的文件类型 [![vb-4](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f98209e.jpg "vb-4")](http://img.blog.csdn.net/20160103005735590) 这里一定要选择VHD,因为我们打算用作者提供的工具(fixvhdwr.exe),然后点击【下一步】;   第5步:选择虚拟硬盘的类型 [![vb-5](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f9a7542.jpg "vb-5")](http://img.blog.csdn.net/20160103113346601) 一定要选择“固定大小”,因为本书的配套工具只支持固定大小的。然后点击【下一步】   第6步:选择虚拟硬盘的位置和大小 [![vb-6](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f9dcc99.jpg "vb-6")](http://img.blog.csdn.net/20160103113357408) VHD文件位置是自己设置的,一定要放在一个你可以找到的地方。其实,放在作者提供的配书工具所在的文件夹是最方便的。我这里放在D盘。 另外,还要选择虚拟硬盘的大小,我选了2GB;作者的要求是,一定要大于50MB![大笑](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fa14f3c.jpg) ,然后点击【创建】;   通过以上6步,我们的虚拟硬盘就创建好了。   下面的任务是把我们的bin文件写入虚拟盘,最后看一下结果。 第1步:打开作者的配书工具——fixvhdwr.exe [![vb-8](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fa25718.jpg "vb-8")](http://img.blog.csdn.net/20160103113410399) 第2步:选择虚拟磁盘文件 [![vb-9](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fa4d943.jpg "vb-9")](http://img.blog.csdn.net/20160103113416235) 找到我们刚才保存虚拟硬盘的目录,选择哪个VHD文件,然后【下一步】; [![vb-10](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fa6ec9a.jpg "vb-10")](http://img.blog.csdn.net/20160103113420127) 第3步:选择数据文件 [![vb-11](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fa8f0fa.jpg "vb-11")](http://img.blog.csdn.net/20160103125455371) 找到我们编译好的c08_mbr.bin文件   第4步:设置起始LBA扇区号,并写入文件 [![vb-12](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8faa9427.jpg "vb-12")](http://img.blog.csdn.net/20160103125459515) 因为是启动文件,所以我们选择0,然后点击【写入文件】;之后可以看到:   [![vb-13](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fac5f8b.jpg "vb-13")](http://img.blog.csdn.net/20160103125502524) 最后点击【完成】; 通过上面4个步骤,我们就把一个.bin文件写入VHD文件了。 用同样的方法,我们可以把c0901.bin(用户程序)写入逻辑扇区100(起始逻辑扇区号一定要和代码中的保持一致);   下面,我们启动虚拟机,看看结果吧: [![vb-14](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fadccdc.jpg "vb-14")](http://img.blog.csdn.net/20160103125507098) 启动虚拟机,我们终于看到了屏幕中央闪烁的时间!   经过上面的折腾,我觉得不是VMware的问题,也不是VirtualBox的问题,很可能是Bochs的问题! 心血来潮,我想到了我安装的Bochs不是最新版本,也许我可以弄个最新版本试一试。 于是我在VMware上安装了Bochs-2.6.8,按照以前的方法,得出的结果又惊喜又困惑,请看图: [![2.6.8_result](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fb073d8.jpg "2.6.8_result")](http://img.blog.csdn.net/20160103125511840) 看到了吗?屏幕中央有时间了! 遗憾的是,这个时间跳动得太快了,也就是说这个时钟走得特别快,根本不是每秒一次的更新频率。我想这还是Bochs的问题吧。   好了,今天就到这里,下次再见![吐舌鬼脸](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fb25553.jpg) !
';

8086实时时钟实验(一)

最后更新于:2022-04-01 16:22:09

### 1.代码清单 ~~~ ;代码清单9-1 ;文件名:c09_1.asm ;文件说明:用户程序 ;创建日期:2011-4-16 22:03 ;=============================================================================== SECTION header vstart=0 ;定义用户程序头部段 program_length dd program_end ;程序总长度[0x00] ;用户程序入口点 code_entry dw start ;偏移地址[0x04] dd section.code.start ;段地址[0x06] realloc_tbl_len dw (header_end-realloc_begin)/4 ;段重定位表项个数[0x0a] realloc_begin: ;段重定位表 code_segment dd section.code.start ;[0x0c] data_segment dd section.data.start ;[0x14] stack_segment dd section.stack.start ;[0x1c] header_end: ;=============================================================================== SECTION code align=16 vstart=0 ;定义代码段(16字节对齐) new_int_0x70: push ax push bx push cx push dx push es .w0: mov al,0x0a ;阻断NMI。当然,通常是不必要的 or al,0x80 out 0x70,al in al,0x71 ;读寄存器A test al,0x80 ;测试第7位UIP jnz .w0 ;以上代码对于更新周期结束中断来说 ;是不必要的 xor al,al or al,0x80 out 0x70,al in al,0x71 ;读RTC当前时间(秒) push ax mov al,2 or al,0x80 out 0x70,al in al,0x71 ;读RTC当前时间(分) push ax mov al,4 or al,0x80 out 0x70,al in al,0x71 ;读RTC当前时间(时) push ax mov al,0x0c ;寄存器C的索引。且开放NMI out 0x70,al in al,0x71 ;读一下RTC的寄存器C,否则只发生一次中断 ;此处不考虑闹钟和周期性中断的情况 mov ax,0xb800 mov es,ax pop ax call bcd_to_ascii mov bx,12*160 + 36*2 ;从屏幕上的12行36列开始显示 mov [es:bx],ah mov [es:bx+2],al ;显示两位小时数字 mov al,':' mov [es:bx+4],al ;显示分隔符':' not byte [es:bx+5] ;反转显示属性 pop ax call bcd_to_ascii mov [es:bx+6],ah mov [es:bx+8],al ;显示两位分钟数字 mov al,':' mov [es:bx+10],al ;显示分隔符':' not byte [es:bx+11] ;反转显示属性 pop ax call bcd_to_ascii mov [es:bx+12],ah mov [es:bx+14],al ;显示两位小时数字 mov al,0x20 ;中断结束命令EOI out 0xa0,al ;向从片发送 out 0x20,al ;向主片发送 pop es pop dx pop cx pop bx pop ax iret ;------------------------------------------------------------------------------- bcd_to_ascii: ;BCD码转ASCII ;输入:AL=bcd码 ;输出:AX=ascii mov ah,al ;分拆成两个数字 and al,0x0f ;仅保留低4位 add al,0x30 ;转换成ASCII shr ah,4 ;逻辑右移4位 and ah,0x0f add ah,0x30 ret ;------------------------------------------------------------------------------- start: mov ax,[stack_segment] mov ss,ax mov sp,ss_pointer mov ax,[data_segment] mov ds,ax mov bx,init_msg ;显示初始信息 call put_string mov bx,inst_msg ;显示安装信息 call put_string mov al,0x70 mov bl,4 mul bl ;计算0x70号中断在IVT中的偏移 mov bx,ax cli ;防止改动期间发生新的0x70号中断 push es mov ax,0x0000 mov es,ax mov word [es:bx],new_int_0x70 ;偏移地址。 mov word [es:bx+2],cs ;段地址 pop es mov al,0x0b ;RTC寄存器B or al,0x80 ;阻断NMI out 0x70,al mov al,0x12 ;设置寄存器B,禁止周期性中断,开放更 out 0x71,al ;新结束后中断,BCD码,24小时制 mov al,0x0c out 0x70,al in al,0x71 ;读RTC寄存器C,复位未决的中断状态 in al,0xa1 ;读8259从片的IMR寄存器 and al,0xfe ;清除bit 0(此位连接RTC) out 0xa1,al ;写回此寄存器 sti ;重新开放中断 mov bx,done_msg ;显示安装完成信息 call put_string mov bx,tips_msg ;显示提示信息 call put_string mov cx,0xb800 mov ds,cx mov byte [12*160 + 33*2],'@' ;屏幕第12行,35列 .idle: hlt ;使CPU进入低功耗状态,直到用中断唤醒 not byte [12*160 + 33*2+1] ;反转显示属性 jmp .idle ;------------------------------------------------------------------------------- put_string: ;显示串(0结尾)。 ;输入:DS:BX=串地址 mov cl,[bx] or cl,cl ;cl=0 ? jz .exit ;是的,返回主程序 call put_char inc bx ;下一个字符 jmp put_string .exit: ret ;------------------------------------------------------------------------------- put_char: ;显示一个字符 ;输入:cl=字符ascii push ax push bx push cx push dx push ds push es ;以下取当前光标位置 mov dx,0x3d4 mov al,0x0e out dx,al mov dx,0x3d5 in al,dx ;高8位 mov ah,al mov dx,0x3d4 mov al,0x0f out dx,al mov dx,0x3d5 in al,dx ;低8位 mov bx,ax ;BX=代表光标位置的16位数 cmp cl,0x0d ;回车符? jnz .put_0a ;不是。看看是不是换行等字符 mov ax,bx ; mov bl,80 div bl mul bl mov bx,ax jmp .set_cursor .put_0a: cmp cl,0x0a ;换行符? jnz .put_other ;不是,那就正常显示字符 add bx,80 jmp .roll_screen .put_other: ;正常显示字符 mov ax,0xb800 mov es,ax shl bx,1 mov [es:bx],cl ;以下将光标位置推进一个字符 shr bx,1 add bx,1 .roll_screen: cmp bx,2000 ;光标超出屏幕?滚屏 jl .set_cursor mov ax,0xb800 mov ds,ax mov es,ax cld mov si,0xa0 mov di,0x00 mov cx,1920 rep movsw mov bx,3840 ;清除屏幕最底一行 mov cx,80 .cls: mov word[es:bx],0x0720 add bx,2 loop .cls mov bx,1920 .set_cursor: mov dx,0x3d4 mov al,0x0e out dx,al mov dx,0x3d5 mov al,bh out dx,al mov dx,0x3d4 mov al,0x0f out dx,al mov dx,0x3d5 mov al,bl out dx,al pop es pop ds pop dx pop cx pop bx pop ax ret ;=============================================================================== SECTION data align=16 vstart=0 init_msg db 'Starting...',0x0d,0x0a,0 inst_msg db 'Installing a new interrupt 70H...',0 done_msg db 'Done.',0x0d,0x0a,0 tips_msg db 'Clock is now working.',0 ;=============================================================================== SECTION stack align=16 vstart=0 resb 256 ss_pointer: ;=============================================================================== SECTION program_trail program_end: ~~~ 以上就是全部的代码了(加载器采用第八章的) 也不知道我这个插件怎么了,显示出的源码歪歪扭扭,没有对齐![哭泣的脸](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f857ea0.jpg) 好吧,咱们就凑合看吧。 ### 2.用户程序结构图 [![用户程序结构图_thumb[2]](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f867755.jpg "用户程序结构图_thumb[2]")](http://img.blog.csdn.net/20160101232123711) ### 3.中断处理程序 最开始的部分是头部,严格遵循第八章作者约定的格式,我们就不多说了。 ~~~ ;=============================================================================== SECTION code align=16 vstart=0 ;定义代码段(16字节对齐) new_int_0x70: push ax push bx push cx push dx push es ~~~ 这里就开始中断处理程序了。首先是把用到的寄存器入栈,这是必须的。 ~~~ .w0: mov al,0x0a ;阻断NMI。当然,通常是不必要的 or al,0x80 out 0x70,al in al,0x71 ;读寄存器A test al,0x80 ;测试第7位UIP jnz .w0 ;以上代码对于更新周期结束中断来说 ;是不必要的 xor al,al or al,0x80 out 0x70,al in al,0x71 ;读RTC当前时间(秒) push ax mov al,2 or al,0x80 out 0x70,al in al,0x71 ;读RTC当前时间(分) push ax mov al,4 or al,0x80 out 0x70,al in al,0x71 ;读RTC当前时间(时) push ax ~~~ 这段代码要细说,有很多新知识。 ### (1)CMOS RAM 在外围设备控制芯片(ICH)内部,集成了实时时钟电路(RTC)和两小块由互补金属氧化物(CMOS)材料组成的静态存储器(CMOS RAM)。实时时钟电路负责计时,而日期和时间的数值则存储在这块存储器中,它们由电脑主板上的一个小纽扣电池提供能量。 日期和时间信息存储在CMOS RAM中,通常CMOS RAM有128个存储单元,而日期和时间信息只占了一小部分容量,其余空间则保存整机的配置信息。 RTC芯片由一个频率为32.768kHz的晶振驱动,经过分频后,用于对CMOS RAM进行每秒一次的时间刷新。 表格9-1 CMOS RAM中的时间信息 <table border="0" cellspacing="0" cellpadding="2" width="400"><tbody><tr><td valign="top" width="100"><p align="center">偏移地址</p></td><td valign="top" width="100"><p align="center">内容</p></td><td valign="top" width="100"><p align="center">偏移地址</p></td><td valign="top" width="100"><p align="center">内容</p></td></tr><tr><td valign="top" width="100"><p align="center">0x00</p></td><td valign="top" width="100"><p align="center">秒</p></td><td valign="top" width="100"><p align="center">0x07</p></td><td valign="top" width="100"><p align="center">日</p></td></tr><tr><td valign="top" width="100"><p align="center">0x01</p></td><td valign="top" width="100"><p align="center">闹钟秒</p></td><td valign="top" width="100"><p align="center">0x08</p></td><td valign="top" width="100"><p align="center">月</p></td></tr><tr><td valign="top" width="100"><p align="center">0x02</p></td><td valign="top" width="100"><p align="center">分</p></td><td valign="top" width="100"><p align="center">0x09</p></td><td valign="top" width="100"><p align="center">年</p></td></tr><tr><td valign="top" width="100"><p align="center">0x03</p></td><td valign="top" width="100"><p align="center">闹钟分</p></td><td valign="top" width="100"><p align="center">0x0a</p></td><td valign="top" width="100"><p align="center">寄存器A</p></td></tr><tr><td valign="top" width="100"><p align="center">0x04</p></td><td valign="top" width="100"><p align="center">时</p></td><td valign="top" width="100"><p align="center">0x0b</p></td><td valign="top" width="100"><p align="center">寄存器B</p></td></tr><tr><td valign="top" width="100"><p align="center">0x05</p></td><td valign="top" width="100"><p align="center">闹钟时</p></td><td valign="top" width="100"><p align="center">0x0c</p></td><td valign="top" width="100"><p align="center">寄存器C</p></td></tr><tr><td valign="top" width="100"><p align="center">0x06</p></td><td valign="top" width="100"><p align="center">星期</p></td><td valign="top" width="100"><p align="center">0x0d</p></td><td valign="top" width="100"><p align="center">寄存器D</p></td></tr></tbody></table> CMOS RAM的访问,需要两个端口:0x70是索引端口,用来指定内存单元;0x71是数据端口,用来读写相应单元里的内容。 举例: mov al,2 out 0x70,al ;指定内存单元为2 in al,0x71 ;读RTC当前时间(分) 需要说明的是,从很早的时候开始,端口0x70的最高位是控制NMI中断的开关,当它为0时,允许NMI中断;为1时,阻断所有的NMI信号。其他7个bit,实际上用来指定CMOS RAM单元的索引号。 作者为了简化问题,所以在访问RTC时,直接关闭NMI,访问结束后,再打开NMI(不管它之前是不是打开的)。 查阅资料,有的朋友说“访问CMOS RAM可能导致产生NMI,所以需要关闭NMI。” 还有一点要注意:CMOS RAM中保存的日期和时间,默认是8421 BCD编码,也就是用0000~1001分别代表它所对应的十进制数。 ~~~ .w0: mov al,0x0a ;访问寄存器A or al,0x80 ;阻断NMI out 0x70,al in al,0x71 ;读寄存器A test al,0x80 ;测试第7位UIP jnz .w0 ;以上代码对于更新周期结束中断来说是不必要的 ~~~ test al,0x80 ,这句是测试寄存器A的bit7 正如书上155页所说: CMOS RAM中的时间和日期会由RTC周期性地更新,在此期间,用户程序不应当访问它们。 寄存器A的bit7为0时,表示更新周期至少在488us内不会启动。换句话说,此时访问时间信息是安全的。 寄存器A的bit7为1时,表示正处于更新周期或者马上就要启动。 可以看到,上面的代码就是反复测试寄存器A的bit7,如果是0,可以向下执行。 ~~~ xor al,al or al,0x80 out 0x70,al in al,0x71 ;读RTC当前时间(秒) push ax ~~~ 这段代码很好理解,就是读出秒,并且把结果压栈(压栈时为了之后显示在屏幕上) ~~~ mov al,2 or al,0x80 out 0x70,al in al,0x71 ;读RTC当前时间(分) push ax mov al,4 or al,0x80 out 0x70,al in al,0x71 ;读RTC当前时间(时) push ax ~~~ 道理同上。 ~~~ mov al,0x0c ;寄存器C的索引。且开放NMI out 0x70,al in al,0x71 ;读一下RTC的寄存器C,否则只发生一次中断 ;此处不考虑闹钟和周期性中断的情况 ~~~ 这里要说一下寄存器C,这个寄存器是只读寄存器。可以通过读取这个寄存器,知道中断是否发生,如果发生,还可以知道中断原因。 寄存器C是8位寄存器。 [3:0]:保留; [7]:中断请求标志,周期性中断/闹钟中断/更新结束中断,任何一种发生都会使这位置1; [6]:周期性中断标志,置1则表示发生了周期性中断 [5]:闹钟中断标志,置1则表示发生了闹钟中断 [4]:更新结束中断标志,置1则表示发生了更新结束中断 注意,对寄存器的读操作将导致[7:4]清零。在中断发生后,我们应该读取这个寄存器,将其清零,否则同样的中断不再产生。 ### (2)把BCD码转换为ascii码 前面的代码中,把时分秒都读取出来并且压栈了。下一步的工作就是出栈,在屏幕上显示。前文已经说过,CMOS RAM中保存的日期和时间,默认是8421 BCD编码,所以我们可以利用一个过程,把BCD编码转换成与其对应的ascii码。 ~~~ bcd_to_ascii: ;BCD码转ASCII ;输入:AL=bcd码 ;输出:AX=ascii mov ah,al ;分拆成两个数字 and al,0x0f ;仅保留低4位 add al,0x30 ;转换成ASCII shr ah,4 ;逻辑右移4位 and ah,0x0f add ah,0x30 ret ~~~ 举个例子来说吧,比如前面我们读取了小时到AL中,比如是12时,那么al=00010010b;前文我们压栈是把AX压进去,也就是说AX的低8位(AL)是有用的。现在我们需要调用这个过程,把00010010b转换成0x3132(因为字符‘1’对应的ASCII码是0x31,字符‘2’对应的ASCII码是0x32)。 mov ah,al ;分拆成两个数字 and al,0x0f ;仅保留低4位(就是个位) add al,0x30 ;把个位转换成ASCII shr ah,4 ;逻辑右移4位 ,ah中是十位数字 and ah,0x0f add ah,0x30 ;把十位转换成ASCII OK,这样之后,AX的高八位就是十位的ASCII,低八位就是个位的ASCII; ### (3)把时间信息显示在屏幕上 ~~~ mov ax,0xb800 mov es,ax pop ax call bcd_to_ascii mov bx,12*160 + 36*2 ;从屏幕上的12行36列开始显示 mov [es:bx],ah mov [es:bx+2],al ;显示两位小时数字 mov al,':' mov [es:bx+4],al ;显示分隔符':' not byte [es:bx+5] ;反转显示属性 ~~~ 前两句让es指向了显示缓冲区; pop ax ;小时出栈 call bcd_to_ascii ;转为ASCII码 mov bx,12*160 + 36*2 ;从屏幕上的12行36列开始显示 mov [es:bx],ah ;显示小时的十位 mov [es:bx+2],al ;显示小时的个位 mov al,':' mov [es:bx+4],al ;显示分隔符':' not byte [es:bx+5] ;反转显示属性 其实前两句可以写成 mov [es:bx+4],':’ ;显示分隔符':' not是按位取反指令,假如之前属性是0x07(黑底白字),那么Not之后就是0xf8(闪烁白底灰色字)。 ~~~ pop ax call bcd_to_ascii mov [es:bx+6],ah mov [es:bx+8],al ;显示两位分钟数字 mov al,':' mov [es:bx+10],al ;显示分隔符':' not byte [es:bx+11] ;反转显示属性 pop ax call bcd_to_ascii mov [es:bx+12],ah mov [es:bx+14],al ;显示两位小时数字 ~~~ 上面的代码同理。 ~~~ mov al,0x20 ;中断结束命令EOI out 0xa0,al ;向从片发送 out 0x20,al ;向主片发送 ~~~ 书上162页已经说明:在中断处理过程的结尾,我们要显式地向8259芯片写中断结束命令EOI(至于具体原因,可以参考361页,图17-17:8259A的初始化命令字)。如果外部中断是8259主片处理的,那么仅发送给主片即可,端口号是0x20;如果外部中断是由从片处理的,那么命令既要发给主片也要发给从片,端口号是0xa0. 中断结束命令的代码是0x20. ~~~ pop es pop dx pop cx pop bx pop ax iret ~~~ 寄存器出栈,用iret命令返回。 ### 4.主程序 ### (1)初始化 ~~~ start: mov ax,[stack_segment] mov ss,ax mov sp,ss_pointer mov ax,[data_segment] mov ds,ax mov bx,init_msg ;显示初始信息 call put_string mov bx,inst_msg ;显示安装信息 call put_string ~~~ 这就是程序的入口了。首先,设置栈段,栈段被安排在整个程序的末尾,保留了256字节。 ~~~ SECTION stack align=16 vstart=0 resb 256 ss_pointer: ;=============================================================================== SECTION program_trail program_end: ~~~ 之后,设置好DS,令其指向数据段;然后显示一些信息。 ### (2)中断初始化和安装 [![70dd1691gac5bc07f4bdf&690_thumb[3]](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f8790f1.jpg "70dd1691gac5bc07f4bdf&690_thumb[3]")](http://img.blog.csdn.net/20160101232125420) 书上158页说:在计算机启动期间,BIOS会初始化中断控制器,将主片的中断号设为从0x08开始,从片的从0x70开始。从上图可以看出来,实时时钟连到了从片的IR0,也就是说实时时钟的中断号是0x70. ~~~ mov al,0x70 mov bl,4 mul bl ;计算0x70号中断在IVT中的偏移 mov bx,ax cli ;防止改动期间发生新的0x70号中断 ~~~ 前文已经说过: **中断向量在中断向量表中的位置=中断类型号×4** **N*4的字单元存放偏移地址;** **N*4+2的字单元存放段基址。** 我们已经知道中断类型号是0x70了,下面要计算它在中断向量表中的位置(也就是计算0x70*4):用乘法指令, AX=AL*r8; 前四句执行后,BX中就是0x70号中断向量在向量表中的偏移。 cli这个指令用来清除IF位标志,相当于屏蔽外部中断。因为在修改中断向量表时,如果表项信息只修改了一部分,这时候发生0x70号中断,将会产生不可预料的问题。 ~~~ push es mov ax,0x0000 mov es,ax mov word [es:bx],new_int_0x70 ;偏移地址。 mov word [es:bx+2],cs ;段地址 pop es ~~~ 将ES压栈(暂时保存),并使它指向中断向量表所在的段,把偏移地址设置为new_int_0x70 ,把段基地址设置为CS。最后恢复ES。 ~~~ mov al,0x0b ;RTC寄存器B or al,0x80 ;阻断NMI out 0x70,al mov al,0x12 ;设置寄存器B,禁止周期性中断,开放更 out 0x71,al ;新结束后中断,BCD码,24小时制 ~~~ 上面的代码用来设置寄存器B;寄存器B与本实验相关的位有: [7]: 0表示更新周期每秒都会发生;1表示中止当前的更新周期,此后也不再产生更新周期; [6]: 0表示禁止周期性中断,1表示允许周期性中断; [5]: 0表示闹钟中断禁止,1表示闹钟中断允许; [4]: 0表示禁止更新结束中断,1表示允许更新结束中断; [3]:该位空着不用; [2]:数据模式,0表示BCD,1表示2进制; [1]: 小时格式,0表示12小时制(bit7为0时表示AM,为1表示PM,举例:在BCD模式下,10010001b表示上午11点),1表示24小时制; [0]:该位空着不用; 从代码可以看出,我们写入寄存器B的值是0x12,也就是: [7]:0,允许更新周期发生; [6]:0,禁止周期性中断; [5]:0,禁止闹钟中断; [4]:1,允许更新结束中断; [3]:0 [2]:0,BCD模式 [1]:1,24小时制 [0]:0 ~~~ mov al,0x0c out 0x70,al in al,0x71 ;读RTC寄存器C,复位未决的中断状态 ~~~ 读寄存器C, 使之开始产生中断信号。注意,在向端口0x70写入al的同时,也打开了NMI,因为这是最后一次在主程序中访问RTC。到此,RTC芯片设置完毕。 ~~~ in al,0xa1 ;读8259从片的IMR寄存器 and al,0xfe ;清除bit 0(此位连接RTC) out 0xa1,al ;写回此寄存器 sti ;重新开放中断 ~~~   8259A内部有一个中断屏蔽寄存器,如下图所示: [![8259A内部结构](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f88f624.jpg "8259A内部结构")](http://img.blog.csdn.net/20160101232127631)   IMR是一个8位的寄存器,位0-7对应着引脚中断IR0-IR7;如果对应的位为0,则允许中断;为1,则屏蔽中断。 我们通过端口0xa1读取从片的IMR寄存器,用and指令清除bit0(其他位保持原样),然后再写回去。这样,关于中断的初始化就完成了。 最后,sti指令将IF置1,打开中断。从这时候开始,随时发生的中断就可以被处理了。 ~~~ mov bx,done_msg ;显示安装完成信息 call put_string mov bx,tips_msg ;显示提示信息 call put_string ~~~ 显示一些信息,表示中断设置和安装已完成。   ~~~ mov cx,0xb800 mov ds,cx mov byte [12*160 + 33*2],'@' ;屏幕第12行,33列 ~~~ 在屏幕12行33列显示一个“@”; ~~~ .idle: hlt ;使CPU进入低功耗状态,直到用中断唤醒 not byte [12*160 + 33*2+1] ;反转显示属性 jmp .idle ~~~ hlt是停机指令,使程序停止运行。这时候处理器进入暂停状态,不执行任何操作。当复位线上有复位信号、CPU响应非屏蔽中断、CPU响应可屏蔽中断3种情况之一发生时,CPU就会脱离暂停状态,执行hlt的下一条指令。   代码分析就到这里吧,下次我们看一下运行结果。![眨眼](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f8abd34.jpg)
';

8086中断系统

最后更新于:2022-04-01 16:22:07

# 80X86中断系统 - 能够处理256个中断 - 用中断向量号0~255区别 - 可屏蔽中断还需要借助专用中断控制器Intel 8259A实现优先权管理 ### 1、中断的分类 中断可以分为内部中断和外部中断。 (1)内部中断 - 除法错中断 - 指令中断 - 溢出中断 - 单步中断 - 断点中断 (2)外部中断 - 非屏蔽中断 - 可屏蔽中断 如果觉得上面的分类太抽象,那么下面的图会给你直观的印象。 [![中断框图](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f81dbd8.jpg "中断框图")](http://img.blog.csdn.net/20160101224459374) 通过这张图,我们可以明白,内部中断是在处理器内部产生。外部中断是通过两个信号线(NMI和INTR)引入处理器内部的。 ### 2.内部中断 ### (1)除法错中断 在执行除法指令时,若除数为0或商超过了寄存器所能表达的范围,则产生一个向量号为0的内部中断,称为除法错中断。 例如: mov bl,0 idiv bl ;除数BL=0,产生除法错中断 再比如: mov ax,200h mov bl,1 div bl ;商=200H,不能用AL表达,产生除法错中断 ### (2)指令中断 在执行中断调用指令INT n时产生的一个向量号为n(0 ~ 255)的内部中断,称为指令中断。 ### (3)溢出中断 在执行溢出中断指令INTO时,若溢出标志OF为1,则产生一个向量号为4的内部中断,被称为溢出中断。 ### (4)单步中断 CPU在执行完一条指令之后,如果检测到标志寄存器的TF位为1,则产生单步中断,单步中断的向量号为1. ### (5)断点中断 主要用在程序调试中,向量号为3,利用“int3(指令机器码为0xcc)”这条指令设置断点,目的是显示断点前程序的执行结果。 **注意:内部中断不受标志寄存器IF位的影响。** ### 3.外部中断 ### (1)非屏蔽中断 通过非屏蔽中断请求信号向微处理器提出的中断请求,微处理器无法禁止,将在当前指令执行结束予以响应,这个中断被称为非屏蔽中断。 非屏蔽中断的向量号为2,非屏蔽中断请求信号为NMI 非屏蔽中断主要用于处理系统的意外或故障。例如:电源掉电前的数据保护,存储器读写错误的处理。 ### (2)可屏蔽中断 通过INTR引脚引入CPU,只有当中断允许标志位IF=1时,才能被CPU响应。通过8259A,一个系统中可有多个可屏蔽中断。 ### 4.实模式下的中断向量表 ### (1)中断向量 中断向量就是中断服务程序的入口地址。它有两部分组成: - 中断服务程序所在段的段基址(2个字节) - 中断服务程序入口的偏移地址(2个字节) ### (2)中断向量表 每个中断向量占4个字节,256种中断向量总共占用1024字节。在8086系统中,所有的中断向量按类型码存放于内存的最低地址(00000H~003FFH)的1K单元中。存放中断向量的这1K单元称为中断向量表。 **中断向量在中断向量表中的位置=中断类型号×4** **N*4的字单元存放偏移地址;** **N*4+2的字单元存放段基址。** ### 5.中断类型号的获取 (1)对于除法出错,单步中断,不可屏蔽中断NMI,断点中断和溢出中断,CPU分别自动提供中断类型号0~4。 (2)对于用户自己确定的软件中断INT n,类型号由n决定。 (3)对外部可屏蔽中断INTR,CPU从可编程中断控制器8259A中获得中断类型号。 说明: 8086有两个引脚可以接收外部的中断请求:INTR和NMI - 当NMI(非屏蔽中断请求)引脚上出现上升沿信号时,CPU立即无条件(不执行中断响应周期,不受标志寄存器IF位的影响)地转入"2号中断处理程序"。 - 当INTR(可屏蔽中断请求)引脚上出现高电平信号时,若IF=0,CPU不响应中断请求。若IF=1,CPU响应中断请求。CPU响应中断时,首先执行"中断响应周期",以便从中断控制器8259获得中断类型码,然后根据中断类型码转入相应的中断处理程序。 中断响应周期由两个总线周期构成 1.第一个总线周期 CPU向外设发出一个低电平的中断应答信号INTA*,表示已经接受申请,要求外设传送中断向量号 2.第二个总线周期 外设传送中断向量号,CPU在T4的下降沿采样数据总线,读入外设传送来的中断向量号(如下图) [![总线周期](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f83c61c.jpg "总线周期")](http://img.blog.csdn.net/20160101224502851) ### 6.中断过程 (1)中断请求 中断源向CPU发出请求中断信号。中断信号将被锁存,直到CPU响应中断后,中断请求信号才被清除。 (2)中断响应 CPU在执行每条指令的最后一个时钟周期检测中断请求信号。若发现中断请求信号有效,在允许中断的条件下,CPU响应中断。 响应中断的过程可以总结为: - 取得中断类型码N; - pushf - TF=0,IF=0 - push CS - push IP - IP=N*4, CS=N*4+2,;转中断服务程序 ### 7.中断处理程序和iret指令 中断处理程序的编写方法和子程序比较相似,下面是常规的步骤: (1)保存用到的寄存器 (2)处理中断 注意:由于IF标志被设置为0,在中断处理中,处理器将不再响应硬件中断。如果希望更高优先级的中断嵌套,可以在编写中断处理程序时,适时用sti指令开放中断。 (3)恢复用到的寄存器 (4)用iret指令返回 iret指令的功能可以用汇编语法描述为: pop IP pop CS popf 需要说明的是:中断向量表的建立和初始化工作是由BIOS在计算机启动时完成的。BIOS为每个中断号填写入口地址,因为它不知道多数中断处理程序的位置,所以一律将它们指向同一个入口地址,在那里只有一条指令:iret. 也就是说,当这些中断发生时,只做一件事情——立即返回。当计算机启动后,操作系统和用户程序再根据自己的需要,修改某些中断的入口地址,使它指向自己的代码。
';

硬盘和显卡的访问与控制(三)

最后更新于:2022-04-01 16:22:03

上一篇博文我们用了很大的篇幅说了加载器,这一篇我们该说说用户程序了。 先看作者的源码吧。 ~~~ ;代码清单8-2 ;文件名:c08.asm ;文件说明:用户程序 ;创建日期:2011-5-5 18:17 ;=============================================================================== SECTION header vstart=0 ;定义用户程序头部段 program_length dd program_end ;程序总长度[0x00] ;用户程序入口点 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: ;=============================================================================== SECTION code_1 align=16 vstart=0 ;定义代码段1(16字节对齐) put_string: ;显示串(0结尾)。 ;输入:DS:BX=串地址 mov cl,[bx] or cl,cl ;cl=0 ? jz .exit ;是的,返回主程序 call put_char inc bx ;下一个字符 jmp put_string .exit: ret ;------------------------------------------------------------------------------- put_char: ;显示一个字符 ;输入:cl=字符ascii push ax push bx push cx push dx push ds push es ;以下取当前光标位置 mov dx,0x3d4 mov al,0x0e out dx,al mov dx,0x3d5 in al,dx ;高8位 mov ah,al mov dx,0x3d4 mov al,0x0f out dx,al mov dx,0x3d5 in al,dx ;低8位 mov bx,ax ;BX=代表光标位置的16位数 cmp cl,0x0d ;回车符? jnz .put_0a ;不是。看看是不是换行等字符 mov ax,bx ;此句略显多余,但去掉后还得改书,麻烦 mov bl,80 div bl mul bl mov bx,ax jmp .set_cursor .put_0a: cmp cl,0x0a ;换行符? jnz .put_other ;不是,那就正常显示字符 add bx,80 jmp .roll_screen .put_other: ;正常显示字符 mov ax,0xb800 mov es,ax shl bx,1 mov [es:bx],cl ;以下将光标位置推进一个字符 shr bx,1 add bx,1 .roll_screen: cmp bx,2000 ;光标超出屏幕?滚屏 jl .set_cursor mov ax,0xb800 mov ds,ax mov es,ax cld mov si,0xa0 mov di,0x00 mov cx,1920 rep movsw mov bx,3840 ;清除屏幕最底一行 mov cx,80 .cls: mov word[es:bx],0x0720 ; space add bx,2 loop .cls mov bx,1920 .set_cursor: mov dx,0x3d4 mov al,0x0e out dx,al mov dx,0x3d5 mov al,bh out dx,al mov dx,0x3d4 mov al,0x0f out dx,al mov dx,0x3d5 mov al,bl out dx,al pop es pop ds pop dx pop cx pop bx pop ax ret ;---------------------------------- 用户程序入口 -------------------------------------------- start: ;初始执行时,DS和ES指向用户程序头部段 mov ax,[stack_segment] ;设置到用户程序自己的堆栈 mov ss,ax mov sp,stack_end mov ax,[data_1_segment] ;设置到用户程序自己的数据段 mov ds,ax mov bx,msg0 call put_string ;显示第一段信息 push word [es:code_2_segment] mov ax,begin push ax ;可以直接push begin,80386+ retf ;转移到代码段2执行 continue: mov ax,[es:data_2_segment] ;段寄存器DS切换到数据段2 mov ds,ax mov bx,msg1 call put_string ;显示第二段信息 jmp $ ;=============================================================================== SECTION code_2 align=16 vstart=0 ;定义代码段2(16字节对齐) begin: push word [es:code_1_segment] mov ax,continue push ax ;可以直接push continue,80386+ retf ;转移到代码段1接着执行 ;=============================================================================== SECTION data_1 align=16 vstart=0 msg0 db ' This is NASM - the famous Netwide Assembler. ' db 'Back at SourceForge and in intensive development! ' db 'Get the current versions from http://www.nasm.us/.' db 0x0d,0x0a,0x0d,0x0a db ' Example code for calculate 1+2+...+1000:',0x0d,0x0a,0x0d,0x0a db ' xor dx,dx',0x0d,0x0a db ' xor ax,ax',0x0d,0x0a db ' xor cx,cx',0x0d,0x0a db ' @@:',0x0d,0x0a db ' inc cx',0x0d,0x0a db ' add ax,cx',0x0d,0x0a db ' adc dx,0',0x0d,0x0a db ' inc cx',0x0d,0x0a db ' cmp cx,1000',0x0d,0x0a db ' jle @@',0x0d,0x0a db ' ... ...(Some other codes)',0x0d,0x0a,0x0d,0x0a db 0 ;=============================================================================== SECTION data_2 align=16 vstart=0 msg1 db ' The above contents is written by LeeChung. ' db '2011-05-06' db 0 ;=============================================================================== SECTION stack align=16 vstart=0 resb 256 stack_end: ;=============================================================================== SECTION trail align=16 program_end: ~~~ 接下来我们分块分析。 ~~~ SECTION header vstart=0 ;定义用户程序头部段 program_length dd program_end ;程序总长度[0x00] ;用户程序入口点 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: ~~~ 这段代码用来定义用户程序的头部。头部格式在上一篇博文已经说过了。 因为标号program_end所在的段没有指定vstart==XX,所以program_end的汇编地址就从程序开头(=0)开始计算,它所代表的汇编地址就是整个程序的大小(以字节计算)。 每个段都有一个汇编地址,它是相对于整个程序开头(0)的,为了方便取得某个段的汇编地址,NASM编译器提供了如下表达式: section.段名称.start 如图所示: ![取得某个段的汇编地址](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f7a949e.jpg "") 因为程序的入口点在code_1段中,所以是 ~~~ dw start ;偏移地址[0x04] dd section.code_1.start ;段地址[0x06] ~~~ 其他语句源码中都有注释,很好理解。 ~~~ put_char: ;显示一个字符 ;输入:cl=字符ascii,ch=字符属性 push ax push bx push cx push dx push ds push es ;以下取当前光标位置 mov dx,0x3d4 mov al,0x0e out dx,al mov dx,0x3d5 in al,dx ;高8位 mov ah,al mov dx,0x3d4 mov al,0x0f out dx,al mov dx,0x3d5 in al,dx ;低8位 mov bx,ax ;BX=代表光标位置的16位数 cmp cl,0x0d ;回车符? jnz .put_0a ;不是。看看是不是换行等字符 mov ax,bx ;此句略显多余,但去掉后还得改书,麻烦 mov bl,80 div bl mul bl mov bx,ax jmp .set_cursor .put_0a: cmp cl,0x0a ;换行符? jnz .put_other ;不是,那就正常显示字符 add bx,80 jmp .roll_screen .put_other: ;正常显示字符 mov ax,0xb800 mov es,ax shl bx,1 mov [es:bx],cl mov [es:bx+1],ch ;这句是我自己加的,我想让字符属性通过ch传递 ;以下将光标位置推进一个字符 shr bx,1 add bx,1 .roll_screen: cmp bx,2000 ;光标超出屏幕?滚屏 jl .set_cursor mov ax,0xb800 mov ds,ax mov es,ax cld mov si,0xa0 mov di,0x00 mov cx,1920 rep movsw mov bx,3840 ;清除屏幕最底一行 mov cx,80 .cls: mov word[es:bx],0x0720 ; space add bx,2 loop .cls mov bx,1920 .set_cursor: mov dx,0x3d4 mov al,0x0e out dx,al mov dx,0x3d5 mov al,bh out dx,al mov dx,0x3d4 mov al,0x0f out dx,al mov dx,0x3d5 mov al,bl out dx,al pop es pop ds pop dx pop cx pop bx pop ax ret ~~~ 以上这段代码是为了在光标位置处显示一个字符,并推进光标到下一个字符,还考虑到了滚屏。这段代码书中有详细的讲解,这里就不赘述了。 唯一需要说明的是,我希望可以显示不同颜色的字,所以在里面加了一句 mov [es:bx+1],ch; ch是字符属性 ~~~ put_string: ;显示串(0结尾)。 ;输入:DS:BX=串地址 mov cl,[bx] or cl,cl ;cl=0 ? jz .exit ;是的,返回主程序 call put_char inc bx ;下一个字符 jmp put_string .exit: ret ~~~ 这段代码又是一个过程,里面调用了put_char,看来过程是可以嵌套的! or cl,cl 这句指令不会影响到cl里面的值,但计算结果会影响标志寄存器的某些位。如果ZF置位,说明cl的内容为0,也就是串结束标志。 ~~~ 我们从程序入口处看,要强调的是,当加载器把执行权交给用户程序的时候,DS和ES都指向用户程序的头部段,也就是指向用户程序的最开始。 start: ;初始执行时,DS和ES指向用户程序头部段 mov ax,[stack_segment] ;设置到用户程序自己的堆栈 mov ss,ax mov sp,stack_end ~~~ mov ax,[stack_segment] ;这句指令是到段的重定位表中取修正后的SS段的段基址。 ~~~ mov ax,[data_1_segment] ;设置到用户程序自己的数据段 mov ds,ax mov bx,msg0 call put_string ;显示第一段信息 ~~~ 从重定位表中获得重定位之后data_1段的基址,赋值给ds,这样之后,ds就不再指向用户程序的头部,而是指向data_1段。 ~~~ push word [es:code_2_segment] mov ax,begin push ax ;可以直接push begin,80386+ retf ;转移到代码段2执行 ~~~ 这段代码用段超越前缀 es:code_2_segment 访问重定位表,把code_2段的基地址压栈。(因为此时ds已经不再指向用户头部了,但是es还是指向用户头部);然后再把code_2段内的一个标号begin(代表偏移地址)也压栈。 cpu执行retf指令时,相当于执行 pop ip pop cs 这样,执行retf的时候,程序就相当于转移了,转移到代码段2执行。 ~~~ ;====================================================== SECTION code_2 align=16 vstart=0 ;定义代码段2(16字节对齐) begin: push word [es:code_1_segment] mov ax,continue push ax ;可以直接push continue,80386+ retf ;转移到代码段1接着执行 ~~~ 代码段2其实什么也没有干,干的事情就是转移到代码段1的continue处,原理和上面一样。 于是开始执行: ~~~ continue: mov ax,[es:data_2_segment] ;段寄存器DS切换到数据段2 mov ds,ax mov bx,msg1 call put_string ;显示第二段信息 jmp $ ~~~ 这段代码就是调用过程,显示信息。 好了,下面我们可以把代码修改一下,显示自己想要的东西。 比如在显示字符串前,给ch赋值,0X02表示绿色,0X04表示红色。 ![用户代码修改1](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f7e3412.jpg "") 我们也可以自定义要显示的字符。 我修改后的用户代码如下: ~~~ ;代码清单8-2 ;文件名:c08.asm ;文件说明:用户程序 ;创建日期:2011-5-5 18:17 ;=============================================================================== SECTION header vstart=0 ;定义用户程序头部段 program_length dd program_end ;程序总长度[0x00] ;用户程序入口点 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: ;=============================================================================== SECTION code_1 align=16 vstart=0 ;定义代码段1(16字节对齐) put_string: ;显示串(0结尾)。 ;输入:DS:BX=串地址 ;ch:属性 mov cl,[bx] or cl,cl ;cl=0 ? jz .exit ;是的,返回主程序 call put_char inc bx ;下一个字符 jmp put_string .exit: ret ;------------------------------------------------------------------------------- put_char: ;显示一个字符 ;输入:cl=字符ascii push ax push bx push cx push dx push ds push es ;以下取当前光标位置 mov dx,0x3d4 mov al,0x0e out dx,al mov dx,0x3d5 in al,dx ;高8位 mov ah,al mov dx,0x3d4 mov al,0x0f out dx,al mov dx,0x3d5 in al,dx ;低8位 mov bx,ax ;BX=代表光标位置的16位数 cmp cl,0x0d ;回车符? jnz .put_0a ;不是。看看是不是换行等字符 mov ax,bx ;此句略显多余,但去掉后还得改书,麻烦 mov bl,80 div bl mul bl mov bx,ax jmp .set_cursor .put_0a: cmp cl,0x0a ;换行符? jnz .put_other ;不是,那就正常显示字符 add bx,80 jmp .roll_screen .put_other: ;正常显示字符 mov ax,0xb800 mov es,ax shl bx,1 mov [es:bx],cl mov [es:bx+1],ch ;以下将光标位置推进一个字符 shr bx,1 add bx,1 .roll_screen: cmp bx,2000 ;光标超出屏幕?滚屏 jl .set_cursor mov ax,0xb800 mov ds,ax mov es,ax cld mov si,0xa0 mov di,0x00 mov cx,1920 rep movsw mov bx,3840 ;清除屏幕最底一行 mov cx,80 .cls: mov word[es:bx],0x0720 ; space add bx,2 loop .cls mov bx,1920 .set_cursor: mov dx,0x3d4 mov al,0x0e out dx,al mov dx,0x3d5 mov al,bh out dx,al mov dx,0x3d4 mov al,0x0f out dx,al mov dx,0x3d5 mov al,bl out dx,al pop es pop ds pop dx pop cx pop bx pop ax ret ;---------------------------------- 用户程序入口 -------------------------------------------- start: ;初始执行时,DS和ES指向用户程序头部段 mov ax,[stack_segment] ;设置到用户程序自己的堆栈 mov ss,ax mov sp,stack_end mov ax,[data_1_segment] ;设置到用户程序自己的数据段 mov ds,ax mov bx,msg0 mov ch,0x02 ;green call put_string ;显示第一段信息 push word [es:code_2_segment] mov ax,begin push ax ;可以直接push begin,80386+ retf ;转移到代码段2执行 continue: mov ax,[es:data_2_segment] ;段寄存器DS切换到数据段2 mov ds,ax mov bx,msg1 mov ch,0x04 ;red call put_string ;显示第二段信息 ;这里我们显示出多彩的Hello mov cx,128 ;循环次数 mov ah,0 @1: push cx mov bx,msg2 mov ch,ah call put_string ;显示Hello pop cx inc ah ;属性值增加1 loop @1 jmp $ ;=============================================================================== SECTION code_2 align=16 vstart=0 ;定义代码段2(16字节对齐) begin: push word [es:code_1_segment] mov ax,continue push ax ;可以直接push continue,80386+ retf ;转移到代码段1接着执行 ;=============================================================================== SECTION data_1 align=16 vstart=0 msg0 db ' This is NASM - the famous Netwide Assembler. ' db 'Back at SourceForge and in intensive development! ' db 'Get the current versions from http://www.nasm.us/.' db 0x0d,0x0a,0x0d,0x0a db ' Example code for calculate 1+2+...+1000:',0x0d,0x0a,0x0d,0x0a db ' xor dx,dx',0x0d,0x0a db ' xor ax,ax',0x0d,0x0a db ' xor cx,cx',0x0d,0x0a db ' @@:',0x0d,0x0a db ' inc cx',0x0d,0x0a db ' add ax,cx',0x0d,0x0a db ' adc dx,0',0x0d,0x0a db ' inc cx',0x0d,0x0a db ' cmp cx,1000',0x0d,0x0a db ' jle @@',0x0d,0x0a db ' ... ...(Some other codes)',0x0d,0x0a,0x0d,0x0a db 0 ;=============================================================================== SECTION data_2 align=16 vstart=0 msg1 db ' The above contents is written by LeeChung. ' db '2011-05-06' db 0x0d,0x0a,0x0d,0x0a db 0 msg2 db 'Hello' db 0 ;=============================================================================== SECTION stack align=16 vstart=0 times 256 db 0 stack_end: ;=============================================================================== SECTION trail align=16 program_end: ~~~ OK,看一下结果吧,这就是多彩的Hello ![修改的用户代码2](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f801bc5.jpg "") 【the end 】
';

硬盘和显卡的访问与控制(二)

最后更新于:2022-04-01 16:22:00

上一篇博文我们讲了如何看到实验结果,这篇博文我们着重分析源代码。 书中作者为了说明原理,约定了一种比较简单地用户程序头部格式,示意图如下(我参考原书图8-15绘制的,左边的数字表示偏移地址): ![用户程序头部结构示意图](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f559e93.jpg "") 所以,如果用户程序要利用本章的源码c08_mbr.asm生成的加载器来加载的话,就应该遵循这种头部格式。 下面我们讲解源码c08_mbr.asm(粘贴的源代码不一定和配书的代码完全一样,因为有些地方我加了注释) ~~~ ;代码清单8-1 ;文件名:c08_mbr.asm ;文件说明:硬盘主引导扇区代码(加载程序) ;创建日期:2011-5-5 18:17 app_lba_start equ 100 ;声明常数(用户程序起始逻辑扇区号) ;常数的声明不会占用汇编地址 SECTION mbr align=16 vstart=0x7c00 ;设置堆栈段和栈指针 mov ax,0 mov ss,ax mov sp,ax mov ax,[cs:phy_base] ;计算用于加载用户程序的逻辑段地址 mov dx,[cs:phy_base+0x02] mov bx,16 div bx mov ds,ax ;令DS和ES指向该段以进行操作 mov es,ax ;以下读取程序的起始部分 xor di,di mov si,app_lba_start ;程序在硬盘上的起始逻辑扇区号 xor bx,bx ;加载到DS:0x0000处 call read_hard_disk_0 ;以下判断整个程序有多大 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 ;回填修正后的入口点代码段基址 ;开始处理段重定位表 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] ;转移到用户程序 ;------------------------------------------------------------------------------- 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 ;------------------------------------------------------------------------------- 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 ;------------------------------------------------------------------------------- phy_base dd 0x10000 ;用户程序被加载的物理起始地址 times 510-($-$$) db 0 db 0x55,0xaa ~~~ app_lba_start equ 100 ;声明常数(用户程序起始逻辑扇区号) 这句话作者假定用户程序从硬盘第100扇区开始。所以在我们把这个源文件对应的.bin文件写入虚拟硬盘的时候,要从逻辑扇区100开始写。 equ 类似于C语言中的#define,用来定义一个常量。 一般使用格式: 符号名 EQU 表达式 作用是左边的符号名代表右边的表达式。 注意:不会给符号名分配存储空间,符号名不能与其它符号同名,也不能被重新定义 SECTION mbr align=16 vstart=0x7c00 解释: NASM编译器用SECTION或者SEGMENT来定义段。mbr是段名称(可以随便起); 注意:如果整个程序都没有段定义语句,那么整个程序自成一个段(这点好像和MASM不同哦!); align=16 表示16字节对齐; vstart=0x7c00,关于这个,我们就不得不多说几句了。 ==================插叙部分================ 汇编地址以及标号的本质: 1. 所谓汇编地址,就是编译器给源程序中每条指令定义的地址,由于编译后的程序可以在内存中浮动(即可以装载在内存中的任意位置),因此直接用绝对地址(20位的实模式下的物理内存地址)来给源程序中的指令定位的话将不利于程序在内存中的浮动; 2. 汇编地址定位规则: (1)一般规则: i. 如果在没有使用特殊指令的一般情况下(特别是vstart指令),整个源程序中第一条指令的汇编地址为0,之后所有指令的汇编地址都是相对于整个源程序第一条指令的偏移地址,即使程序中分了很多段也是如此。在这种情况下,如果将整个源程序看做一个段的话则汇编地址就是段内偏移地址; ii. 在NASM中,所有的标号实质上就是其所在处指令的汇编地址,在编译后会将所有标号都替换成该汇编地址值(即立即数); (2)特殊规则: i. 如果在定义段的时候使用了vstart伪指令,比如 “section my_segment vstart=15”, 则会提醒汇编器,该段起始指令的汇编地址是15,段内的其它指令的汇编地址都是距该段起始指令地址的偏移量加上15;因此,vstart伪指令就是指定段的起始汇编地址;如果vstart=0,则段内的汇编地址就是段内的偏移地址!(这种手法经常使用!) ii. 使用NASM规则的标准段,是指section .data、section .text、section .bss,这三种标准段都默认包含有vstart=0,因此段内的指令以及标号的汇编地址都是段内偏移地址,并且在加载程序的时候会自动使cs指向.text,ds指向.bss,es指向.data,而无需人手工执行对段寄存器赋值的步骤,而对于i.中的定义段的方式则没有这种自动的步骤,需要亲手对段寄存器进行赋值(是这样吗?从网上搜来的,我不能肯定。) (3) 引用标号: i. 和MASM不一样的是NASM大大简化了对标号的引用,不需要再用seg和offset对标号取段地址和偏移地址了; ii. 在NASM中,标号就是一个立即数,而这个立即数就是汇编地址; iii. 在NASM中不再有MASM中数据标号的概念,也就不存在什么arr[5]之类的内存寻址形式了! iv. 在NASM中所有出现标号的地方都会用标号的汇编地址替换,因此诸如mov ax, tag之类的指令,仅仅就是将一个立即数(tag的汇编地址)传送至ax而已,而不是取tag地址内的数据了!如果要取标号处内存中的数据就必须使用[ ](类似C语言中的指针运算符*); ==================插叙结束================ 处理器加电或者复位后,BIOS会执行硬件检测和初始化程序,如果没有错误,接下来就会进行操作系统引导。 BIOS会根据CMOS(一块可读写的RAM芯片,保存系统当前的硬件配置和用户的设定参数)里记录的启动顺序逐个地来尝试加载启动代码。 具体的过程是BIOS将磁盘的第一扇区(磁盘最开始的512字节,也就是主引导扇区)载入内存,放在0X0000:0X7C00处,然后检查这个扇区的最后两个字节是不是“0x55AA”,如果是则认为这是一个有效的启动扇区,如果不是就会尝试下一个启动介质; 如果主引导扇区有效,则以一个段间转移指令 jmp 0x0000:0x7c00 跳过去继续执行; 如果所有的启动介质都判断过后仍然没有找到可启动的程序,那么BIOS会给出错误提示。 所以,代码中的vstart=0x7c00不是空穴来风,而是根据代码被加载的实际位置决定的。 ![主引导程序加载到内存后的地址变化](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f5a0dc9.jpg "") 当这段程序刚被加载到内存后, CS=0x0000, IP=0x7c00 如上图所示,假设不写vstart=0x7c00,那么标号“number”的偏移地址就从程序头(认为是0)开始算起,为0x012e; 但是实际上“number”的段内偏移地址是0x7d2e(0x012e+0x7c00=0x7d2e)! 为了修正这个偏移地址的差值,于是有vstart=0x7c00,也就是说段内所有指令的汇编地址都在原来的基础上加上0x7c00. 这里还要再补充一点,如果看这个源文件对应的列表文件,是看不出来偏移地址被加了0x7c00的。 列表文件的一个截图如下: ![列表文件的截图](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f5e565e.jpg "") 看到了吗?第一条指令的汇编地址,还是从0开始的! 而且 SECTION mbr align=16 vstart=0x7c00 这句话还是出现在了列表文件里。 我的理解是,列表文件仅仅是对源码的第一遍扫描吧。在后面的扫描中,0x7c00就起作用了。 举个例子吧, 上图有一行 16 00000007 2EA1[CA00] mov ax,[cs:phy_base] 列表文件的末尾有 151 000000CA 00000100 phy_base dd 0x10000 也就是说 phy_base 这个标号的汇编地址就是00CA(这时候7C00还没有起作用) 我们再看一下编译后的二进制文件 ![编译后的二进制文件](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f602277.jpg "") 在偏移为0x07的地方,对应的指令码是 2EA1CA7C 注意到其中的CA7C(低字节在前面)了吗? 这个就是00CA+7C00=7CCA的结果啊! 我们继续看代码, ;设置堆栈段和栈指针 mov ax,0 mov ss,ax mov sp,ax 定义栈需要两个连续的步骤,即初始化SS和SP. *——————-小贴士—————- 原书P158上方:处理器在设计的时候就规定,当遇到修改段寄存器SS的指令时,在这条指令和下一条指令执行完毕期间,禁止中断,以此来保护栈。也就是说,我们应该在修改SS的指令之后,紧接着一条修改SP的指令。 ——————————————–* 因为已经设置了SP=SS=0,所以第一次执行PUSH指令时,先把SP减2,即0x0000-0x000=0xFFFE(借位被忽略);然后把内容送入SS:SP指向的内存单元处。如下图所示(文章中画的只是示意图,不是按照比例画的,凑合看) ![栈的示意图](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f620ee9.jpg "") ~~~ mov ax,[cs:phy_base] ;计算用于加载用户程序的逻辑段地址 mov dx,[cs:phy_base+0x02] mov bx,16 div bx mov ds,ax ;令DS和ES指向该段以进行操作 mov es,ax ~~~ 代码的末尾部分有 phy_base dd 0x10000 ;用户程序被加载的物理起始地址 也就是说作者安排把用户程序加载到物理内存0x10000处,(我们完全可以修改成别的16字节对齐的地址,只要把用户程序加载到一个空闲的地方就可以。) 上面这几行的意思是根据物理地址计算出逻辑段地址,[DX:AX]是被除数,BX的内容是除数(16),计算结果在AX(对于本程序,结果就是0x1000)中。然后令DS和ES都指向这个段。 ~~~ ;以下读取程序的起始部分 xor di,di mov si,app_lba_start ;程序在硬盘上的起始逻辑扇区号 xor bx,bx ;加载到DS:0x0000处 call read_hard_disk_0 ~~~ 这段代码的最后调用了过程 read_hard_disk_0,我们看一下过程调用的代码,我在代码中加了一些注释: ~~~ read_hard_disk_0: ;从硬盘读取一个逻辑扇区 ;输入:DI:SI=起始逻辑扇区号 ; DS:BX=目标缓冲区地址 ;使用LBA28寻址方式 push ax push bx push cx push dx; 用到的寄存器压栈保存 mov dx,0x1f2 mov al,1 out dx,al ;读取的扇区数为1 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 ~~~ 要理解这段,先看下面的示意图(参照原书图8-11画的) inc dx ;0x1f6 mov al,0xe0 ;LBA28模式,主盘 or al,ah ;LBA地址27~24 out dx,al ![端口0x1F6各位的含义](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f67ac18.jpg "") mov al,0xe0 表示选择LBA模式,选择主硬盘 注意,在调用这个过程的时候,DI:SI=起始逻辑扇区号,DI的低四位是有效的,高四位应该为0,其实这里我觉得应该加一句, mov al,0xe0 这句后面加一句 and ah,0x0f 目的是把DI的高四位清零,万一调用者忘记清零了,这样做可以防止意外发生。 ~~~ inc dx ;0x1f7 mov al,0x20 ;读命令 out dx,al ~~~ 当把起始LBA扇区号设置好后,就可以发出读命令了。上面的代码表示向端口0x1F7写入0x20,请求读硬盘。 接下来等待读请求完成。端口0x1F7既是命令端口,也是状态端口。部分状态位的含义如图: ![端口0x1f7](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f6bb505.jpg "") ~~~ .waits: in al,dx ;读端口的值 and al,0x88 ;提取出bit3和bit7 cmp al,0x08 ;bit3==1且bit7==0说明准备好了 jnz .waits ;否则继续检查 ~~~ 一旦硬盘准备好了,就可以读取数据了。0x1F0是硬盘接口的数据端口,是16位的。可以连续从这个端口读取数据。 mov cx,256 in ax,dx 这两句话就表示读取了一个字的数据(16位)到AX中 ~~~ mov cx,256 ;总共要读取的字数 mov dx,0x1f0 .readw: in ax,dx mov [bx],ax ;读取的数据放在数据段,偏移地址由BX指定 add bx,2 loop .readw ~~~ 现在我们再回到那部分代码,就很容易理解了。 ~~~ xor di,di ;di清零 (因为我们传入逻辑扇区号是100,不超过16 bits) mov si,app_lba_start ;程序在硬盘上的起始逻辑扇区号 xor bx,bx ;加载到DS:0x0000处 call read_hard_disk_0 ~~~ 执行到这里,内存大概如下图所示: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f70257f.jpg "") ~~~ ;以下判断整个程序有多大 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 ;恢复数据段基址到用户程序头部段 ~~~ 上面这段代码是为了把剩余的用户程序读到内存里(以扇区为单位) 我们分别讲解。 ~~~ ;以下判断整个程序有多大 mov dx,[2] 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 ~~~ 因为已经约定了用户程序的头部4个字节是用户程序的总长度,所以这里取总长度到[dx:ax]中,把[dx:ax]除以512,就能得到有几个扇区。dx存放余数,ax存放商。 如果dx==0,那么就把ax减一(因为前面已经读了一个扇区),继续执行@1;如果dx!=0,那么剩余的扇区数就是ax,然后跳到@1; 开始执行@1处的代码时,ax已经保存了还要读取的扇区数,但是这个值也有可能为0,如果为0,就不用再读取了, jz direct就可以;如果不为0,就执行下面的代码。 好了,如果你觉得上面说得不够清楚,那么看这个简单的流程图吧: ![是否继续读扇区流程图](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f73d3b8.jpg "") ~~~ ;读取剩余的扇区 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 ;恢复数据段基址到用户程序头部段 ~~~ mov ax,ds add ax,0x20 mov ds,ax ;这三行表示调整ds的位置,让ds指向最后读入的块的末尾,也就是将要读入的块的开始。其他语句都好理解,这里就不解释了。 接下来是处理段的重定位表。我们要修正每个表项的值。 为什么要修正呢?看图就明白了。 ![修正段地址](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f751b87.jpg "") 用户程序在编译的时候,每个段的段地址都是相对于程序开头(0)计算的。但是用户程序被加载器加到到物理地址[phy_base]的时候,相当于每个段的物理地址都向后偏移了[phy_base],所以我们要修正这个差值。 我们看看代码: ~~~ 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 ~~~ add ax,[cs:phy_base] adc dx,[cs:phy_base+0x02]; 这两句其实是做了一个20位数的加法,修正后的物理地址是[dx:ax]; shr ax,4 ror dx,4 and dx,0xf000 or ax,dx; 这四句是求出段基地址(16位),也就是逻辑段地址,结果在AX中。然后回填到原处(仅覆盖低16位,高16位不用管)。 为什么要求出段基地址呢?因为在用户程序中,对段寄存器赋值,都是从这里引用的。 ~~~ ;计算入口点代码段基址 direct: mov dx,[0x08] mov ax,[0x06] call calc_segment_base mov [0x06],ax ;回填修正后的入口点代码段基址 ;开始处理段重定位表 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] ;转移到用户程序 ~~~ 只要参考本文开头的用户程序头部示意图,上面这段代码不难理解。 需要说明的是 jmp far [0x04] ;这个是16位间接绝对远转移指令。一定要使用关键字far。处理器执行这条指令的时候,会访问DS所指向的数据段,从偏移地址0x04处取出两个字(低字是偏移地址,高字是段基址),用低字代替IP的内容,用高字代替CS的内容,于是就可以转移了。
';

硬盘和显卡的访问与控制(一)

最后更新于:2022-04-01 16:21:58

本文是《x86汇编语言:从实模式到保护模式》(电子工业出版社)的读书实验笔记。 这篇文章我们先不分析代码,而是说一下在Bochs环境下如何看到实验结果。 1. 需要的源码文件 第一个文件是加载程序 ~~~ ;代码清单8-1 ;文件名:c08_mbr.asm ;文件说明:硬盘主引导扇区代码(加载程序) ;创建日期:2011-5-5 18:17 app_lba_start equ 100 ;声明常数(用户程序起始逻辑扇区号) ;常数的声明不会占用汇编地址 SECTION mbr align=16 vstart=0x7c00 ;设置堆栈段和栈指针 mov ax,0 mov ss,ax mov sp,ax mov ax,[cs:phy_base] ;计算用于加载用户程序的逻辑段地址 mov dx,[cs:phy_base+0x02] mov bx,16 div bx mov ds,ax ;令DS和ES指向该段以进行操作 mov es,ax ;以下读取程序的起始部分 xor di,di mov si,app_lba_start ;程序在硬盘上的起始逻辑扇区号 (扇区号为28位,存放在di:si) xor bx,bx ;加载到DS:0x0000处 call read_hard_disk_0 ;以下判断整个程序有多大 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 ;回填修正后的入口点代码段基址 ;开始处理段重定位表 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] ;转移到用户程序 ;------------------------------------------------------------------------------- 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 ;------------------------------------------------------------------------------- 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 ;------------------------------------------------------------------------------- phy_base dd 0x10000 ;用户程序被加载的物理起始地址 times 510-($-$$) db 0 db 0x55,0xaa ~~~ 需要说明的是: 书上假设是从硬盘启动,本文假设从软盘启动 第二个文件是用户程序 ~~~ ;代码清单8-2 ;文件名:c08.asm ;文件说明:用户程序 ;创建日期:2011-5-5 18:17 ;=============================================================================== SECTION header vstart=0 ;定义用户程序头部段 program_length dd program_end ;程序总长度[0x00] ;用户程序入口点 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: ;=============================================================================== SECTION code_1 align=16 vstart=0 ;定义代码段1(16字节对齐) put_string: ;显示串(0结尾)。 ;输入:DS:BX=串地址 mov cl,[bx] or cl,cl ;cl=0 ? jz .exit ;是的,返回主程序 call put_char inc bx ;下一个字符 jmp put_string .exit: ret ;------------------------------------------------------------------------------- put_char: ;显示一个字符 ;输入:cl=字符ascii push ax push bx push cx push dx push ds push es ;以下取当前光标位置 mov dx,0x3d4 mov al,0x0e out dx,al mov dx,0x3d5 in al,dx ;高8位 mov ah,al mov dx,0x3d4 mov al,0x0f out dx,al mov dx,0x3d5 in al,dx ;低8位 mov bx,ax ;BX=代表光标位置的16位数 cmp cl,0x0d ;回车符? jnz .put_0a ;不是。看看是不是换行等字符 mov ax,bx ;此句略显多余,但去掉后还得改书,麻烦 mov bl,80 div bl mul bl mov bx,ax jmp .set_cursor .put_0a: cmp cl,0x0a ;换行符? jnz .put_other ;不是,那就正常显示字符 add bx,80 jmp .roll_screen .put_other: ;正常显示字符 mov ax,0xb800 mov es,ax shl bx,1 mov [es:bx],cl ;以下将光标位置推进一个字符 shr bx,1 add bx,1 .roll_screen: cmp bx,2000 ;光标超出屏幕?滚屏 jl .set_cursor mov ax,0xb800 mov ds,ax mov es,ax cld mov si,0xa0 mov di,0x00 mov cx,1920 rep movsw mov bx,3840 ;清除屏幕最底一行 mov cx,80 .cls: mov word[es:bx],0x0720 ; space add bx,2 loop .cls mov bx,1920 .set_cursor: mov dx,0x3d4 mov al,0x0e out dx,al mov dx,0x3d5 mov al,bh out dx,al mov dx,0x3d4 mov al,0x0f out dx,al mov dx,0x3d5 mov al,bl out dx,al pop es pop ds pop dx pop cx pop bx pop ax ret ;---------------------------------- 用户程序入口 -------------------------------------------- start: ;初始执行时,DS和ES指向用户程序头部段 mov ax,[stack_segment] ;设置到用户程序自己的堆栈 mov ss,ax mov sp,stack_end mov ax,[data_1_segment] ;设置到用户程序自己的数据段 mov ds,ax mov bx,msg0 call put_string ;显示第一段信息 push word [es:code_2_segment] mov ax,begin push ax ;可以直接push begin,80386+ retf ;转移到代码段2执行 continue: mov ax,[es:data_2_segment] ;段寄存器DS切换到数据段2 mov ds,ax mov bx,msg1 call put_string ;显示第二段信息 jmp $ ;=============================================================================== SECTION code_2 align=16 vstart=0 ;定义代码段2(16字节对齐) begin: push word [es:code_1_segment] mov ax,continue push ax ;可以直接push continue,80386+ retf ;转移到代码段1接着执行 ;=============================================================================== SECTION data_1 align=16 vstart=0 msg0 db ' This is NASM - the famous Netwide Assembler. ' db 'Back at SourceForge and in intensive development! ' db 'Get the current versions from http://www.nasm.us/.' db 0x0d,0x0a,0x0d,0x0a db ' Example code for calculate 1+2+...+1000:',0x0d,0x0a,0x0d,0x0a db ' xor dx,dx',0x0d,0x0a db ' xor ax,ax',0x0d,0x0a db ' xor cx,cx',0x0d,0x0a db ' @@:',0x0d,0x0a db ' inc cx',0x0d,0x0a db ' add ax,cx',0x0d,0x0a db ' adc dx,0',0x0d,0x0a db ' inc cx',0x0d,0x0a db ' cmp cx,1000',0x0d,0x0a db ' jle @@',0x0d,0x0a db ' ... ...(Some other codes)',0x0d,0x0a,0x0d,0x0a db 0 ;=============================================================================== SECTION data_2 align=16 vstart=0 msg1 db ' The above contents is written by LeeChung. ' db '2011-05-06' db 0 ;=============================================================================== SECTION stack align=16 vstart=0 resb 256 stack_end: ;=============================================================================== SECTION trail align=16 program_end: ~~~ 2.利用源码生成.bin文件 nasm -f bin c08_mbr.asm -o c08mbr.bin nasm -f bin c08.asm -o c08usr.bin 这时候会提示: c08.asm:203: warning: uninitialized space declared in stack section: zeroing “这句话的意思是,c08.asm源程序的第203行声明了未初始化的空间。”本实验的栈空间可以不初始化,所以不用管这个警告。 3.把c08mbr.bin写入启动软盘文件 dd if=c08mbr.bin of=a.img 4.制作一个空的硬盘 因为根据源码,我们知道用户程序在硬盘上,所以我们要制作一个硬盘镜像文件——利用工具bximage 在命令行输入 bximage,其他操作如图所示 ![利用bximage制作硬盘映像文件](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f513574.jpg "") ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f52fe8a.jpg "") 注意到最下面的提示了吗? “The following line should appear in your bochsrc: ata0-master: type=disk, path=”c.img”, mode=flat, cylinders=2, heads=16, spt=63” 由于我们启动Bochs的时候没有用默认的配置文件,所以我们需要在自己的配置文件中添加这一行 ~~~ ata0-master: type=disk, path="c.img", mode=flat, cylinders=2, heads=16, spt=63 ~~~ 需要注意的是,我们这张硬盘有2016个扇区(每个扇区大小是512字节) 5.把用户程序c08usr.bin写入硬盘镜像 `dd if=c08usr.bin of=c.img bs=512 seek=100 conv=notrunk` 注意:notrunc的含义是,如果目标文件(这里就是c.img)比来源文件大,不截断目标文件多出来的那部分内容。 比如c.img已经有1024字节了,而c08usr.bin假设只有512字节,如果seek=0,那么不加conv=notrunc,c.img就会变成512字节。 【dd命令说明】 if=输入文件 of=输出文件 ibs = n_bytes 一次读取n_bytes字节,即读入缓冲区的字节数。 obs = n_bytes 一次写入n_bytes字节,即写 入缓冲区的字节数。 bs = n_bytes 同时设置读/写缓冲区的字节数(等于设置ibs和obs)。 count=n_blocks:仅拷贝 n_blocks 个块,块大小等于 ibs 指定的字节数 conv=sync:把每个输入记录的大小都调到ibs的大小(不足的话用Null填充)。 skip=xxx指在备份时对if 后面的部分也就是原文件跳过多少块再开始备份(block size由ibs指定 ) seek=xxx指在备份时对of 后面的部分也就是目标文件跳过多少块再开始写(block size由obs指定 ) 6.启动Bochs,查看结果 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8f541019.jpg "") 我们可以看到,应用程序成功地被加载器加载了。 关于本文的源码,我们下次再说明。 Goodbye
';

读书笔记之引言

最后更新于:2022-04-01 16:21:56

有幸结识了《X86汇编语言:从实模式到保护模式》一书。我觉得这本书非常好,语言活泼,通俗易懂,源码丰富,受益匪浅。读罢一遍,意犹未尽。于是打算再读一遍,并把自己的读书所学总结成笔记,一来给自己留个纪念,二来供各位访客参考。初出茅庐,牛刀小试,如有错误,实属必然。还请广大博友批评指正,不吝赐教。   在此也向本书的作者——李忠、王晓波(我联想到了作家王小波,其实他曾经也是程序员![吐舌鬼脸](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-29_56d3a8fb25553.jpg) )、余洁表示敬意。希望他们能出更多的好书。   关于本书的配书文件和源代码,我已经上传到 [http://download.csdn.net/detail/u013490896/9388139](http://download.csdn.net/detail/u013490896/9388139 "http://download.csdn.net/detail/u013490896/9388139") 欢迎朋友下载。   我曾写过一首集句诗,贴在这里,以之自勉。百折不回,贵在坚持!                      劝学(集句) 少年易学老难成,一寸光阴不可轻。 窗竹影摇书案上,野泉声入砚池中。 板凳须坐十年冷,文章不写半句空。 纸上得来终觉浅,绝知此事要躬行。
';

前言

最后更新于:2022-04-01 16:21:53

> 原文出处:[《x86汇编语言:从实模式到保护模式》读书笔记](http://blog.csdn.net/column/details/protectedmode.html) 作者:[longintchar](http://blog.csdn.net/longintchar) **本系列文章经作者授权在看云整理发布,未经作者允许,请勿转载!** # 《x86汇编语言:从实模式到保护模式》读书笔记 > 《x86汇编语言:从实模式到保护模式》这本书令我受益匪浅。我利用工作之余,把对这本书的理解总结成读书笔记,可以看成是对书上内容的总结与延伸。
';