书接上文,本章将基于采用汇编语言,编写一段在ROM-BIOS中的程序,帮助理解X86汇编语言与显卡控制原理。
显卡和显存
无论是独显还是集显,控制显示器的最小单位是像素,一个像素对应屏幕上的一个点,通过控制这些像素,可以形成文字和美丽的图像。显卡控制像素的方式是通过自身的存储器——Video RAM(VRAM),也就是显存,要显示的内容都预先写入显存。
NOTE: 显存是按字节访问的
工作模式
文本模式和图形模式是显卡的两种基本工作模式,可以用指令访问显卡,设置它的显示模式。在不同的工作模式下,显卡对显存内容的解释是不同的。本文仅讨论文本模式,图形模式大概就是多个字节或比特去控制一个像素的属性,从而构成多彩的图形吧,咱也不知道瞎猜吧😅。
在文本模式下,显卡从显存中依次提取字符的ASCII
码交给字符发生器和控制电路,从而控制显示器进行显示。
所以要显示字符,处理器需要访问显存,把字符的ASCII 码写入显存。为加快显存的读写速率,芯片的设计者们将显存映射到内存空间中,如下图展示的8086的显存映射。可见0xB8000~0xBFFFF这段物理地址空间,由显卡来提供,用来显示文本。
汇编指令
- ; —— 注释
- mov —— 数据传送指令
mov dst, src
dst - 目的操作数
src - 源操作数
目的操作数是通用寄存器或者内存单元;源操作数是和目的操作数具有相同位宽的通用寄存器、内存单元、立即数。
mov ah, bh ;将bh寄存器中的内容复制到ah中
mov ax, dx ;将dx寄存器中的内容复制到ax中
;以下两条指令通过寄存器限制了位宽
mov [0x02], bh ;将bh寄存器中的内容复制到偏移地址为0x02的8位内存单元中
mov ax, [0x06] ;将偏移地址为0x06的16位内存单元里的内容复制到ax寄存器中
; 偏移地址0x00,可以是字节单元或字单元,无法判断
; 0x4c即解释为8位的0x4c,也可解释为16位的0x004c
; 在这种情况下,编译器将报错,所以必须用“byte”或者“word”进行修饰
mov byte [0x00], 0x4c
mov word [0x00], 0x4c
; 目的操作数和源操作数不允许同时为内存单元
- jmp —— 转移指令
用于使处理器脱离当前的执行序列,转移到指定的地方执行
jmp [option] addr
option - 可选项
addr - 目标地址
jmp 0x5000:0xf0c0 ;转移到段地址为0x5000,偏移地址为0xf0c0的地址执行
jmp near 0x001E ;关键字`near`表示目标位置在当前代码段内,无需指定段地址
- xor —— 异或指令
xor dst, src
dst - 通用寄存器和内存单元
src - 通用寄存器、内存单元和立即数
mov ax, 0x0002
xor ax, 0xf0f1 ; 结果保存在ax中,此时ax = 0xf0f3
- add —— 加法指令
add dst, src
dst - 目的操作数:8 位或者16 位的通用寄存器;指向8 位或者16 位实际操作数的内存地址
src - 源操作数:与目的操作数相同数据宽度的8 位或者16 位通用寄存器;指向8 位或者16 位实际操作数的内存地址;立即数
add ax, 0x0002 ; 寄存器AX的内容和立即数0x0002想加,结果在AX中
add ax, bx
add [lable_a], cx ; 内存单元的值和寄存器cx的值想加,结果写会内存
- div —— 除法指令
8086处理器可以做两种类型的除法
1、用16 位的二进制数除以8 位的二进制数
在这种情况下,被除数必须在寄存器AX 中,必须事先传送到AX 寄存器里。除数可以由8 位的通用寄存器或者内存单元提供。指令执行后,商在寄存器AL 中,余数在寄存器AH中。
mov ax, 0x0005
mov cl, 0x02
div cl ; AL 中的商是0x02,AH 中的余数是0x01。
2、用32 位的二进制数除以16 位的二进制数
在这种情况下,因为16 位的处理器无法直接提供32 位的被除数,故要求被除数的高16 位在DX 中,低16 位在AX 中。除数可以由16 位的通用寄存器或者内存单元提供,指令执行后,**商在AX 中,余数在DX **中。
mov ax, 0x0005
mov dx, 0x0002 ;DX:AX = 0x00020005
mov cx, 0x2000 ;AX 中的商是0x0010,DX 中的余数是0x0005。
- 段超越前缀
一般情况下,如果没有附加任何指示,段地址默认在段寄存器DS中,如果我们希望使用段寄存器ES来访问内存,需要使用段超越前缀es:
。
mov ax [es:0x01] ;ES寄存器中的值+0x01为地址的内存单元
- DB、DW、DD、DQ —— 伪指令
DB(Declare Byte)用于声明字节数据
DW(Declare Word)用于声明字数据
DD(Declare Double Word)用于声明双字数据
DQ(Declare Quad Word)用于声明四字数据。
DB 0x55 ; 声明一个值为0x55的数据,占位一个字节
NOTE
虽然我们定义一个字节数据,但是我们并不知道这个数据的内存地址,也就无法访问它!!!这个时候,我们可以 借助标号来代表数据的起始地址。
- 标号 —— 代表并指示指令所在的汇编地址
在NASM 汇编语言里,每条指令的前面都可以拥有一个标号,以代表和指示该指令的汇编地址。标号并不是必需的,只有在我们需要引用某条指令的汇编地址时,才使用标号。
data_1:
DB 0x55 ; 标号data_1指向DB定义的数据的起始地址
显示例程
屏幕上的每个字符对应着显存中的两个连续字节,前一个是字符的ASCII 代码,后面是字符的显示属性,包括字符颜色(前景色)和底色(背景色)。如下图所示,字符“H”的ASCII 代码是0x48,其显示属性是0x07;字符“e”的ASCII 代码是0x65,其显示属性是0x07。
字符的显示属性的编码如下图所示
;代码清单5-1
;文件名:c05_mbr.asm
;文件说明:硬盘主引导扇区代码
;创建日期:2011-3-31 21:15
;主引导扇区代码,ROM-BOIS将这段代码加载到0x0000:0x7c00执行
;此时段地址为0x0000,故CS寄存器=0x0000
mov ax,0xb800 ;初始化ES段寄存器,0xb800为显存段起始地址
mov es,ax
; 以下显示字符串"Label offset:"
; 屏幕上的每个字符对应着显存中的两个连续字节,前一个是字符的ASCII码
; 后面是字符的显示属性,包括字符颜色(前景色)和底色(背景色)
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的偏移地址(0x012E),并将其传送到ax中
mov bx,10 ;初始化除数
;设置数据段的基地址
mov cx,cs ;CS代码段寄存器,CS = 0x0000
mov ds,cx ;DS数据段寄存器,数据和代码公用一个段,数据放置最后
;求个位上的数字
mov dx,0 ;使用32位被除数的方式,dx高位清零,低16位在ax中
div bx ;bx = 10
; 进行div运算后,余数在dx中,余数小于10,使用dl即可访问
; number是段内偏移地址,被加载到0x0000:0x7c00处后,
; 实际的段偏移地址应该加载0x7c00,因为DS=0x0000
mov [0x7c00+number+0x00],dl ;保存个位上的数字
;求十位上的数字
xor dx,dx ;使用异或运算清零dx,商保留在ax中无需处理
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 ; ASCII转换
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 ;伪指令times 可用于重复它后面的指令若干次
db 0x55,0xaa ; 主引导扇区有效标志 0x55 和0xAA。
最后,因为时间关系,本章就写下这些内容了,后面将完善更多的汇编指令或另立章节阐述。