外部硬件中断:处理器接两根线NMI
传输非屏蔽中断(即无法屏蔽的中断)和INTR
传输可屏蔽中断
IR0到IR7优先级依次降低
处理器中中断标志位起决定作用,IF为0时屏蔽所有INTR引脚来的信号都被屏蔽
BIOS创建中断向量表
实时时钟和CMOS RAM
CMOS RAM中的日期和时间通常使用二进制形式的十进制编码(BCD)来表示
实时时钟RTC电路可以发出的中断信号类型:
- 周期性中断信号
- 更新周期结束中断
- 闹钟中断
周期性中断信号
以固定周期进行中断
寄存器A指定时基和速率
寄存器B控制周期性中断是否运行
更新周期结束中断
更新周期:每隔一秒更新时间、检查数据有效性、检查闹钟、写回更新的数据
闹钟中断
实时时钟发出的中断信号的类型判断:
寄存器C只读,读取后自动清零
程序中修改SS寄存器和修改SP寄存器的指令必须相邻,否则可能会在中间发生中断导致栈错误
安装中断处理过程
SECTION code align=16 vstart=0 ;定义代码段(16字节对齐)
new_int_0x70:...iret...
start:mov ax,[stack_segment]mov ss,axmov sp,ss_pointermov ax,[data_segment]mov ds,axmov al,0x70 ; 中断号0x70mov bl,4mul bl ; 中断号乘4得到中断向量表IVT中对应中断的入口地址所在的地址mov bx,axcli ; 关中断,防止中断发生push esmov ax,0x0000 ; IVT所在段的地址mov es,axmov word [es:bx],new_int_0x70 ;写入自定义的中断处理过程的偏移地址mov word [es:bx+2],cs ;写入自定义的中断处理过程的段地址pop es
启用更新周期结束中断
mov al,0x0b ; RTC寄存器Bor al,0x80 ;最高位置1,NMI引脚外接一个与门的输出,输入一个是端口0x70最高位的反相,另一个是NMI中断信号,置1可阻断NMI out 0x70,al ; 0x70为索引端口,用于指定CMOS RAM内的单元mov al,0x12 ;设置寄存器B,禁止周期性中断,开放更新结束后中断,BCD码,24小时制 0001_0010out 0x71,al ; 0x71为数据端口,将寄存器B的设置信息输出给寄存器Bmov al,0x0c ; RTC寄存器Cout 0x70,alin al,0x71 ;读RTC寄存器C,复位未决的中断状态in al,0xa1 ;读8259从片的IMR寄存器(中断屏蔽寄存器,0为允许1为屏蔽)and al,0xfe ;清除最低位,此位连接RTC,开放RTC中断out 0xa1,al ;写回此寄存器 sti ;重新开放中断
中断处理过程:
等待更新周期结束
SECTION code align=16 vstart=0 ;定义代码段(16字节对齐)
new_int_0x70:push axpush bxpush cxpush dxpush es.w0: mov al,0x0a ; 寄存器Aor al,0x80 out 0x70,al ;阻断NMI。当然,通常是不必要的in al,0x71 ;读寄存器Atest al,0x80 ;本质是与操作,但是结果不保留,ZF为0表示测试位不是0,测试第7位UIP jnz .w0
更新周期时间段:
读取BCD码的时间:
xor al,al ; 清零al,秒的内容在CMOS RAM中存放的地址为0x00
or al,0x80 ; 阻断NMI
out 0x70,al ; 索引端口
in al,0x71 ;读RTC当前时间(秒,8位)
push axmov al,2
or al,0x80
out 0x70,al
in al,0x71 ;读RTC当前时间(分,8位)
push axmov al,4
or al,0x80
out 0x70,al
in al,0x71 ;读RTC当前时间(时,8位)
push axmov al,0x0c ;寄存器C的索引。最高位为0,开放NMI
out 0x70,al
in al,0x71 ;读一下RTC的寄存器C,清零寄存器C,否则只发生一次中断,因为已经把闹钟和周期性中断关闭了,所以不考虑
BCD码转ASCII:
bcd_to_ascii: ;BCD码转ASCII;输入:AL=bcd码,8位;输出:AX=asciimov ah,al ;分拆成两个数字 and al,0x0f ;仅保留低4位 add al,0x30 ;转换成ASCII shr ah,4 ;逻辑右移4位,保留高4位 add ah,0x30ret
显示并退出中断:
pop ax ; 取出小时的BDC码
call bcd_to_ascii
mov bx,12*160 + 36*2 ;从屏幕上的12行36列开始显示,行号*80*2 + 列号*2 = 显存中的偏移地址mov [es:bx],ah
mov [es:bx+2],al ;显示两位小时数字mov [es:bx+4], ':' ;显示分隔符':'
not byte [es:bx+5] ;反转显示属性 pop ax
call bcd_to_ascii
mov [es:bx+6],ah
mov [es:bx+8],al ;显示两位分钟数字mov [es:bx+10], ':' ;显示分隔符':'
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 axiret
处理器进入低功耗状态:hlt
指令,在该指令行停机,但能被外部中断唤醒并继续执行之后的代码
.idle:hlt ;使CPU进入低功耗状态,直到用中断唤醒not byte [12*160 + 33*2+1] ;反转显示属性 jmp .idle
内部中断:处理器内部中断,处理器直接到中断对应处理过程处执行代码
软中断:代码产生中断
int 中断号
int3
,在调试时将原指令机器码改为0XCC(即int3
)从而中断去处理调试相关程序,调试完改行后将0XCC改回原机器码into
溢出中断,标志寄存器OF为1时才进入中断处理过程
BIOS中断:
ROM BIOS搜索C0000~E0000这个区域,当有55AA这字则执行对应外设的功能调用例程代码,这个代码初始化设备,并把设备的外部中断号放入中断向量表中
mov cx,msg_end-message ; 获取message的字符长度
mov bx,message.putc:mov ah,0x0e ; 0x0e命令mov al,[bx]int 0x10 ; 0x10中断,ah为0x0e,查bios中断表可知是显示字符(光标前移)功能,AL=字符,BL=前景色(图形模式)inc bx ; 选中下一个字符loop .putc.reps:mov ah,0x00int 0x16 ; 0x16中断,ah为0x00,从键盘读字符功能mov ah,0x0emov bl,0x07int 0x10 ; 显示字符中断jmp .reps
32位处理器的寄存器扩展和扩充
段寄存器:CS, DS ES, FS, GS, SS,长度还是16位
- 实模式:(段地址<<4)+偏移地址 最大10FFEF,8086只有20根地址线,只能访问FFFF,80386有32根地址线,可以访问到10FFEF,FFFF~10FFEF这个区域称为高位内存区(HMA)。虽然32位处理器有32根地址线,但是实模式下超出10FFEF的内存是无法访问的
- 保护模式:在描述符表中登记每个段的各种信息。段寄存器选中一个描述符,处理器取出描述符段基地址,和程序中给出的偏移地址组合,得到完整的内存地址
在保护模式下,一个段想被访问就必须去登记
gdt_size dw 0 ; 一个字
gdt_base dd 0x00007e00 ;GDT的物理地址 一个双字;计算GDT所在的逻辑段地址
mov ax,[cs:gdt_base+0x7c00] ;低16位,主引导扇区开始是0x7c00,需要加上
mov dx,[cs:gdt_base+0x7c00+0x02] ;高16位
mov bx,16
div bx ; 右移4位
mov ds,ax ;逻辑段地址,令DS指向该段以进行操作
mov bx,dx ;逻辑偏移地址,段内起始偏移地址
S=1时
段界限:低32位0 ~ 15,高32位16 ~ 19
G位:高双字的第23位,粒度,用于解释段界限的单位,G=0段界限以字节为单位,G=1以4k字节为单位
实际使用的段界限 = 描述符中的段界限 * 0x1000G + 0xFFF * G
P位:高双字的第15位,段存在位,P=10段不存在,访问报错
L位:高双字的第21位,64位代码段标志
D/B位:高双字的第22位,操作尺寸(是代码段时)/栈上部边界(是数据段时),D/B=0该段按16位进行操作,D/B=1该段按32位进行操作
AVL位:高双字的第20位,保留位
安装存储器的段描述符
;DS:BX指向GDT起始地址
;创建0#描述符,它是空描述符,这是处理器的要求
mov dword [bx+0x00],0x00
mov dword [bx+0x04],0x00 ;创建#1描述符,保护模式下的代码段描述符
mov dword [bx+0x08],0x7c0001ff
mov dword [bx+0x0c],0x00409800
;初始化描述符表寄存器GDTR
mov word [cs:gdt_size+0x7c00], 15 ;主引导程序从7c00开始,加上偏移量gdt_size,定位到描述符表的界限,两个描述符,8字节*2-1(总字节数减一) lgdt [cs:gdt_size+0x7c00] ; 指定gdt_size的地址,取出6字节(2字节描述符表界限,4字节描述符表起始地址)赋给GDTR寄存器
打开A20地址线:为了使用到地址为0x100000以上的内存,开启第21条地址线(A20)
in al, 0x92 ;南桥芯片内的0x92端口
or al, 0000_0010B ; 第1位置1
out 0x92, al ;打开A20
进入保护模式:设置CR0寄存器的PE位(第0位)为1
cli ;保护模式下中断机制尚未建立,应禁止中断
mov eax,cr0
or eax,1 ; 最低位PE置1
mov cr0,eax
保护模式下的内存访问:
段选择器内容不再是逻辑段地址,而是称为段选择子
mov cx,00000000000_10_000B ;加载数据段选择子(0x10)
mov ds,cx ; 此时ds的描述符高速缓存器中存放目标段基地址mov byte [0x00],'P' ; 使用ds的描述符高速缓存器存放的段地址进行内存访问(0xb800)
实模式下:
指令操作尺寸:一段指令的操作数的长度
32位特有内存寻址:
可以看到操作尺寸为16位数据和32位数据时,相同格式的汇编指令的机器指令相同
- 16位操作尺寸允许8位或16位操作数,以及16位有效地址
- 32位操作尺寸允许8位或32位操作数,以及32位有效地址
- 32位处理器支持16位和32位操作尺寸
- 同一时刻处理器只能按一种操作尺寸工作,这种操作尺寸称为默认操作尺寸
指定默认操作尺寸:bits 16/32
;创建#2描述符,保护模式下的代码段描述符(主引导程序)
mov dword [bx+0x08],0x7c0001ff ; D位是1,即用32位操作尺寸
mov dword [bx+0x0c],0x00409800
...;以下进入保护模式... ...
jmp dword 0000_0000_0000_0010_0_00B:flush ;16位的描述符选择子:32位偏移,选择第3个描述符表项(主引导程序)
bits 32 ; 因为在描述符中选择32位操作尺寸,所以之后的代码需要默认尺寸32位来编译,在此之前的代码是按16位编译
flush:...
;在创建描述符表或进行段选择时,都需要对描述符表进行操作,这时对DS赋值有两种方式,传统编译器编译结果不同
mov ds, ax ;16位操作尺寸,编译8E D8;32位操作尺寸,编译66 8E D8
mov ds, eax ;16位操作尺寸,编译66 8E D8;32位操作尺寸,编译8E D8
;在32位操作尺寸下优先选后者
;NASM编译两者结果无论什么操作尺寸,都是8E D8
修改段寄存器时的保护:
- 检查选择子的合法性
- 确认描述符类型——描述符类型是否与段寄存器匹配,P位是否为1对于CS和SS的选择器,不允许向其传送为0的选择子
代码段执行时的保护:
- 执行的代码未超过段界限
数据段:
;创建1#描述符,这是一个数据段,对应0~4GB的线性地址空间
mov dword [ebx+0x08],0x0000ffff ;基地址为0,段界限为0xfffff
mov dword [ebx+0x0c],0x00cf9200 ;粒度为4KB,存储器段描述符
;这个数据段囊括了整个4G的地址空间,包括代码段,所以虽然代码段无法直接通过代码段的描述符写入,但是可以通过这个数据段写入;创建数据段描述符,实际用做第一个栈段
mov dword [ebx+0x18],0x6c0007ff ; 基地址0x00006c00,向上扩展,D/B=1
mov dword [ebx+0x1c],0x00409200 ; 界限0x7ff,粒度字节,数据段描述符
;段的扩展方向决定了处理器检查段界限时的检查方向,与栈的推进方向无关
;描述符中的段界限决定了偏移量的变化范围,即栈的容量
;描述符的D/B=1时,使用ESP,反之使用SP
;初始化时将SP/ESP设为段大小,即段界限+1(0x800)
;压栈,高位向低位推进
push dword 0x072e074d ;压入一个双字
; | 07 | 0x07ff
; | 2e |
; | 07 |
; | 4d | <- esp = 0x07fc
向上扩展的数据段,在写入数据之前进行检查:操作数的有效地址(SP/ESP - 压栈值的大小,算的是存入数据的空间) + 操作数的大小(字节)- 1 <= 实际使用的段界限
检查通过后即可写入,写入后SP/ESP指向写入的最后一个栈元素
;出栈
pop dword [0x0b800]
; | 07 | 0x07ff
; | 2e |
; | 07 |
; | 4d | <- esp = 0x07fc
向上扩展的数据段,在弹出数据之前进行检查:操作数的有效地址(SP/ESP) + 操作数的大小(字节)- 1 <= 实际使用的段界限
同样,弹出的数据在存入数据段时也要进行检查:000b8000 + 4 - 1 <= ffff_ffff
内存线性地址回绕特性
向下扩展的段作为栈段:
;创建数据段描述符,实际用作第二个栈段
mov dword [ebx+0x20],0x7c00fffe ;基地址为0c00007c00,向下扩展,D/B=1
mov dword [ebx+0x24],0x00cf9600 ;段界限为0xffffe,粒度为4KB,数据段描述符
;初始化使SP/ESP=0
向下扩展的数据段,在写入数据之前进行检查:操作数的有效地址(SP/ESP - 压栈值的大小,算的是存入数据的空间)> 实际使用的段界限
;压栈
; |-ffffffff
; |--fffffffc <- esp=0-4=fffffffc 负数
; |--fffff000 实际使用段界限+1
; 00007c00 | 07 |-------------|-00000000 段基地址
; | 68 | <- esp
向下扩展的数据段,在弹出数据之前进行检查:操作数的有效地址(SP/ESP)> 实际使用的段界限
xchg al,ah
,交换两操作数内容,可以同时是寄存器,不能同时是内存地址
冒泡排序:
;开始冒泡排序
mov ecx,pgdt-string-1 ;遍历次数=串长度-1
@@1:push ecx ;32位模式下的loop使用ecx xor bx,bx ;32位模式下,偏移量可以是16位,也可以 @@2: ;是后面的32位 mov ax,[string+bx] cmp ah,al ;ah中存放的是源字的高字节 jge @@3 xchg al,ah mov [string+bx],ax @@3:inc bx loop @@2 pop ecx loop @@1