[李忠老师从实模式到保护模式] 第1-10章总结

第1-10章对应王爽汇编语言的全部内容,简要总结.

bochs调试器使用基本和gdb相同,使用help命令可以查看所有命令, help p可以查看p的用法

1. 查看0x7c00开始的内存数据 512B 以16进制格式 Byte为单位显示

xp /512xb 0x7c00
xp: 查看内存数据
/512: 512B
/x:16进制格式
/b: Byte为单位显示

2. 在0x7c00处设置断点

b 0x7c00

3. c -> continue

4. r命令可以查看寄存器,但是为啥看不了cs/es/ds的值

看不了cs/es/ds的值 -> 使用srg命令查看

5. s -> step 单步

step 有个缺点,执行mov到显存的指令后,无法在终端中看出来,需要使用next指令

6. n -> next

7. u -> 查看后续的指令

u/17 查看后续的17条指令

8. info reg -> 查看寄存器信息

info flags(16bits)/eflags(32bits)/rflags(64bits) 查看标志寄存器 -> 若以小写显示标志,那么就是0,否则就是1

9. jmp

jmp cs:ip -> 段转移 修改cs:ip
jmp reg -> 段间转移 修改 ip
jmp short 8位相对偏移量 -> EB 8位相对偏移量 --> short可选,如果不写short且小于FF那么编译器就用 jmp short
jmp near 16位相对偏移量 -> E9 16位相对偏移量, near可选,如果写了near那么一定要用16位的相对偏移量

10. div -> 只能用于无符号除法操作 , idiv -> 有符号数除法操作 (机器码 F6)

div 8位寄存器或内存地址 -> 商AL, 余数AH -> div bh/ div byte [0x2002]
div 16位寄存器或内存地址 -> 被除数是32位的 被除数低16位AX,高16位DX; 商AX; 余数DX -> div bx / div word [0x2002]
div 32位寄存器或内存地址 -> 80386以后支持,8086不支持 -> 被除数是64位的 被除数低32位EAX,高32位EDX; 商EAX; 余数EDX -> div ebx / div dword [0x2002]
div 64位寄存器或内存地址 -> 32位处理器不支持,64位处理器才支持 -> 被除数是128位的 被除数低64位RAX,高64位RDX; 商RAX; 余数RDX -> div rbx / div qword [0x2002]

11. xor -> exclusive or -> xor ax, ax 设置ax=0

xor r/m, r/m/imm ; r: 寄存器, m是内存地址, imm立即数

为什么使用xor ax,ax 取代 mov ax,0 ?
因为xor ax,ax左右操作数都是寄存器,执行速度比mov ax,0更快

12. add r/m, r/m/imm sub r/m, r/m/imm neg r/m (取反)

13. times

times 5 mov ax,bx -> 执行5次 mov ax, bx

14. 续行符 \

mytest db ‘a’
‘b’
–>
mytest db ‘a’
db ‘b’

15. 串传送指令 -> movsb/movsw

准备工作:
1. DS:SI 原始数据串段地址:偏移地址
2. ES:DI 目标位置的段地址:偏移地址
3. 设置DF方向标志寄存器 -> cld : 方向标志清零指令,就是递增传输,++i ; std: 方向标志置1指令,就是递减传输,–i
4. 设置cx寄存器,即设置循环次数,每次循环会–cx,当cx0,ZF就为1,结束movsb/movsw
5. rep movsb/movsw, 配合rep指令就可以实现循环调用movsb/movsw的功能,直到cx
0

16. $ / $$

$: 当前行的汇编地址 -> jmp $ , 重复跳转到自身
$$: 当前程序段的起始汇编地址

17. LOOP

机器码 E2 8位相对偏移量 -> cx!=0?跳转到标号:往下执行

18. 8086只能用BX(Base Address register)/SI(Source index)/DI(destination index)/BP(Base pointer register)提供偏移地址

允许的基址变址组合: bx+si/bx+di/bp+si/bp+di

19. inc r/m dec r/m

20. jns -> 根据sf标志位的值,sf==1?向下执行:跳转到标号处

21. 有符号的符号扩展指令

cbw ; 将AL中的有符号数扩展到AX,若AL=FD(-3),扩展后AX=FFFD(-3),后面类推
cwde ; 将AX中的有符号数扩展到EAX
cdqe ; 将EAX中的有符号数扩展到RAX

