硬盘和显卡的访问与控制

        离开主引导扇区,前方是操作系统。
        和主引导扇区程序一样,操作系统也位于硬盘是上。操作系统安装到硬盘上,安装过程不但要把操作系统的指令和数据写入硬盘,通常还需更新主引导扇区内容。
        操作系统通常肩负处理器管理,内存分配,程序加载,进程调度,外围设备【显卡,硬盘,声卡等】的控制和管理等任务。
        编译好的程序,加载到内存并运行。首先要读取硬盘,决定把它加载到内存的什么位置。程序通常是分段的,载入内存后,还要重新计算段地址,叫做段的重定位。
        本章,我们把主引导扇区改造成一个程序加载器,或者说一个加载程序,它的功能是加载用户程序,执行该程序【将处理器控制权交给该程序】。总的说,本章目标:
       1.模拟操作系统加载应用程序过程,演示段的重定位方法。最终理解8086处理器的分段内存管理机制。
       2.学习x86处理器过程调用的程序执行机制。
       3.以读硬盘扇区,控制屏幕光标为实例,了解x86处理访问外围硬件设备的方法。
       4.总结JMP和CALL指令的全部格式
       5.认识更多的x86处理器指令,如in,out,shl,shr,rol,ror,jmp,call,ret等。

1.1.本章代码清单

        主引导扇区程序、加载器 被加载的用户程序

1.2.用户程序的结构

1.2.1.分段,段的汇编地址和段内汇编地址

        处理器的工作模式是将内存分成逻辑上的段,指令的获取和数据的访问一律按"段地址:偏移地址"的方式进行。
        相应地,一个规范的程序,应包括代码段,数据段,附加段,栈段。这样,段的划分,段与段之间的界限在程序加载到内存之前就准备好了。
        8-2,代码段和数据段是以段的形式组织的。
        NASM编译器使用汇编指令"SECTION"或者"SEGMENT"来定义段。一般格式是:

SECTION 段名称
SEGMENT 段名称

        
        每个段都要求给出名字,就是段名称,主要用来引用一个段,可以是任意名字,只要它们彼此之间不重复,混淆。
        段只用来分隔程序中的不同内容。一旦定义段,则,后面的内容就属于该段,除非又出现另一个段的定义。有时候,程序并不以段定义语句开始。这时,这些内容默认地自成一个段。最为典型是,整个程序都没有段定义语句。这时,整个程序自成一个段。
        NASM对段的数量无限制。Intel处理器要求段在内存中起始物理地址起码是16字节对齐的。地址需为16倍数。相应地,汇编语言源程序中定义的各个段,也有对齐方面的要求。具体是,在段定义中使用"align="子句,用于指定某个SECTION的汇编地址对齐方式。如"align=32"表示段是32字节对齐的.源程序编译阶段,编译器将根据align子句确定段的起始汇编地址。
        段的汇编地址其实就是段内第一个元素【数据,指令】的汇编地址。
        每个段都有一个汇编地址,它是相对于整个程序开头【0】的。为了方便取得该段的汇编地址,NASM编译器提供了以下的表达式,可用在你的程序中:

ccccsection.段名称.start.

        
        尽管定义了段,但是,引用某个标号时,该标号处的汇编地址依然是从整个程序的开头计算的,而不是从段的开头处计算的。vstart可以解决这个问题。
        因为段code的定义中有"vstart=0"子句,所以,段内标号"putch"的汇编地址要从它所在段的开头计算,从0开始计算。
        最后一个段trail的定义中没有包含"vstart=0",相应的,该段内的标号"program_end",它的汇编地址就要从整个程序开头计算。

1.2.2.用户程序头部

         加载器
         用户程序【协议头部,程序体】
        加载器,用户程序编写者,之间必须有一个协议。比如说,在用户程序内部的某个固定位置,包含一些基本的结构信息,每个用户程序都必须把自己的情况放在这里,而加载器也固定在这个位置读取。
        头部需要在源程序以一个段的形式出现。

