《x86汇编语言:从实模式到保护模式》读书笔记
书籍简介
image阅读目的
本意是为了调试coredump时候,通过寄存器、汇编命令定位产生coredump原因。了解常见的汇编命令,实现能够看懂基本的汇编操作,所表示含义。最终目标:能够看懂基本的汇编操作,表示的内容。
环境安装
随书配套文档
https://www.cnblogs.com/leec/p/8081720.html
下载软件 nasm
https://www.nasm.us/pub/nasm/releasebuilds/2.14.02/win64/
到安装目录运行编译:
PS F:\Program Files\NASM> .\nasm -f bin c05_mbr.asm -o c05_mbr.bin
安装virtual box,创建虚拟机的时候,按照文档 《booktool\相关教程\VirtualBox安装手册.pdf》,因为有些作者自己写的小程序,只支持某些配置的虚拟机。
基本概念
处理器基本工作:取指令,执行指令,数据访问,万变不离其宗。而处理器访问内存地址通过内存的分段机制来实现。分段机制为: 逻辑地址(20位)=段寄存器地址左移4位(16位)+偏移地址(16位)。
使用上述方式是有历史原因,由于早期只有16位的寄存器,16位的只能达到64KB内存,为了能够表示更大的内存,8086提供20位的地址线可以达到1MB。将16位的段地址+16位的偏移地址不足以得到20位的物理地址,于是将16位的端寄存器左移4位+偏移地址,形成20位的物理地址。
分段机制最重要两个寄存器。CS:代码段寄存器,用于表示cpu从代码段哪个位置执行;DS:数据段寄存器,指向数据段。
机器语言与汇编语言区别
6 00000000 B800B8 mov ax,0xb800 ;指向文本模式的显示缓冲区
7 00000003 8EC0 mov es,ax
8
9 ;以下显示字符串"Label offset:"
10 00000005 26C60600004C mov byte [es:0x00],'L'
11 0000000B 26C606010007 mov byte [es:0x01],0x07
如上所示,“0000000B 26C606010007” 这些即为机器语言,已经转换为二进制数,需要根据机器指令来进行识别,而汇编语言还能够看懂含义。
常见寄存器
通用寄存器,可以作为特殊寄存器,也可以作为通用寄存器
AX Accumulator Register
BX Base Address Register
CX Counter Register
DX Data Register
SI Source Index
DI Destination Index
8 - 16 - 32 - 64位的进化
AH/AL AX EAX RAX
E表示扩展,即从16位,变身为32位。
CS 代码段寄存器 code segment
DS 数据段寄存器 data segment
ES 附加段寄存器 extra
FS
GS
SS 栈寄存器 stack segment
SP 栈指针 Stack Point
IP 指令指针寄存器
CPU取指令,依赖于[CS:IP],即将CS<<4+IP,形成20位地址,取指令运行,同时IP往后移动。
8086的 是CS左移动4位+IP内容。
8086 加电启动后,CS为0xffff,指令指针寄存器IP=0x0000.其他寄存器也都是0x0000.这个物理地址0xFFFF0即位于ROM-BIOS部分,从BIOS进行启机。
(下面章节有一张内存分配示意图表示此处为BIOS的物理地址)
常见汇编命令
ASCII 基本内容
LF 表示换行 line feed
CR 回车 carriage return
xor dx,dx 异或,可用于清零操作。
mov A,B 表示的是赋值 A=B
div A 做除法运算 DX:AX/A,将商存储在AX寄存器,余数存储在DX寄存器
add al,0x30 表示al+=0x30
inc 自加1
jp 标志位为1,跳转
jnp 标志位为0,跳转
[ ] 表示当前表示的是地址,相当于取地址* 的功能。
两个内存单元直接是不能直接进行操作的,包括+-*/
mov ax,[0x02]
mov [0x01],ax
如上所示,ds+0x02 地址的值,赋值到 ds+0x01 地址上,需要经过一个ax寄存器作为中转。
[ ] 使用时候如果没有明确指定段,则默认段使用的是DS。明确段则表示如下所示
[es:0x00]
常见缩写
DB declare Byte 1字节
W Word 2字节
D Double Word 双字=4字节
Q quad Word 4杀也是这个,4字=8字节
实验
实验1 编写主引导扇区代码
实验目的
在一个空硬盘中添加主引导扇区程序,实现屏幕打印字符串的功能。
;代码清单5-1
;文件名:c05_mbr.asm
;文件说明:硬盘主引导扇区代码
;创建日期:2011-3-31 21:15
mov ax,0xb800 ;指向文本模式的显示缓冲区
mov es,ax
;以下显示字符串"Label offset:"
mov byte [es:0x00],'L'
mov byte [es:0x01],0x07
mov byte [es:0x02],'a'
mov byte [es:0x03],0x07
mov byte [es:0x04],'b'
mov byte [es:0x05],0x07
mov byte [es:0x06],'e'
mov byte [es:0x07],0x07
mov byte [es:0x08],'l'
mov byte [es:0x09],0x07
mov byte [es:0x0a],' '
mov byte [es:0x0b],0x07
mov byte [es:0x0c],"o"
mov byte [es:0x0d],0x07
mov byte [es:0x0e],'f'
mov byte [es:0x0f],0x07
mov byte [es:0x10],'f'
mov byte [es:0x11],0x07
mov byte [es:0x12],'s'
mov byte [es:0x13],0x07
mov byte [es:0x14],'e'
mov byte [es:0x15],0x07
mov byte [es:0x16],'t'
mov byte [es:0x17],0x07
mov byte [es:0x18],':'
mov byte [es:0x19],0x07
mov ax,number ;取得标号number的偏移地址
mov bx,10
;设置数据段的基地址
mov cx,cs
mov ds,cx
;求个位上的数字
mov dx,0
div bx
mov [0x7c00+number+0x00],dl ;保存个位上的数字
;求十位上的数字
xor dx,dx
div bx
mov [0x7c00+number+0x01],dl ;保存十位上的数字
;求百位上的数字
xor dx,dx
div bx
mov [0x7c00+number+0x02],dl ;保存百位上的数字
;求千位上的数字
xor dx,dx
div bx
mov [0x7c00+number+0x03],dl ;保存千位上的数字
;求万位上的数字
xor dx,dx
div bx
mov [0x7c00+number+0x04],dl ;保存万位上的数字
;以下用十进制显示标号的偏移地址
mov al,[0x7c00+number+0x04]
add al,0x30
mov [es:0x1a],al
mov byte [es:0x1b],0x04
mov al,[0x7c00+number+0x03]
add al,0x30
mov [es:0x1c],al
mov byte [es:0x1d],0x04
mov al,[0x7c00+number+0x02]
add al,0x30
mov [es:0x1e],al
mov byte [es:0x1f],0x04
mov al,[0x7c00+number+0x01]
add al,0x30
mov [es:0x20],al
mov byte [es:0x21],0x04
mov al,[0x7c00+number+0x00]
add al,0x30
mov [es:0x22],al
mov byte [es:0x23],0x04
mov byte [es:0x24],'D'
mov byte [es:0x25],0x07
infi: jmp near infi ;无限循环
number db 0,0,0,0,0
times 203 db 0
db 0x55,0xaa
实验效果:
硬盘启动的时候,从第一个扇区启动。将上述代码编译生成bin文件后,需要修改虚拟机硬盘的VHD文件,可以使用作者提供的工具 booktool\配书源码和工具\fixvhdwr.exe 进行修改 VHD 磁盘文件。或者使用 dd 刷写也是一样的,目的都是将特定二进制文件刷写到硬盘第一个扇区。
away@ubuntu:~/share$ sudo dd if=c05_mbr.bin of=LEARN___ASM.vhd seek=0 bs=512 count=1 conv=notrunc
[sudo] password for away:
1+0 records in
1+0 records out
512 bytes copied, 0.00191636 s, 267 kB/s
away@ubuntu:~/share$ ls -lah
-rwxr--r-- 1 nobody nogroup 512 Nov 24 2012 c05_mbr.bin
-rwxr--r-- 1 nobody nogroup 516M Oct 3 01:29 LEARN___ASM.vhd
其中, conv=notrunc 表示不进行截断,即 of 文件的大小不变。即将二进制文件 c05_mbr.bin 刷写到 LEARN___ASM.vhd 的前512字节空间,同时不改变 LEARN___ASM.vhd 其他内容、不改变 LEARN___ASM.vhd 的大小。
通过 virtualbox 加载修改过的 LEARN___ASM.vhd 文件,即可看到实验结果如下所示: ScreenClip [2].png实验过程
处理器基本工作:取指令,执行指令,数据访问,万变不离其宗。而这些,通过分段来实现。分段机制为: 逻辑地址(20位)=段寄存器地址左移4位(16位)+偏移地址。
8086有32位,但是早期只有20根地址线,于是只能使用1MB内存。之后的8086都有32根地址线。设备加电后,CS=0xFFFF,IP=0x0000,即第一条要运行的指令地址为0xFFFF0。BIOS运行后,会一直执行到物理内存地址0x7c00位置,即MBR代码加载的内存位置,从而继续运行MBR的代码。
首先了解下硬盘的结构,硬盘由CHS模式表示,由大到小为磁头、柱面、扇区,设备上电后,BIOS读取硬盘的0面0道1扇区,即主引导扇区(MBR)。MBR和BIOS、BOOT还不一样,MBR是BIOS已经正常运行后,再读取MBR内容。
主引导扇区大小为512Byte,ROM-BIOS将它加载到逻辑地址0x0000:0x7c00的位置,然后判断MBR是否有效。有效的标记是最后两个字节必须是0x55和0xAA。
至于为何MBR加载的内存地址是0x0000:0x7c00,这是最初定方案的人设置的,定死了。本实验将一段程序写入硬盘的MBR,然后在屏幕显示。
再了解下屏幕如何显示内容,为了将字符能够在屏幕显示,需要将要显示的内容转换为ASCII码,然后写入显存,由显卡在屏幕显示。
对于8086设备,可以访问1MB内存,内存的分配如下所示: ScreenClip [3].png其中,0xB8000即显存的开始位置。为了访问这块地址,采用“段地址+偏移地址的形式”。于是0xB8000可以表示为0xB800:0x0000。
mov ax,0xb800 ;指向文本模式的显示缓冲区
mov es,ax
设置显存的段地址
mov byte [es:0x00],'L'
mov byte [es:0x01],0x07
设置地址0xB800:0x0000和0xB800:0x0001的值。对于显存,两个字节表示一个字符。'L' 表示ASCII代码,0x07 表示显示属性,即黑底白字,无闪烁。以上代码表示,要显示'L'这一字符。
下一步要显示数字,先获取数字,这里取的是代码的偏移地址,暂存在ax寄存器。
mov ax,number ;取得标号number的偏移地址
程序加载后,内存地址将发生变化,如下所示:
ScreenClip [4].png
其中,0x0000:0x7c00的位置为MBR代码加载的位置。
此时CS=0x0000,IP=0x7C00。
最终效果:显示偏移量,其中302,即12E,根据list文件,可得就是这个偏移位置。
100 0000012E 0000000000 number db 0,0,0,0,0
实验2 硬盘和显卡的访问与控制
实验目的:
将主引导程序修改为程序加载器,加载用户程序、执行程序。
认识汇编命令 call、 jmp、ret的过程。
本实验有两套程序,一个为MBR程序,用于加载用户。一个为真正的用户程序。
MBR程序代码清单:
;代码清单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
用户程序代码清单:
;代码清单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
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:
实验效果:
上一个实验,在主引导扇区直接实现字符串的打印。而在本实验中,将主引导扇区更改,修改为程序加载器,通过主引导扇区加载用户程序,从而实现执行用户程序,在屏幕打印字符串。进行实验前,思考两个问题
Q1:8086默认的内存加载地址是0x7c00,加载MBR的时候,使用的内存地址即0x7c00。那么内存哪个位置是空的,可以用于加载用户程序呢?
phy_base dd 0x10000 ;用户程序被加载的物理起始地址
手动设置内存加载地址为0x10000,即可用于加载用户程序。
mov ax,[cs:phy_base] ;计算用于加载用户程序的逻辑段地址
mov dx,[cs:phy_base+0x02]
储存加载用户程序的内存物理地址于DX:AX中。
Q2:用户程序,位于硬盘哪个位置,用户程序的起始逻辑扇区号是多少等等。
A2:用户程序任意位置,本实验使用的用户程序加载在100扇区,这是在使用作者的工具fixvhdwr.exe时候选择的,使用dd也可以实现,就是计算下偏移量,然后在偏移量下加载二进制文件。
app_lba_start equ 100 ;声明常数(用户程序起始逻辑扇区号)
实验过程:
用户程序在用户程序的头部,需要包含用户程序大小、用户程序入口等信息,以方便加载器的加载。
code_entry dw start ;偏移地址[0x04]
dd section.code_1.start ;段地址[0x06]
这里的段地址 section.code_1.start,只是编译时候时候的段地址,在用户程序加载的时候会再计算一波,得到当时的段地址。用户程序的头两个字存的是偏移地址、段地址。在cpu加载用户程序的时候,保存这两个地址到DX:AX。经过移位除法操作后,得到段地址,储存在DS和ES寄存器。
最终加载了用户程序后,显示效果如下所示: ScreenClip [6].png设备交互
设备有外围设备=输入输出设备,由于有多款设备,以后需要随时增加/减少,于是引入bus 总线技术,外围设备+处理器都连接在总线上。当不需要的时候,就从总线上脱离。
设备抢占IO,需要增加ICH (IO Controller Hub),连接不同总线,并协调IO对处理器的访问。ICH芯片=南桥。ICH又再外接一堆设备,比如 USB总线、IDE/SATA总线、PCIE总线。
处理器与外围设备IO打交道,通过的是端口号。和网络通信时候,使用端口号来识别进程很类似。往一个端口写入0x20,表示读数据;写入0x30,表示写数据。
mov al,0x20 ;读命令
out dx,al
表示,从 dx 表示的扇区,读数据。in 表示外设给 cpu 发指令;out 表示 cpu 给外设发命令。
call 调度
通过控制硬盘控制器端口读取扇区数据,即修改端口号,可实现修改读取的路径。
;以下读取程序的起始部分
xor di,di
mov si,app_lba_start ;程序在硬盘上的起始逻辑扇区号
xor bx,bx ;加载到DS:0x0000处
call read_hard_disk_0
硬盘的加载扇区号,存在 SI 寄存器里。从扇区中取数据,而后调用 read_hard_disk_0 函数,read_hard_disk_0 的具体细节可以不用理会,只需知道是从 DI:SI 这个扇区号读取数据即可,数据缓存到 DS:BX。
call 调度时候的参数传递:参数传递通过寄存器实现,运行 call 后,会改变寄存器的值,在 ret 后需要恢复现场。这一过程如下所示:
ScreenClip [7].png
在没有ret之前,call 使用后,寄存器不断地进栈,在 ret 后,恢复到没有 call 之前的寄存器状态。
加载用户程序
最初设置用户记载内存的时候,就已经设置了DS和ES为0x10000的位置,再加载用户程序的时候,随着用户程序的大小增加,DS往后移动即可。
add ax,0x20 ;得到下一个以512字节为边界的段地址
mov ds,ax
用户被加载的位置,是由DS和ES所指向的逻辑段。在往内存添加扇区,需要创造一个逻辑段,如果用户程序过大,而需要不断更改DS,让逻辑段能够存放下用户程序。逻辑段每段大小为一个扇区大小,即512B,即0x200,右移动4位即0x20.
根据上面一段操作,即可在0x10000的位置,加载第100扇区的程序。
jmp far [0x04] ;转移到用户程序
cpu 会取出 [DS:0x04]的两个字,传给CS和IP。CS和IP用于确认运行的代码位置,由于DS已经经过了改变,定位到了用户程序,此时就可以运行用户程序。这就是jmp的功能,jmp后加段地址与偏移地址,跳转到指定代码段。
用户程序初始化
加载程序加载时候,将DS、ES移动到用户程序的头部,SS为加载程序的栈位置。SS和DS还需要手动移动到用户程序的对应位置。
mov ax,[stack_segment] ;设置到用户程序自己的堆栈
mov ss,ax
mov sp,stack_end
mov ax,[data_1_segment] ;设置到用户程序自己的数据段
mov ds,ax
用户程序部分的代码就是在屏幕打印字符串,和上一个实验的打印字符串对比,使用了更多的call、jmp,更精简、模块化。
call 会改变IP、CS的值,因为CS:IP就是为了指向代码位置。在栈中的程序运行完了,CS、IP又会回到未入栈的时候的值。栈就是一块普通的空间,用来存放数据,数据计算完,再回退。可以人为实现,不过计算机帮我们管理了岂不是更好。
call 与 ret 匹配。
mov ax,[es:data_2_segment] ;段寄存器DS切换到数据段2
mov ds,ax
记住,DS为数据段,更改DS的值,即可以切换到另一个数据段。
实验3 中断使用
实验目的:了解中断的使用
用户程序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:
用户程序2:
;代码清单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:
实验效果
同样,也是加载在100扇区,沿用上一实验的加载器,只是用户程序更换了下,加载用户程序1,显示效果如下所示: ScreenClip [8].png时间不断变化,@不停闪烁。
加载用户程序2,显示效果如下所示: ScreenClip [9].png屏幕会显示键盘敲的内容,即'Please press the keys on the keyboard ->'后的内容
实验过程
中断分类:
硬件中断:硬件有两根中断线,NMI和INTR,用来接收中断消息
- NMI 非屏蔽中断 Not Maskable Interrupt,比较严重事件
- INTR 可屏蔽中断
内部中断:发生错误的时候,非法指令时候,产生的中断
软中断: 调试设置断点使用
- BIOS中断
中断管理,通过中断控制器(PIC)。所谓的中断处理,就是cpu从执行与中断有关的程序,需要将中断号码与程序对应起来。所以就有中断向量表的存在,从物理地址0x00000到0x0003ff,这1k空间,专门存放每个中断的程序入口地址。中断达到的时候,直接从这部分空间查找,找对应的中断处理程序,得到中断处理程序的偏移地址和段地址,传到CS和IP,执行中断处理程序。
硬件中断实验:
时钟由RTC提供,RTC设置如下,修改寄存器设置RTC配置。
mov al,0x0b ;RTC寄存器B
or al,0x80 ;阻断NMI
out 0x70,al
mov al,0x12 ;设置寄存器B,禁止周期性中断,开放更
out 0x71,al ;新结束后中断,BCD码,24小时制
允许RTC的中断被处理
in al,0xa1 ;读8259从片的IMR寄存器
and al,0xfe ;清除bit 0(此位连接RTC)
out 0xa1,al ;写回此寄存器
而后,就是 cpu 被RTC的中断唤醒,唤醒后进入休眠状态,又被唤醒,所以会不断出现 @ 闪烁的情况。
有点像曾经51单片机,那种按个按钮,灯亮一下的处理方式……
BIOS中断:
在计算机加电、BIOS运行期间即可使用。
mov ah,0x00
int 0x16
键盘的中断号为16.键盘敲下的字符的ASCII码,会储存在AL中。
使用软中断10,将字符显示在屏幕上
mov ah,0x0e
mov bl,0x07
int 0x10
敲敲键盘就会发现,回车是到行首,但是不会到下一行。BackSpace是退格,但是不删除字符。
后续保护模式的部分,还没看,有需要再添加。