cwd ; 将AX有符号数扩展到DX:AX , 若AX=FFFD(-3),扩展后DX=FFFF,AX=FFFD,后面类推
cdq ; 将EAX中的有符号数扩展到EDX:EAX
cdo ; 将RDA中的有符号数扩展到RDA:RAX

e.g.
mov ax,-6002
cwd
mov bx,-10
idiv bx

22. flags介绍

CF(0): Carry Flag 进位/借位
PF(2): Parity Flag 奇偶标志,算术操作结果低8位有偶数个1,此标志是1
OF(11): Overflow Flag 溢出标志,溢出为1
e.g.
ah=1111 1101, add ah,5 -> OF=0 因为AH是负数
ah=115, add ah,ah ; -> OF=1 因为结果是230

ZF(6): Zero Flag 运算结果=0时此标志是1
SF(7): sign flag 运算结果最高位设置此标志,0正1负
AF(4): Adjust flag 算术操作在结果的位3产生进位或借位时,标志是1. 用于2进制编码的十进制算法中(BCD码),用得很少
e.g.
mov ah,247
sub ah,8 ; 执行后AF=1

1111 0111
0000 1000
-> 1110 1111

在这里插入图片描述

23 转移指令

下面仅列出部分转移指令
在这里插入图片描述
转移指令不影响标志位,但是要依赖标志位.

jl : jmp if less -> cmp dh, 0 ; jl negb -> 按有符号数实现了dh小于0时转移 ,有符号转移jl
jae: jmp above or equal -> cmp dh, 0x80 ; jae negb -> 按无符号数实现了dh小于0时转移,无符号转移jae
jcxz: jmp if cx is zero

cmp r/m, r/m/imm ; 影响CF/OF/SF/ZF/AF/PF

24. push r/m pop r/m 寄存器只能是16位的

注意栈平衡,没有栈平衡的程序是有bug的

25. print-stack 打印栈ss:sp内容

26. 寻址方式

a. 寄存器寻址 如 mov ax,cx / add bx,0xf000 / inc dx
b. 立即数寻址 如 mov bx,0xf000
c. 内存寻址 如 mov ax,[0x5c0f] / add word [0x0230],0x5000 / xor byte [es:mydata],0x05
d. 基址寻址 使用基址寄存器(bx/bp)的寻址方式就是基址寻址 如 mov bx,sp; mov dx,[ss:bx+2]
e. 变址寻址 使用变址寄存器(si/di)的寻址方式就是变址寻址 如 mov [si+0x100],al
f. 基址变址寻址 使用基址寄存器+变址寄存器+可选的偏移量 看王爽汇编的总结

27. 第8章 最后的习题2

jmp near start

message db '1+2+3+...+1000='

start:
	mov ax,0x7c0
	mov ds,ax

	mov ax,0xb800
	mov es,ax

	mov si,message
	mov di,0
	mov cx,start-message

	s0:
		mov al,[ds:si]
		mov [es:di],al
		mov byte [es:di+1],0x07
		inc si
		add di,2
		loop s0
	
	xor ax,ax
	xor dx,dx
	mov cx,1000

	s1:
		add ax,cx
		adc dx,0
		loop s1
	
	xor cx,cx
	mov ss,cx
	mov sp,cx
	mov bx,10
	
	s2:
		inc cx
		div bx
		or dl,0x30
		push dx
		xor dx,dx
		cmp ax,0
		jne s2
	
	s3:
		pop dx
		mov [es:di],dl
		inc di
		mov byte [es:di],0x07
		inc di
		loop s3
	
	jmp near $
times 510-($-$$) db 0
db 0x55,0xaa

28. 段的声明

SECTION seg_name ALIGN=16 VSTART=0x100 --> 声明seg_name的段,对齐长度为16B(可选),段地址从起始地址为0x100(可选)处开始计算

29. 用户程序

a. 用户程序头部至少包含如下信息:
	i. 程序总长度
	ii. 入口
	iii. 段重定位表项数
	iv. 段重定位表

在这里插入图片描述

30. 加载器的工作流程

a. 读取用户程序的起始扇区 -> 用户程序起始逻辑扇区号是100
b. 把整个用户程序都读入内存
c. 计算段的物理地址和逻辑段地址(段的重定位)
d. 转移到用户程序执行(将处理器的控制权交给用户程序)