SECTION header vstart=0


        而且,因为它是"头部",所以,该段当然必须是第一个被定义的段,且总是位于整个源程序的开头。
        用户程序头部起码要包含以下信息。
        1.用户程序的尺寸,即以字节为单位的大小。
        加载器需根据这一信息来决定读取多少个逻辑扇区。伪指令dd用于声明和初始化一个双字,即一个32位的数据。双字在内存中的存放也是按低端序的。
        2.应用程序的入口点,包括段地址和偏移地址。
        加载器不清楚用户程序的分段情况,更不知道第一条要执行的指令在用户程序中的位置。因此,必须在头部给出第一条指令的段地址和偏移地址,这就是所谓的应用程序入口点。
        默认下,用户程序开始运行时,执行的第一条指令是其代码段内的第一条指令。即入口点位于其代码段内偏移地址为0的地方。
        入口点的段地址用伪指令dd声明,类似section.code_1.start,这样,仅仅是编译阶段确定的汇编地址,在用户程序加载到内存后,需根据加载的实际位置重新计算【浮动】。
        3.段重定位表
        程序加载到内存后,每个段的地址必须重新确定一下。
        段的重定位是加载器的工作,它需知道每个段在用户程序内的位置。为此,需在用户程序头部建立一张段重定位表。
        段重定位表,每个表项用伪指令dd声明并初始化为一个双字.

1.3.加载程序【器】的工作流程


1.3.1.初始化和决定加载位置


        加载器要加载一个用户程序,并使之开始执行,需决定两件事。
        第一,看看内存中的什么地方是空闲的,即从哪个物理内存地址开始加载用户程序;第二,用户程序位于硬盘上的什么位置,它的起始逻辑扇区号是多少。
        常数用伪指令equ声明的,它的意思是"等于"。app_lba_start equ 100.表示用标号app_lba_start代表数值100。
        常数意思是在程序运行期间不变的数。和其他伪指令db,dw,dd不同,用equ声明的数值不占用任何汇编地址,也不在运行时占用任何内存位置。它仅仅代表一个数值。
        加载用户程序需确定一个内存物理地址。
        A0000~FFFFF一般为ROM BIOS区间。
        10000~9FFFF一般为应用可用空间。
        00000~0FFFF一般为加载器,引导扇区,栈空间。

1.3.2.准备加载用户程序


        我们将主引导扇区程序定义成一个段。 SECTION mbr align=16 vstart=0x7c00 因为vstart=0x7c00,段内一个标号a编译后的值=标号a距离段起始位置偏移+vstart

1.3.3.外围设备及其接口


        加载器的下一个工作是从硬盘读取用户程序,就是访问其他硬件。和处理器打交道的硬件很多,不单单是硬盘,还有显示器,网络设备,扬声器【喇叭】和话筒【麦克风】,键盘,鼠标等。
        一般来说,把这些设备分成两种,一种是输入设备,比如键盘,鼠标,麦克风,摄像头等;另一种是输出设备,比如显示器,打印机,扬声器等。输入设备和输出设备统称输入输出设备。
        不同的设备,有不同的连线数量,线里面传送的信号也不一样,各自的插头和插孔也千差万别。这里需要一些信号转换器和变速齿轮,这就是I/O接口。

        举几个例子,麦克风和扬声器需要一个I/O接口,即声卡,才能与处理器沟通;显示器也需要一个I/O接口,即显卡,才能与处理器沟通;USB键盘同样需要一个I/O接口,即USB接口,才能与处理器沟通。很显然,不同的外围设备,都有各自不同的I/O接口。
        I/O接口可以是一个电路板,也可能是一块小芯片。无论如何,它是一个典型的变换器,或者说是一个翻译器,在一边,它按处理器的信号规程工作,负责把处理器的信号转换成外围设备能接受的另一种信号;在另一边,它也做同样的工作,把外围设备的信号变换成处理器可以接受的形式。
        后面还有两个麻烦的问题。
        1.不可能将所有的I/O接口直接和处理器相连。
        2.每个设备的I/O接口都抢着和处理器说话,发生冲突怎么办。
        对第一个问题的解答是采用总线技术。总线可认为是一排电线,所有的外围设备,包括处理器,都连接到这排电线上。但是,每个连接到这排电线上的器件都必须拥有电子开关,以使它们随时能同这排电线连接,或者从这排电线上断开【脱离】。因此,这排公共电线就称为总线。
        对第二个问题的解答是使用输入输出控制设备集中器芯片,该芯片的作用是连接不同的总线,并协调各个I/O接口对处理器的访问。个人计算机上,这块芯片成为南桥。

        处理器通过局部总线和输入输出控制设备集中器芯片相连。

