鱼C论坛

 找回密码
 立即注册
查看: 3265|回复: 0

[学习笔记] X86汇编语言-从实模式到保护模式—笔记(6)

[复制链接]
发表于 2017-10-31 11:39:04 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能^_^

您需要 登录 才可以下载或查看,没有账号?立即注册

x
本帖最后由 兰陵月 于 2017-12-5 21:56 编辑

第8章  硬盘和显卡的访问与控制
操作系统通常肩负着处理器管理、内存分配、程序加载、进程(即已经位于内存中的程序)调度、外围设备(显卡、硬盘、声卡等)的控制和管理等任务。
程序可以有千千万万个,但加载过程却是固定的。
8.2  用户程序的结构
【8.2.1 分段、段的汇编地址和段内汇编地址】
一个规范的程序,应当包括代码段、数据段、附加段和栈段。这样一来,段的划分和段与段之间的界限在程序加载到内存之间就已经准备好了。
定义段的指令:SECTION或者SEGMENT。一般格式是:
SECTION 段名称    或者   SEGMENT 段名称
处理器并不知道段的用途,不知道它是数据段,还是代码段,或是栈段。段只用来分隔程序中的不同内容。
一旦定义段,那么,后面的内容就都属于该段,除非又出现了另一个段的定义。
Intel处理器要求段在内存中的起始物理地址起码是16字节对齐。这句话的意思是,必须是16的倍数,或者说该物理地址必须能被16整除。
汇编语言源程序中定义的各个段,也有对齐方面的要求。具体做法是:在段定义中使用“align=”子句,用于指定某个SECTION的汇编地址对齐方式。比如align=16表示16字节对齐,align=32表示32字节对齐。
段的汇编地址其实就是段内第一个元素(数据、指令)的汇编地址。
为了取得该段的汇编地址,可以用section.段名称.start表达式。
段定义语句还可以包含“vstart=”子句。尽管定义了段,但是,引用某个标号时,该标号处的汇编地址依然是从整个程序的开头计算的,而不是从段的开头处计算的。如果加入了子句vstart=0,则该标号的汇编地址从它所在段的开头计算,而且从0开始。
【8.2.2 用户程序头部】
一般来说,加载器和用户程序是在不同时间、不同的地方,由不同的人或公司开发的。这就意味着,它们彼此并不了解对方的结构和功能。事实上,也不需要了解。
加载器和用户程序约定在用户程序头部进行沟通。
头部需要在源程序以一个段的形式出现,该段当然必须是第一个被定义的段,且总是位于整个源程序的开头。
用户程序头部起码要包含以下信息:
1、用户程序的尺寸,即以字节为单位的大小。这对加载器来说是很重要的,加载器需要根据这一信息来决定读取多少个逻辑扇区(在本书中,所有程序在 硬盘上所占用的逻辑扇区都是连续的)。
2、应用程序的入口点,包括段地址和偏移地址。加载器并不清楚用户程序的分段情况,更不知道第一条要执行的指令在用户程序中的位置。因此,必须在头部给出第一条指令的段地址和偏移地址,这就是所谓的应用程序入口点(Entry Point)。
入口点的段地址是用伪指令dd声明的,并初始化位汇编地址section.code_1.start,这是一个32位的地址。尽管在16位的环境中,一个段最长为64KB,但它却可以起始于任何20位的物理地址。你不可能用16位的单元保存20位的地址,所以,只能保存为32位的形式。
3、段重定位表。用户程序可能包含不止一个段,比较大的程序可能会包含多个代码段和多个数据段。段的重定位是加载器的工作,它需要知道每个段在用户程序内的位置,即它们分别位于用户程序内的多少字节处。为此,需要在用户程序头部建立一张段重定位表。
用户程序可以定义的段在数量上是不确定的,因此,段重定位表的大小,或者说表项数是不确定的。
8.3  加载程序(器)的工作流程
【8.3.1 初始化和决定加载位置】
从大的方面来说,加载器要加载一个用户程序,并使之开始执行,需要决定两件事。第一,看看内存中的什么地方是空闲的,即从哪个物理内存地址开始加载用户程序;第二,用户程序位于硬盘上的什么位置,它的起始逻辑扇区号是多少。如果你连它在哪里都不知道,怎么找得到它呢!
常数用伪指令equ声明,它的意思是“等于”。常数是在程序运行期间不变的数。用equ声明的数值不占用任何汇编地址,也不在运行时占用任何内存位置。它仅仅代表一个数值,就这么简单。
P0120-1.jpg
    上图是自己设计的随意例子程序编译后的列表文件内容,可以看到用equ声明的内容并未占用汇编地址,程序的汇编地址0x00000000是从标号data_BianLiangOne处开始的。与书上讲述的内容刚好应正。
