第八章(下)
加载用户程序
因为程序需要决定要读取的扇区数目,首先需要确定整个程序的大小
program_length dd program_end ;程序总长度[0x00],在8-2中
mov dx,[2] ;加载器部分
mov ax,[0]
接下来用存放在dx,ax寄存器中的内容除以512(存放在bx中)利用循环得到总扇区数目
div bx
cmp dx,0
jnz @1 ;未除尽
dec ax ;已经读了一个扇区,扇区总数减1
注意减一是因为一开始已经读了一个扇区
接着使用昨天的read_hard_disk函数,在硬盘中读取内容
mov ax,ds ;数据存放在内存中ds:ax区域
add ax,0x20 ;得到下一个以512字节为边界的段地址
mov ds,ax
xor bx,bx ;每次读时,偏移地址始终为0x0000
inc si ;下一个逻辑扇区
call read_hard_disk_0
loop @2 ;循环读,直到读完整个功能程序
pop ds ;恢复数据段基址到用户程序头部段
注意这里第二行,ax加上0x20的原因是防止数据一直处在一个逻辑段中,导致段内偏移寄存器过大。因此将段寄存器增加512字节,每一次段内偏移都从开始,同时也不会浪费空间。
其余的指令是为了给read_hard_disk传递参数设置的
用户程序重定位
我们现在已经有了读取的部分程序,即程序开头,现在需要确定每一部分程序在内存空间中的地址
简单来说,输入的是硬盘空间中的地址,输出的是内存中的段基地址
calc_segment_base: ;计算16位段地址
;输入:DX:AX=32位物理地址
;返回:AX=16位段基地址
push dx
add ax,[cs:phy_base]
adc dx,[cs:phy_base+0x02] ;这两句将硬盘空间中地址存到dx:ax中
shr ax,4 ;AX右移四位,空出开头四位地址位置
ror dx,4 ;设置段间地址位置
and dx,0xf000 ;去除dx后几位
or ax,dx ;两寄存器相加,得到总段基地址
pop dx
ret
尽管DX:AX是32位地址,但是他只有20位有效。获得段基地址的方法就是抛弃小的后四位,再把大的四位放在前面(因为其他的位作为段内偏移)以上程序就是干这件事的
这里shr和x86-64中汇编不同,始终为逻辑右移,并且多余的位会被存放在寄存器cf中
ror指令指,向右移动并且把溢出的位放在cf和目标寄存器开头的位置,像一个圆圈的循环
之后再将之前程序头部中已经定义过的各个段的位置放到上述程序中,处理过后输出到[bx]中,接下来,准备将控制权交给用户程序
将控制权交给用户程序
入口方式是:间接绝对远跳转指令
jmp far [0x04] ;转移到用户程序
;用户程序部分
code_entry dw start ;偏移地址[0x04]
dd section.code_1.start ;段地址[0x06]
注意跳转之后,处理器会自动重写CS和IP寄存器的内容,那么寄存器会无脑直接获得下一次执行的位置
无条件转移指令(jmp)
1.相对短转移:0XEB
jmp short infinite
注意infinite代表的立即数或者标号的值只能是最大一字节大小,同时也必须有short关键字
2.16位相对近转移:0XE9
jmp near infinite
jmp near 0x3000
和之前那个指令的区别是,此处可以是两字节跳转大小
以上两指令立即数部分都是相对于目标地址的偏移量,这也就是相对的含义
3.16位间接绝对近转移
jmp (near) bx
jmp (near) cx
注意,此时不是相对偏移,而是直接用bx/cx中的内容代替指令指针寄存器ip中的内容
4.16位直接接绝对远转移
jmp 0x0000:0x7c00
5.16位间接绝对远转移
jmp_far dw 0x33c0,0xf000
jmp far [jmp_far]
jmp far [bx]
注意这句话的意思是:从cs=0x33c0,ip=0xf000处取出两个字,分别用来代替段寄存器cs和指针寄存器ip的内容
这里16位的意思就是,取出的大小是两个字的大小
监测点8.3
- 0010_0110_0101_0101
- a.给出标号的情况下,是绝对转移
jmp near label_proc
jmp short label_proc
b.
jmp bx
c.
jmp [bx]
d:
jmp 0xf000:0x0002
e:
jmp_far dw [0x82],[0x80] ;0x82存放段地址,需要倒置
f:
附加段:*附加段寄存器ES:存放当前执行程序中一个辅助数据段的段段地址。*
先计算ES:IP地址:ES:(BX+DI+0X08)
接着把两个字节移到相应寄存器,同e
用户程序的工作流程
初始化寄存器和栈切换
start:
;初始执行时,DS和ES指向用户程序头部段
mov ax,[stack_segment] ;设置到用户程序自己的堆栈
mov ss,ax
mov sp,stack_end
mov ax,[data_1_segment] ;设置到用户程序自己的数据段
mov ds,ax
以上程序所做工作
- 设置ss,sp(栈指针) 从 加载器的空间到用户程序的空间
- 设置dx,将段基址从加载器拿出到用户部分
调用字符串显示例程
这里先把想要输出的字符串预先db定义,之后通过判断字符是否为0来判断是否到达结尾。在判断到达结尾之后exit,未到达结尾则调用put_char函数,之后继续循环。直到exit
介绍:屏幕光标
屏幕光标继承于历史悠久的显卡。
光标在屏幕上的位置储存在显卡的两个光标寄存器中,每个寄存器是8位的,合起来形成一个16位的数值
标准VGA模式,每行80个字符,总共25行
因此,最右下角的光标序号是25*80-1(因为从0开始)=1999。将这个值储存在之前说的16位寄存器中,就可以得到光标位置了
之后是显示文本阶段
要处理的是 换行符,回车符(回到行首)和普通可见字符
注意这里的换行不是默认到行首,而是直接在VGA模式下,行数+1
在显示字符的时候,用到一个技巧:光标*2即为要显示的字符
.put_other: ;正常显示字符
mov ax,0xb800
mov es,ax
shl bx,1 ;光标*2为ascii码存放的位置
mov [es:bx],cl
;以下将光标位置推进一个字符
shr bx,1 ;恢复原先光标位置
add bx,1 ;光标位置自增
在处理换行的时候,我们还要注意是否超出屏幕的显示区域
在朴实无华的机器语言中,处理滚动屏幕内容的方法是
将数据从一个内存区域搬运到另外一个(整体向上移动),再清除最后一行使之变为空白
核心指令:movsw
源区域开始位置:DS:SI
目标区域开始位置:ES:DI
cx:传送字数
在这里:源区域是第二行第一列开始,目标区域从第一行第一列开始,传递字符1920(24*80)(正好一个字符需要一个字节表示)
执行rep movsw完成工作
接着重置光标,完成接下来的指令
作业下一次一定补上!今天太晚了,睡觉啦