芯片内部:
公用总线。
处理器接口。
USB接口。【通过USB总线连接鼠标,键盘,U盘】
IDE/SATA接口【通过IDE/SATA总线连接硬盘】
时钟、DMA、中断、定时器、网络、LPC、电源管理等接口
PCI、PCIE接口【通过PCI(E)总线、扩展槽连接独立的声卡,显卡等】

        不管什么设备,都必须通过它自己的I/O接口电路同输入输出控制设备集中器【芯片】相连。为了方便,最好在主板上做一些插槽,同时,每个设备的I/O接口电路都设计成插卡。想连上该设备时,就把它的I/O接口卡插上,不需要时,拔下。

        为此,ICH还提供对PCI或PCI Express总线的支持,该总线向外延伸,连接着主板上的若干扩展槽。举例,如想连接显示器,就先插入显卡,再把显示器连接到显卡。

        除了局部总线和PCI Express总线,每个I/O接口卡可能连接不止一个设备。如USB接口,可能连键盘,鼠标,U盘等。这就涉及线路复用和仲裁,故它们也有自己的总线体系,称之为通信总线或设备总线。        

        当处理器想同某个设备说话时,ICH接到通知。它负责提供相应的传输通道和其他辅助支持,并命令其他无关设备闭嘴。某个设备要跟处理器说话,也类似。

