硬盘和显卡的访问与控制
硬盘和显卡的访问与控制
编译好的程序放在硬盘上,加载到内存才能执行,接下来把主引导扇区代码改为一个加载器,用来加载硬盘上的程序并将处理器的控制权交给该程序
用户程序代码
NASM 编译器使用 SECTION 或 SEGMENT 来设置分段,align=16 表示是 16 字节对齐的
用户程序涉及到在屏幕上显示内容,需要用到显卡,显卡的操作很复杂很多寄存器只能通过索引寄存器访问(端口号 0x3d4)通过向他写入一个值来指定一个内部寄存器,比如两个8位的光标寄存器(0xe 和 0xf)他俩分别提供光标位置的高八位和第八位
;代码清单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] ;从DS:BX取字符
or cl,cl ;判断是不是/x00
jz .exit ;是0,返回主程序
call put_char ;否则调用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 ;索引寄存器端口号0x3d4
mov al,0x0e ;通过索引寄存器告诉显卡要操作0x0e寄存器
out dx,al ;
mov dx,0x3d5 ;从数据端口读
in al,dx ;得到高8位读到了al寄存器
mov ah,al ;光标位置的高八位放在ah寄存器
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 ;每行80个字符
div bl ;当前光标位置除以80可以得到当前行号
mul bl ;在乘以80得到当前行行首的光标数值,与ax相乘,放在DX:AX
mov bx,ax ;放在ax中
jmp .set_cursor ;跳转到设置光标的代码段
.put_0a:
cmp cl,0x0a ;比较一下是不是换行符
jnz .put_other ;不是,那就正常显示字符
add bx,80 ;是的话直接设置光标数值加80
jmp .roll_screen ;跳转到滚屏
.put_other: ;正常显示字符
mov ax,0xb800
mov es,ax ;设置显存地址
shl bx,1 ;光标位置占用了1个字符(2个字节)的位置,左移1位相当于乘以2
mov [es:bx],cl ;移动到显示器上
;以下将光标位置推进一个字符
shr bx,1 ;右移一位还原回正常的光标位置
add bx,1 ;写完之后光标向后移动一位
.roll_screen:
cmp bx,2000 ;比较是不是小于2000光标,是否超出屏幕
jl .set_cursor ;是的话没有超出光标直接移动光标就行了
mov ax,0xb800 ;设置显存地址
mov ds,ax
mov es,ax
cld ;设置位零,从低地址往高地址复制
mov si,0xa0 ;源地址的偏移地址
mov di,0x00 ;目的地址的偏移地址
mov cx,1920 ;要传送的单位,24*80
rep movsw ;循环传送
mov bx,3840 ;第25行第1列在显存中的偏移地址是3840
mov cx,80 ;设置循环修次数
.cls:
mov word[es:bx],0x0720 ;往偏移地址写的内容是0x0720表示空格,这样就有一行空的
add bx,2
loop .cls
mov bx,1920 ;最后把光标位置换回来
.set_cursor: ;设置光标
mov dx,0x3d4 ;索引寄存器端口号
mov al,0x0e
out dx,al ;通过索引寄存器告诉显卡要操作0x0e寄存器
mov dx,0x3d5
mov al,bh ;bx寄存器是目前光标位置,bh是高八位
out dx,al
mov dx,0x3d4
mov al,0x0f
out dx,al ;通过索引寄存器告诉显卡要操作0x0e寄存器
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.start 表示 header 相对于整个程序开头的汇编地址
section.code.start 表示 code 相对于整个程序开头的汇编地址
其中,段声明语句中的 vstart 表示该段里面的标号是从该段开始计算的,比如 code_2 的 begin 是相对于 code_2 开头的汇编地址
主引导扇区代码(加载器)
加载器需要从硬盘中读取程序代码,处理器通过端口与外围设备交流,这些端口就是位于I/O接口电路中的一些寄存器,每个I/O接口可能有好多个端口,连接硬盘的PATA/SATA接口有个命令端口,当向该端口写入 0x20 时表示从硬盘读取数据;写入 0x30 时表示向硬盘写入数据,体现在汇编代码中 in 指令表示从端口读,out 表示从端口向外围设备发送数据,源操作数是 dx,目的操作数是 ax 或 al
主硬盘接口分配的端口号是 0x1f0~0x1f7,副硬盘接口分配的端口号是 0x170~0x177
想要从硬盘上读逻辑扇区
首先要设置读取扇区的数量,写在 0x1f2,如果写入的是 0 表示读 256 个,每读一个扇区 0x1f2 的值就减 1,对应 87-89 行代码
然后设置起始 LBA 扇区号,从 1f3 到 1f5 分别存放 LBA 参数的 0-7、8-15、16-23,1f6 比较特别,高三位是 111,第四位 0 表示主盘 1 表示从盘,第三位是 LBA 参数的 24-27,对应 91-106 行代码
然后向 0x1f7 写入 0x20 请求读硬盘,硬盘从0x1f7端口收到0x20后将第七位置为1进行准备操作,准备结束后再将第七位置为 0,第三位置为 1,所以使用 0x88 进行与运算,看看是不是准备好了,对应 113-116 行
0x20 0010 0000
0x88 1000 1000
----------------------and
0x00 0000 0000
0xa0 1010 0000
0x88 1000 1000
----------------------and
0x80 1000 0000
0x28 0010 1000
0x88 1000 1000
----------------------and
0x08 0000 1000
接下来开始读数据,0x1f0 是硬盘接口的数据端口,使用 in 进行读取每次读取一字,对应代码 118-124 行
;代码清单8-1
;文件名:c08_mbr.asm
;文件说明:硬盘主引导扇区代码(加载程序)
;创建日期:2011-5-5 18:17
app_lba_start equ 100 ;声明常数(用户程序起始逻辑扇区号,这个是烧录程序时自己决定的,后面我们烧录在第100号扇区了,所以就得写100)
;常数的声明不会占用汇编地址
SECTION mbr align=16 vstart=0x7c00 ;段内所有汇编地址都从0x7c00开始计算
;初始化栈段寄存器和栈指针
mov ax,0
mov ss,ax
mov sp,ax
mov ax,[cs:phy_base] ;加载phy_base,用户程序的逻辑段地址
mov dx,[cs:phy_base+0x02]
mov bx,16
div bx ;dx:ax的值即0x00010000除以16,相当于右移4位得到用户程序的段地址
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 ;调用read_hard_disk_0,同时将下一条指令的地址压到栈上
;以下判断整个程序有多大
mov dx,[2] ;ds:2读取程序的大小即program_end(高16位)
mov ax,[0] ;读取程序的大小低16位
mov bx,512 ;每扇区512字节
div bx ;做个除法,看一下需要读取多少扇区
cmp dx,0 ;如果有余数表示有不够1扇区的内容
jnz @1 ;跳到@1继续读取
dec ax ;已经读了一个扇区,扇区总数减1
@1: ;当余数不为0时
cmp ax,0 ;若ax值为0表示读取完了
jz direct ;跳转到direct
;读取剩余的扇区
push ds ;保存当前ds的值,构造一个新的段用来读取一个扇区的数据
mov cx,ax ;循环次数(剩余扇区数)
@2:
mov ax,ds
add ax,0x20 ;段地址加上0x20,到时候*0x10就相当于以下一个扇区为段地址
mov ds,ax ;设置段地址
xor bx,bx ;偏移地址始终为0x0000
inc si ;逻辑扇区号+1,表示读取下一个逻辑扇区
call read_hard_disk_0 ;调用读取的函数
loop @2 ;循环读,直到读完整个功能程序
pop ds ;用户程序段地址恢复到寄存器
;计算入口点代码段基址
direct:
mov dx,[0x08] ;获取用户程序header记录的程序入口偏移地址
mov ax,[0x06] ;dd双字,需要用两个寄存器存放
call calc_segment_base ;调用calc_segment_base
mov [0x06],ax ;ax作为calc_segment_base的返回值,存放着入口点代码段基址
;开始处理段重定位表
mov cx,[0x0a] ;需要重定位的项目数量
mov bx,0x0c ;重定位表首地址
realloc:
mov dx,[bx+0x02] ;32位地址的高16位
mov ax,[bx] ;低16位
call calc_segment_base ;计算地址
mov [bx],ax ;回填段的基址
add bx,4 ;下一个重定位项(每项占4个字节)
loop realloc
jmp far [0x04] ;转移到用户程序的code_entry记录的start这里
;-------------------------------------------------------------------------------
read_hard_disk_0: ;从硬盘读取一个逻辑扇区
;输入:DI:SI=起始逻辑扇区号
; DS:BX=目标缓冲区地址
push ax ;保存变量
push bx
push cx
push dx
mov dx,0x1f2 ;向0x1f2端口写入要读取的扇区数
mov al,1 ;1表示读取1个,0表示读取全部256个
out dx,al ;使用out向端口写数据
inc dx ;dx加一,0x1f3,设置起始LBA扇区号,一共28位
mov ax,si ;si是程序在硬盘上的起始逻辑扇区号100
out dx,al ;写入LBA地址7~0
inc dx ;0x1f4
mov al,ah ;写入0x00
out dx,al ;LBA地址15~8
inc dx ;0x1f5
mov ax,di ;写入0x00n
out dx,al ;LBA地址23~16
inc dx ;0x1f6
mov al,0xe0 ;LBA28模式,主盘
or al,ah ;这里如果ah低四位中存放的是LBA地址27~24才有用
out dx,al ;LBA地址27~24
inc dx ;0x1f7,设置从硬盘读取
mov al,0x20 ;0x20表示读命令
out dx,al ;向端口写入
.waits:
in al,dx ;硬盘从0x1f7端口收到0x20后将第七位置为1进行准备
and al,0x88 ;准备结束后再将第七位置为0,第三位置为1,用0x88与一下
cmp al,0x08 ;看看是不是第七位为0,第三位为1
jnz .waits ;不忙,且硬盘已准备好数据传输
mov cx,256 ;总共要读取256字(一个扇区512字节),用作循环计数器
mov dx,0x1f0 ;设置端口
.readw:
in ax,dx ;从端口读一个字
mov [bx],ax ;写到ds:bx
add bx,2 ;读的是字,两个字节,加2
loop .readw ;循环读,一直到cx位0,即读完一个扇区
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] ;先加低16位
adc dx,[cs:phy_base+0x02] ;现在dx:ax是代码段起始地址,adc表示带进位假发
shr ax,4 ;逻辑右移,移出来的位放在CF寄存器,左边补零
ror dx,4 ;循环右移,移出来的位既放在CF也补到左边的空位
and dx,0xf000 ;只留dx的高四位,从低四位补过来的(为啥不直接and 0xf呢?)
or ax,dx ;返回结果放在ax中
pop dx ;还原寄存器的值
ret
;-------------------------------------------------------------------------------
phy_base dd 0x10000 ;用户程序被加载的物理起始地址,低四位必须为0要16字节对齐
times 510-($-$$) db 0
db 0x55,0xaa