程序中app_lba_start来代表数值100。以后语句中可以用app_lba_start直接代替100这个立即数。
加载用户程序需要确定一个内存物理地址,用伪指令dd来声明,32位宽度。这个物理地址并无特定要求,只要它是空闲的,并且是16字节对齐的即可,即最低4位必须是0,这样才能形成一个有效的段地址。
【8.3.2 准备加载用户程序】
主引导扇区程序定义成一个段。“vstart=0x7c00”子句表示段内所有元素的汇编地址都将从0x7c00开始计算。
用除法来得到0x10000处的段地址:
访问标号phy_base,将被除数的低地址部分放在AX中,访问标号phy_base+0x02,将被除数的高地址部分放在DX中,除数位BX,进行除法运算,得到的商就是段地址的数值,在AX中。然后将该段地址的值给DS和ES寄存器,这样DS和ES就指向了1000为段地址的段。
【8.3.3 外围设备及其接口】
所有和计算机主机连接的设备,都围绕在主机周围,叫做外围设备。一般来说,分成两种,一种是输入设备,一种是输出设备。
每种外围设备信号传送的型号和方式都不一样,处理器需要与这些设备通信时,就要通过I/O接口来进行。I/O接口就是信号转换器的作用。
I/O设备种类多样,甚至还有没有发明出来的设备,不可能每个都直接与处理器相连接。而且每个I/O设备都要争着和处理器通信,这样就有可能发生冲突。解决这些问题的办法是利用总线技术和输入输出控制设备集中器(I/O Controller Hub,ICH),在计算机主板上它叫做“南桥”。
在ICH内部,集成了一些常规的外围设备接口,如USB、PATA(IDE)、SATA、老式总线接口(LPC)、时钟等。不管什么设备,都必须通过它自己的I/O接口电路同ICH连接。
当处理器想和某个设备通信时,ICH会接到通知。然后,它负责提供相应的传输通道和其他辅助支持,并命令所有其他无关设备闭嘴。同样,当某个设备要跟处理器说话,情况也是一样。
【8.3.4 I/O端口和端口访问】
外围设备和处理器之间的通信是通过相应的I/O接口进行的。具体来说,处理器是通过端口(Port)来和外围设备打交道的。本质上,端口就是一些寄存器,类似于处理器内部的寄存器。不同之处在于,这些叫做端口的寄存器位于I/O接口电路中。
端口大约分为命令端口、状态端口、参数端口、数据端口。
每个端口都有自己的数据宽度。
端口在不同的计算机系统中有着不同的实现方式。有些系统中,端口号是映射到内存地址空间的;有些系统中,端口独立编址的,不和内存发生关系。
端口独立编址的系统中,计算机还有一个引脚M/IO#(#代表低电平有效)。
Intel系统中,只允许65536(十进制数)个端口存在,端口号从0到65535。
in指令,CPU从端口读取数据,存放在AL或者AX中。
in指令的目的操作数必须是寄存器AL或AX,当访问8位端口时,使用寄存器AL;当问16位端口时,使用AX。in指令的源操作数应当是寄存器DX。in al,dx机器码为0xEC,in ax,dx机器码为0xED。
【8.3.5 通过硬盘控制器端口读扇区数据】
硬盘的基本读写单位是扇区,要读就至少读一个扇区,要写就至少写一个扇区,不可能仅读写一个扇区中的几个字节。这样一来,就使得主机和硬盘之间的数据交换是成块的,所以硬盘是典型的块设备。
CHS模式读写,向硬盘控制器分别发送磁头号、柱面号和扇区号(扇区在某个柱面的编号)。
逻辑扇区编址LBA28方法发展到LBA48方法。本章采用LBA28模式。
个人计算机上的主硬盘控制器被分配了8位端口,端口号从0x1f0到0x1f7。
从硬盘上读取数据的过程如下:
1.设置要读取的扇区数量。这个数值写入0x1f2端口,这是个8位端口,因此最多读写255个扇区。如果写入的值为0,则表示要读取256个扇区。每读一个扇区,这个数值就减一(如果数值为0,则可以很好理解为什么可以多读一个扇区)。
2.设置起始LBA扇区号。扇区的读写是连续的,因此只需要给出第一个扇区的编号就可以了。28位的扇区号太长,需要将其分成4段,分别写入端口0x1f3、0x1f4、0x1f5、0x1f6端口。其中0x1f3端口存放的是0~7位;0x1f4号端口存放的是8~15位;0x1f5号端口存放的是16~23位;最后4位在0x1f6端口。
0x1f6端口的低4位(即第0位到第3位,共4位)存放扇区地址的第24~27位。其余的4位中,第4位用于指示硬盘号,0表示主盘,1表示从盘。第6位用于指示模式,0为CHS模式,1为LBA模式。第5位和第7位固定为1。
3.向端口0x1f7写入0x20,请求硬盘读。这也是一个8位端口。
4.等待读写操作完成。端口0x1f7既是命令端口,又是状态端口。在通过这个端口发送读写命令之后,硬盘就忙乎开了。在其忙乎的过程中,它将0x1f7端口的第7位置1,表明自己很忙。一旦硬盘系统准备就绪,它再将此位清零,说明自己忙完了,同时将第3位置1,意思是准备好了,请求主机发送或者接收数据。
5.连续取出数据。0x1f0是硬盘接口的数据端口,而且还是一个16位端口。一旦硬盘控制器空闲,且准备就绪,就可以连续从这个端口写入或者读取数据。
最后,0x1f1端口是错误寄存器,包含硬盘驱动器最后一次执行命令后的状态(错误原因)。
【8.3.6 过程调用】【P128】
读写硬盘是经常要做的事情,尤其对于操作系统来说。
过程又叫例程,处理器可以用过程调用指令转移到这段代码里执行,在遇到过程返回指令时重新返回到调用处的下一条指令紧接着执行。
在第8.3.1节里,我们已经定义了常量app_lba_start,它代表100,也就是用户程序在硬盘上的起始逻辑扇区号。我们在编程的时候认定用户程序是存储在硬盘上逻辑扇区100开始的一段位置,因此,最后编译好程序后,我们在使用fixvhdwr.exe程序把用户程序写入硬盘时,也应该在选择“LBA连续直写模式,起始LBA扇区号:”这一步的时候,将起始扇区号选择为100,否则程序运行时就不能按照设计的情况读写硬盘。
调用过程需要知道该过程的地址。一般来说,过程的第一条指令需要一个标号,以方便引用该过程。这个标号就是过程名。
调用过程可能会用到参数,参数传递最简单的办法是通过寄存器。例题通过DI储存逻辑扇区的高16位,SI储存逻辑扇区的低16位,存放处的内存单元偏移地址在寄存器BX中。这些都是作为参数使用的。
调用过程中可能用到的寄存器在过程返回后,可能需要继续使用,因此在过程的开头,应当将这些可能用到的寄存器压入栈中,并在返回到调用点之前出栈恢复。
【此处从“调用……”到“……POP CS”处的内容,从王爽《汇编语言》第3版第10章得来,P190页开始】
调用过程用“CALL”和“RET”、“RETF”相互配合
CPU执行CALL指令时,进行两步操作:
(1)将当前的IP或CS和IP压入栈中。
(2)转移到标号处执行。
CPU执行ret指令时,相当于进行:POP IP
CPU执行retf指令时,相当于进行:
POP IP
POP CS
调用过程的指令是“CALL”,8086处理器支持四种调用方式。
第一种是16位相对近调用。近调用的意思是被调用的目标过程位于当前代码段内,而非另一个不同的代码段,所以只需要得到偏移地址即可。
操作码为0xE8,后跟16位的操作数(有符号)。因为是相对调用,故该操作数是当前CALL指令相对于目标过程的偏移量。计算过程如下:用目标过程的汇编地址减去当前CALL指令的汇编地址,再减去当前CALL指令以字节为单位的长度(3),保留16位的结果。也就是说被调用过程的首地址必须位于当前call指令-32768~32767字节的地方。
近调用的特征是在指令中使用关键字“near”。但“near”不是必需的,如果call指令中没有提供任何关键字,则编译器认为该指令是近调用。因此“call near 过程名”=“call 过程名”。
“call 0x0500”这句的理解。编译后,0x0500并不会出现在机器码中,它其实也是一个标号,因此编译后,0x0500消失了,取而代之的是经过计算而得出来的1个16位的两个汇编地址相减再减3的相对量。
第二种是16位间接绝对近调用。这种调用也是近调用,只能调用当前代码段内的过程,指令中的操作数不是偏移量,而是被调用过程的真实偏移地址,故称为绝对地址。不过,这个偏移地址不是直接出现在指令中,而是由16位的通用寄存器或者16位的内存单元间接给出。call cx、call [0x3000]、call [bx]、call [bx+si+0x02]等等。
间接绝对近调用指令在执行时,处理器首先按以上的方法计算被调用过程的偏移地址,然后将指令指针寄存器IP的当前值压栈,最后用计算出来的偏移地址取代寄存器IP原有的内容。由于间接绝对近调用的机器指令操作数是16位的绝对地址,因此,它可以调用当前代码段任何位置处的过程。
第三种是16位直接绝对远调用。这种调用属于段间调用,即调用另一个代码但内的过程,所有称为远调用。元调用既需要被调用过程所在的段地址,也需要该过程在段内的偏移地址。16位是针对偏移地址来说的,不是针对段地址。“直接”的意思是段地址和偏移地址直接在call中给出了。call 0x2000:0x3000,机器码9A 30 00 00 20。偏移地址在前,段地址在后。
第四种是16位间接绝对远调用。也属于段间调用,被调用过程位于另一个代码段内,而且,被调用过程所在的段地址和偏移地址是间接给出的。“16位”也是用来限制偏移地址的。必须使用关键字“far”。
尽管call指令通常需要ret/retf和它配对,遥相呼应,但ret/retf指令却并不依赖于call指令。
call指令在执行过程调用时不影响任何标志位,ret/retf指令对标志位也没有任何影响。
检测点8.2
1.call label_proc
2.call bx
3.call [bx]
4.call 0xf000:0x0002
5.call far [0x80]
6.call far [BX+DI+0x08]
【8.3.7 加载用户程序】
程序第30~33行:
mov dx,[2]
mov ax,[0]
mov bx,512                      ;512字节每扇区
div bx
相除后,dx放余数,ax放商(即扇区数)。如果dx为0,则说明程序长度刚好是512的倍数。如果dx不为0,而ax为0,则说明程序不足512字节。如果dx不为0,而ax>=1,则说明程序超过1个扇区长度且不是有不足一个扇区长度的尾数。
程序第34、35、36行
cmp dx,0
jnz @1
dec ax
意思是如果dx不为0则转移到标号@1处,如果dx为0,则执行下面的dec ax语句,这句没执行前,ax的值是程序有多少扇区长度,执行之后ax的值为扇区数-1,因为此时已经读了第一个扇区,所以剩下未读的扇区就是ax-1。
程序第37~39行:
@1:
      cmp ax,0   ;考虑实际长度小于等于512个字节的情况
      jz direct