1.3.4.I/O端口和端口访问

        外围设备和处理器间通信通过相应I/O接口进行的。

        具体,处理器通过端口来和外围设备打交道。本质上,端口就是一些寄存器。类似于处理器内部的寄存器。不同之处在于,这些叫端口的寄存器位于I/O接口电路中。

        端口是处理器和外围设备通过I/O接口交流的窗口,每一个I/O接口可能拥有好几个端口,分别用于不同目的。比如,连接硬盘的PATA/SATA接口有几个端口,分别是命令端口【向该端口写入0x20,表明从硬盘读数据;写入0x30时,表明向硬盘写数据】,状态端口【处理器根据这个端口的数据来判断硬盘工作是否正常,操作是否正常,发生了哪种错误】,参数端口【处理器通过这些端口告诉硬盘读写的扇区数量,起始的逻辑扇区号】和数据端口【连续取得要读出的数据,或连续地发送要写入硬盘的数据】。

        端口不过是位于I/O接口上的寄存器,所以,每个端口有自己的数据宽度。早期系统中,端口可是8位,16位,现在有些端口会是32位。

        端口在不同计算机系统中有不同的实现方式。一些计算机系统中,端口号是映射到内存地址空间的。比如,0x00000~0xE0000是真实的物理内存地址,而0xE0001~0xFFFFF是从很多I/O接口那里映射过来的,访问这部分地址时,实际上是在访问I/O接口。

        另一些计算机系统中,端口是独立编址的,不和内存发生关系。这种计算机中,处理器的地址线既连接内存,也连接每一个I/O接口。但是,处理器还有一个特殊的引脚M/IO#,这里,"#"表示低电平有效。即处理器访问内存时,会让M/IO#引脚呈高电平,这里,和内存相关的电路会打开;相反,如果处理器访问I/O端口,则M/IO#引脚呈低平,内存电路被禁止。同时,处理器发出的地址和M/IO#信号一起用于打开某个I/O接口,如果该I/O接口分配的端口号与处理器地址吻合的话。

        本章,只讲独立编址的端口。

        所有端口都是统一编号的,比如0x0001,0x0002,...。每个I/O接口电路都分配了若干端口,比如I/O接口A有3个端口,端口号分别是0x0021~0x0023;I/O接口B需要5个端口,端口号分别是0x0303~0x0307。

        现实的一个例子是计算机的PATA/SATA接口。每个PATA和SATA接口分配了8个端口。但ICH芯片内部通常集成了两个PATA/SATA接口,分别是主硬盘接口,副硬盘接口。这样,主硬盘接口分配的端口号是0x1f0~0x1f7,副硬盘接口分配的端口号是0x170~0x177。

        在Intel系统中,只允许65536个端口存在,端口号从0到65535【0x0000~0xffff】。因为是独立编址,所以端口的访问不能使用类似于mov这样的指令,取而代之是in和out指令。

        in指令是从端口读,它的一般形式是:

        in        al, dx

        或者

        in        ax,dx

        in指令的目的操作数必须是寄存器AL或AX,访问8位端口时,用AL;访问16位端口时,用AX。in指令的源操作数应当是寄存器DX。

        in指令不影响任何标志位。

        相应地,如果要通过端口向外围设备发送数据,则必须通过out指令。

        out指令正好和in指令相反,目的操作数可以是8位立即数或寄存器DX,源操作数必须是寄存器AL或AX。

        out        0x37, al        ;写0x37号端口【8位端口】

        out        0xf5, ax        ;写0xf5号端口【16位端口】

        out         dx, al           ;端口号在寄存器dx中,是一个8位端口

        out        dx, ax           ;端口号在寄存器dx中,是一个16位端口

 1.3.5.通过硬盘控制器端口读扇区数据

        硬盘读写的基本单位是扇区。这样一来,使得主机和硬盘间的数据交换是成块的,所以,硬盘是典型的块设备。

        从硬盘读写数据,最经典的方式是向硬盘控制器分别发送磁头号,柱面号,扇区号【扇区在某个柱面上的编号】,称为CHS模式。

        实际上,很多时候,我们不关心扇区的物理位置,希望所有扇区都能统一编址。这就是逻辑扇区,它把硬盘上所有可用的扇区都一一从0编号,不管它位于哪个盘面,柱面。

        最早的逻辑扇区编制方法是LBA28,使用28个比特来表示逻辑扇区号,从逻辑扇区0x0000000到0xFFFFFFF,共可以表示2^28个扇区。每个扇区有512字节,所以LBA28可以管理128的硬盘。

        业界又共同推出了LBA48,采用48个比特来表示逻辑扇区号。如此,就可管理131072TB的硬盘容量了。

       1GB = 1024MB

        1TB = 1024GB

        本章中,我们将采用LBA28来访问硬盘。

        前面说过,个人计算机上的主硬盘控制器被分配了8位端口,端口号从0x1f0到0x1f7。假设现在要从硬盘上读逻辑扇区,则,整个过程如下:

        第一步,设置要读取的扇区数量。这个数值要写入0x1f2端口。这是个8位端口,因此每次只能读写255个扇区。

        mov        dx,0x1f2

        mov        al,0x01

        out          dx,al

        注意,如果写入值为0,表示要读取256个扇区。每读一个扇区,这个数值就减1。因此,如果读写过程发生错误,该端口包含着尚未读取的扇区数。

        第二步,设置起始LBA扇区号。扇区的读写是连续的。28位的扇区号太长,需将其分成4段,分别写入端口0x1f3、0x1f4、0x1f5、0x1f6号端口。

        其中,0x1f3号端口存放0~7位;0x1f4存接下来8位;0x1f5存接下来8位;0x1f6存剩余4位。假定要读写的起始逻辑扇区号为0x02,可编写如下代码:

        mov               dx,0x1f3

        mov               al,0x02

        out                 dx,al

        inc                 dx

        mov              al,0x00

        out                ax,al

        inc                dx

        out                dx,al

        inc                dx

        mov              al,0xe0

        out                dx,al

        注意以上代码最后4行,现行体系下,每个PATA/SATA接口允许挂接两块硬盘,分别是主盘和从盘。0x1f6端口的低4位用于存放逻辑扇区号的24~27位,第4位用于指示硬盘号,0表示主盘,1表示从盘。高3位是"111",表示LBA模式。

        第三步,向端口0x1f7写入0x20,请求硬盘读。是一个8位端口。

        mov dx,0x1f7

        mov  al,0x20

        out dx,al 

        第四步,等待读写操作完成。端口0x1f7既是命令端口,又是状态端口。在通过这个端口发送读写命令之后,硬盘就忙乎开了。以下是端口0x1f7部分状态位含义解释。

        在它内部操作期间,它将0x1f7端口的第7位置"1",表明自己很忙。一旦硬盘系统准备就绪,它再将此位清零,说明自己忙完了。同时将第3位置"1",意思是准备好了,请求主机发送或接收数据。第0位,为1表明命令执行错误。具体错误原因可访问端口0x1f1。完成这一步典型代码如下:

        mov      dx,0x1f7