31. in / out 指令

a. in al,dx / in ax,dx -> in / out指令不影响任何标志位
b. in al, imm8 / in ax, imm8
c. out dx/imm8, al/ax

; 读或写操作例子
out 0x60, al/ax

mov dx, 0x3c0
out dx, ax
; 假定DS已经指向存放扇区数据段,BX是段内偏移地址
mov cx,256
mov dx,0x1f0
.readw:
	in ax,dx
	mov [bx],ax
	add bx,2
	loop .readw

32. shr/ror/shl/rol指令

shr/shl: 逻辑右左移动
ror/rol: 循环右左移动

33. resb/resw/resd imm

reserved bytes / words / double words -> 保留imm数量 * sizeof(byte / word / double words)的空间
resb 256 与 times 256 db 0 是有区别的,前者不会初始化这部分空间为0,而后者会

34. mul/imul

mul: 无符号整数乘法
imul: 有符号整数乘法

35. c09_mbr.asm -> 程序加载器 解析

加载器程序任务:

  1. 将用户程序从硬盘拷贝到内存中
  2. 计算加载到内存中的用户程序的实际段基地址,并填写到用户程序头部表和重定位表中
  3. 将cs:ip设置到用户程序的起始地址

流程:

         ;代码清单9-1
         ;文件名:c09_mbr.asm
         ;文件说明:硬盘主引导扇区代码(加载程序) 
         ;创建日期:2011-5-5 18:17,修改于2021-10-3 15:35
         
         app_lba_start equ 100           ;声明常数(用户程序起始逻辑扇区号)
                                         ;常数的声明不会占用汇编地址
                                    