第一种情况:如果dx不为0跳转过来此处,则再考虑ax是否为0,如果ax为0,代表商为0,则说明程序不足512字节,跳转到direct处。那处已经开始正式剖析用户程序其他详细信息。如果ax不为0,则不跳转,继续往下执行。此时ax不用减1,因为程序不能被512整除,说明还有小于512字节的尾数,这个尾数同样占用一个扇区。也就是说程序长度所占用的扇区数,实际上商(ax)+1(ax,dx都不为零的情况下),前面已经读取了一个扇区。因此还要读取的扇区个数为(商(ax)+1)-1,这个值等于ax。
第二种情况:dx为0,执行dec ax,此时ax的值如果是0,则说明div bx后,ax的值为1,说明整个程序刚好一个扇区。而这个扇区已经读取完毕了,所以跳转到direct处。同第一种,direct处开始正式剖析用户程序其他详细信息。如果在dx为0,且ax不为0(此时ax已经自减过一次),实际上就代表还有ax个扇区要读取。(这个ax不是商,是商自减过一次之后的值)
经过上面的步骤,如果ax为0,则直接处理用户程序表头(即重定位),如果不为0,则继续读取余下的扇区。下面的程序就是读取余下的扇区,把ax的值作为循环次数。
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
【8.3.8 用户程序重定位】
用户程序在编写的时候是分段的。因此,加载器下一步的工作是计算和确定每个段的段地址。
逻辑右移指令shr,会将操作数连续地向右移动指定的次数,每移动一次,“挤”出来的比特被移动到标志寄存器的CF位,左边空出来的位置用比特“0”填充。
shr指令的目的操作数可以是8位或16位的通用寄存器或者内存单元,源操作数是数字1、8位立即数或者寄存器CL。
shr r/m8,1  ;目操是8位通用寄存器/内存单元,源操作数是1
shr r/m16,1  ;目操是16位通用寄存器/内存单元,源操作数是1
shr r/m8,imm8  ; 目操是8位通用寄存器/内存单元,源操是8位立即数
shr r/m16,imm8  ;目操是16位通用寄存器/内存单元,源操是8位立即数
shr r/m8,cl  ;目操是8位通用寄存器/内存单元,源操是寄存器cl
shr r/m16,cl  ; 目操是16位通用寄存器/内存单元,源操是寄存器cl
和8086处理器不同,80286之后的IA-32处理器在执行本指令时,会先将源操作数高3位清零。也就是说,最大的移位次数是31。
shl是逻辑左移指令,语法同shr。
ror循环右移。循环右移指令执行时,每右移一次,移出的比特既送到标志寄存器的CF位,也送进左边空出的位。ror、rol、shl、shr的指令格式都是相同的。
【8.3.9 将控制权交给用户】
现在,用户程序已经在内存中准备就绪,剩下的工作就是把处理器的控制权交给它。交接工作很简单,加载器通过一个16位的间接绝对远转移指令,跳转到用户程序入口点。
入口点是两个连续的字,低字是偏移地址,位于用户程序头部内偏移为0x04的地方;高字时段地址,位于用户程序头部内偏移为0x06的地方,而且这两个字的内容已经重设过,重定位过。
处理器执行jmp far [0x04],直接跳转到了用户程序区域。
【8.3.10 8086处理器的无条件转移指令】
一是相对短转移。
操作码为0xEB,操作数是相对于目标位置的偏移量,仅1字节,是个有符号数。属于段内转移指令,而且只允许转移到距离当前指令-128~127字节的地方。相对短转移指令必须使用关键字“short”。
编译阶段,编译器会检查标号的值,如果数值超过一字节所能允许的数值范围,则无法通过编译。否则,编译器用目标位置的汇编地址减去当前指令的汇编地址,再减去当前指令的长度2,保留1字节的结果,作为机器指令的操作数。
相对短转移指令的汇编语言操作数只能是标号和数值。数值和标号是等价的。在编译阶段,都被用来计算一个8位的偏移量。
二是16位相对近转移
16位相对近转移指令的转移范围稍大一些。它的机器指令操作码为0xE9,而且,该指令的长度为3字节,操作码0xE9后面还有一个16位(2字节)的操作数。
相对的意思同样是指它的操作数是一个相对量,是相对于目标位置处的偏移量。在源程序编译阶段,编译器用目标位置的汇编地址减去当前指令的汇编地址,再减去当前指令的长度3,保留16位的结果,作为机器指令的操作数。它是一个有符号数,因此可以转移到当前指令-32768~32767字节的地方。
现在版本的NASM编译器中,如果没有指定关键字short或者near,那么,如果目标位置距离当前指令-128~127字节,则自动采用short;否则,采用near。
三是16位间接绝对近转移
也是近转移,即只在段内转移。但是,转移到的目标偏移地址不是在指令中直接给出的,而是用一个16位的通用寄存器或者内存地址来间接给出的。比如“jmp near bx”、“jmp near cx”,关键字near可以省略,间接绝对近转移原本就是near的。
其他寻址方式同样适用。如“jmp [标号]”、“ jmp [bx]”、“jmp [bx+si]”等等。要注意“jmp bx”和“jmp [bx]”的区别。
四是16位直接绝对远转移
直接在指令中给出段地址和偏移地址的转移指令,就是直接绝对远转移。“16位”仅仅用来限定偏移地址部分,指偏移地址是16位的。
五是16位间接绝对远转移(jmp far)
远转移的目标地址可以通过访问内存来间接得到,这叫间接远转移,但是要使用关键字“far”。关键字“far”的作用是告诉编译器,该指令应当编译成一个远转移。
处理器执行这条指令后,访问段寄存器DS所指向的数据段,从指令中给出的偏移地址处取出两个字,分别用来替代段寄存器CS和指令指针寄存器IP的内容。
“16位”的意思是,要转移到的目标位置的偏移地址是16位的。
检测点8.3
1、以下指令执行后,寄存器AX中的内容是多少?
mov ax,0x55aa  ;AX=0x55aa  0101 0101 1010 1010
ror ax,8  ;AX=0xAA55
shr ax,2  ;AX=0x2A95
2、按题目的要求写出相应的指令:
a.无条件转移到当前段内标号label_proc处。
jmp label_proc
b.无条件转移到当前段内的另一个位置,偏移地址在寄存器BX中。
jmp bx
c.无条件转移到当前段内的另一个位置,偏移地址保存在当前附加段内由寄存器BX所指向的内存单元中;
jmp [ES:BX]
d.无条件转移,段地址为0xf000,偏移地址为0x0002。
jmp 0xf000:0x0002
e.无条件转移,段地址和偏移地址存放在当前数据段内偏移地址为0x80的地方,低字是目标处的偏移地址,高字为目标处段地址。
jmp far [0x80]
f.无条件转移,段地址和偏移地址存放在当前附加段内,低字位目标的偏移地址,高字为目标的段地址,这两个字在当前附加段内的偏移地址可以用BX+DI+0x80得到。
jmp far [BX+DI+0x80]
8.4  用户程序的工作流程
【8.4.1 初始化段寄存器和栈切换】
进入用户程序后,用户程序的头等大事就是初始化处理器的各个段寄存器DS、ES、SS,以便访问专属于自己的数据。CS就不用初始化了,因为那是加载器负责做的事情。要不然用户程序怎么可能执行呢。
伪指令resb的意思是从当前位置开始,保留指定数量的字节,但不初始化它的值。在源程序编译时,编译器会保留一段内存区域,用来存放编译后的内容。当它看到这条伪指令时,它仅仅是跳过指定数量的字节,而不管里面的原始内容是什么。内存是反复使用的,谁也不知道以前的使用者在这里留下了什么。也就是说,跳过的这段空间,每个字节的值是不确定的。resw、resd作用类似。
resb 256  ;保留256个字节长的未初始化空间。
resw 100  ;保留100个字长的未初始化空间。
resd 50   ;保留50个双字长的未初始化空间。
【8.4.2 调用字符串显示例程】
为太长的行使用续行符“\”。
0x0d回车符,回车之后,光标将移动到行首。
0x0a是换行符,换行之后,光标将移动到下行。
如果又回车又换行,光标将移动到下行的行首。
字符串结束处给一个0,用来标志字符串的结束。这样的字符串称为0终止的字符串,在高级语言里经常使用。
【8.4.3 过程的嵌套】
允许在一个过程中调用另一个过程,这称为过程嵌套。因为每次调用过程时,处理器都把返回地址压在栈中,返回时从栈中取得返回地址,所以,只要栈是安全的,嵌套的过程都能层层返回。
过程嵌套的层数在原则上是没有限制的,唯一的限制是栈的大小。实模式下,栈的空间最大是64KB,每执行一次过程调用需要2字节或4字节,这还没有包括在每个过程内部消耗的栈空间。
【8.4.4 屏幕光标控制】
光标是在屏幕上有规律地闪动的一条小横线,通常用于指示下一个要显示的字符位置。
光标在屏幕上的位置保存在显卡内部的两个光标寄存器中,每个寄存器是8位的,合起来形成一个16位的数值。
光标寄存器是可读可写的。因为显卡从来不自动移动光标位置,这个任务是程序员的。
【8.4.5 取当前光标位置】
对显卡的访问通过索引寄存器间接访问。索引寄存器的端口号是0x3d4,可以向它写入一个值,用来指定内部的某个寄存器。比如,两个8位的光标寄存器,其索引值分别是14(0x0e)和15(0x0f),分别用于提供光标位置的高8位和低8位。
指定了光标寄存器之后,要读写数据,通过数据端口0x3d5。
要学会画流程图。
【8.4.6 处理回车和换行字符】
mul指令执行后,要是结果的高一半为全0,则OF和CF清零,否则置1.对SF、ZF、AF和PF标志的影响未定义。
【8.4.7 显示可打印字符】
标准模式下,屏幕上可以同时显示2000个字符。光标占用一个字符的位置,但整个屏幕只有一个,只能出现在2000个字符位置中的一个上。典型地,程序员要用光标位置来记载和跟踪下一个字符应当显示在什么位置。光标用来指示字符位置,而一个字符在显存中对应两个字节。如此一来,可以将光标位置乘以2,来得到该位置(字符)在显存中的偏移地址。
逻辑左移一次,相当于乘以2;逻辑右移一次,相当于除以2。
不管是换行,还是正常显示字符后推进光标,都可能会使寄存器BX的内容超过1999。
【8.4.8 滚动屏幕内容】
滚动屏幕内容,实际上就是将屏幕上第2~25行的内容整体往上提一行,最后用黑底白字的空白字符填充第25行,使这一行什么也不显示。
【8.4.9 重置光标】
不管是回车、换行,还是显示可打印的字符,上面的各处都给出了光标位置的新数值。因此,需要根据光标位置的新数值写入相应端口,以在新的位置显示光标。
【8.4.10 切换到两一个代码段中执行】
   
第8章习题
1、修改本章源程序8-2,在不使用retf指令的情况下,从段code_1转移到段code_2执行。
mov bp,sp
jmp far [bp]
2、思考一下,如果去掉代码清单8-1的第38、36行,会发生什么情况?
如果去掉代码清单8-1的第38、39行,这时如果ax刚好为0,即程序长度不足一个扇区或刚好一个扇区的情况,那么ax=0作为循环次数给了cx那么程序会读取硬盘接下来的65536个扇区。65536个扇区总共有65536×512byte,即33,554,432字节,即32,768KB,即32M,实际上CPU能访问的内存根本没这么大,只有1M不到,因此会循环覆盖到能够载入的空间,然后把内存高端部分写入,导致系统崩溃死机。

本帖被以下淘专辑推荐:

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2024-3-28 22:58

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表