.waits:

        in          al,dx

        and       al,0x88 

        cmp      al,0x08

        jnz        .waits

        第五步,连续取出数据。0x1f0是硬盘接口的数据端口,且还是一个16位端口。一旦硬盘控制器空闲,且准备就绪,就可连续从这个端口写入或读取数据。下面的代码假定是从硬盘读一个扇区【512字节或256字节】,读取的数据存放到由段寄存器DS指定的数据段,偏移地址由寄存器BX指定。

        mov        cx,256

        mov        dx,0x1f0

.readw:

        in ax,dx

        mov [bx],ax

        add bx,2

        loop .readw    

        最后,0x1f1端口是错误寄存器,包含硬盘驱动器最后一次执行命令后的状态【错误原因】。

1.3.6.过程调用

        如果每次读写硬盘都按上面5个步骤写一堆代码,程序势必很大,也令人烦恼。

        好在处理器支持一种叫过程调用的指令执行机制。过程又叫例程,或者子程序,子过程,子例程,不管怎么称呼,实质都是一段普通的代码。处理器可用过程调用指令转移到这段代码执行,在遇到过程返回指令时重新返回到调用处的下一条指令接着执行。

        过程调用开始一般用push指令保护现场。返回前用pop指令恢复现场。

        要调用过程,需要该过程的地址。一般过程的第一条指令需要一个标号,以方便引用。

        参数传递最简单的办法就是通过寄存器。这里,主程序把起始逻辑扇区号的高16位存放在寄存器DI中【只有低12位有效,高4位为0】,低16位存放在寄存器SI中;并约定将读出来的数据存放到由段寄存器DS指向的数据段中,起始偏移地址在寄存器BX中。

        调用过程前,程序会用到一些寄存器,在过程返回后,可能还要继续用。为不失连续性,在过程的开头,应当将本过程要用到【内容肯定会被破坏】的寄存器临时压栈,并在返回到调用点之前出栈恢复。

        以后,将把过程所在的标号作为过程的名字,即过程名。

        调用过程的指令是"call"。8086处理器支持四种调用方式。        

        第一种是16位相对近调用。近调用的意思是被调用的目标过程位于当前代码段内,而非另一个不同的代码段,所以,只需得到偏移地址即可。

        16位相对近调用是三字节指令,操作码为0xE8,后跟16位的操作数,因为是相对调用,故该操作数是当前call指令相对于目标过程的偏移量。计算过程如下:用目标过程的汇编地址减去当前call指令的汇编地址,再减去当前call指令以字节为单位的长度【3】,保留16位的结果。举个例子:

        call near proc_1        ;等价于call proc_1,即默认也是近调用

        近调用的特征是在指令中使用关键字"near"。"proc_1"是程序中的一个标号。在编译阶段,编译器用标号proc_1处的汇编地址减去本指令的汇编地址,再减去3,作为机器指令的操作数。

         因为16位相对近调用的操作数是两个汇编地址相减的相对量,所以,如果被调用过程在当前指令前方,则相对量是正数;反之,是负数。所以,机器指令操作数是16位有符号数。

        指令执行阶段,处理器看到操作码0xE8,就知它应调用一个过程。于是,它用指令指针寄存器IP的当前内容加上指令中的操作数,再加上3,得到新的偏移地址。接着将IP的原有内容压入栈。最后,用刚才计算出的偏移地址取代IP原有内容。

        第二种是16位间接绝对近引用。也是近调用,只能调用当前代码段内的过程,指令中的操作数是被调用过程的真实偏移地址。这个偏移地址不是直接出现在指令中,而是由16位的通用寄存器或16位的内存单元间接给出。

        call cx                        ;目标地址在cx中

        call [0x3000]              ;要访问内存才能得到目标偏移地址

        call [bx]                      ;

        call [bx+si+0x02]        ;

         间接绝对近调用在执行时,处理器首先按以上方法计算被调用过程的偏移地址,然后将指令指针寄存器IP的当前值压栈,最后用计算出来的偏移地址取代寄存器IP原有的内容。

        第三种是16位直接绝对远调用。属于段间调用。很容易想到,远调用既需要被调用过程所在段的段地址,也需要该过程在段内的偏移地址。

        "16位"是针对偏移地址来说的,不是限定段地址,尽管段地址事实上也是16位的;"直接"的意思是,段地址和偏移地址直接在call指令中给出了。这里的地址是绝对地址。比如:

        call        0x2000:0x0030

        这条指令编译后的机器码为9A 30 00 00 20

        处理器在执行时,首先将代码段寄存器CS的当前内容压栈,接着再把指令指针寄存器IP的当前内容压栈。接着,用指令中给出的段地址代替CS原有的内容,用指令中给出的偏移地址代替IP原有的内容,直接导致处理器从新的位置开始执行。

        第四种是16位间接绝对远调用。段间调用。被调用过程所在的段地址和偏移地址是间接给出的。还有,这里的"16位"同样用来限定偏移地址的。下面是几个例子:

        call far [0x2000]

        call far [proc_1]

        call far [bx]

        call far [bx+si] 

        间接远程调用必须使用关键字"far"。

        因为是远调用,也就是段间调用。所以,必须给出被调用过程的段地址和偏移地址。但,段地址和偏移地址在内存中的其他位置,指令中仅仅给出的是该位置的偏移地址。

        假如在数据段内声明了标号proc_1并初始化了两个字。

        proc_1 dw 0x0102,0x2000 

        这两个字分别是某个过程的段地址和偏移地址。按处理器要求偏移地址在前,段地址在后。

        则,为了调用该过程,可以在代码段内使用这条指令:

        call far [proc_1] 

        从[proc_1]处取得两个字;接着,将代码段寄存器CS和指令指针寄存器IP的当前内容分别压栈;最后,用刚才取得的段地址和偏移地址分别取代CS和IP的原值。

        过程执行完了,还得返回到调用点继续执行下一条指令,这称为过程返回。

        ret和retf经常用做call和call far的配对指令。ret是近返回指令,当它执行时,处理器只做一件事,就是从栈中弹出一个字到指令指针寄存器IP中。

        retf是远返回指令。当它执行时,处理器分别从栈中弹出两个字到指令指针寄存器IP和代码段寄存器CS中。

        一般过程开始前会将过程中需要写的寄存器内容先push到栈。过程结束返回前,再一次pop出来。

        需要说明的是,尽管call通常需要和ret/retf配对。但ret/retf并不以来于call指令。

        call指令在执行过程调用时不影响任何标志位,ret、retf指令对标志位也没任何影响。