SECTION mbr align=16 vstart=0x7c00                                     

         ;设置堆栈段和栈指针 为什么放在0x0请看p144 图9-7
		 ;0x0到0x0~0xffff作为主引导扇区程序(加载器)和栈空间
		 ;其中0x0:0xffff是栈顶,从高地址向低地址走
         mov ax,0
         mov ss,ax
         mov sp,ax
      
		 ; [cs:phy_base]存放加载用户程序的起始地址,phy_base的声明在最后
		 ; 将起始地址的段地址计算出来保存在ds/es中,后续使用
		 ; 计算方式是: 段地址=物理地址/16,将余数丢掉
         mov ax,[cs:phy_base]            ;计算用于加载用户程序的逻辑段地址
         mov dx,[cs:phy_base+0x02]
         mov bx,16
         div bx            
         mov ds,ax                       ;令DS和ES指向该段以进行操作
         mov es,ax
    
		 ;标号direct之前的代码的任务是:
		 ;循环从硬盘中读取扇区,直到把所有用户程序代码全部读出硬盘
		 ;读出来的程序存放在[phy_base:0]处开始的地址
		 ;
         ;以下读取程序的起始部分 
		 ;DI:SI=起始逻辑扇区号; DS:BX=目标缓冲区地址; 所以我们需要初始化di,si,bx的值
         xor di,di
		 ;程序在硬盘上的起始逻辑扇区号,为什么是100呢?这个是人为规定了,如果你规定105,那么就把用户程序烧录到105号扇区开始就可以了.
         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?为什么除尽的情况下要减一?
		 ; 举个例子:65/16 = 4,余数为1.那么我实际还要再读取4次,因为最后一次还有1个Bytes数据没有读
		 ;       而:64/16 = 4,余数为0.那么我实际还要再读取3次,因为前面已经读取一次了要减去.
		 ; 所以从这个例子可以知道:
		 ; 有余数的情况下,不用减,因为要多读一次来读取余数的那部分内容,而前面多读了一次又要减1
		 ; 多读一次,再少读一次,两个相抵,就不用减了.
		 ; 而没有余数的情况下,因为前面多读一次要减1.
		 ;
		 ; 再思考下不足16的情况:
		 ; 15 / 16 = 0,余数为15,这种情况下不会执行dec ax,会走到jz direct转而执行重定位逻辑
		 ;
         jnz @1                          ;未除尽,因此结果比实际扇区数少1
         dec ax                          ;已经读了一个扇区,扇区总数减1 
   @1:
         cmp ax,0                        ;考虑实际长度小于等于512个字节的情况 
         jz direct
         
         ;读取剩余的扇区
         push ds                         ;以下要用到并改变DS寄存器 

         mov cx,ax                       ;循环次数(剩余扇区数)
   @2:
		 ;
		 ;为什么要加上0x20?
		 ;因为用户程序被加载的位置是由ds/es指向的逻辑段
		 ;一个逻辑段最大也就64KB,即[0x0-0xffff],如果程序更大的话,又绕回到0x0,把0x0处的代码覆盖了
		 ;所以解决该问题的一个方案是:
		 ;每次往内存中加载一个扇区前,都重新将前面的数据尾部构造一个新的逻辑段
		 ;并把要读取的数据加载到这个新段内.
		 ;如此,每个段的大小是512Bytes,即16进制的0x200,右移4位是0x20,那么0x20就是各段地址差值
		 ;
		 ;为什么每个段长度是512Bytes?
		 ;这是因为每个sector的长度是512Bytes,所以这里的段的长度是512Bytes(没有使用满)
		 ;所以可以看到,每次我们只偏移0x20,为的就是能够读取下一个sector
		 ;
         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                          ;恢复数据段基址到用户程序头部段 
      
         ;计算入口点代码段基址 
		 ;这段代码做的事情就是计算实际加载到内存中的代码段基地址
		 ;并将代码段基地址更新到用户程序头部表的:
		 ;1. 入口点代码段基地址
		 ;2. 重定位表项基地址
   direct:
		 ;
		 ;[0x08] / [0x06] 存放什么内容? 为什么存放在这两个位置?
		 ;参考书p157,图9-15.在用户程序头部内,偏移为0x06处的双字,存放的是入口点代码段的汇编地址
		 ;加载器首先将高字和低字分别传送到寄存器DX/AX
		 ;然后调用过程calc_segment_base来计算代码段在内存中的段地址
		 ;重定位表项内容是什么? 
		 ;书p158-159,9.3.8节
		 ;简单来说每个重定位表项原先存储的是各自的逻辑段地址
		 ;由于该程序是个加载器,所以我们的任务是将逻辑段地址修正成实际加载到内存中的段地址
		 ;
         mov dx,[0x08]
         mov ax,[0x06]
         call calc_segment_base
         mov [0x06],ax                   ;回填修正后的入口点代码段基址 
      
         ;开始处理段重定位表
         mov cx,[0x0a]                   ;需要重定位的项目数量,同样参考p157 图9-15
         mov bx,0x0c                     ;重定位表首地址
          
 realloc:
         mov dx,[bx+0x02]                ;32位地址的高16位 
         mov ax,[bx]					 ;注意,这里用bx索引,所以段超越前缀是"ds:"
         call calc_segment_base
         mov [bx],ax                     ;回填段的基址
         add bx,4                        ;下一个重定位项(每项占4个字节) 
         loop realloc 
      
         jmp far [0x04]                  ;转移到用户程序  ,p157 图9-15,重定位完成后,[0x04]存储的是入口点:偏移地址
 
;-------------------------------------------------------------------------------
read_hard_disk_0:                        ;从硬盘读取一个逻辑扇区
                                         ;输入:DI:SI=起始逻辑扇区号
                                         ;      DS:BX=目标缓冲区地址
         push ax
         push bx
         push cx
         push dx
      
		 ; 1. 设置要读取的扇区数量
         mov dx,0x1f2
         mov al,1
         out dx,al                       ;读取的扇区数,要读取1个扇区,读取扇区数传入0x1f2端口

		 ; 2. 设置起始LBA扇区号(28位,故要把28位从低到高分别写入端口0x1f3/0x1f4/0x1f5/0x1f6)
         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

		 ; 3.向端口0x1f7写入0x20,	请求硬盘读
         inc dx                          ;0x1f7
         mov al,0x20                     ;读命令
         out dx,al

		 ; 4. 判断并等待硬盘准备就绪,0x1f7既是命令端口,又是状态端口.
		 ;    发送读写命令后,硬盘开始处理命令,等待硬盘处理完毕进入就绪状态(0x08) p151
  .waits:
         in al,dx
         and al,0x88
         cmp al,0x08
         jnz .waits                      ;不忙,且硬盘已准备好数据传输 

		 ; 5. 连续取出数据并存放到ds:[bx].0x1f0既是硬盘接口的数据端口,还是16位读写端口
         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位段基地址 
		 ;
		 ; 解释一下这个函数在做的事情:
		 ; 1. 先将用户程序在内存中的物理起始地址的低16位加到寄存器AX中
		 ; 2. 再将起始地址的高16位加到寄存器DX中
		 ; 3. 根据1/2,我们得到DS:AX是入口点代码段的起始物理地址,物理地址=dx*16+AX
		 ;    所以只需将该32位数右移4位即可得到逻辑段地址
		 ; 4. 移动逻辑是将ax的低4位不要了,然后将dx的低4位移到高4位,再将dx的低4位不要了,最后和ax做个或操作
		 ;    此时就实现了将dx的低4位搬到了ax的高4位的效果,得到逻辑段地址
		 ;
         push dx                          
         add ax,[cs:phy_base] ; 使用了段超越前缀"cs:"且没有加上0x7c00,因为段定义使用了"vstart=0x7c00"
         adc dx,[cs:phy_base+0x02]
         shr ax,4 ; ax右移4位,该指令执行后ax最高4位都是0
		 ; dx循环右移4位,并将最高4位(也就是右移前最低4位)挪到ax的最高4位
         ror dx,4
         and dx,0xf000
         or ax,dx
         
         pop dx
         
         ret

