汇编语言是面向机器的程序设计语言!
面向机器的程序设计语言服务的机器硬件:CPU和存储器
数据存储在存储器中,包括内存条,硬盘,显卡,磁盘,光盘, CPU通过向这些硬件发送相关的指令来完成操作这些硬件,并来操作这些设备中的数据。
CPU是如何发送指令的,会发送什么样的指令? 这个和硬件有关系。
工厂和仓库的关系就好比了CPU和存储器,仓库不只一个,同样我们的存储器也不止一个。
工厂需要加工产品需要向仓库取原料,同样加工好的产品需要放回仓库。 过程就是工厂加工人员得到一份仓库材料领用单,这个就是工厂发送的指令,上面包含着仓库的地址(即是几号仓库),领用材料,当领用人到达仓库后,将这个指令交给仓库管理员,然后就回去,仓库会将材料送入等待工厂工人手中。工人同样需要把加工好的产品交给仓库,同样需要一份授权指令,如果没有,仓库人员吃了,就没有根据了。。
那么这样一个过程,体现在三个方面:工厂加工,仓库取出和仓库存入
那么计算机是如何来处理这一过程:CPU处理,存储器读出 和写入数据,如何写入。通过CPU 发送相关的指令来告诉存储器操作。 CPU如何来获取这些数据? 毕竟不是人,没那么智能,那么CPU将CPU与存储器的连接线分为三部分: 地址总线,数据总线,控制总线
CPU向控制总线中发送一条读取的指令,当然存储器需要知道,读取那部分的数据,同时也需要向地址总线上发送一个指令来告诉存储器操作那个地址的数据,假如是读操作,存储器接受到读的指令,然后将读取的数据推送到数据总线中,CPU得到地址总线中的数据,进行相关的处理,把数据处理好之后,向 存储器发送一个写入的指令,同时向地址总线中推送一个地址,存储器将这个数据写入指定的地址中。
CPU是运算中心,由运算器,控制器,寄存器组成,相对于外部总线来说,通过控制CPU寄存器,就可以间接的实现对存储器的控制。寄存器是有限存贮容量的高速存贮部件,它们可用来暂存指令、数据和位址。指令就是指要执行什么操作,数据就是操作所要的数据,位置就是操的数据所在的位置。
CPU中:
1.运算器进行信息处理
2.寄存器进行信息存储
3.控制器进行与各个部件的控制工作
4.内部总线连接这各种器件,在他们之间进行数据的传递
相关名词
标号 有一个符号标识了内存处的位置,如 start: mov ax,10,start标识一个标号,同样还有一种标号及标识了内存位置,还标识了长度 如 a db 1,3,4,5,6,7,8 a 标识了位置和长度。
伪指令 用于告诉汇编程序如何进行汇编的指令,它既不控制机器的操作也不被汇编成机器代码,只能为汇编程序所识别并指导汇编如何进行。 将相对于程序或相对于寄存器的地址载入寄存器中
借位 数学概念,再做减法时,低位不足时向高位借位,200-10=190,200的十位向百位借位,使得百位变成1.cpu做减法运算时,也需要借位
进位 在一位上得到得数值大于进制数就向高位加1
在进行汇编编程中,CPU已经将这些步骤封装的很完美了,我们只需要针对CPU操作,就能实现对各个存储器的访问,操作,当然还包括CPU的运算。
寄存器
在8086的CPU上,包含着AX,BX,CX,DX,SI,DI,SP,IP,CS,SS,DS,ES等寄存器,通过这些寄存器,我们可以直接的操作CPU 寄存中的数据,和间接的操作存储器中的数据。CPU的寄存器也是一块存储空间。
寄存器的单位有:
字节(byte) 8位
字(word) 16位
双字(dword) 32位
其中AX,BX,CX,DX在CPU存储器中是占16个字节(1个字)的存储器,继而AX,BX,CX,DX,又可以分为AL,AH,BL,BH,CL,CH,DL,DH,分开后的每个寄存器占8个字节
AX 称为累加器,常用于存放算术逻辑运算中的操作数,另外所有的I/O指令都使用累加器与外设接口传送信息,运算结果放在这个寄存器中
BX 称为基址寄存器,常用来存放访问内在时的基地址,[bx] 表示ds:[bx] bx这个基址段偏移
CX 称为计数寄存器,在循环和串操作指令中用作计数器,如基本的loop,rep,jcxz 指令都是通过CX计数进行操作
DX 称为数据寄存器,在寄存器间接寻址中的I/O指令中存放I/O端口的地址.可通过在32运算中,AX放地位,DX放高位来实现.
通常我们向段寄存器中存入一个立即数,都是通过AX作为一个间接的寄存来实现的。在8086CPU中,无法直接的写入一个立即数。同样通过BX,CI,DI实现段的偏移寻址,而 无法通过AX来实现,如[AX]就是一个错误的。CX作为计数器来实现循环等操作,而jcxz通过比较CX==0 来实践计数循环位移跳转。
段寄存器:
段的概念:内存并没有将内存分段,而是CPU 将内存进行管理实现的一种方式,可以实现更大范围,快速的内存寻址。段的大小为16的整数倍,但最大只能是64KB,CPU 将内存进行分段后的寻址方式为: 段地址*16+ 偏移地址 。
CPU提供一个段地址,一个偏移地址,段地址和偏移地址通过地址总线送入一个成为加法器的部件,加法器将两个16位地址进行合并成20位总线地址。也即将中间的数据进行合并处理。为什么是20位,而不是32位,应为早期的CPU地址总线为20位,而CPU寄存器只有16位,16位寄存器无法选址20位中的数据,故用分段的假象方式来实现寻址。然后20位地址被通过地址总线送往存储器.
CS:代码段寄存器,对应的偏移IP。CS:IP告诉了CPU执行的执行,再段CS初,以及偏移IP处。假设CS:1P=0010h:0000h 和段的大小为16,那么CPU将从存储器:00100+0000h处进行读取指令执行。修改CS:IP就可以指定CPU执行我们指定的指令代码。
DS:数据段寄存器,对应的偏移寄存器有 BX,DI,SI。默认的数据段,通常用来保存的储存器数据地址。可以通过DS,IP来改变CPU对内存数据的访问。
SS:堆栈寄存器,对应的偏移寄存器有SP。 以堆栈方式存放数据,先进后出原理,在CPU中,通常有个SP寄存器指向堆栈的偏移,如果是16,则每次偏移2个地址。由于SP总是指向的栈顶,比如果0-15,那么初始状态下SP 指向15,每次往栈中存放数据,SP进行相关偏移,即SP=SP-2,当取出栈中的数据时 ,SP= SP+2。一般情况下,指令的执行也是存放在栈中,代码的返回值也会放入栈中,代码块的嵌套调用也会一层一层的PUSH到栈中。
出栈和入栈操作: push ax
pop ax
ES:
在windows下,查看CPU和内存,用机器指令和汇编指令编程是通过debug来完成的,当然可以通过第三方。
debug模式 下的指令有:
R 查看,改变CPU寄存器的内容,通过 如r 查看, r ax 改变ax寄存器的值
D 查看当前地址处的内容,也可以指定内容处 如:d 13da:0010 也可以访问指定的段 d cs:0000
E 可以修改内存中的数据 e 1000:0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 修改段1000处偏移0处连续的数据为0
U 将内存出的机器命令转换为汇编命令显示输出,默认使用的是CS:IP处的指令,也可以指定如:u 0010:0010
T 执行一条机器指令
A 以汇编形式的格式在内存cs:ip 处写入一条指令
Q 退出当前debug模式
P 执行循环、重复的字符串指令、软件中断或子例程。
汇编特殊说明
0000h表示
0000b表示
bword ptr 表示
word ptr
dword ptr
offset 操作符用来实现取得标号XX处的偏移地址
段寄存器的访问:
访问的段有:DS,和 SS
通过mov 指令,将寄存器的数据移入段中,或者堆栈中。
语法
数据段
mov [偏移],寄存器 (不能直接的将一个立即数放入段中)
堆栈段
push 寄存器
pop 寄存器 (不能将立即数压入栈中或者出栈)
在压入栈的过程中,sp会自动进行相关的自减操作,让自己指向栈顶,同样出栈过程中会进行自加操作,同样是其SP指向栈顶。有些书上写了不会自减要自己实现,在我实践的过程中确实自减了。
寄存器的特殊用途
bx 基址寄存器
cx 计数寄存器
sp 堆栈偏移
ip 代码段的偏移
si 是源变址寄存器,用于存放源操作数的偏移地址
di 是目的寄存器,用于存放目的操作数的偏移地址,并且DI的内容有自动修改的功能,故称为目的变址寄存器
si,di左右偏移地址与bx的区别在于,si,di的偏移不会改变默认的段偏移,只是一个读的状态,而通常用 bx来实现,就会改变寄存器的ds的段的偏移为当前 bx的偏移。bi,si用于多次,循环读取某个段的数据。
标志寄存器
这是一个存放条件标志、控制标志寄存器,主要用于反映处理器的状态和运算结果的某些特征及控制指令的执行。
ZF (Zero Flag)零标志
PF (Parity Flag)奇偶标识,1为偶数,0位奇数
SF (Sign Flag)符号标志
CF (Carry Flag)进位表志。用来反应运算是否产生进位或者借位。如果产生借位或进位,则值为:1,否则0
OF(Overflow Flag) 溢出标志
AF (Auxiliary Carry Flag)辅助进位标志
常用汇编指令
mov 传送指令
add 加法指令,执行相加后的结果会放入ax寄存器中
sub 减法指令,执行相减后的救过放入ax寄存器中
div 出发指令,执行当前操作数除以ax寄存器中的数,结果的商保存在ax中,余数存放在ds中
mul 乘法指令,8位乘法,一个放在al中,一个放在8位寄存器或内存单元中,结果放在ax 中。如果两个16位相乘,一个放在ax中,一个放在内存单元中,就结果高位放在ds中,低位放在ax中
loop 跳转到指定的代码偏移处,循环执行,每次检查cx==0,并做自减操作
jmp 跳转到指定代码偏移处,执行。jmp bword ptr 内存单元地址,此时的ip=ip+8位偏移,以补码形式计算-128-127 区间
jcxz 与jmp类似,只是在跳转代码偏移前会进行cx==0的判断,如果为0则,跳转,否则继续执行
call 与jmp相似,不能实现短转移,跳转时将CS,IP压栈,实现保存功能
ret 利用栈中的数据,实现近转移,即修改IP,CALL会将CS,IP压栈,配合使用
retf 利用栈中的数据,实现远转移,就同时修改CS,IP,CALL会将CS,IP入栈,retf进行出栈操作到cs,ip中
int 实现中断,通过中断数实现中断不同的功能 int 0 即可显示 overflow!
iret
and 逻辑与
or 逻辑或
指令详解
DIV 是除法指令,使用div做除法的时候应注意以下问题:
1、除数:有8位和16位两种,在一个寄存器或者内存中。
2、被除数:默认放在AX或(DX和AX)中,如果除数为8位,被除数为16位,被除数默认在AX中存放,如果除数为16位,被除数为32位,被 除数则在(DX和AX)中存放,DX存放高16位,AX存放低16位。
3、结果:如果除数是8位,则AL存储除法操作的商,AH存储除法操作的余数;如果除数是16位,则AX存储除法操作的商,DX存储除法操作的余数。
CALL,RET
cpu执行ret指令时,进行下面两步操作:
(1)(ip)=((ss)*16+sp)
(2)(sp)=(sp)+2
就是从栈里提出ip
用汇编语言描述即:
pop ip
而retf则为
pop ip
pop cs
call指令与ret指令刚好相反,
call是将ip压入栈中,再进行转移。
不过在此要注意的是 call压入的ip是call指令下一条指令的ip。
相当于:
push ip
jmp near(far) ptr 标号(内存单元地址) /*call指令不能实现短转移*/
call指令与ret指令结合就可以编写子程序
masm工具的应用
模块化代码设计
将代码,数据,堆栈放入不同的段
下面代码创建一个堆栈段,数据段,并将它进行初始化,但我们并不知道段的实际分配地址,这是由于编译器自动为我们进行了分配
assume cs:code,ds:data,ss:stack
data segment
db 0123h,0456h,0789h.......................
data ends
stack segment
dw 0,0,0,0,0........................
stack ends
code segment
start:mov ax,data
mov ds,ax
mov ax,stack
mov ss,ax
......................
code ends
end start
定义多个段,可以通过ax实现 ds,ss对段数据定义的引用,cpu不允许段直接引用一个立即数。
同样我们也可以来实现直接设置段
如
DATA =1000
INSTALL= 10200
assume cs:code
code segment
start: mov ax,DATA
mov ds,ax
同样可以是实现引用指定的段,只是前面一种方法由编译器来分配段,并进行相关的初始化
call与ret指令共同支持了汇编语言中的模块化设计。
在实际编程中,我们需要将参数和结果传给某个模块,或者将返回值进行返回。
可以通过寄存器来实现,将参数和结果放入寄存器,然后程序进行取存
那么通常我们需要传递一组数据同样可以将数据压入栈中或者数据段中进行实现。这里的数据段我们可以通过di,si同时来实现对段的访问,而不改变段的偏移
例如我们做个3的3次幂的运算操作
assume cs:code
code segment
start :mov ax,3
call mul_ax
mov ax,4c00h
int 21h
mul_ax:mov bx,ax
mul bx
mul bx
ret
code ends
end start
汇编代码解读
.model tiny
.386p
;// SYSSIZE是要加载的节数(16字节为1节)。3000h共为30000h字节=192kB
;// 对当前的版本空间已足够了。
SYSSIZE = 3000h ;// 指编译连接后system模块的大小。
;// 这里给出了一个最大默认值。
SETUPLEN = 4 ;// setup程序的扇区数(setup-sectors)值
BOOTSEG = 07c0h ;// bootsect的原始地址(是段地址,以下同)
INITSEG = 9000h ;// 将bootsect移到这里
SETUPSEG = 9020h ;// setup程序从这里开始
SYSSEG = 1000h ;// system模块加载到10000(64kB)处.
ENDSEG = SYSSEG + SYSSIZE ;// 停止加载的段地址
;// DEF_ROOT_DEV: 000h - 根文件系统设备使用与引导时同样的软驱设备.
;// 301 - 根文件系统设备在第一个硬盘的第一个分区上,等等
ROOT_DEV = 301h;//指定根文件系统设备是第1个硬盘的第1个分区。这是Linux老式的硬盘命名
;//方式,具体值的含义如下:
;//设备号 = 主设备号*256 + 次设备号
;// (也即 dev_no = (major<<8 + minor)
;//(主设备号:1-内存,2-磁盘,3-硬盘,4-ttyx,5-tty,6-并行口,7-非命名管道)
;//300 - /dev/hd0 - 代表整个第1个硬盘
;//301 - /dev/hd1 - 第1个盘的第1个分区
;//... ...
;//304 - /dev/hd4 - 第1个盘的第4个分区
;//305 - /dev/hd5 - 代表整个第2个硬盘
;//306 - /dev/hd6 - 第2个盘的第1个分区
;//... ...
;//309 - /dev/hd9 - 第1个盘的第4个分区
;/* ************************************************************************
; boot被bios-启动子程序加载至7c00h(31k)处,并将自己移动到了
; 地址90000h(576k)处,并跳转至那里。
; 它然后使用BIOS中断将'setup'直接加载到自己的后面(90200h)(576.5k),
; 并将system加载到地址10000h处。
;
; 注意:目前的内核系统最大长度限制为(8*65536)(512kB)字节,即使是在
; 将来这也应该没有问题的。我想让它保持简单明了。这样512k的最大内核长度应该
; 足够了,尤其是这里没有象minix中一样包含缓冲区高速缓冲。
;
; 加载程序已经做的够简单了,所以持续的读出错将导致死循环。只能手工重启。
; 只要可能,通过一次取取所有的扇区,加载过程可以做的很快的。
;************************************************************************ */
code segment ;// 程序从_main标号开始执行。
assume cs:code
start: ;// 以下10行作用是将自身(bootsect)从目前段位置07c0h(31k)
;// 移动到9000h(576k)处,共256字(512字节),然后跳转到
;// 移动后代码的 go 标号处,也即本程序的下一语句处。
mov ax,BYTE PTR BOOTSEG ;// 将ds段寄存器置为7C0h
mov ds,ax
mov ax,BYTE PTR INITSEG ;// 将es段寄存器置为9000h
mov es,ax
mov cx,256 ;// 移动计数值 = 256字 = 512 字节
sub si,si ;// 源地址 ds:si = 07C0h:0000h
sub di,di ;// 目的地址 es:di = 9000h:0000h
rep movsw ;// 重复执行,直到cx = 0;移动1个字
; jmp INITSEG:[go] ;// 间接跳转。这里INITSEG指出跳转到的段地址。
db 0eah ;// 间接跳转指令码
dw go
dw INITSEG
go: mov ax,cs ;// 将ds、es和ss都置成移动后代码所在的段处(9000h)。
mov ds,ax ;// 由于程序中有堆栈操作(push,pop,call),因此必须设置堆栈。
mov es,ax
;// put stack at 9ff00. 将堆栈指针sp指向9ff00h(即9000h:0ff00h)处
mov ss,ax
mov sp,0FF00h ;/* 由于代码段移动过了,所以要重新设置堆栈段的位置。
; sp只要指向远大于512偏移(即地址90200h)处
; 都可以。因为从90200h地址开始处还要放置setup程序,
; 而此时setup程序大约为4个扇区,因此sp要指向大
; 于(200h + 200h*4 + 堆栈大小)处。 */
;// 在bootsect程序块后紧跟着加载setup模块的代码数据。
;// 注意es已经设置好了。(在移动代码时es已经指向目的段地址处9000h)。
load_setup:
;// 以下10行的用途是利用BIOS中断INT 13h将setup模块从磁盘第2个扇区
;// 开始读到90200h开始处,共读4个扇区。如果读出错,则复位驱动器,并
;// 重试,没有退路。
;// INT 13h 的使用方法如下:
;// ah = 02h - 读磁盘扇区到内存;al = 需要读出的扇区数量;
;// ch = 磁道(柱面)号的低8位; cl = 开始扇区(0-5位),磁道号高2位(6-7);
;// dh = 磁头号; dl = 驱动器号(如果是硬盘则要置为7);
;// es:bx ->指向数据缓冲区; 如果出错则CF标志置位。
mov dx,0000h ;// drive 0, head 0
mov cx,0002h ;// sector 2, track 0
mov bx,0200h ;// address = 512, in INITSEG
mov ax,0200h+SETUPLEN ;// service 2, nr of sectors
int 13h ;// read it
jnc ok_load_setup ;// ok - continue
mov dx,0000h
mov ax,0000h ;// reset the diskette
int 13h
jmp load_setup
ok_load_setup:
;/* 取磁盘驱动器的参数,特别是每道的扇区数量。
; 取磁盘驱动器参数INT 13h调用格式和返回信息如下:
; ah = 08h dl = 驱动器号(如果是硬盘则要置位7为1)。
; 返回信息:
; 如果出错则CF置位,并且ah = 状态码。
; ah = 0, al = 0, bl = 驱动器类型(AT/PS2)
; ch = 最大磁道号的低8位,cl = 每磁道最大扇区数(位0-5),最大磁道号高2位(位6-7)
; dh = 最大磁头数, 电力= 驱动器数量,
; es:di -> 软驱磁盘参数表。 */
mov dl,00h
mov ax,0800h ;// AH=8 is get drive parameters
int 13h
mov ch,00h
;// seg cs ;// 表示下一条语句的操作数在cs段寄存器所指的段中。
mov cs:sectors,cx ;// 保存每磁道扇区数。
mov ax,INITSEG
mov es,ax ;// 因为上面取磁盘参数中断改掉了es的值,这里重新改回。
;// Print some inane message 在显示一些信息('Loading system ... '回车换行,共24个字符)。
mov ah,03h ;// read cursor pos
xor bh,bh ;// 读光标位置。
int 10h
mov cx,27 ;// 共24个字符。
mov bx,0007h ;// page 0, attribute 7 (normal)
mov bp,offset msg1 ;// 指向要显示的字符串。
mov ax,1301h ;// write string, move cursor
int 10h ;// 写字符串并移动光标。
;// ok, we've written the message, now
;// we want to load the system (at 10000h) 现在开始将system 模块加载到10000h(64k)处。
mov ax,SYSSEG
mov es,ax ;// segment of 010000h es = 存放system的段地址。
call read_it ;// 读磁盘上system模块,es为输入参数。
call kill_motor ;// 关闭驱动器马达,这样就可以知道驱动器的状态了。
;// 此后,我们检查要使用哪个根文件系统设备(简称根设备)。如果已经指定了设备(!=0)
;// 就直接使用给定的设备。否则就需要根据BIOS报告的每磁道扇区数来
;// 确定到底使用/dev/PS0(2,28)还是/dev/at0(2,8)。
;// 上面一行中两个设备文件的含义:
;// 在Linux中软驱的主设备号是2(参加第43行注释),次设备号 = type*4 + nr, 其中
;// nr为0-3分别对应软驱A、B、C或D;type是软驱的类型(2->1.2M或7->1.44M等)。
;// 因为7*4 + 0 = 28,所以/dev/PS0(2,28)指的是1.44M A驱动器,其设备号是021c
;// 同理 /dev/at0(2,8)指的是1.2M A驱动器,其设备号是0208。
;// seg cs
mov ax,cs:root_dev
cmp ax,0
jne root_defined ;// 如果 ax != 0, 转到root_defined
;// seg cs
mov bx,cs:sectors ;// 取上面保存的每磁道扇区数。如果sectors=15
;// 则说明是1.2Mb的驱动器;如果sectors=18,则说明是
;// 1.44Mb软驱。因为是可引导的驱动器,所以肯定是A驱。
mov ax,0208h ;// /dev/ps0 - 1.2Mb
cmp bx,15 ;// 判断每磁道扇区数是否=15
je root_defined ;// 如果等于,则ax中就是引导驱动器的设备号。
mov ax,021ch ;// /dev/PS0 - 1.44Mb
cmp bx,18
je root_defined
undef_root: ;// 如果都不一样,则死循环(死机)。
jmp undef_root
root_defined:
;// seg cs
mov cs:root_dev,ax ;// 将检查过的设备号保存起来。
;// 到此,所有程序都加载完毕,我们就跳转到被
;// 加载在bootsect后面的setup程序去。
; jmp SETUPSEG:[0] ;// 跳转到9020:0000(setup程序的开始处)。
db 0eah
dw 0
dw SETUPSEG
;//------------ 本程序到此就结束了。-------------
;// ******下面是两个子程序。*******
;// 该子程序将系统模块加载到内存地址10000h处,并确定没有跨越64kB的内存边界。
;// 我们试图尽快地进行加载,只要可能,就每次加载整条磁道的数据
;//
;// 输入:es - 开始内存地址段值(通常是1000h)
;//
sread dw 1+SETUPLEN ;// 当前磁道中已读的扇区数。开始时已经读进1扇区的引导扇区
head dw 0 ;// 当前磁头号
track dw 0 ;// 当前磁道号
read_it: ;// 测试输入的段值。必须位于内存地址64KB边界处,否则进入死循环。
mov ax,es ;// 清bx寄存器,用于表示当前段内存放数据的开始位置。
test ax,0fffh
die:
jne die ;// es值必须位于64KB地址边界!
xor bx,bx ;// bx为段内偏移位置。
rp_read:
;// 判断是否已经读入全部数据。比较当前所读段是否就是系统数据末端所处的段(#ENDSEG),如果
;// 不是就跳转至下面ok1_read标号处继续读数据。否则退出子程序返回。
mov ax,es
cmp ax,ENDSEG ;// have we loaded all yet? 是否已经加载了全部数据?
jb ok1_read
ret
ok1_read:
;// 计算和验证当前磁道需要读取的扇区数,放在ax寄存器中。
;// 根据当前磁道还未读取的扇区数以及段内数据字节开始偏移位置,计算如果全部读取这些
;// 未读扇区,所读总字节数是否会超过64KB段长度的限制。若会超过,则根据此次最多能读
;// 入的字节数(64KB - 段内偏移位置),反算出此次需要读取的扇区数。
;// seg cs
mov ax,cs:sectors ;// 取每磁道扇区数。
sub ax,sread ;// 减去当前磁道已读扇区数。
mov dx,ax ;// ax = 当前磁道未读扇区数。
mov cl,9
shl dx,cl ;// dx = ax * 512 字节。
add dx,bx ;// cx = cx + 段内当前偏移值(bx)
;// = 此次读操作后,段内共读入的字节数。
jnc ok2_read ;// 若没有超过64KB字节,则跳转至ok2_read处执行。
je ok2_read
xor ax,ax ;// 若加上此次将读磁道上所有未读扇区时会超过64KB,则计算
sub ax,bx ;// 此时最多能读入的字节数(64KB - 段内读偏移位置),再转换
shr ax,cl ;// 成需要读取的扇区数。
ok2_read:
call read_track
mov dx,ax ;// dx = 该此操作已读取的扇区数。
add ax,sread ;// 当前磁道上已经读取的扇区数。
;// seg cs
cmp ax,cs:sectors ;// 如果当前磁道上的还有扇区未读,则跳转到ok3_read处。
jne ok3_read
;// 读该磁道的下一磁头面(1号磁头)上的数据。如果已经完成,则去读下一磁道。
mov ax,1
sub ax,head ;// 判断当前磁头号。
jne ok4_read ;// 如果是0磁头,则再去读1磁头面上的扇区数据
inc track ;// 否则去读下一磁道。
ok4_read:
mov head,ax ;// 保存当前磁头号。
xor ax,ax ;// 清当前磁道已读扇区数。
ok3_read:
mov sread,ax ;// 保存当前磁道已读扇区数。
shl dx,cl ;// 上次已读扇区数*512字节。
add bx,dx ;// 调整当前段内数据开始位置。
jnc rp_read ;// 若小于64KB边界值,则跳转到rp_read处,继续读数据。
;// 否则调整当前段,为读下一段数据作准备。
mov ax,es
add ax,1000h ;// 将段基址调整为指向下一个64KB段内存。
mov es,ax
xor bx,bx
jmp rp_read
;// 读当前磁道上指定开始扇区和需读扇区数的数据到es:bx开始处。
;// al - 需读扇区数; es:bx - 缓冲区开始位置。
read_track:
push ax
push bx
push cx
push dx
mov dx,track ;// 取当前磁道号。
mov cx,sread ;// 取当前磁道上已读扇区数。
inc cx ;// cl = 开始读扇区。
mov ch,dl ;// ch = 当前磁道号。
mov dx,head ;// 取当前磁头号。
mov dh,dl ;// dh = 磁头号。
mov dl,0 ;// dl = 驱动器号(为0表示当前驱动器)。
and dx,0100h ;// 磁头号不大于1
mov ah,2 ;// ah = 2, 读磁盘扇区功能号。
int 13h
jc bad_rt ;// 若出错,则跳转至bad_rt。
pop dx
pop cx
pop bx
pop ax
ret
;// 执行驱动器复位操作(磁盘中断功能号0),再跳转到read_track处重试。
bad_rt:
mov ax,0
mov dx,0
int 13h
pop dx
pop cx
pop bx
pop ax
jmp read_track
;///*
;//* 这个子程序用于关闭软驱的马达,这样我们进入内核
;//* 后它处于已知状态,以后也就无须担心它了。
;//*/
kill_motor:
push dx
mov dx,3f2h ;// 软驱控制卡的驱动端口,只写。
mov al,0 ;// A驱动器,关闭FDC,禁止DMA和中断请求,关闭马达。
out dx,al ;// 将al中的内容输出到dx指定的端口去。
pop dx
ret
sectors dw 0 ;// 存放当前启动软盘每磁道的扇区数。
msg1 db 13,10 ;// 回车、换行的ASCII码。
db "Loading my system ..." ;// 我加了my,共有27个字符了
db 13,10,13,10 ;// 共24个ASCII码字符。
org 508 ;// 表示下面语句从地址508(1FC)开始,所以root_dev
;// 在启动扇区的第508开始的2个字节中。
root_dev dw ROOT_DEV ;// 这里存放根文件系统所在的设备号(init/main.c中会用)。
boot_flag dw 0AA55h ;// 硬盘有效标识。
code ends
end