8.3.7.加载用户程序        

        用户程序头部结构

偏移0x00处4字节存储用户程序总长度

偏移0x04处2字节存储偏移地址

偏移地址0x06处4字节存储入口点所在代码段的汇编地址

偏移地址0x0A处2字节存储段重定位表项数

偏移地址0x0C存储段重定位表格

此后为用户程序指令和数据

 8.3.8.用户程序重定位

        逻辑右移指令执行时,会将操作数连续向右移动指定次数,每移动一次,"挤"出来的比特被移动到标志寄存器的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, c1        ;目的操作数是8位通用寄存器、内存单元,源操作数是寄存器CL

shr r/m16, c1        ;目的操作数是16位通用寄存器、内存单元,源操作数是寄存器CL。

        源操作数为1的逻辑右移是特殊设计的优化指令,如,shr ax,1,它的机器码是D1 E8;类似的shr ax,5则为C1 E8 05。

        和8086处理器不同,80286之后的IA-32处理器在执行本指令时,会先将源操作数的高3位清零。

        shr的配对指令是逻辑左移指令shl,它的指令格式和shr相同,只不过它只是向左移动。

        尽管DX:AX中是32位的用户程序起始物理内存位置。理论上只有20位有效。

        ror是循环右移。每右移一次,移出的比特既送到标志寄存器的CF位,也送进左边空出的位。

        ror配对指令是rol。ror,rol,shl,shr的指令格式都是相同的。