;-------------------------------------------------------------------------------
		 ; 为什么加载在这里?
		 ; 书p144,[0x10000,0x9ffff]都可以作为起始地址进行加载,但要满足两个条件
		 ; 1.区域是空闲的
		 ; 2.程序长度可以存放的进去(如果选择在0x9ffff进行加载,那么就会是错误的,因为程序长度>可用地址空间)
         phy_base dd 0x10000             ;用户程序被加载的物理起始地址
         
 times 510-($-$$) db 0
                  db 0x55,0xaa

36 文本输出程序

         ;代码清单9-2
         ;文件名:c09.asm
         ;文件说明:用户程序 
         ;创建日期:2011-5-5 18:17,修改于2021-10-3 15:35
         
;===============================================================================
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

         ;以下取当前光标位置
		 ;0x3d4是操作显卡的地址,通过数据端口从0x0e号端口读1B数据传送到AH
		 ;这是屏幕光标位置的高8位
         mov dx,0x3d4
         mov al,0x0e
         out dx,al
         mov dx,0x3d5
         in al,dx                        ;高8位 
         mov ah,al

		 ;取屏幕光标位置的低8位,与高8位同理
         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                     ;不是。看看是不是换行等字符 
		 ;字符是0x0d->回车: 先将当前光标位置除以80,余数不要,就是当前行号
		 ;在乘以80,就得到当前行首的字符数,若乘以2就是行首光标偏移
         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

         ;以下将光标位置推进一个字符
		 ;将寄存器BX的内容除以2,恢复bx光标的身份并+1,来推进一个字符
		 ;在恢复前bx是用作偏移值
         shr bx,1
         add bx,1

 .roll_screen:
		 ;标准VGA文本模式是25行,每行80个字符
		 ;25*80=2000,即VGA文本模式一个屏幕可以塞得下2000个字符
         cmp bx,2000                     ;光标超出屏幕?滚屏
         jl .set_cursor

         push bx

		 ;si=0xa0是第二行第一列的偏移(一行80个字符,80*2=160=0xa0)
		 ;di=0x00是第一行第一列的偏移
		 ;cx=1920是24行乘以80个字符/行,共拷贝1920次
		 ;每次拷贝一个word -> rep movsw
		 ;
		 ;思路是:
		 ;	从第二行第一个字符开始 将
		 ;	第二行第一个字符到最后一行最后一个字符 拷贝到
		 ;	第一行第一个字符到倒数第二行最后一个字符
		 ; 一共拷贝24行
		 ;
         mov ax,0xb800
         mov ds,ax
         mov es,ax
         cld
         mov si,0xa0
         mov di,0x00
         mov cx,1920
         rep movsw
		 ;第25行第1列在显存中的偏移地址是3840
		 ;为此,cls循环的目的是将最后一行的每个字符拷贝黑底白字的空格字符
		 ;0x720,0x20是空格,0x07是黑底白字
         mov bx,3840                     ;清除屏幕最底一行
         mov cx,80
 .cls:
         mov word[es:bx],0x0720
         add bx,2
         loop .cls

		 ;pop从栈中恢复bx的原始光标位置数值,然后减去80
		 ;减去80的原因是:滚屏成功后光标要往上挪一行
         pop bx
         sub bx,80

 .set_cursor:
		 ; 重新设置光标位置
		 ; 0x3d4是索引寄存器端口号,可以对该端口号写入一个值来指定内部的某个寄存器
		 ; 0x0e和0x0f是两个8位的光标寄存器,分别提供光标的高8位和低8位
         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:

37 中断处理程序

         ;代码清单10-1
         ;文件名:c10_1.asm
         ;文件说明:用户程序 
         ;创建日期:2011-4-16 22:03;修改于2021-10-14 9:00
         
;===============================================================================
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循环我们测试并等待RTC周期结束.如何得知RTC周期是否结束呢?
	  ; 通过测试寄存器A的第7位UIP(Update In Process).见P181 表10-2
	  ; 但这个测试指令对于正常运行中的程序来说是必要的
	  ; 而对于中断处理过程中的场景下,是冗余的判断条件.
	  ; 因为中断发生时,本身就说明对CMOS RAM的访问是安全的
  .w0:                               
      mov al,0x0a                        ;阻断NMI。当然,通常是不必要的
      or al,0x80                          
      out 0x70,al
      in al,0x71                         ;读寄存器A
      test al,0x80                       ;测试第7位UIP 
      jnz .w0                            ;以上代码对于更新周期结束中断来说 
                                         ;是不必要的 
      
	  ; 分别访问CMOS RAM的0/2/4号单元,从中读取秒/分/时并压入栈中
	  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

      ; 读一下RTC的寄存器C,使得所有中断标志位复位.
	  ; 等于是告诉RTC,中断已经得到处理,可以进行下一个中断.
	  ; 否则,RTC看到中断未被处理,将不再产生中断信号.
	  ; RTC产生中断的原因通过读寄存器C来判断
	  ; 这个场景下,除了更新周期结束中断,其他的中断都被关闭了
      mov al,0x0c                        ;寄存器C的索引。且开放NMI 
      out 0x70,al
      in al,0x71                         ;读一下RTC的寄存器C,否则只发生一次中断
                                         ;此处不考虑闹钟和周期性中断的情况 
      mov ax,0xb800
      mov es,ax

	  ; 将刚刚按顺序压栈的秒分时,逆序出栈并输出到屏幕
	  ; 这里调用了名为bcd_to_ascii的方法,目的是将bcd码转为ascii码以在屏幕上显示
	  ; 这里的not指令用于反转颜色,以便使用者知道触发了中断
      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                   ;显示分隔符':' 也可以写为 mov byte [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 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                  ;显示两位秒数字
      
	  ; 8259中断控制器无法知道中断什么时候才能被处理结束
	  ; 如果不清除相应的位,下一次从同一个引脚出现的中断将得不到处理
	  ; 所以我们必须在中断处理过程的结尾,显式地对8259芯片编程来清除该标志位
	  ; 方法是向8259芯片发送中断结束命令(End Of Interrupt, EOI),代码是0x20
	  ; 如果外部中断是8259主片处理的,那么EOI发送给主片即可,端口号是0x20
	  ; 如果是从片处理的,那么EOI既要发给从片(端口号0xa0)又要发给主片
      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
      
	  ; 每个中断占两个字,4个字节,高位存放段地址,低位存放偏移地址
	  ; 所以计算中断在中断表位置的方式就是 INTR_NR * 4
      mov al,0x70
      mov bl,4
      mul bl                             ;计算0x70号中断在IVT中的偏移
      mov bx,ax                          

	  ; 设置0x70号中断的段地址和段偏移地址
	  ; 在配置中断和设置中断向量表时需要屏蔽中断以防设置到一半被中断调用了
      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

	  ; CMOS RAM访问通过两个端口进行: 
	  ; 	1. 0x70或0x74是索引端口,用来指定CMOS RAM内的单元
	  ; 	2. 0x71或0x75是数据端口,用来读写相应单元的内容
	  ;
	  ; 设置RTC(Real Time Clock)工作状态,使它能够产生中断信号给8259中断控制器
	  ; 设置更新周期结束时产生中断.每当RTC更新了CMOS RAM中的日期时间后,发出此中断
	  ; 更新周期每秒一次,因此该中断也每秒发出一次
      mov al,0x0b                        ;RTC寄存器B p180 表10-1 寄存器B是0x0B
      or al,0x80                         ;阻断NMI bit7设置为1就可以阻断NMI 注bit从0开始
      out 0x70,al
	  ; 允许更新周期照常发生,禁止周期性中断,禁止闹钟功能,允许更新周期结束中断,使用24小时制,日期和时间用BCD编码
	  ; 参考P182 表10-3
      mov al,0x12                        ;设置寄存器B,禁止周期性中断,开放更 
      out 0x71,al                        ;新结束后中断,BCD码,24小时制 

	  ; 读取寄存器C用于得知中断具体原因,同时也使之产生中断信号.
	  ; 注:寄存器C是只读的,我们做读操作来触发寄存器C的复位,读会使得寄存器C会自动触发复位
	  ;    如果不读取它的话,相应的位不会被清零,同样的中断也不再产生
	  ; 下面的代码读寄存器C的内容使之产生中断信号.
	  ; 向索引端口0x70写入的同时,也打开了NMI.这是最后一次在主程序中访问RTC
      mov al,0x0c
      out 0x70,al
      in al,0x71                         ;读RTC寄存器C,复位未决的中断状态

	  ; 通过端口0xa1读取8259从片的IMR寄存器
	  ; 用and指令清除第0位,其他各位保持原状,然后再写回去.然后RTC中断就能被8259处理了
      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列
       
	   ;这边做的事情就是停机,等待某个外部中断使处理器恢复执行
	   ;一旦处理器来到hlt指令后,则立即使他继续处于停机状态
 .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

         push bx

         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

         pop bx
         sub bx,80

 .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:

8259A资料:https://blog.csdn.net/longintchar/article/details/79439466

38 通过中断进行字符输入和打印的程序

         ;代码清单10-2
         ;文件名:c10_2.asm
         ;文件说明:用于演示BIOS中断的用户程序 
         ;创建日期:2012-3-28 20:35;修改于2021-10-14 9:00
         
;===============================================================================
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:
	  ; 调用BIOS 0x10中断的0x0e号功能 该功能用于在屏幕上的光标位置处写一个字符并推进光标位置
      mov ah,0x0e
      mov al,[bx]
      int 0x10
      inc bx
      loop .putc

 .reps:
	  ; 使用软中断0x16从键盘读字符,需要在AH中指定0x00号功能
	  ; 该中断返回后,寄存器AL中为字符的ASCII码
      mov ah,0x00
      int 0x16
      
	  ; 又使用了一次0x10号中断的0x0e号功能,将键盘读取的字符显示在屏幕上
      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:

39 中断

中断相比与call调用的优势:

  1. int指令不需要知道目标程序的入口地址.jmp和call必须直接或间接给出目标位置的CS:IP.
  2. 如果调用别人的代码或OS的功能,OS不会给出磁盘读写例程的CS:IP(因为OS经常改,那么CS:IP也会经常变化).
  3. 有了软中断,OS每次加载完毕后,以中断处理程序的形式提供硬盘读写功能,并把例程的CS:IP填入中断向量表中.无论什么时候,用户需要该例程时,发出软中断即可,不需要知道具体地址.
    在这里插入图片描述

在这里插入图片描述
中断屏蔽寄存器,将它的对应中断设为0就可以屏蔽中断,设为1就可以允许中断.设备统一都接到8259A中断处理器上.它负责中断管理,仲裁,根据优先级来顺序触发中断等功能.优先级和引脚有关,优先级:IR0>IR1>…

即使允许中断,最终决定是否处理中断的权力在CPU手上,根据IF位来决定是否处理中断.从片级联在主片的IR2引脚上,所以从片仲裁处优先级最高的中断后,再参与主片的优先级中断仲裁.当正在进行中断处理时,来了一个优先级更高的中断,那么就会阻塞当前的中断处理,优先为更高优先级的中断服务,这称为中断嵌套.

在这里插入图片描述
在这里插入图片描述
中断服务寄存器: ISR Interrupt Service Register , 中断过程处理完成后,通过发送EOI End Of Interrupt 来结束中断, 需要同时向从片和主片发送,并恢复原来的寄存器数据(数据出栈)
在这里插入图片描述
中断号N*4 -> 中断N的入口地址

显示实模式下的中断向量表:

xp /512xh 0

可以看到很多段地址和偏移地址是相同的,这是因为多种中断可以用一个中断处理程序处理.

中断处理流程:

  1. 保护断点现场
  2. 执行中断处理程序
  3. 遇到iret(Interrupt Return)后,返回到断点继续执行
    在这里插入图片描述

内部中断

发生在处理器内部,执行指令的过程中出现了问题或故障引起的,比如:

  1. div 0时或除法溢出时,产生0号中断.
  2. 遇到非法指令时,产生中断6
  3. 内部中断不受标志寄存器IF的影响,不需要中断识别总线周期,它们的中断类型是固定的,可以立即转入相应的处理过程.

软中断

int3 ; 机器码0xCC, 断点中断指令, 又称陷阱中断
int imm8 ; 机器码0xCD imm8是中断号 . int 3与int3是不一样的中断指令(前者是0xCD03, 后者是0xCC),但是效果相同,都是调用3号中断
into ; 机器码0xCE,溢出中断指令,执行时若OF==1,则产生4号中断,否则什么也不做

int3执行流程:
当在某个指令上设置了断点,那么这个指令的第一个字会被修改成0xCC,然后当处理器执行到0xCC后,触发中断,进入中断流程.中断结束后,0xCC又会被修改为原来的字

BIOS中断

在这里插入图片描述
BIOS中断表:(书中附录C )

在这里插入图片描述
Q: BIOS如何建立功能调用中断的?又是如何知道访问硬件的方式的?
A: BIOS为一些简单的外围设备提供初始化代码和功能调用代码,并填写中断向量表.但也有一些BIOS中断是外部设备自己建立的.

  1. 每个外部设备接口,包括各种板卡(网卡/显卡/键盘接口电路/硬件控制器等)都有自己的只读存储器(Read Only Memory, ROM),类似于BIOS芯片.在这些ROM中有自己功能的调用例程以及本设备初始化的代码.按照规范,前两个单元内容是0x55,0xAA,第三个单元是本ROM中以512字节为单元的代码长度,第四个单元开始是实际的ROM代码.
  2. 内存物理地址从0xA0000到0xFFFFF,有相当一部分空间是给外围设备的.若设备存在,它自带的rom会映射到分配给它的地址范围内.
  3. 计算机启动期间,BIOS会以2KB为单位搜索内存地址C0000~E0000之间的区域.当它发现某个区域的头2字节是0x55,0xaa时,意味着该区域有ROM代码存在,是有效的.接着,它对该区域做累加和检查,看结果是否和第三个单元相符.若是,就从第四个单元进入.这时,处理器执行的是硬件自带的程序指令,这些指令初始化外部设备的相关寄存器和工作状态.最后填写相关的中断向量表,使他们指向自带的中断处理过程.

40 RTC和CMOS RAM

在这里插入图片描述
在这里插入图片描述
读端口0xA1读的是从片的中断屏蔽寄存器,写端口0xA1写的也是从片的中断屏蔽寄存器.
那么0xA0是做啥的?
在这里插入图片描述

在这里插入图片描述

周期性中断信号

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

更新周期结束中断

在这里插入图片描述
UIE: 0表示不产生更新周期结束中断
SET: 0表示更新周期每秒都会正常发生; 1表示不产生更新周期. 要想安全地产生日历和时间,要先将此位置1.

在这里插入图片描述
UIP: 只读,写入不会改变此位状态. 0则表示更新周期在488uS内不会启动,此时访问日历或时间是安全的. 1意味着正处于或正要启动更新周期.更新周期不会多于1984uS.

更新周期内,CMOS RAM 0x00 ~ 0x09(时间,闹钟,年月日)会脱离总线,所以访问这部分数据需要避开更新周期.
所以我们可以通过UIP判断是否可以读这几个数据.
在这里插入图片描述

闹钟中断

在这里插入图片描述

UIE为0表示不产生闹钟中断信号.

中断的判别

在这里插入图片描述
从片的IR0只能知道是否有中断,那么中断产生时,要想知道是什么类型的中断,只能读取寄存器C来判断.

IRQF: 是否有中断发生,有是1. 对其读操作将导致位7清0
对寄存器C的读操作会导致PF/AF/UF清零.寄存器C是只读的,不能写入.对寄存器C的读会导致它的所有bit位清0.
0~3位是保留位,不使用.
不读取寄存器C的话,同样的中断不再产生(因为上次中断还没有处理)

在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值