8.3.9.将控制权交给用户程序

        处理器执行指令

jmp far [0x04]

        时,会访问段寄存器DS所指向的数据段,从偏移地址为0x04地方取出两个字,分别传送到代码段寄存器CS和指令指针寄存器IP。

8.3.10.8086处理器的无条件转移指令

        1.相对短转移

        操作码为0xEB,操作数是相对于目标位置的偏移量,仅1字节,是个有符号数。由此,该指令属于段内转移指令,且只允许转移到距离当前指令-128~127字节的地方。相对短转移指令需用关键字"short"。例如:

jmp short infinite

        在源程序编译阶段,编译器会检查标号infinite所代表的值,如数值超过了一字节所能允许的数值范围,则无法通过编译。否则,编译器用目标位置汇编地址减去当前指令的汇编地址,再减去当前指令长度【2】,保留1字节的结果,作为机器指令的操作数。

       相对短转移指令的汇编语言操作数只能是标号和数值。

jmp short 0x2000 

        数值和标号等价的,编译阶段,都用来计算一个8位的偏移量。

        在指令执行时,处理器把指令中的操作数加上2,再加到指令寄存器IP上,会导致指令的执行流程转向目标地址处。

        2.16位相对近转移

        它的机器指令操作码为0xE9,且该指令的长度为3字节,操作码0xE9后还有一个16位【2字节】的操作数。

        因为是近转移,故属于段内转移。"相对"意思是它的操作数是一个相对量,是相对于目标位置处的偏移量。源程序编译阶段,编译器用目标位置汇编地址减去当前指令汇编地址,再减去当前指令长度【3】,保留16位结果,作为机器指令操作数。是一个16位有符号数。

        16位相对近转移应使用关键字"near",比如:

jmp near infinite

jmp near 0x3000

        早先的NASM版本中,关键字near可省略。最近版本,若没指定short或near,则如目标位置距离当前指令-128~127字节,则采用short。否则用near。

        3.16位间接绝对近转移

        只在段内转移。转移到的目标偏移地址不是指令中直接给出的,而是用一个16位通用寄存器或内存地址来间接给出的。比如:

jmp near bx

jmp near cx

        指令中的关键字"near"可以省略,间接绝对近转移原本就是near的,以上两指令执行时,处理器将用寄存器BX或CX的内容取代指令指针寄存器IP的当前内容。

        其他形式

jump_dest dw 0xc000 ;jump_dest是一个标号;dw 0xc000是在一个位置放入数值位0xc000的字

jmp [jump_dest]

jmp [bx]

jmp [bx+si]  

        4.16位直接绝对远转移

        很早,见过这样指令

jmp 0x0000:0x7c00 

        编译后,其机器指令为

EA 00 7c 00 00 

        像这种直接在指令中给出段地址和偏移地址的转移指令,就是直接绝对远转移指令。

        5.16位间接绝对远转移

        远转移的目标地址可通过访问内存来间接得到,这叫间接远转移。但要使用关键字"far"。假如在某程序的数据段内声明了标号jump_far,并在其后初始化了两个字:

jump_far        dw 0x33c0, 0xf000 

        它们分别是某个程序片段的偏移地址和段地址。为了转移到该程序片段上执行,可使用下面的转移指令:

jmp far [jump_far] 

        关键字"far"的作用是告诉编译器,该指令应当编译成一个远转移。处理器执行这条指令后,访问段寄存器DS所指向的数据段,从指令中给出的偏移地址处取出两个字,分别来替代段寄存器CS和指令指针寄存器IP的内容。

        16位间接绝对远转移指令的操作数可是任何一种内存寻址方式。再给出几个:

jmp far [bx]

jmp far [bx+si] 

        最后,"16位"的意思是,要转移到的目标位置的偏移地址是16位的。

8.4.用户程序的工作流程

8.4.1.初始化段寄存器和栈切换 

        因为加载器已经完成了重定位工作,所以用户程序的头等大事是初始化处理器各个段寄存器DS,ES,SS。

        伪指令resb的意思是从当前位置开始,保留指定数量的字节,但不初始化它们的值。在源程序编译时,编译器会保留一段内存区域,用来存放编译后的内容。看到这条伪指令时,它仅仅跳过指定数量字节。

resb 256

        将在编译后的内容中保留256字节,resb不是唯一用来声明未初始化数据的指令,以下是另外一些:

resw 100        ;声明100个未初始化字

resd 50         ;声明50个未初始化的双字

        栈段stack的定义中有"vstart=0"子句,保留的256字节,其汇编地址分别是0~255。所以,标号stack_end处的汇编地址实际上是256。

8.4.2.调用字符串显示例程

        回车和换行的概念起源于老式打印机。回车意味着光标移动到行首。换行意味着,移动到下一行。回车+换行,意味着,移动到下一行行首。

8.4.3.过程的嵌套

        允许在一个过程中调用另一个过程,这称为过程嵌套。因为每次调用过程时,处理器都把返回地址压在栈中,返回时从栈中取得返回地址,所以,只要栈是安全的,嵌套的过程都能层层返回。

        过程嵌套层数,唯一限制是栈大小。实模式下,栈的空间最大是64KB。

8.4.4.屏幕光标控制        

        多年前形成的VGA显示标准在每块显卡中都完好地保留下来了,包括对光标的支持。原因很简单,在显卡中集成一块支持128个ASCII代码的字符发生器非常方便,在程序中显示一个字符也只要给出它的ASCII码。显示图形代价大,计算机加电启动时,其他一些没必要使用图形模式场合,这是最好的选择。

        光标在屏幕上的位置保存在显卡内部的两个光标寄存器中,每个寄存器是8位的,合起来形成一个16位的数值。比如,0表示光标在屏幕上第0行第0列,80表示它在第1行第0列,因为标准VGA文本模式是25行,每行80个字符。这样算,光标在屏幕右下角时,该值为25*80-1=1999。

        光标寄存器是可读可写的。你可从中读出光标的位置,也可通过它设置光标的位置。显卡从不自动移动光标位置,任务是你的。

8.4.5.取得当前光标位置 

        显卡的操作非常复杂,内部的寄存器也不是一般的多。为不过多占用主机的I/O空间,很多寄存器只能通过索引寄存器间接访问。

        索引寄存器的端口号是0x3d4,可以向它写入一个值,用来指定内部的某个寄存器。比如,两个8位的光标寄存器,其索引值分别是14【0x0e】和15【0x0f】,分别用于提供光标位置的高8位和低8位。

        指定好了寄存器后,要对它读写,可通过数据端口0x3d5来进行。

8.4.6.处理回车和换行字符

        mul是乘法指令,格式如下:

mul r/m8

mul r/m16

        以上,"r"表示通用寄存器,"m"表示内存单元。即mul指令可用8位的通用寄存器或内存单元中的数和寄存器AL中的内容相乘,结果是16位,在AX寄存器中;也可用16位通用寄存器或内存单元的数和寄存器AX中的内容相乘,结果是32位,高16位和低16位分别在DX和AX中。

        举几个例子:

mul        bx

mul        bx

mul        byte [bx]

mul        byte [bx+di]

mul        word [0x2000]

        mul指令执行后,要是结果高一半为全0,则OF和CF清零,否则置1。对SF,ZF,AF和PF标志的影响未定义。

8.4.7.显示可打印字符

        标准模式下,屏幕上可同时显示2000个字符。光标占用一个字符的位置,但整个屏幕只有一个,只能出现在2000个字符位置中的一个。程序员要用光标位置来记载和跟踪下一个字符应显示在什么位置。光标用来指示字符位置,一个字符在显存对应两个字节。

8.4.8.滚动屏幕内容

        滚动屏幕,实质上是将屏幕上第2~25行内容整体往上提一行,最后用黑底白字的空白字符填充第25行。

8.4.9.重置光标

8.4.10.切换到另一个代码段中执行

        一个程序中,对段的数量没限制。可有多个代码段,多个数据段,甚至可有多个栈段。

8.4.11.访问另一个数据段

8.5.编译和运行程序并观察结果

        

       

猜你喜欢

转载自blog.csdn.net/x13262608581/article/details/125014288