精华内容
下载资源
问答
  • 汇编语言(王爽)实验十 编写子程序

    万次阅读 多人点赞 2016-07-28 16:09:21
    标 题:汇编实验10—— 编写子程序 作 者: XHS_12302 时 间: 链 接: 实验10编写子程序 在这次实验中,我们将要编写3个子程序,通过它们来认识几个常见的问题和掌握解决这些问题的方法。同前面的所有实验一样,...

    标 题: 汇编实验10—— 编写子程序
    作 者: XHS_12302
    时 间: 2016_7_28 16:56

     

     

     

     

     

    实验10编写子程序


      在这次实验中,我们将要编写3个子程序,通过它们来认识几个常见的问题和掌握解决这些问题的方法。同前面的所有实验一样,这个实验是必须要独立完成的,在后面的课程中,将要用到这个实验中编写的3个子程序。
    1.  显示字符串
    问题
    显示字符串是现实工作中经常要用到的功能,应该编写一个通用的子程序来实现这个功能。我们应该提供灵活的调用接口,使调用者可以决定显示的位置(行、列)、内容和颜色。

    提示
    (1)  子程序的入口参数是屏幕上的行号和列号,注意在子程序内部要将它们转化为显存中的地址,首先要分析一下屏幕上的行列位置和显存地址的对应关系:
    (2)  注意保存子程序中用到的相关寄存器:
    (3)  这个子程序的内部处理和显存的结构密切相关,但是向外提供了与显存结构无关的接口。通过调用这个子程序,进行字符串的显示时可以不必了解显存的结构,为编程提供了方便。在实验中,注意体会这种设计思想。

    子程序描述
    名称:show_str
    功能:在指定的位置,用指定的颜色,显示一个用0结束的字符串。
    参数:(dh)=行号(取值范围0~24),(dl)=列号(取值范围0~79),
        (cl)=颜色,ds:si指向字符串的首地址
    返回:无
    就用举例:在屏幕的8行3列,用绿色显示data段中的字符串。

     

     

     

     

    代码:

     

    assume cs:code
       data segment
       db 'Welcome to masm!',0
       data ends
    
     code segment
       start:  mov dh,8
               mov dl,3
               mov cl,2
               mov ax,data
               mov ds,ax
               mov si,0
               call show_str
               mov ax,4c00h
               int 21h
       show_str: push dx
                 push cx
                 push ds
                 push si
    
         mov ax,0b800h
         mov es,ax
         
         mov al,160
         mul dh
         mov bx,ax
         mov al,2
         mul dl
         add bx,ax
        mov al,cl
        
             s: mov cl,[si] 
                  jcxz ok
                  mov dx,[si]
                  mov es:[bx],dx
                  mov es:[bx+1],al
                  inc si
                  add bx,2
                  loop s
    
     ok:             
      pop si
      pop ds
      pop cx
      pop dx
    ret
    code ends
    end start 

     

     

    实验截图

     

     

     

    2.  解决除法溢出的问题
    问题
    前面讲过,div指令可以做除法。当进行8位除法的时候,用al存储结果的商,ah存储结果的余数:进行16位除法的时候,用ax存储结果的商,dx存储结果的余数。可是,现在有一个问题,如果结果的商大于ah或ax所能存储的最大值,那么将如何?
    比如,下面的程序段:
     mov bh,1
     mov ax,1000
     div bh
    进行的是8位除法,结果的商为1000,而1000在ah中放不下,
    又比如,下面的程序段:
    mov ax,1000h
    mov dx,1
    mov bx,1
    div bx
    进行的是16位除法,结果的商为11000H,而11000H在ax中存放不下。
    我们在用div指令做除法的时候,很可能发生上面的情况:结果的商过大,超出了寄存器所能存储的范围。当CPU执行div等除法指令的时候。如果发生这样的情况,将引发CPU的一个内部错误。这个错误被称为:除法溢出。我们可以通过特殊的程序来处理这个错误, 这里我们不讨论这个错误的处理,这是后面的课程中要涉及的内容。

    好了,我们已经清楚了问题的所在:用div指令做除法的时候可能产生除法溢出。由于有这样的问题,在进行除法运算的时候要注意除数和被除数的值,比如1000000/10就不能用div指令来计算。那么怎么办呢?我们用下面的子程序divdw解决。


    子程序描述
    名称:divdw
    功能:进行不会产生溢出的除法运算,被除数为dword型,除数为word型,结果为dword型。
    参数:(ax)=dword型数据的低16位
        (dx)=dword型数据的高16位
        (cx)=除数
    返回:(dx)=结果的高16位,(ax)=结果的低16位
        (cx)=余数
    应用举例:计算1000000/10(F4240H/0AH)

      mov ax,4240h
      mov v dx,000fh
      mov cx,0ah
      call divdw


    提示
    给出一个公式:
    X:被除数,范围:[0,FFFF FFFF]
    N:除数,范围:[0,FFFF]
    H:X高16位,范围:[0,FFFF]
    L:X低16位,范围:[0,FFFF]
    int():描述性运算符,取商,比如:rem(38/10)=8
    rem():描述性运算符,取答数,比如:rem(38/10)=8
    公式:X/N=int(H/N)*65536+[rem(H/N)*65536+L]/N
    这个公式将可能产生溢出的除法运算:X/N,转变为多个不会产生溢出的除法运算。
    公式中,等号右边的所有除法运算都可以用div指令来做,肯定不会导致除法溢出。

    代码:

     

    assume cs:code
        code segment
        start:mov ax,4240h
              mov dx,000fh
              mov cx,0Ah
              call divdw
              mov ax,4c00H
              int 21h
    divdw:
          push ax
          mov ax,dx
          mov dx,0
          div cx
          mov bx,ax
          pop ax
          div cx
          mov cx,dx
          mov dx,bx
          ret    
        code ends
    end start

     

     

     

    程序截图:

     

    3.数值显示
    问题
    编程,将data段中的数据以十进制的形式显示出来。
    data segment
    dw 123,12666,1,8,3,38
    data ends
      这些数据在内存中都是二进制信息,标记了数值的大小。要把它们显示到屏幕上,成为我们能够读懂的信息,需要进行信息的转化。比如,数值12666,在机器中存储为二进制信息:0011000101111010B(317AH),计算机可以理解它。而我们要在显示器上读到可以理解的数值12666,我们看到的应该是一串字符:“12666”。由于 显卡遵循的是ASCII编码,为了让我们能在显示器上看到这串字符,它在机器中应以ASCII码的形式存储为:31H、32H、36H、36H、36H(字符“0”~“9”对应的ASCII码为30H~39H)。
      通过上面的分析可以看到,在概念世界中,有一个抽象的数据12666,它表示了一个数值的大小。在现实世界中它可以有多种表示形式,可以在电子机器中以高低电平(二进制)的形式存储,也可以在纸上、黑板上、屏幕上以人类的语言“12666”来书写。现在,我们面临的问题就是,要将同一抽象的数据,从一种表示形式转化为另一种表示形式。
      可见,要将数据用十进制形式显示到屏幕上,要进行两步工作:
    (1)  将用二进制信息存储的数据转变为十进制形式的字符串:
    (2)  显示十进制形式的字符串。
    第二步我们在本次实验的第一个子程序中已经实现,在这里只要调用一下show_str即可。我们来讨论第一步,因为将二进制信息转变为十进制形式的字符串也是经常要用到的功能,我们应该为它编写一个通用的子程序。


    子程序描述
    名称:dtoc
    功能:将word型数据转变为表示十进制数的字符串,字符串以0为结尾符。
    参数:(ax)=word型数据
        ds:si指向字符串的首地址
    返回:无
    应用举例:编程,将数据12666以十进制的形式在屏幕的8行3列,用绿色显示出来。
    在显示时我们调用本次实验中的第一个子程序show-str。



    提示
      下面我们对这个问题进行一下简单地分析。
    (1)  要得到字符串“12666”,就是要得到一列表示该字符的ASCII码:31H、32H、36H、36H、36H。
    十进制数码字符对应的ASCII码=十进制数码值+30H
    要得到表示十进制数的字符串,先求十进制数每位的值。
    例:对于12666,先求得每位的值:1、2、6、6、6。再将这些数分别加上30H,便得到了表示12666的ASCII码串:31H、32H、36H、36H、36H。
    (2)  那么,怎样得到每位的值呢?采用如图10.2所示的方法。

    图10.2除10取余法示意


      可见,用10除12666,共除5次,记下每次的余数,就得到了每位的值。
    (3)  综合以上分析,可得出处理过程如下:
    用12666除以10,循环5次,记下每次的余数;将每次的余数分别加30H,便得到了表示十进制数的ASCII码串,如图10.3所示。



    图10.3得到十进制每位数字字符的方法示意

     


    (4)  对(3)的质疑:
    在已知数据是12666的情况下,知道进行5次循环。可在实际问题中,数据的值是多少
    程序员并不知道,也就是说,程序员不能事先确定循环次数。
      那么,如何确定数据各位的值已经全部求出了呢?我们可以看出,只要是除到商为0,各位的值就已经全部求出。可以使用jcxz指令来实现相关的功能。

     

     

     

     

     

    代码:

     

    assume cs:code
      data segment
      db 16 dup(0)
      data ends
    
     
      code segment
        start:mov ax,12666
              mov bx,data
              mov ds,bx
              mov si,0
              call dtoc
              mov dh,8
              mov dl,3
              mov cl,2
              call show_str
              mov ax,4c00h
              int 21h
    dtoc:
           mov cx,ax    ;17
           jcxz bk
           push ax
           mov al,ah
           mov ah,0
           mov bl,10
           div bl
           mov cl,al
           mov ch,ah
           pop ax
           mov ah,ch
           div bl
           mov dl,ah
           mov dh,0
           push dx
           mov ah,cl
           jmp short dtoc   ;29
         bk:pop ax 
            add ax,30h
            mov [si],al
            
            pop ax 
            add ax,30h
            mov [si+1],al
           
            pop ax 
            add ax,30h
            mov [si+2],al
            
            pop ax 
            add ax,30h
            mov [si+3],al    ;44
            
            pop ax 
            add ax,30h
            mov [si+4],al
            mov byte ptr [si+5],0
            ret
           
         
    show_str:
         mov si,0
         mov ax,0b800h
         mov es,ax
         
         mov al,160
         mul dh
         mov bx,ax
         mov al,2
         mul dl
         add bx,ax
         mov al,cl
        
             s: mov cl,[si] 
                  jcxz ok
                  mov dx,[si]
                  mov es:[bx],dx
                  mov es:[bx+1],al
                  inc si
                  add bx,2
                  loop s
    
     ok: ret    
      code ends
    end start


    程序运行截图

     

     

    至此    实验完成    

                               题中有不足之处  希望不吝赐教

                                             文中有的 文字是借鉴别人的    除了代码和截图

                                                 完成实验参考过别的资料,在第二个实验中受过小甲鱼视频(328/2)的启发

                                                                                                   视频资料上云盘找:链接: http://pan.baidu.com/s/1nvpQiLz 密码: et5r

                                                                                                                                                                                                                                     日期:2016_7_28

    展开全文
  • 什么是汇编语言

    万次阅读 多人点赞 2018-11-19 21:21:37
    汇编语言(assembly language)是一种用于电子计算机、微处理器、微控制器或其他可编程器件的低级语言,亦称为符号语言。在汇编语言中,用助记符(Mnemonics)代替机器指令的操作码,用地址符号(Symbol)或标号...

           汇编语言(assembly language)是一种用于电子计算机微处理器微控制器或其他可编程器件的低级语言,亦称为符号语言。在汇编语言中,助记符(Mnemonics)代替机器指令操作码,用地址符号(Symbol)或标号(Label)代替指令或操作数地址。在不同的设备中,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令。普遍地说,特定的汇编语言和特定的机器语言指令集是一一对应的,不同平台之间不可直接移植。

           许多汇编程序为程序开发、汇编控制、辅助调试提供了额外的支持机制。有的汇编语言编程工具经常会提供宏,它们也被称为宏汇编器。

           汇编语言不像其他大多数的程序设计语言一样被广泛用于程序设计。在今天的实际应用中,它通常被应用在底层,硬件操作和高要求的程序优化的场合。驱动程序、嵌入式操作系统和实时运行程序都需要汇编语言。

            Microsoft 宏汇编器(称为 MASM)是windows下常用的汇编器。Microsoft Visual Studio 的大多数版本(专业版,旗舰版,精简版……)都包含 MASM。在运行 Microsoft Windows 的 x86 系统中,其他一些有名的汇编器包括:TASM(Turbo 汇编器),NASM(Netwide 汇编器)和 MASM32(MASM 的一种变体)。GAS(GNU 汇编器)和 NASM 是两种基于 Linux 的汇编器。在这些汇编器中,NASM 的语法与 MASM 的最相似。汇编语言最古老的编程语言,在所有的语言中,它与原生机器语言最为接近。它能直接访问计算机硬件,要求用户了解计算机架构和操作系统。

    什么是汇编器和链接器?

           汇编器(assembler)是一种工具程序,用于将汇编语言源程序转换为机器语言。链接器(linker)也是一种工具程序,它把汇编器生成的单个文件组合为一个可执行程序。还有一个相关的工具,称为调试器(debugger),使程序员可以在程序运行时,单步执行程序并检查寄存器和内存状态。

    MASM 能创建哪些类型的程序?

    32 位保护模式(32-Bit Protected Mode)32 位保护模式程序运行于所有的 32 位和 64 位版本的 Microsoft Windows 系统。它们通常比实模式程序更容易编写和理解。从现在开始,将其简称为 32 位模式。
    64 位模式(64-Bit Mode)64 位程序运行于所有的 64 位版本 Microsoft Windows 系统。
    16 位实地址模式(16-Bit Real-Address Mode)16 位程序运行于 32 位版本 Windows 和嵌入式系统。 64 位 Windows 不支持这类程序。

    汇编语言与机器语言有什么关系?

           机器语言(machine language)是一种数字语言, 专门设计成能被计算机处理器(CPU)理解。所有 x86 处理器都理解共同的机器语言。
    汇编语言(assembly language)包含用短助记符如 ADD、MOV、SUB 和 CALL 书写的语句。汇编语言与机器语言是一对一(one-to-one)的关系:每一条汇编语言指令对应一条机器语言指令。寄存器(register)是 CPU 中被命名的存储位置,用于保存操作的中间结果

    C++ 和 Java 与汇编语言有什么关系?

           高级语言如 Python、C++ 和 Java 与汇编语言和机器语言的关系是一对多(one-to-many)。比如,C++ 的一条语句就会扩展为多条汇编指令或机器指令。

    汇编语言可移植吗?

    一种语言,如果它的源程序能够在各种各样的计算机系统中进行编译和运行,那么这种语言被称为是可移植的(portable)。
    例如,一个 C++ 程序,除非需要特别引用某种操作系统的库函数,否则它就几乎可以在任何一台计算机上编译和运行。Java 语言的一大特点就是,其编译好的程序几乎能在所有计算机系统中运行。
    汇编语言不是可移植的,因为它是为特定处理器系列设计的。目前广泛使用的有多种不同的汇编语言,每一种都基于一个处理器系列。
    对于一些广为人知的处理器系列如 Motorola 68x00、x86、SUN Sparc、Vax 和 IBM-370,汇编语言指令会直接与该计算机体系结构相匹配,或者在执行时用一种被称为微代码解释器(microcode interpreter)的处理器内置程序来进行转换。

    发展历程

           说到汇编语言的产生,首先要讲一下机器语言。机器语言是机器指令的集合。机器指令展开来讲就是一台机器可以正确执行的命令。电子计算机的机器指令是一列二进制数字。计算机将之转变为一列高低电平,以使计算机的电子器件受到驱动,进行运算。

           上面所说的计算机指的是可以执行机器指令,进行运算的机器。这是早期计算机的概念。在我们常用的PC机中,有一个芯片来完成上面所说的计算机的功能。这个芯片就是我们常说的CPU(Central Processing Unit,中央处理单元)。每一种微处理器,由于硬件设计和内部结构的不同,就需要用不同的电平脉冲来控制,使它工作。所以每一种微处理器都有自己的机器指令集,也就是机器语言。CPU 只负责计算,本身不具备智能。你输入一条指令(instruction),它就运行一次,然后停下来,等待下一条指令。这些指令都是二进制的,称为操作码(opcode),比如加法指令就是00000011。编译器的作用,就是将高级语言写好的程序,翻译成一条条操作码。对于人类来说,二进制程序是不可读的,根本看不出来机器干了什么。为了解决可读性的问题,以及偶尔的编辑需求,就诞生了汇编语言。汇编语言是二进制指令的文本形式,与指令是一一对应的关系。比如,加法指令00000011写成汇编语言就是 ADD。只要还原成二进制,汇编语言就可以被 CPU 直接执行,所以它是最底层的低级语言

           早期的程序设计均使用机器语言。程序员们将用0, 1数字编成的程序代码打在纸带或卡片上,1打孔,0不打孔,再将程序通过纸带机或卡片机输入计算机,进行运算。这样的机器语言由纯粹的0和1构成,十分复杂,不方便阅读和修改,也容易产生错误。程序员们很快就发现了使用机器语言带来的麻烦,它们难于辨别和记忆,给整个产业的发展带来了障碍,于是汇编语言产生了。

           汇编语言的主体是汇编指令。汇编指令和机器指令的差别在于指令的表示方法上。汇编指令是机器指令便于记忆的书写格式。

    操作:寄存器BX的内容送到AX中
    1000100111011000              机器指令
    mov ax,bx                    汇编指令

           此后,程序员们就用汇编指令编写源程序。可是,计算机能读懂的只有机器指令,那么如何让计算机执行程序员用汇编指令编写的程序呢?这时,就需要有一个能够将汇编指令转换成机器指令的翻译程序,这样的程序我们称其为编译器。程序员用汇编语言写出源程序,再用汇编编译器将其编译为机器码,由计算机最终执行。 

    语言特点

           汇编语言是直接面向处理器(Processor)的程序设计语言。处理器是在指令的控制下工作的,处理器可以识别的每一条指令称为机器指令。每一种处理器都有自己可以识别的一整套指令,称为指令集。处理器执行指令时,根据不同的指令采取不同的动作,完成不同的功能,既可以改变自己内部的工作状态,也能控制其它外围电路的工作状态。

           汇编语言的另一个特点就是它所操作的对象不是具体的数据,而是寄存器或者存储器,也就是说它是直接和寄存器和存储器打交道,这也是为什么汇编语言的执行速度要比其它语言快,但同时这也使编程更加复杂,因为既然数据是存放在寄存器或存储器中,那么必然就存在着寻址方式,也就是用什么方法找到所需要的数据。例如上面的例子,我们就不能像高级语言一样直接使用数据,而是先要从相应的寄存器AX、BX 中把数据取出。这也就增加了编程的复杂性,因为在高级语言中寻址这部分工作是由编译系统来完成的,而在汇编语言中是由程序员自己来完成的,这无异增加了编程的复杂程度和程序的可读性。

           再者,汇编语言指令是机器指令的一种符号表示,而不同类型的CPU 有不同的机器指令系统,也就有不同的汇编语言,所以,汇编语言程序与机器有着密切的关系。所以,除了同系列、不同型号CPU 之间的汇编语言程序有一定程度的可移植性之外,其它不同类型(如:小型机和微机等)CPU 之间的汇编语言程序是无法移植的,也就是说,汇编语言程序的通用性和可移植性要比高级语言程序低。

           正因为汇编语言有“与机器相关性”的特性,程序员用汇编语言编写程序时,可充分对机器内部的各种资源进行合理的安排,让它们始终处于最佳的使用状态。这样编写出来的程序执行代码短、执行速度快。汇编语言是各种编程语言中与硬件关系最密切、最直接的一种,在时间和空间的效率上也最高的一种。

    总体特点

    1.机器相关性

           这是一种面向机器的低级语言,通常是为特定的计算机或系列计算机专门设计的。因为是机器指令的符号化表示,故不同的机器就有不同的汇编语言。使用汇编语言能面向机器并较好地发挥机器的特性,得到质量较高的程序。

    2.高速度和高效率

           汇编语言保持了机器语言的优点,具有直接和简捷的特点,可有效地访问、控制计算机的各种硬件设备,如磁盘、存储器、CPUI/O端口等,且占用内存少,执行速度快,是高效的程序设计语言

    3.编写和调试的复杂性

           由于是直接控制硬件,且简单的任务也需要很多汇编语言语句,因此在进行程序设计时必须面面俱到,需要考虑到一切可能的问题,合理调配和使用各种软、硬件资源。这样,就不可避免地加重了程序员的负担。与此相同,在程序调试时,一旦程序的运行出了问题,就很难发现。

    优点

    1、因为用汇编语言设计的程序最终被转换成机器指令,故能够保持机器语言的一致性,直接、简捷,并能像机器指令一样访问、控制计算机的各种硬件设备,如磁盘、存储器CPUI/O端口等。使用汇编语言,可以访问所有能够被访问的软、硬件资源。

    2、目标代码简短,占用内存少,执行速度快,是高效的程序设计语言,经常与高级语言配合使用,以改善程序的执行速度和效率,弥补高级语言在硬件控制方面的不足,应用十分广泛。

    缺点

    1、汇编语言是面向机器的,处于整个计算机语言层次结构的底层,故被视为一种低级语言,通常是为特定的计算机或系列计算机专门设计的。不同的处理器有不同的汇编语言语法和编译器,编译的程序无法在不同的处理器上执行,缺乏可移植性;

    2、难于从汇编语言代码上理解程序设计意图,可维护性差,即使是完成简单的工作也需要大量的汇编语言代码,很容易产生bug,难于调试;

    3、使用汇编语言必须对某种处理器非常了解,而且只能针对特定的体系结构和处理器进行优化,开发效率很低,周期长且单调。 

    语言组成

    数据传送指令

    这部分指令包括通用数据传送指令MOV、条件传送指令CMOVcc、堆栈操作指令PUSH/PUSHA/PUSHAD/POP/POPA/POPAD、交换指令XCHG/XLAT/BSWAP、地址或段描述符选择子传送指令LEA/LDS/LES/LFS/LGS/LSS等。注意,CMOVcc不是一条具体的指令,而是一个指令簇,包括大量的指令,用于根据EFLAGS寄存器的某些位状态来决定是否执行指定的传送操作。

    整数和逻辑运算指令

    这部分指令用于执行算术和逻辑运算,包括加法指令ADD/ADC、减法指令SUB/SBB、加一指令INC、减一指令DEC、比较操作指令CMP、乘法指令MUL/IMUL、除法指令DIV/IDIV、符号扩展指令CBW/CWDE/CDQE、十进制调整指令DAA/DAS/AAA/AAS、逻辑运算指令NOT/AND/OR/XOR/TEST等。

    移位指令

    这部分指令用于将寄存器或内存操作数移动指定的次数。包括逻辑左移指令SHL、逻辑右移指令SHR、算术左移指令SAL、算术右移指令SAR、循环左移指令ROL、循环右移指令ROR等。

    位操作指令

    这部分指令包括位测试指令BT、位测试并置位指令BTS、位测试并复位指令BTR、位测试并取反指令BTC、位向前扫描指令BSF、位向后扫描指令BSR等。

    条件设置指令

    这不是一条具体的指令,而是一个指令簇,包括大约30条指令,用于根据EFLAGS寄存器的某些位状态来设置一个8位的寄存器或者内存操作数。比如SETE/SETNE/SETGE等等。

    控制转移指令

    这部分包括无条件转移指令JMP、条件转移指令Jcc/JCXZ、循环指令LOOP/LOOPE/LOOPNE、过程调用指令CALL、子过程返回指令RET、中断指令INTn、INT3、INTOIRET等。注意,Jcc是一个指令簇,包含了很多指令,用于根据EFLAGS寄存器的某些位状态来决定是否转移;INT n是软中断指令,n可以是0到255之间的数,用于指示中断向量号。

    串操作指令

    这部分指令用于对数据串进行操作,包括串传送指令MOVS、串比较指令CMPS、串扫描指令SCANS、串加载指令LODS、串保存指令STOS,这些指令可以有选择地使用REP/REPE/REPZ/REPNE和REPNZ的前缀以连续操作。

    输入输出指令

    这部分指令用于同外围设备交换数据,包括端口输入指令IN/INS、端口输出指令OUT/OUTS。

    高级语言辅助指令

    这部分指令为高级语言的编译器提供方便,包括创建栈帧的指令ENTER和释放栈帧的指令LEAVE。

    控制和特权指令

    这部分包括无操作指令NOP、停机指令HLT、等待指令WAIT/MWAIT、换码指令ESC、总线封锁指令LOCK、内存范围检查指令BOUND、全局描述符表操作指令LGDT/SGDT、中断描述符表操作指令LIDT/SIDT、局部描述符表操作指令LLDT/SLDT、描述符段界限值加载指令LSR、描述符访问权读取指令LAR、任务寄存器操作指令LTR/STR、请求特权级调整指令ARPL、任务切换标志清零指令CLTS、控制寄存器和调试寄存器数据传送指令MOV、高速缓存控制指令INVD/WBINVD/INVLPG、型号相关寄存器读取和写入指令RDMSR/WRMSR、处理器信息获取指令CPUID、时间戳读取指令RDTSC等。

    浮点和多媒体指令

    这部分指令用于加速浮点数据的运算,以及用于加速多媒体数据处理的单指令多数据(SIMD及其扩展SSEx)指令。这部分指令数据非常庞大,无法一一列举,请自行参考INTEL手册。

    虚拟机扩展指令

    这部分指令包括INVEPT/INVVPID/VMCALL/VMCLEAR/VMLAUNCH/VMRESUME/VMPTRLD/VMPTRST/VMREAD/VMWRITE/VMXOFF/VMON等。 

    相关技术

    汇编器

    典型的现代汇编器(assembler)建造目标代码,由解译组语指令集的易记码(mnemonics)到操作码(OpCode),并解析符号名称(symbolic names)成为存储器地址以及其它的实体。使用符号参考是汇编器的一个重要特征,它可以节省修改程序后人工转址的乏味耗时计算。基本就是把机器码变成一些字母而已,编译的时候再把输入的指令字母替换成为晦涩难懂机器码。

    编译环境

    用汇编语言等非机器语言书写好的符号程序称为源程序,汇编语言编译器的作用是将源程序翻译成目标程序。目标程序是机器语言程序,当它被安置在内存的预定位置上后,就能被计算机的CPU处理和执行。

    汇编的调试环境总的来说比较少,也很少有非常好的编译器。编译器的选择依赖于目标处理器的类型和具体的系统平台。一般来说,功能良好的编译器用起来应当非常方便,比如,应当可以自动整理格式、语法高亮显示,集编译、链接和调试为一体,方便实用。

    对于广泛使用的个人计算机来说,可以自由选择的汇编语言编译器有MASMNASMTASMGAS、FASM、RADASM等,但大都不具备调试功能。如果是为了学习汇编语言,轻松汇编因为拥有一个完善的集成环境,是一款非常适合初学者的汇编编译器。 

    发展前景

    汇编语言是机器语言的助记符,相对于比枯燥的机器代码易于读写、易于调试和修改,同时优秀的汇编语言设计者经过巧妙的设计,使得汇编语言汇编后的代码比高级语言执行速度更快,占内存空间少等优点,但汇编语言的运行速度和空间占用是针对高级语言并且需要巧妙设计,而且部分高级语言在编译后代码执行效率同样很高,所以此优点慢慢弱化。而且在编写复杂程序时具有明显的局限性,汇编语言依赖于具体的机型,不能通用,也不能在不同机型之间移植。常说汇编语言是低级语言,并不是说汇编语言要被弃之,相反,汇编语言仍然是计算机(或微机)底层设计程序员必须了解的语言,在某些行业与领域,汇编是必不可少的,非它不可适用。只是,现在计算机最大的领域为IT软件,也是我们常说的计算机应用软件编程,在熟练的程序员手里,使用汇编语言编写的程序,运行效率与性能比其它语言写的程序相对提高,但是代价是需要更长的时间来优化,如果对计算机原理及编程基础不扎实,反而增加其开发难度,实在是得不偿失,对比2010年前后的软件开发,已经是市场化的软件行业,加上高级语言的优秀与跨平台,一个公司不可以让一个团队使用汇编语言来编写所有的东西,花上几倍甚至几十倍的时间,不如使用其它语言来完成,只要最终结果不比汇编语言编写的差太多,就能抢先一步完成,这是市场经济下的必然结果。

     

    但是,迄今为止,还没有程序员敢断定汇编语言是不需要学的,同时,汇编语言(Assembly Language)是面向机器的程序设计语言,设计精湛的汇编程序员,部分已经脱离软件开发,挤身于工业电子编程中。对于功能相对小巧但硬件对语言设计要求苛刻的行业,如4位单片机,由于其容量及运算,此行业的电子工程师一般负责从开发设计电路及软件控制,主要开发语言就是汇编,c语言使用只占极少部分,而电子开发工程师是千金难求,在一些工业公司,一个核心的电子工程师比其它任何职员待遇都高,对比起来,一般电子工程师待遇是程序员的十倍以上。这种情况是因为21世纪以来,学习汇编的人虽然也不少,但是真正能学到精通的却不多,它相对于高级语言难学,难用,适用范围小,虽然简单,但是过于灵活,学习过高级语言的人去学习汇编比一开始学汇编的人难得多,但是学过汇编的人学习高级语言却很容易,简从繁易,繁从简难。对于一个全面了解微机原理的程序员,汇编语言是必修语言。

    实际应用

    随着现代软件系统越来越庞大复杂,大量经过了封装的高级语言如C/C++Pascal/Object Pascal也应运而生。这些新的语言使得程序员在开发过程中能够更简单,更有效率,使软件开发人员得以应付快速的软件开发的要求。而汇编语言由于其复杂性使得其适用领域逐步减小。但这并不意味着汇编已无用武之地。由于汇编更接近机器语言,能够直接对硬件进行操作,生成的程序与其他的语言相比具有更高的运行速度,占用更小的内存,因此在一些对于时效性要求很高的程序、许多大型程序的核心模块以及工业控制方面大量应用。 

    历史上,汇编语言曾经是非常流行的程序设计语言之一。随着软件规模的增长,以及随之而来的对软件开发进度和效率的要求,高级语言逐渐取代了汇编语言。但即便如此,高级语言也不可能完全替代汇编语言的作用。就拿Linux内核来讲,虽然绝大部分代码是用C语言编写的,但仍然不可避免地在某些关键地方使用了汇编代码。由于这部分代码与硬件的关系非常密切,即使是C语言也会显得力不从心,而汇编语言则能够很好扬长避短,最大限度地发挥硬件的性能。

    首先,汇编语言的大部分语句直接对应着机器指令,执行速度快,效率高,代码体积小,在那些存储器容量有限,但需要快速和实时响应的场合比较有用,比如仪器仪表和工业控制设备中。

    其次,在系统程序的核心部分,以及与系统硬件频繁打交道的部分,可以使用汇编语言。比如操作系统的核心程序段、I/O接口电路的初始化程序、外部设备的低层驱动程序,以及频繁调用的子程序动态连接库、某些高级绘图程序、视频游戏程序等等。

    再次,汇编语言可以用于软件的加密和解密、计算机病毒的分析和防治,以及程序的调试和错误分析等各个方面。

    最后,通过学习汇编语言,能够加深对计算机原理和操作系统等课程的理解。通过学习和使用汇编语言,能够感知、体会和理解机器的逻辑功能,向上为理解各种软件系统的原理,打下技术理论基础;向下为掌握硬件系统的原理,打下实践应用基础。

    附注:寄存器和内存模型

    学习汇编语言,首先必须了解两个知识点:寄存器和内存模型。

    先来看寄存器。CPU 本身只负责运算,不负责储存数据。数据一般都储存在内存之中,CPU 要用的时候就去内存读写数据。但是,CPU 的运算速度远高于内存的读写速度,为了避免被拖慢,CPU 都自带一级缓存和二级缓存。基本上,CPU 缓存可以看作是读写速度较快的内存。

    但是,CPU 缓存还是不够快,另外数据在缓存里面的地址是不固定的,CPU 每次读写都要寻址也会拖慢速度。因此,除了缓存之外,CPU 还自带了寄存器(register),用来储存最常用的数据。也就是说,那些最频繁读写的数据(比如循环变量),都会放在寄存器里面,CPU 优先读写寄存器,再由寄存器跟内存交换数据。

    寄存器不依靠地址区分数据,而依靠名称。每一个寄存器都有自己的名称,我们告诉 CPU 去具体的哪一个寄存器拿数据,这样的速度是最快的。有人比喻寄存器是 CPU 的零级缓存。

    1、寄存器的种类

    早期的 x86 CPU 只有8个寄存器,而且每个都有不同的用途。现在的寄存器已经有100多个了,都变成通用寄存器,不特别指定用途了,但是早期寄存器的名字都被保存了下来。

    EAX

    EBX

    ECX

    EDX

    EDI

    ESI

    EBP

    ESP

    上面这8个寄存器之中,前面七个都是通用的。ESP 寄存器有特定用途,保存当前 Stack 的地址。

    我们常常看到 32位 CPU、64位 CPU 这样的名称,其实指的就是寄存器的大小。32 位 CPU 的寄存器大小就是4个字节。

    2、内存模型:Heap

    寄存器只能存放很少量的数据,大多数时候,CPU 要指挥寄存器,直接跟内存交换数据。所以,除了寄存器,还必须了解内存怎么储存数据。

    程序运行的时候,操作系统会给它分配一段内存,用来储存程序和运行产生的数据。这段内存有起始地址和结束地址,比如从0x1000到0x8000,起始地址是较小的那个地址,结束地址是较大的那个地址

    程序运行过程中,对于动态的内存占用请求(比如新建对象,或者使用malloc命令),系统就会从预先分配好的那段内存之中,划出一部分给用户,具体规则是从起始地址开始划分(实际上,起始地址会有一段静态数据,这里忽略)。举例来说,用户要求得到10个字节内存,那么从起始地址0x1000开始给他分配,一直分配到地址0x100A,如果再要求得到22个字节,那么就分配到0x1020。

    这种因为用户主动请求而划分出来的内存区域,叫做 Heap(堆)。它由起始地址开始,从低位(地址)向高位(地址)增长。Heap 的一个重要特点就是不会自动消失,必须手动释放,或者由垃圾回收机制来回收。

    3、内存模型:Stack

    除了 Heap 以外,其他的内存占用叫做 Stack(栈)。简单说,Stack 是由于函数运行而临时占用的内存区域

    请看下面的例子。

    int main(){int a=2;int b=3;}

    上面代码中,系统开始执行main函数时,会为它在内存里面建立一个帧(frame),所有main的内部变量(比如a和b)都保存在这个帧里面。main函数执行结束后,该帧就会被回收,释放所有的内部变量,不再占用空间。

    如果函数内部调用了其他函数,会发生什么情况?

    int main(){int a=2;int b=3;return add_a_and_b(a,b);}

    上面代码中,main函数内部调用了add_a_and_b函数。执行到这一行的时候,系统也会为add_a_and_b新建一个帧,用来储存它的内部变量。也就是说,此时同时存在两个帧:main和add_a_and_b。一般来说,调用栈有多少层,就有多少帧。

    等到add_a_and_b运行结束,它的帧就会被回收,系统会回到函数main刚才中断执行的地方,继续往下执行。通过这种机制,就实现了函数的层层调用,并且每一层都能使用自己的本地变量。

    所有的帧都存放在 Stack,由于帧是一层层叠加的,所以 Stack 叫做栈。生成新的帧,叫做"入栈",英文是 push;栈的回收叫做"出栈",英文是 pop。Stack 的特点就是,最晚入栈的帧最早出栈(因为最内层的函数调用,最先结束运行),这就叫做"后进先出"的数据结构。每一次函数执行结束,就自动释放一个帧,所有函数执行结束,整个 Stack 就都释放了。

    Stack 是由内存区域的结束地址开始,从高位(地址)向低位(地址)分配。比如,内存区域的结束地址是0x8000,第一帧假定是16字节,那么下一次分配的地址就会从0x7FF0开始;第二帧假定需要64字节,那么地址就会移动到0x7FB0。

    汇编语言保留字

    保留字(reserved words)有特殊意义并且只能在其正确的上下文中使用。默认情况下,保留字是没有大小写之分的。比如,MOV 与 mov、Mov 是相同的。

    保留字有不同的类型:

    • 指令助记符,如 MOV、ADD 和 MUL。
    • 寄存器名称。
    • 伪指令,告诉汇编器如何汇编程序。(不是机器指令)
    • 属性,提供变量和操作数的大小与使用信息。例如 BYTE 和 WORD。
    • 运算符,在常量表达式中使用。
    • 预定义符号,比如 @data,它在汇编时返回常量的整数值。

    下表是常用的保留字列表。

    $PARITY?DWORDSTDCALL
    ?PASCALFARSWORD
    @BQWORDFAR16SYSCALL
    @FREAL4FORTRANTBYTE
    ADDRREAL8FWORDVARARG
    BASICREAL10NEARWORD
    BYTESBYTENEAR16ZERO?
    CSDORDOVERFLOW? 
    CARRY?SIGN?  

    伪指令

    DW 定义字(2字节).

    PROC 定义过程.

    ENDP 过程结束.

    SEGMENT 定义段.

    ASSUME 建立段寄存器寻址.

    ENDS 段结束.

    END 程序结束.

    伪指令 (directive) 是嵌入源代码中的命令,由汇编器识别和执行。伪指令不在运行时执行,但是它们可以定义变量、宏和子程序;为内存段分配名称,执行许多其他与汇编器相关的日常任务。

    默认情况下,伪指令不区分大小写。例如,.data,.DATA 和 .Data 是相同的。

    下面的例子有助于说明伪指令和指令的区别。DWORD 伪指令告诉汇编器在程序中为一个双字变量保留空间。另一方面,MOV 指令在运行时执行,将 myVar 的内容复制到 EAX 寄存器中:

    myVar DWORD 26
    mov eax,myVar

    尽管 Intel 处理器所有的汇编器使用相同的指令集,但是通常它们有着不同的伪指令。比如,Microsoft 汇编器的 REPT 伪指令对其他一些汇编器就是无法识别的。

    定义段

    汇编器伪指令的一个重要功能是定义程序区段,也称为段 (segment)。程序中的段具有不同的作用。如下面的例子,一个段可以用于定义变量,并用 .DATA 伪指令进行标识:

    .data

    .CODE 伪指令标识的程序区段包含了可执行的指令:

    .code

    .STACK 伪指令标识的程序区段定义了运行时堆栈,并设置了其大小:

    .stack 100h

    指令

    指令(instruction)是一种语句,它在程序汇编编译时变得可执行。汇编器将指令翻译为机器语言字节,并且在运行时由 CPU 加载和执行。

    一条指令有四个组成部分:

    • 标号(可选)
    • 指令助记符(必需)
    • 操作数(通常是必需的)
    • 注释(可选)

    不同部分的位置安排如下所示:

    [label: ] mnemonic [operands] [;comment]

    现在分别了解每个部分,先从标号字段开始。

    1) 标号

    标号(label)是一种标识符,是指令和数据的位置标记。标号位于指令的前端,表示指令的地址。同样,标号也位于变量的前端,表示变量的地址。标号有两种类型:数据标号和代码标号。

    数据标号标识变量的位置,它提供了一种方便的手段在代码中引用该变量。比如,下面定义了一个名为 count 的变量:

    count DWORD 100

    汇编器为每个标号分配一个数字地址。可以在一个标号后面定义多个数据项。在下面的例子中,array 定义了第一个数字(1024)的位置,其他数字在内存中的位置紧随其后:

    array DWORD 1024, 2048
    DWORD 4096, 8192

    程序代码区(指令所在区段)的标号必须用冒号(:)结束。代码标号用作跳转和循环指令的目标。例如,下面的 JMP 指令创建一个循环,将程序控制传递给标号 target 标识的位置:

    target:
    mov ax,bx
    ...
    jmp target

    代码标号可以与指令在同一行上,也可以自己独立一行:

    L1: mov ax, bx
    L2 :

    标号命名规则要求,只要每个标号在其封闭子程序页中是唯一的,那么就可以多次使用相同的标号。

    2) 指令助记符

    指令助记符(instruction mnemonic)是标记一条指令的短单词。在英语中,助记符是帮助记忆的方法。相似地,汇编语言指令助记符,如 mov, add 和 sub,给出了指令执行操作类型的线索。下面是一些指令助记符的例子:

    助记符说明助记符说明
    MOV传送(分配)数值MUL两个数值相乘
    ADD两个数值相加JMP跳转到一个新位置
    SUB从一个数值中减去另一个数值CALL调用一个子程序

    3) 操作数

    操作数是指令输入输出的数值。汇编语言指令操作数的个数范围是 0〜3 个,每个操作数可以是寄存器、内存操作数、整数表达式和输入输岀端口。
    生成内存操作数有不同的方法,比如,使用变量名、带方括号的寄存器等。变量名暗示了变量地址,并指示计算机使用给定地址的内存内容。下表列出了一些操作数示例:

    示例操作数类型示例 操作数类型
    96 整数常量eax寄存器
    2+4整数表达式count内存

    现在来考虑一些包含不同个数操作数的汇编语言指令示例。比如,STC 指令没有操作数:

    stc ;进位标志位置 1

    INC 指令有一个操作数:

    inc eax ;EAX 加 1

    MOV 指令有两个操作数:

    mov count, ebx ;将 EBX 传送给变量 count

    操作数有固有顺序。当指令有多个操作数时,通常第一个操作数被称为目的操作数,第二个操作数被称为源操作数(source operand)。

    一般情况下,目的操作数的内容由指令修改。比如,在 mov 指令中,数据就是从源操作数复制到目的操作数。

    IMUL 指令有三个操作数,第一个是目的操作数,第二个和第三个是进行乘法的源操作数:

    imul eax,ebx,5

    在上例中,EBX 与 5 相乘,结果存放在 EAX 寄存器中。

    4) 注释

    注释是程序编写者与阅读者交流程序设计信息的重要途径。程序清单的开始部分通常包含如下信息:

    • 程序目标的说明
    • 程序创建者或修改者的名单
    • 程序创建和修改的日期
    • 程序实现技术的说明

    注释有两种指定方法:

    • 单行注释,用分号(;)开始。汇编器将忽略在同一行上分号之后的所有字符。
    • 块注释,用 COMMENT 伪指令和一个用户定义的符号开始。汇编器将忽略其后所有的文本行,直到相同的用户定义符号出现为止。

    示例如下:

    COMMENT !
    This line is a comment.
    This line is also a comment.
    !

    其他符号也可以使用,只要该符号不出现在注释行中:

    COMMENT &
    This line is a comment.
    This line is also a comment.
    &

    当然,程序员应该在整个程序中提供注释,尤其是代码意图不太明显的地方。

    5) NOP(空操作)指令

    最安全(也是最无用)的指令是 NOP(空操作)。它在程序空间中占有一个字节,但是不做任何操作。它有时被编译器和汇编器用于将代码对齐到有效的地址边界

    在下面的例子中,第一条指令 MOV 生成了 3 字节的机器代码。NOP 指令就把第三条指令的地址对齐到双字边界(4 的偶数倍)

    00000000 66 8B C3 mov ax,bx
    00000003 90 nop ;对齐下条指令
    00000004 8B D1 mov edx,ecx

    x86 处理器被设计为从双字的偶数倍地址处加载代码和数据,这使得加载速度更快。

    展开全文
  • 由PDF文件智能扫描成的Word文字版!可以直接复制其中的文字,自学最好的资料,希望对你有帮助。
  • 汇编语言程序如何用word录入源程序?也就是用microsoft word编辑源程序代码,如何使它成为.asm文件,并在DOS系统下编译等。
  • 汇编的简易入门,新手可以入门,学过多可用于快速的复习。
  • 这两天的pwn题环境都是在Linux中,采用的汇编语言是 AT&T 格式。之前学习的是Intel格式的8086汇编,今天学习了下AT&T汇编语言。 1、汇编指令 基于 x86 架构 的处理器所使用的汇编指令一般有两种格式: ...

    这两天的pwn题环境都是在Linux中,采用的汇编语言是 AT&T 格式。之前学习的是Intel格式的8086汇编,今天学习了下AT&T汇编语言。

    1、汇编指令

    基于 x86 架构 的处理器所使用的汇编指令一般有两种格式:

    • Intel 汇编:DOS(8086处理器)、Windows
    • AT&T汇编:Linux, Unix, Mac OS,


    2、AT&T与Intel 汇编语法

    操作Intel格式AT&T格式
    寄存器命名push eaxpushl %eax
    常数\立即操作数push 50pushl $50
    操作数顺序操作数排列是从源(右)到目的(左),如add eax(目的),edx(源)(eax=eax+edx)操作数排列是从源(左)到目的(右),如add %edx(源),%eax(目的)(%eax=%eax+%edx)
    操作数的字长后缀b、w、l分别表示操作数为字节(byte,8 比特)、字(word,16 比特)和长字(long,32比特)用前缀byte ptr 和 word ptr表示 
    绝对转移指令jmpjmp eax操作数前要加上'*'作为前缀,如jmp *eax
    调用指令callcall eax操作数前要加上'*'作为前缀,如call *eax
    内存引用section:[base+index*scale+displacement]section:displacement(base,index,scale)
    远程转移指令jmp far section:offsetljump $section, $offset
    远程子调用指令lcall $section, $offsetcall far section:offset
    内存操作数mov ebx, [ebp - 3]movl -3(%ebp), %ebx

     

     

     

     

     

     

     

     

     

     

     

     

     

     

    3、函数的调用流程(内存)

    1. push 参数
    2. push 函数的返回地址
    3. push bp (保留bp之前的值,方便以后恢复)
    4. mov bp, sp (保留sp之前的值,方便以后恢复)
    5. sub sp,空间大小 (分配空间给局部变量)
    6. 保护可能要用到的寄存器
    7. 使用CC(int 3)填充局部变量的空间
    8. --------执行业务逻辑--------
    9. 恢复寄存器之前的值
    10. mov sp, bp (恢复sp之前的值)
    11. pop bp (恢复bp之前的值)
    12. ret (将函数的返回地址出栈,执行下一条指令)
    13. 恢复栈平衡 (add sp,参数所占的空间)

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

    展开全文
  • 汇编--词法分析
  • 汇编语言 王爽 检测点答案 第一章.检测点1.1 (1) 13 (2) 1024,0,1023 (3) 8192,1024 (4) 2^30,2^20,2^10 (5) 64,1,16,4 (6) 1,1,2,2,4 (7) 512,256 (8) 二进制 注:符号'指求幂运算(如: 2^30指2的30次方) 第二章 检测...
  • 汇编语言笔记(全)

    万次阅读 多人点赞 2019-08-01 11:40:05
    汇编语言 最近系统的学了下汇编语言,下面是学习笔记,用的书是清华大学出版社出版的汇编语言第三版,作者王爽(最经典的那版)。 汇编语...

    汇编语言

    最近系统的学了下汇编语言,下面是学习笔记,用的书是清华大学出版社出版的汇编语言第三版,作者王爽(最经典的那版)。

    基础知识

    汇编语言指令组成
    • 汇编指令:机器码的助记符,有对应的机器码。
    • 伪指令:没有对应的机器码,编译器执行,机器不执行。
    • 其他符号:如+-*/有编译器识别,无对应机器码。
    CPU与外部器件交互需要
    • 存储单元地址(地址信息)
    • 器件选择,读写命令(控制信息)
    • 数据(数据信息)
    总线

    总线就是一根根导线的集合,分为

    • 地址总线,越宽(数量越多)代表可以寻址的范围越大
    • 数据总线,越宽代表一次性读写的数据越多(8根1字节)
    • 控制总线,越宽代表对器件控制操作越多
    小结

    汇编指令和机器指令一一对应

    每一种cpu都有自己的汇编指令集

    在存储器中指令和数据都是二进制,没有任何区别

    CPU可以直接使用的信息存放在存储器中(内存)

    接口卡

    CPU无法直接控制显示器,键盘等的外围设备,但CPU通过直接控制这些外围设备在主板上的接口卡来控制这些设备。

    存储器

    随机存储器(RAM):带电存储,关机丢失,可读可写

    • 用于存放CPU使用的绝大部分程序和数据,主随机存储器由装在主板上的RAM和扩展插槽的RAM组成。
    • 其他接口卡上也可能有自己的RAM

    只读存储器(ROM):关机不丢,只能读取

    • 主板上的ROM装有系统的BIOS(基本输入输出系统)。

    • 其他接口卡上也可能有自己的ROM,一般装着相应的BIOS。

    (P10图)

    内存地址空间

    以上这些内存都和CPU总线相连,CPU都通过控制总线向他们发出内存读写命令。所以CPU都把他们当内存对待,看做一个一个由若干存储单元组成的逻辑存储器,即内存地址空间(一个假想的逻辑存储器P11图)。

    内存地址空间中的各个不同的地址段代表不同的存储设备,内存地址空间大小收到CPU地址总线长度限制。

    寄存器

    内部总线

    之前讨论的总线是CPU控制外部设备使用的总线,是将CPU和外部部件连接的。而CPU内部由寄存器,运算器,控制器等组成,由内部总线相连,内部总线负责连接CPU内部的部件。

    通用寄存器

    8086CPU寄存器都是16位的,一共14个,分别是AX,BX,CX,DX,SI,DI,SP,BP,IP,CS,SS,DS,ES,PSW。其中AX,BX,CX,DX四个寄存器通常存放一般性的数据,称为通用寄存器。

    而且为了兼容上一代的8位寄存器,这四个寄存器可以拆开成两个8位的寄存器来使用。称为AH,AL,BH,BL,CH,CL,DH,DL。低八位(编号0-7)构成L寄存器,高八位构成H寄存器。

    8086CPU可以处理以下两种数据

    • 字节byte,8位
    • 字word,连个字节,16位。分别称为高位字节和低位字节。
    简单的汇编指令
    指令操作高级语言
    mov ax,18将18存入AX寄存器AX=18
    add ax,8将AX寄存器中的数加8AX=AX+8
    mov ax,bx将BX中的数据存入AXAX=BX
    add ax,bx将AX中的数据和BX中的数据相加存入AXAX=AX+BX

    汇编指令或寄存器名称不区分大小写。

    注:AX寄存器当做两个8位寄存器al和ah使用的时候,CPU就把他们当做两个8位寄存器使用,而不会看成是一个16未分开,即如果al进行加法运算C5+93=158,即add al,93,al会变成58,ax则是0058而不是0158。

    CPU位结构

    16位结构的CPU指的是运算器一次最多处理16位数据,寄存器宽度16,寄存器和运算器之间通路也是16位。

    CPU表示物理地址

    如果物理总线宽度超过寄存器宽度,CPU寻址方法是两个寄存器输出一个地址,当地址总线宽度20的时候,P21图。一个寄存器输出短地址,另一个输出偏移地址。然后通过地址加法器合并为一个20位的地址,然后通过内部总线送给控制电路,控制电路通过地址总线送给内存。

    公式:物理地址=段地址x16+偏移地址(这里的x16其实就是左移四位,P21图)

    虽然这么表示,但内存并没有被分为一段一段的,是CPU划分的段。段地址x16称为基础地址,所以我们可以根据需求把任意的基础地址加上不超过一个寄存器表示的最长(64KB)的偏移地址来表示地址。而且一个实际地址往往可以有各种不同的方法表示,通常我们表示21F60H这个地址通过下面方法:

    • 2000:1F60
    • 2000H段中的1F60单元中
    段寄存器与指令指针寄存器

    8086CPU有四个段寄存器:CS,DS,SS,ES

    除此之外,IP寄存器称为指令指针寄存器,所以任意时刻可以读取从CSx16+IP单元开始,读取一条指令执行。也就是说,CPU将IP指向的内容当做指令执行。

    P26图,CPU执行一段指令。另外,8086CPU开机时CS被置为FFFFH,IP被置为0000H,也就是说刚开机的第一条指令从FFFF0H开始读取执行。

    CPU将CS:IP指向的内存中的内容当做指令,一条指令被执行了,那一定被CS:IP指向过。

    修改CS,IP

    CS和IP寄存器不可以使用传送指令mov来改变,而能改变CS,IP内容的指令是转移指令。

    jmp指令用法:

    • jmp 段地址:偏移地址 同时修改CS和IP的值 如jmp 2AE3:3 结果CS=2AE3H IP=0003H
    • jmp 某一合法寄存器 只修改IP的值 如jmp ax,将IP的值置为AX中的值(AX不变)
    小结

    8086CPU有四个段寄存器,CS是用来存放指令的段地址的段寄存器

    IP用来存放指令的偏移地址

    CS:IP指向的内容在任意时刻会被当做指令执行

    使用转移指令修改CS和IP的内容

    实验

    Debug命令:

    • R:查看,改变CPU寄存器内容
      • 直接-r查看寄存器内容
      • -r 寄存器名,改变寄存器内容
    • D:查看内存中内容
      • -d直接查看
      • -d 段地址:偏移地址 查看固定地址开始的内容
      • -d 段地址:偏移地址 结尾偏移地址 查看指定范围内存
    • E:改写内存中内容
      • -e 起始地址 数据 数据 数据 …
      • 提问方式修改 -e 段地址:偏移地址 从这个地址开始一个一个改,空格下一个,回车结束
      • 也可以写入字符 ‘a’
    • U:将内存中的机器指令翻译成汇编指令
      • -u 段地址:偏移地址
    • T:执行一条机器指令
      • -t 执行cs:ip指向的命令
    • A:以汇编指令格式在内存中写入一条机器指令
      • -a 段地址:偏移地址 从这个地址开始一行一行的写入汇编语句

    寄存器(内存访问)

    内存到寄存器的储存

    寄存器是16位的,可以存放一个字即两个字节,而内存中的一个存储单元是一字节。所以一个寄存器可以存两个存储单元的内容,高地址存储单元存在高位字节中,低地址存储单元存在低位字节中。

    字单元:存放一个字型数据的两个地址连续的内存单元。

    DS寄存器

    与CS类似,DS寄存器存放的是要从内存中读取的数据的段地址。我们想要使用mov指令从内存10000H(1000:0)中的数据送给AL时,如下:

    mov al,[0]

    后面的[0]指的是内存的偏移地址是0,CPU会自动从DS寄存器中提取段地址,所以应该首先将段地址1000H写入DS寄存器中。但却不能直接使用mov ds,1000指令,只能从其他寄存器中转传入DS寄存器。所以完整命令如下:

    mov bx,1000
    mov ds,bx
    mov al,[0]
     
    • 1
    • 2
    • 3

    当然,从AL寄存器中将数据送入内存只要反过来使用mov就可以了,mov [0],al

    如果需要传输字型数,只要使用对应的16位寄存器就可以了,传输的是以相应地址开始的一个字型数据(连续两个字节)。如mov [0],cx。

    mov,add,sub

    mov常见语法:

    mov 寄存器,数据       mov ax,8
    mov 寄存器,寄存器     mov ax,bx
    mov 寄存器,内存单元    mov ax,[0]
    mov 内存单元,寄存器    mov [0],ax
    mov 段寄存器,寄存器    mov ds,ax
    mov 寄存器,段寄存器    mov ax,ds
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    add,sub常见语法:

    add 寄存器,数据        add ax,8
    add 寄存器,寄存器      add ax,bx
    add 寄存器,内存单元    add ax,[0]
    add 内存单元,寄存器    add [0],ax
    subadd一样
     
    • 1
    • 2
    • 3
    • 4
    • 5

    注意,add,sub不可以操作段寄存器。

    栈是一种后进先出的存储空间,从栈顶出栈入栈。LIFO(last in first out)

    入栈指令:push ax ax中的数据送入栈顶

    出栈指令:pop ax 栈顶送入ax

    入栈和出栈指令都是以字为单位的。P58图

    栈寄存器SS,SP与push,pop

    CPU通过SS寄存器和SP寄存器来知道栈的范围,段寄存器SS存放的是栈顶的段地址,SP寄存器存放的是栈顶的偏移地址。所以,任意时刻SS:SP指向栈顶元素。

    指令push ax执行过程:

    1. SP=SP-2,SP指针向前移动两格代表新栈顶
    2. AX中的数据送入SS:SP目前指向的内存字单元,P59图

    所以栈顶在低地址,栈底在高地址。初始状态下,SP指向栈底的下一个单元。

    反之pop ax执行过程相反。

    8086CPU并不会自己检测push是否会超栈顶,pop是否会超栈底。

    push和pop可以加寄存器,段寄存器,内存单元(直接偏移地址[address])

    指定栈空间通常通过指定SS来进行,如:

    指定10000H~1000FH为栈空间
    mov ax,1000
    mov ss,ax
    mov sp 0010
     
    • 1
    • 2
    • 3
    • 4

    注:将一个寄存器清零 sub ax,ax 两个字节,mov ax,0 三个字节

    注:若设定一个栈段为10000H~1FFFFH,栈空的时候SP=0(要知道入栈操作先SP-2,然后再送入栈)

    实验

    Debug中的t命令一次执行一条指令,但如果执行的指令修改了ss段寄存器,下一条命令也会紧跟着执行(中断机制)。

    简单编程

    一个汇编语言程序
    1. 编写
    2. 编译(masm5.0)
    3. 连接
    一些伪指令功能
    assume cs:codesg
    
    codesg segment
    
    mov ax,0123
    mov bx,0456
    add ax,bx
    add ax,ax
    
    mov ax,4c00
    int 21
    
    codesg ends
    
    end
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    涉及到的一些知识:

    • XXX segment···XXXends
      • segment和ends成对出现,代表一个段的开始和结束。
      • 一个汇编程序可以有多个段,代码,数据和栈等,至少要有一个段。
    • end
      • end代表一个汇编程序结束,遇到end编译器停止编译。
    • assume
      • assume 假设,假设某一个段寄存器和程序中的一个段关联。
      • 可以理解为将这个段寄存器指向程序段的段地址
    • 标号(codesg)
      • 一个标号代表一个地址
    • 程序返回mov ax,4c00 int 21
      • 暂时记住这两条指令代表程序返回

    编译和连接方法,P83。

    注:编译器只能发现语法错误而无法发现逻辑错误。

    CPU执行一个程序,需要有另一个程序将它加载进内存(即将CS:IP指向它),一般情况下我们通过DOS执行这个.exe,所以是DOS程序将它加载进入内存。当这个程序运行结束,再返回DOS程序继续执行。如果是DOS调用Debug调用.exe,那么先返回Debug再返回DOS。

    DOS加载一个.exe时,先在内存中找到一段内存,起始段地址SA,然后分配256字节的PSP区域,用来和被加载程序通信。在之后的段地址SA+10就是程序开始的段地址。CS:IP指向它,DS=SA。

    注:在Debug中,最后的int 21指令要使用P命令执行。

    [BX]和loop指令

    内存单元的描述

    内存单元可以使用[数字]表示,当然也可以使用[寄存器]表示,如[bx],mov ax,[bx],mov al,[bx]

    为了表示方便,使用()来表示一个内存单元或寄存器中的内容,如(ax),(20000H),或((dx)*16+(bx))表示ds:bx中的内容,但不可写为(1000:0),((dx):0H)。而(X)中的内容由具体寄存器名或运算来决定。

    我们使用idata来表示常亮。所以以下语句可以这么写:mov ax,[idata] mov ax,idata。

    loop指令

    loop指令格式:loop 标号。

    loop指令通常用来实现循环功能,当执行loop指令时,CPU进行两步操作:

    1. (cx)=(cx)-1
    2. (cx)不为零则跳至标号处执行程序。

    所以CX中存放的是循环次数,一个简单的例子如下(计算2^12):

    assume cs:code
    code segment
    
    mov ax,2
    
    mov cx,11
    s:add ax,ax
    loop s
    
    mov ax,4c00h
    int 21h
    
    code ends
    end
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    所以使用loop注意三点:

    1. 先设置cx的值 mov cx,循环次数
    2. 设置标号与执行循环的程序段 s:执行程序段
    3. 在程序段最后写loop loop

    注:在汇编语言中,数据不能以字母开头,所以大于9fffH的数据,要在开头加0,如0A000H

    注:debug中G命令 g 0012表示CPU从当前CS:IP开始一直执行到0012处暂停。P命令可以将loop部分一次执行完毕,直到(CX)=0,或使用g loop的下一条命令。

    Debug和masm编译器对指令的不同处理

    mov ax,[0]这条指令在Debug和masm中有着不同的解释,Debug是将DS:0内存中的数据送给AX,而masm中则是mov ax,0,即将0送入AX。

    解决方法1:先将偏移地址送入BX,然后再使用mov ax,[bx]

    解决方法2:直接显式给出地址,如mov al,ds:[0] (相应的段寄存器还有CS,SS,ES这些在汇编语言中可以称为“段前缀”)当然,这种写法通过编译器之后会变成Debug中的mov al,[0]

    注:inc bx bx值加一

    安全的编程空间

    在之前没有提到的一个问题,如果在写程序之前不看一眼要操作的内存,就直接开始使用的话,万一改写了内存中重要的系统数据,可能会引起系统崩溃。所以我们一般在一个安全的内存空间中操作。一般操作系统和合法程序都不会使用0:200~0:2ff这256字节的空间,所以我们可以在这里操作。

    学习汇编语言的目的就是直接和硬件对话,而不理会操作系统,这在DOS(实模式)下是可以做到的,但在windows或Unix这种运行与CPU保护模式的操作系统上却是不可能的,因为这种操作系统已经将CPU全面严格的管理了。

    段前缀的使用

    将ffff:0~ffff:b中的数据转存入0:200~0:20b中:

    assume cs:code
    code segment
    
    mov ax,0ffffh
    mov ds,ax
    
    mov ax,0020h
    mov es,ax
    
    mov bx,0
    
    mov cx,12
    s:mov dl,[bx]
    mov es:[bx],dl
    inc bx
    loop s
    
    mov ax,4c00h
    int 21h
    
    code ends
    end
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    [bx]直接使用的时候默认段前缀是ds,但要使用其他的段前缀,如es就要在前面加上。

    程序的段

    数据段

    一般一个程序想要使用内存空间,有两种方法,在程序加载的时候系统分配或在需要使用的时候向系统申请,我们先考虑第一种情况。所以我们应事先将所需的数据存入内存中的某一段中,但我们又不可以随意的指定内存地址,以下面的求8个数据累加和的代码为例:

    assume cs:code
    code segment
    
    dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
    
    mov bx,0
    mov ax,0
    
    mov cx,8
    s:add ax,cs:[bx]
    add bx,2
    loop s
    
    mov ax,4c00h
    int 21h
    
    code ends
    end
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    代码第一行的dw是定义字类型数据,define word的意思。这里定义了8个字类型数据,占16字节。由于是在程序最开始定义的dw,所以数据段的偏移地址为0,也就是说第一个数据0123h的地址是CS:[0]第二个0456h的地址是CS:[2]以此类推。

    所以这个程序加载之后CS:IP指向的是数据段的第一个数据,我们要是想成功执行,需要把IP置10,指向第一条指令mov bx,0,所以我们想要直接执行(不在Debug中调整IP)的话,需要指定程序开始的地方:

    ···
    dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
    
    start:mov bx,0
    ···
    code ends
    end start
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在第一条指令前加start,后面的end变成end start,end除了通知编译器程序在哪里结束之外,也可以通知程序的入口在哪,也就是第一条语句,在这里编译器就知道了mov bx,0是程序的第一条指令。也就是说,我们想要CPU从何处开始执行程序,只要在源程序中使用end 标号指定就好了。

    所以有如下框架:

    assume cs:code
    code segment
    ···数据···
    start:
    ···代码···
    code ends
    end start
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    栈段

    看下面一段使8个数逆序存放的代码:

    assume cs:codesg
    codesg segment
    
    dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
    dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    
    start:mov ax,cs
    mov ss,ax
    mov sp,30h
    
    mov bx,0
    mov cx,8
    s:push cs:[bx]
    add bx,2
    loop s
    
    mov bx,0
    mov cx,8
    s0:pop cs:[bx]
    add bx,2
    loop s0
    
    mov ax,4c00h
    int 21h
    
    codesg ends
    end start
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    在定义了8个字型数据之后,又定义了16个取值为0的字型数据,用作栈空间。所以dw这个定义不仅仅用来定义数据,也可以用来开辟内存空间留给之后的程序使用。

    数据,代码,栈的程序段

    在8086CPU中,一个段的长度最大为64KB,所以如果我们将数据或栈空间定义的比较大,就不能像前面一样编程了。我们需要将代码,数据,栈放入不同的段中:

    assume cs:code,ds:data,ss:stack
    data segment
    dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
    data ends
    
    stack segment
    dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
    srack ends
    
    code segment
    start:mov ax,stack
    mov ss,ax
    mov sp,20h
    
    mov ax,data
    mov ds,ax
    
    mov bx,0
    
    mov cx,8
    s:push [bx]
    add bx,2
    loop s
    
    mov bx,0
    
    mov cx,8
    s0:pop [bx]
    add bx,2
    loop s0
    
    mov ax,4c00h
    int 21h
    
    code ends
    end start
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    我们可以这样在写代码时就将程序分为几个段,这段代码中,mov ax,data的意思是将data段的段地址送入ax寄存器。但我们不可以使用mov ds,data这样是错误的,因为在这里data被编译器视为一个数值。

    在这里将数据命名为data,代码命名为code,栈命名为stack只是为了方便阅读,CPU并不能理解,和start,s,s0一样,只在源程序中使用。而assume cs:code,ds:data,ss:stack这段代码也并不能让CPU的cs,ds,ss指向对应的段,因为assume是伪指令,CPU并不认识,它是由编译器执行的。源程序中end start语句指明了程序的入口,在这个程序被加载后,CS:IP被指向start处,开始执行第一条语句,这样CPU才会将code段当做代码执行。而当CPU执行

    mov ax,stack
    mov ss,ax
    mov sp,20h
     
    • 1
    • 2
    • 3

    这三条语句后才会将stack段当做栈空间开使用。也就是说,CPU如何区分哪个段的功能,全靠我们使用汇编指令对ds,ss,cs寄存器的内容设置来指定。

    灵活定位内存地址

    and和or指令

    and:逻辑与指令,按位与运算,如:

    mov al,01100011B
    and al,00111011B
     
    • 1
    • 2

    执行结果是al=00100011B,所以我们想要把某一位置零的时候可以使用and指令。

    or:逻辑或指令,按位或运算,如:

    mov al,01100011B
    or al,00111011B
     
    • 1
    • 2

    执行结果是al=01111011B,or指令可以将相应位置1。

    ASCII码和字符形式的数据

    在汇编语言中我们可以使用’···’的方式指明数据是以字符形式给出的,编译器会自动将它们转化为ASCII码。例如:

    assume cs:code,ds:data
    data segment
    db 'unIX'
    db 'foRK'
    data ends
    code segment
    start:mov al,'a'
    mov bl,'b'
    mov ax,4c00h
    int 21h
    code ends
    end start
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    db和dw类似,只不过定义的是字节型数据,然后通过’unIX’相继在接下来四个字节中写下75H,6EH,49H,58H即unIX的ASCII值。同理,mov al,’a’也是将’a’的ASCII值61H送入al寄存器。

    使用and和or指令改变一串字符串字母的大小写,将第一串全变为大写,第二串全变为小写:

    首先分析ASCII码:

    大写  十六进制    二进制         小写  十六进制    二进制
     A      41      01000001        a       61     01100001
     B      42      01000010        b       62     01100010
     C      43      01000011        c       63     01100011
     
    • 1
    • 2
    • 3
    • 4

    可见,只有第5位(从右往左数,从0开始计数)在大写和小写的二进制中是不一样的,所以我们只要把所有字母的二进制第五位置零,那就是大写,置1就是小写。代码如下:

    assume cs:codesg,ds:datasg
    
    datasg segment
    db 'BaSiC'
    db 'iNfOrMaTiOn'
    datasg ends
    
    codesg segment
    start:mov ax,datasg
    mov ds,ax
    mov bx,0
    
    mov cx,5
    s:mov al,[bx]
    and al,11011111B
    mov [bx],al
    inc bx
    loop s
    
    mov bx,5
    
    mov cx,11
    s0:mov al,[bx]
    or al,00100000B
    mov [bx],al
    inc bx
    loop s0
    
    mov ax,4c00h
    int 21h
    
    codesg ends
    end start
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    [bx+idata]的内存表示方法与数组处理

    除了使用[bx]来表示一个内存单元外,我们还可以使用[bx+idata]来表示一个内存单元,他表示的意思是偏移地址为(bx)+idata(bx中的数值加idata)的内存单元。当然也可写为[idata+bx],除此之外还可写为,200[bx],[bx].200。

    既然有了这种表示方法,我们就可以使用这种方法来操作数组,刚才将两个字符串改变大小写的代码的循环部分可以如下优化:

    ···
    s:mov al,[bx]
    and al,11011111B
    mov [bx],al
    mov al,[5+bx]
    or al,00100000B
    mov [5+bx],al
    inc bx
    loop s
    ···
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    当然也可写为0[bx]和5[bx],注意这种写法和C语言中数组的相似之处:C语言中数组表示为a[i],汇编语言中表示为5[bx]。

    SI和DI寄存器

    SI和DI功能和BX相似,但不可以拆分为两个8位寄存器。也就是说下面代码等价:

    mov bx|si|di,0
    mov ax,[bx|si|di]
    mov ax,[bx|si|di+123]
     
    • 1
    • 2
    • 3

    所以在这里可以使用更方便的方式:[bx+si]和[bx+di],这两个式子表示偏移地址为(bx)+(si)的内存单元,使用方法如:mov ax,[bx+si]等价于mov ax,[bx][si]。

    当然,有了这些表示方法,自然就有[bx+si+idata]和[bx+di+idata],相似的,也可以写成

    mov ax,[bx+200+si]
    mov ax,[200+bx+si]
    mov ax,200[bx][si]
    mov ax,[bx].200[si]
    mov ax,[bx][si].200
     
    • 1
    • 2
    • 3
    • 4
    • 5

    那我们总结一下这些内存寻址方法:

    • [idata]用一个常量表示偏移地址,直接定位一个内存单元
    • [bx]用一个变量表示偏移地址,定位一个内存单元
    • [bx+idata]用一个常量和一个变量表示偏移地址,可在一个起始地址的基础上间接定位一个内存单元
    • [bx+si]用两个变量表示偏移地址
    • [bx+si+idata]用两个变量和一个常量表示偏移地址

    使用双循环,使用一个寄存器暂存cs的值,如:

    ···
    mov cx,4
    s0:mov dx,cx
    mov si,0
    
    mov cx,3
    s:mov al,[bx+si]
    and al,11011111b
    mov [bx+si],al
    inc si
    loop s
    
    add bx,16
    mov cx,dx
    loop s0
    ···
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    假如循环比较复杂,没有多余的寄存器可用,我们可以使用内存暂存cx或其他数据:

    ···
    dw 0
    ···
    mov cx,4
    s0:mov ds:[40H],cx
    mov si,0
    
    mov cx,3
    s:mov al,[bx+si]
    and al,11011111b
    mov [bx+si],al
    inc si
    loop s
    
    add bx,16
    mov cx,ds:[40H]
    loop s0
    ···
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    这么使用的话注意需要在数据段声明用来暂存的内存,好在程序加载时分配出来。当然,在需要暂存的地方,还是建议使用栈:

    ···
    dw 0,0,0,0,0,0,0,0
    ···
    mov ax,stacksg
    mov ss,ax
    mov sp,16
    ···
    mov cx,4
    s0:push cx
    mov si,0
    
    mov cx,3
    s:mov al,[bx+si]
    and al,11011111b
    mov [bx+si],al
    inc si
    loop s
    
    add bx,16
    pop cx
    loop s0
    ···
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    数据处理的两个基本问题

    两个基本问题
    1. 处理的数据在什么地方
    2. 要处理的数据有多长

    接下来的讨论中,使用reg来表示一个寄存器,使用sreg来表示一个段寄存器。所以:

    • reg:ax,bx,cx,dx,ah,al,bh,bl,ch,cl,dh,dl,sp,bp,si,di
    • sreg:ds,ss,cs,es
    bx,si,di和bp

    在8086CPU中,只有这四个寄存器可以使用[···]来进行内存寻址,可以单个出现,或以下面组合出现(常数可以随意出现在这些表示方法中):

    • bx+si/di
    • bp+si/di

    注:如果使用了bp来寻址,而没有显式的表明段地址,默认使用ss段寄存器,如:

    mov ax,[bp]              ;(ax)=((ss)*16+(bp))
    mov ax,[bp+idata]        ;(ax)=((ss)*16+(bp)+idata)
    mov ax,[bp+si]           ;(ax)=((ss)*16+(bp)+(si)+idata)
     
    • 1
    • 2
    • 3
    数据的位置

    绝大部分机器指令都是用来处理数据的,基本可分为读取,写入,运算。在机器指令这个层面上,并不关心数据是什么,而关心指令执行前数据的位置。一般数据会在三个地方,CPU内部,内存,端口。

    汇编语言中使用三个概念来表示数据的位置:

    • 立即数(idata)
      • 对于直接包含在机器指令中的数据,在汇编语言中称为立即数
      • 例:mov ax,1 add bx,2000h
    • 寄存器
      • 指令要处理的数据在寄存器中,在汇编指令中给出相应寄存器名
      • 例:mov ax,bx mov ds,ax
    • 段地址(SA)和偏移地址(EA)
      • 指令要处理的数据在内存中,在指令中使用[X]方式给出,SA在某个段寄存器中
      • 例:mov ax,[0] mov ax,[di]

    总结一下寻址方式:

    寻址方式含义名称
    [idata]EA=idata;SA=(DS)直接寻址
    [bx|si|di|bp]EA=(bx|si|di|bp);SA=(DS)寄存器间接寻址
    [bx|si|di|bp+idata]EA=(bx|si|di|bp+idata);SA=(DS)寄存器相对寻址
    [bx|bp+si|di]EA=(bx|bp+si|di);SA=(DS|SS)基址变址寻址
    [bx|bp+si|di+idata]EA=(bx|bp+si|di+idata);SA=(DS|SS)相对基址变址寻址
    数据的长度

    8086CPU中可以指定两种尺寸的数据,byte和word,所以在使用数据的时候要指明数据尺寸。

    • 在有寄存器参与的时候使用寄存器的种类区分
      • 字:mov ax,1
      • 字节:mov al,1
    • 在没有寄存器参与的时候,使用X ptr指明内存单元长度,X是word或byte
      • 字:mov word ptr ds:[0],1 add word ptr [bx],2
      • 字节:mov byte ptr ds:[0],1 add byte ptr [bx],2
    • 其他默认指明处理类型的指令
      • push [1000H],push默认只进行字操作

    灵活使用寻址方式的例子,修改下面内存空间中的数据:

    段seg:60

    起始地址内容
    00‘DEC’
    03‘Ken Oslen’
    0C137
    0E40
    10‘PDP’
    ···
    mov ax,seg
    mov ds,ax
    mov bx,60h
    
    mov word ptr [bx].0ch,38    ;第三字段改为38
    
    add word ptr [bx].0eh,70    ;第四字段改为70
    
    mov si,0
    mov byte ptr [bx].10h[si],'v'   ;修改最后一个字段的三个字符
    inc si
    mov byte ptr [bx].10h[si],'A'
    inc si
    mov byte ptr [bx].10h[si],'X'
    ···
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    这段代码中地址的使用类似c++中结构体的使用。[bx].idata.[si],就类似与c++中的dec.cp[i]。dec是结构体,cp是结构体中的字符串成员,[i]表示第几个字符。

    div指令

    div是除法指令,需要注意以下三点:

    • 除数:8位或16位,在一个reg或内存单元中
    • 被除数:默认在AX或DX中,如果除数8位,被除数则为16位,放在AX中;如果除数16位,则被除数32位,在DX和AX中,DX存放高16位,AX放低16位。
    • 结果,除数8位,结果(商)存放在AL中,AH存放余数;如果除数16位,则AX存放商,DX存放余数

    格式:div reg或div 内存单元,所以div byte ptr ds:[0]表示:

    (al)=(ax)/((ds)*16+0)的商;
    (ah)=(ax)/((ds)*16+0)的余数;
     
    • 1
    • 2

    div word ptr es:[0]表示:

    (al)=[(dx)*10000H+(ax)]/((es)*16+0)的商
    (ah)=[(dx)*10000H+(ax)]/((es)*16+0)的余数
     
    • 1
    • 2

    例:计算100001/100,因为100001(186A1H)大于65535,则需要存放在ax和dx两个寄存器,那么除数100只能存放在一个16位的寄存器中,实现代码:

    mov dx,1
    mov ax,86A1H
    mov bx,100
    div bx
     
    • 1
    • 2
    • 3
    • 4

    执行之后(ax)=03E8H(1000),(dx)=1。

    伪指令dd

    dd是一个伪指令,类似dw,但dd是用来定义dword(double word,双字),如:

    dd 1  ;2字,4字节
    dw 1  ;1字,2字节
    db 1  ;1字节
     
    • 1
    • 2
    • 3

    将data段中第一个数据除以第二个数据,商存入第三个数据:

    ···
    data segment
    dd 100001
    dw 100
    dw 0
    data ends
    ···
    mov ax,data
    mov ds,ax
    mov ax,ds:[0]
    mov dx,ds:[2]
    div word ptr ds:[4]
    mov ds:[6],ax
    ···
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    总结一下div相关:

    • div后面跟的是除数
    • 被除数位数是除数两倍
    • 被除数存在ax中或ax+dx(ax低,dx高)
    • 商在ax或al中,余数在ah或dx中(高余数,低商)
    dup

    dup是一个操作符,由编译器识别,和db,dw,dd配合使用,如:

    db 3 dup (0)表示定义了三个值是0的字节,等价于db 0,0,0

    db 3 dup (1,2,3)等价于db 1,2,3,1,2,3,1,2,3 共九个字节

    db 3 dup (‘abc’,’ABC’)等价于db ‘abcABCabcABCabcABC’

    综上,db|dw|dd 重复次数 dup (重复内容)

    转移指令原理

    转移指令

    可以修改IP或同时修改CS,IP的系统指令称为转移指令,可分为以下几类:

    • 转移行为:
      • 只修改IP,称为段内转移,如jmp ax
      • 同时修改CS和IP,称为段间转移,如jmp 1000:0
    • 修改范围(段内转移):
      • 短转移:修改IP范围-128~127
      • 近转移:修改IP范围-32768~32767
    • 转移指令分类:
      • 无条件转移:jmp
      • 条件转移
      • 循环指令
      • 过程
      • 中断
    offset操作符

    offset是由编译器处理的符号,它能去的标号的偏移地址,如:

    start:mov ax,offset start
    s:mov ax,offset s
     
    • 1
    • 2

    这里就是将start和s的偏移地址分别送给ax,也就是0和3

    jmp指令

    jmp是无条件转移指令,可以只修改IP也可以同时修改CS和IP,只要给出两种信息,要转移的目的地址和专一的距离。

    依据位移的jmp指令:jmp short 标号(转到标号处执行指令)。这个指令实现的是段内短转移,对IP修改范围是-128~127,指令结束后CS:IP指向标号的地址,如:

    0BBD:0000   start:mov ax,0  (B80000)
    0BBD:0003   jmp short s   (EB03)
    0BBD:0005   add ax,1    (050100)
    0BBD:0008   s:inc ax    (40)
     
    • 1
    • 2
    • 3
    • 4

    执行之后ax值为1,因为跳过了add指令。

    还应注意的是,jmp short短转移指令并不会在机器码中直接写明需要转移的地址(0BBD:0008),jmp的机器码是EB03并没有包含转移的地址,这里的转移距离是相对计算而出的地址,来看下面的执行过程:

    1. (CS)=0BBDH,(IP)=0006H,CS:IP指向EB03(jmp short s)
    2. 读取指令EB03进入指令缓冲器
    3. (IP)=(IP)+指令长度,即(IP)=(IP)+2=0008H,之后CS:IP指向add ax,1
    4. CPU指向指令缓冲器中的指令EB03
    5. 执行之后(IP)=000BH,指向inc ax

    在jmp short s的机器码中,包含的并不是转移的地址,而是转移的位移,这里的位移是相对计算出来的,用8位一字节来表示,所以表示范围是-128~127,用补码表示。计算方法如是,8位位移=标号处地址-jmp下一条指令的地址。当然还有一种类似的指令是jmp near ptr 标号,是近转移,原理一样,只是表示位移的是字类型16位,表示范围-32768~32767。

    jmp+地址远转移

    jmp far ptr 标号实现的是段间转移,也就是远转移,它的机器码中指明了转移的目的地址的CS和IP的值,如下面例子:

    0BBD:0000   start:mov ax,0    (B80000)
    0BBD:0003   mov bx,0    (BB0000)
    0BBD:0006   jmp far ptr s    (EA0B01BD0B)
    0BBD:000B   db 256 dup (0)    
    0BBD:010B   s:add ax,1    
    0BBD:010X   inc ax
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    可以看出,jmp的机器码中明确指明了跳转位置s的地址0BBD:010B,在低位的是IP的值,高位的是CS的值。

    jmp+寄存器|内存转移

    jmp+寄存器:jmp 16位reg,实现的是(IP)=(16位reg),之前讨论过,直接修改IP的值为寄存器中的值。

    jmp+内存:jmp加内存使用的时候有两种用法:

    • jmp word ptr 内存单元地址(段内转移)
      • 从内存单元地址处开始存放一个座位转移目的的偏移地址的字
      • 内存单元支持任何寻址方式
      • 如jmp word ptr ds:[0],执行后(IP)=0123H(ds:[0]中的值是123H)
    • jmp dword ptr 内存单元地址(段间转移)
      • 从内存单元地址处开始存放两个字,高位存放段地址,低位偏移地址作为转移的目的地址
      • (CS)=(内存单元地址+2),(IP)=(内存单元地址),支持任一种寻址方式
      • 如jmp dword ptr [bx]跳转到0:123H
    jcxz指令

    jcxz指令为条件转移指令,所有的条件转移指令都是短转移,转移范围是-128~127。使用格式是jcxz 标号,功能是如果(cx)=0则跳转到标号处执行;如果(cx)!=0,那么什么也不做继续执行代码。

    loop指令

    loop为循环指令,所有的循环指令都是短转移,转移范围是-128~127。使用格式是loop 标号,功能是如果(cx)!=0那么跳转到标号处执行;如果(cx)=0那么什么也不做继续执行程序。

    根据位移进行转移的指令总结

    下面几条指令是根据位移进行转移(相对计算转移位置,而不是直接提供转移目的的IP和CS的值)

    • jmp short 标号
    • jmp near ptr 标号
    • jcxz 标号
    • loop 标号

    这些指令之所以是间接计算标号的位置,是为了方便在代码中浮动装配,使得循环体或这些指令的代码段在任何位置都可以执行(不要超跳转范围)。而编译器会对跳转的范围进行检测,如果跳转超过了范围,编译器会报错。

    注:jmp 2100:0是debug使用的汇编指令,编译器并不认识。

    call和ret指令

    ret和retf

    ret和call都是转移指令,都是修改IP的值,或同时修改CS和IP。

    ret指令用栈中的数据修改IP,实现的是近转移;retf指令用栈中的数据修改CS和IP的值,实现远转移。格式:直接用 ret。

    ret执行步骤:

    1. (IP)=((SS)*16+(SP))
    2. (SP)=(SP)+2

    retf执行步骤:

    1. (IP)=((SS)*16+(SP))
    2. (SP)=(SP)+2
    3. (CS)=((SS)*16+(SP))
    4. (SP)=(SP)+2

    所以ret指令相当于 pop ip,执行retf指令相当于执行pop ip,pop cs。

    call指令

    call指令也是一个转移指令,执行格式:call 目标(具体使用接下来说明),call的执行步骤:

    1. 将当前的IP或CS和IP入栈
    2. 转移

    call不能实现短转移,但它实现转移的原理和jmp相同。

    根据位移转移:call 标号,近转移,16位转移范围,也是使用相对的转移地址。

    执行步骤:

    1. (SP)=(SP)-2
    2. ((SS)*16+(SP))=(IP)
    3. (IP)=(IP)+16

    所以执行这条命令相当于执行push ip,jmp near ptr 标号。

    直接使用地址进行(远)转移:call far ptr 标号,执行步骤:

    1. (SP)=(SP)-2
    2. ((SS)*16+(SP))=(CS)
    3. (SP)=(SP)-2
    4. ((SS)*16+(SP))=(IP)
    5. (CS)=标号所在的段的段地址
    6. (IP)=标号的偏移地址

    所以执行call far ptr 标号相当于执行push cs,push ip,jmp far ptr 标号

    使用寄存器的值作为call的跳转地址:call 16位reg

    1. (SP)=(SP)-2
    2. ((SS)*16+(SP))=(IP)
    3. (IP)=(16为reg)

    相当于执行push ip,jmp 16位reg

    使用内存中的值作为call的跳转地址:call word ptr 内存单元地址,当然还有call dword ptr 内存单元地址,这样进行的就是远转移。

    联合使用ret和call

    联合使用ret和call实现子程序的框架:

    assume cs:code
    code segment
    main:
    ···
    call sub1
    ···
    mov ax,4c00h
    int 21h
    
    sub1:
    ···
    call sub2
    ···
    ret
    
    sub2:
    ···
    ret
    code ends
    end main
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    mul指令

    mul是乘法指令,使用时应注意,两个相乘的数,要么都是8位,要么都是16位,如果是8位,那么其中一个默认放在al中,另一个在一个8位reg或字节内存单元中;若是16位,则一个默认在ax中,另一个在16位reg或字内存单元中。如果是8位乘法, 则结果放在ax中,结果是16位;若是16位乘法,结果默认在ax和dx中,dx高位,ax低位,共32位。

    格式:mul reg 或 mul 内存单元,支持内存单元的各种寻址方式。

    如mul word ptr [bx+si+8]代表:

    (ax)=(ax)*((ds)*16+(bx)+(si)+8)低16位
    (dx)=(ax)*((ds)*16+(bx)+(si)+8)高16位
     
    • 1
    • 2

    例:计算100*10

    mov al,100
    mov bl,10
    mul bl
     
    • 1
    • 2
    • 3
    参数的传递和模块化编程

    看下面一段程序:计算data中第一行的数的立方存在第二行

    assume cs:code
    data segment
    dw 1,2,3,4,5,6,7,8
    dd 0,0,0,0,0,0,0,0
    data ends
    
    code segment
    start:mov ax,data
    mov ds,ax
    mov si,0
    mov di,16
    
    mov cs,8
    s:mov bx,[si]
    call cube
    mov [di],ax
    mov [di].2,dx
    add si,2
    add di,4
    loop s
    
    mov ax,4c00h
    int 21h
    
    cube:mov ax,bx
    mul bx
    mul bx
    ret
    
    code ends
    end start
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    寄存器冲突

    观察下面将data中的数据全转化为大写的代码:

    assume cs:code
    data segment
    db 'word',0
    db 'unix',0
    db 'wind',0
    db 'good',0
    data ends
    
    code segment
    start:mov ax,data
    mov ds,ax
    mov bx,0
    
    mov cx,4
    s:mov si,bx
    call capital
    add bx,5
    loop s
    
    mov ax,4c00h
    int 21h
    
    capital:mov cl,[si]
    mov ch,0
    jcxz ok
    and byte ptr [si],11011111b
    inc si
    jmp short capital
    ok:ret
    code ends
    end start
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    这段代码有一个问题出在,主函数部分使用cx设置循环次数4次,在循环中调用了子函数,而子函数中有一个判断语句jcxz也是用了cx,并且在之前修改了cx的值,造成逻辑错误。虽然修改的方法有很多,但我们应遵循以下的标准:

    • 编写调用子程序的程序不必关心子程序使用了什么寄存器
    • 编写子程序不用关心调用子程序的程序使用了什么寄存器
    • 不会发生寄存器冲突

    针对这三点,我们可以如下修改代码:

    ···
    capital:push cx
    push si
    
    change:mov cl,[si]
    mov ch,0
    jcxz ok
    and byte ptr [si],11011111b
    inc si
    jmp short change
    
    ok:pop si
    pop cx
    ret
    ···
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    虽然和上面的程序中没有冲突的是si,但我们保险起见,在子程序开始时将子程序用到的所有的寄存器的内容存入栈中,在返回之前在出栈回到相应寄存器中。这样无论调用子程序的程序使用了什么寄存器,都不会产生寄存器冲突。

    标志寄存器

    标志寄存器

    CPU中有一种特殊的寄存器——标志寄存器(不同CPU中的个数和结构都可能不同),主要有以下三种作用:

    1. 存储相关指令的某些执行结果
    2. 为CPU执行相关质量提供行为依据
    3. 控制CPU相关工作方式

    8086CPU中的标志寄存器有16位,其中存储的信息通常被称为程序状态字(PSW),标志寄存器以下简称为flag。标志位如图:

    15  14  13  12  11  10  9   8   7   6   5   4   3   2   1   0
                    OF  DF  IF  TF  SF  ZF      AF      PF      CF
     
    • 1
    • 2

    如上图所示,1,3,5,12,13,14,15位没有使用,没有任何意义,而其他几位都有不同的含义。

    ZF标志

    ZF位于flag第6位,零标志位,功能是记录相关指令执行后结果是否为0,如果结果为0,则ZF=1,否则ZF=0。如:

    mov ax,1
    sub ax,1
     
    • 1
    • 2

    执行后结果为0,ZF=1。一般情况下,运算指令(如add,sub,mul,div,inc,or,and)影响标志寄存器,而传送指令(如mov,push,pop)不影响标志寄存器。

    PF标志

    flag的第2位是PF标志位,奇偶标志位,功能是记录相关指令执行后,其结果的所有bit中1的个数是否为偶数,若1的个数是偶数,pf=1,如果是奇数,fp=0。如:

    mov al,1
    add al,10
     
    • 1
    • 2

    执行后结果为00001011b,有3个1,所以PF=0。

    SF标志

    flag的第7位是SF标志位,符号标志位,它记录相关指令执行后,结果是否为负,如果结果为负,则sf=1,结果为正,sf=0。计算机中通常用补码表示数据,一个数可以看成有符号数或无符号数,如:

    00000001B,可以看成无符号1或有符号+1
    10000001B,可以看成无符号129或有符号-127
     
    • 1
    • 2

    也就是说对于同一个数字,可以当做有符号数运算也可以当做无符号数运算。如:

    mov al,10000001b
    add al,1
     
    • 1
    • 2

    这段代码结果是(al)=10000010b,可以将add指令进行的运算当做无符号运算,那么相当于129+1=130,也可以当做有符号运算,相当于-127+1=-126。SF标志就是在进行有符号运算的时候记录结果的符号的,当进行无符号运算的时候SF无意义(但还会影响SF,只是对我们来说没有意义了)。

    CF标志

    flag的第0位是CF标志位,进位标志位,一般情况下载进行无符号运算时,他记录了运算结果的最高有效为向更高为的进位值,或从更高位的借位值。加入一个无符号数据是8位的,也就是0-7个位,那么在做加法的时候就可能造成进位到第8位,这时并不是丢弃这个进位,而是记录在falg的CF位上。如:

    mov al,98h
    add al,al
     
    • 1
    • 2

    执行后al=30h,CF=1。当两个数据做减法的时候有可能向更高位借位,如97h-98h借位后相当于197h-198h,CF也可以用来记录借位,如:

    mov al,97h
    sub al,98h
     
    • 1
    • 2

    执行后(al)=FFH,CF=1记录了向更高位借位的信息。

    OF标志

    在进行有符号运算的时候,如果结果超过了机器能表示的范围称为“溢出”。机器能表示的范围是指如8位寄存器存放或一个内存单元存放,表示范围就是-128~127,16位同理。如果超出了这个范围就叫做溢出,如:

    mov al,98
    add al,99
    
    mov al,0F0H
    add al,088H
     
    • 1
    • 2
    • 3
    • 4
    • 5

    第一段代码(al)=(al)+99=98+99=197超过了8位能表示的有符号数的范围,第二段代码结果(al)=(al)+(-120)=(-16)+(-12-)=-136也超过了8位有符号的范围,所以计算的结果是不可信的。如第一段代码计算之后(al)=0C5H,换成补码表示的是-59,98+99=-59很明显是不正确的结果。

    flag的第11位是OF标志位,溢出标志位,一般情况下,OF记录有符号数运算结果是否溢出,如果溢出则OF=1,如果没有溢出,OF=0。所以CF是对无符号数的标志,OF是对有符号的标志。但对于一个运算指令,他们是同时生效的,只不过这个指令究竟是有符号还是无符号,是看实际的操作的。有符号CF无意义,无符号OF无意义。

    adc指令

    adc是带进位加法指令,利用了CF标志位上记录的进位值。格式:adc 操作对象1,操作对象2。功能:操作对象1=操作对象1+操作对象2+CF。如abc ax,bx实现的是(ax)=(ax)+(bx)+CF,如:

    mov ax,2
    mov bx,1
    sub bx,ax
    adc ax,1
     
    • 1
    • 2
    • 3
    • 4

    注意这段代码,首先ax中的值是2,bx中的值是1,然后进行(bx)-(ax)的计算,结果是-1造成了无符号的借位,此时CF=1,在进行adc ax,1时,进行的是(ax)+1+CF=2+1+1=4。仔细分析一下就可以发现,如果把整个加法分开,低位先相加,然后高位相加再加上进位CF, 就是一个完整的加法运算,也就是说add ax,dx这个指令可以拆分为:

    add al,bl
    adc ah,bh
     
    • 1
    • 2

    所以有了adc这个指令我们就可以完成一些更庞大的数据量的加法运算。如计算1EF000H+000H的值:

    mov ax,001eh
    mov bx,0f000h
    add bx,1000h
    adc ax,0020h
     
    • 1
    • 2
    • 3
    • 4

    注:inc和loop指令不影响CF位。

    sbb指令

    sbb和adc类似,是带借位的减法,格式:sbb 操作对象1,操作对象2,执行的功能是操作对象1=操作对象1-操作对象2-CF,如:sbb ax,bx即(ax)=(ax)-(bx)-CF。sbb指令影响CF。

    cmp指令

    cmp是比较指令,cmp的功能相当于减法,只是不保存结果。cmp执行后影响标志寄存器,其他相关指令通过识别被影响的标志位来得知结果。格式:cmp 操作对象1,操作对象2,执行功能是计算对操作对象1-操作对象2但不保存结果,仅仅根据结果对标志位进行设置,如:cmp ax,ax结果为0,但并不保存在ax中,执行之后zf=1,pf=1,sf=0,cf=0,of=0。若执行cmp ax,bx通过标志位就可以判断结果:

    若(ax)=(bx)则(ax)-(bx)=0,zf=1
    若(ax)!=(bx)则(ax)-(bx)!=0,zf=0
    若(ax)<(bx)则(ax)-(bx)产生借位,cf=1
    若(ax)>=(bx)则(ax)-(bx)不产生借位,cf=0
    若(ax)>(bx)则(ax)-(bx)既不产生借位,结果又不为0,cf=0zf=0
    若(ax)<=(bx)则(ax)-(bx)既可能借位,结果可能为0,cf=1zf=1
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    但实际上往往会出现溢出,如34-(-96)=82H(82H是-126的补码),但应该等于130超出了补码表示的范围,所以sf=1。我们可以同时检验sf和of两个来验证cmp的结果:cmp ah,bh

    • 若sf=1,of=0说明没有溢出,那么sf的计算结果正确(ah)<(bh)
    • 若sf=1,of=1说明出现了溢出,那么sf结果相反(ah)>(bh)
    • 若sf=0,of=1说明有溢出,那么sf结果相反(ah)<(bh)
    • 若sf=0,of=0说明没有溢出,那么结果正确(ah)>=(bh)
    检测比较结果的条件转移指令

    下面几条指令和cmp一起使用,检测不同的标志位来达到不同的条件跳转效果:

    指令含义检测的标志位
    je等于则转移zf=1
    jne不等于转移zf=0
    jb小于转移cf=1
    jnb不小于转移cf=0
    ja大于转移cf=0且zf=0
    jna不大于转移cf=1或zf=1

    指令中的字母含义如下:

    • e:equa;
    • ne:not equal
    • b:below
    • nb:not below
    • a:above
    • na:not above

    上面的检测都是在cmp进行无符号比较时的检测位,有符号数检测原理一样,只是检测的标志位不同而已。下面看一个例子,如果(ah)=(bh)则(ah)=(ah)+(ah),否则(ah)=(ah)+(bh)

    cmp ah,bh
    je s
    add ab,bh
    jmp short ok
    s:add ah,ah
    ok:···
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这里注意的是,je检测的是zf位,而不管之前执行的是什么指令,只要zf=1就会发生转移,所以cmp的位置需要仔细的把控,当然是否和cmp配合使用也是取决于编程者,下面例子实现了统计data中数值为8的字节个数,然后用ax保存:

    ···
    data segment
    db 8,11,8,1,8,5,63,38
    data ends
    ···
    mov ax,data
    mov ds,ax
    mov bx,0
    mov ax,0
    mov cx,8
    s:cmp byte ptr [bx],8
    jne next
    inc ax
    next:inc bx
    loop s
    ···
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    DF标志位和串传送指令

    flag的第10位是DF标志位,方向标志位,在串处理中,每次操作si,di的增减。

    • df=0每次操作后si,di递增
    • df=1每次操作后si,di递减

    串传送指令,movsb,这个指令相当于执行:

    1. ((es)*16+(di))=((ds)*16+(si))

    2. 如果df=0:(si)=(si)+1,(di)=(di)+1

    如果df=1:(si)=(si)-1,(di)=(di)-1

    可以看出,movsb是将DS:SI指向的内存单元中的字节送入ES:DI中,然后根据DF的值对SI和DI增减1

    同理mobsw就是将DS:SI指向的内存单元中的字送入ES:DI中,然后根据DF的值对SI和DI增减2

    但一般来说,movsb和movsw都是和rep联合使用的,格式:rep movsb,这相当于:

    s:movsb
    loop s
     
    • 1
    • 2

    所以rep的作用是根据cx的值重复执行后面的串传送指令,由于每次执行movsb之后si和di都会自行增减,所以使用rep可以完成(cx)个字节的传送。movsw也一样。

    由于DF位决定着串传送的方向,所以这里有两条指令用来设置df的值:

    clddf=0
    stddf=1
     
    • 1
    • 2

    例子:使用串传送指令将data段中第一个字符串复制到他后面的空间中:

    ···
    data segment
    db 'Welcome to masm!'
    db 16 dup (0)
    data ends
    
    mov ax,data
    mov ds,ax
    mov si,0
    mov es,ax
    mov di,16
    mov cx,16
    cld
    rep movsb
    ···
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    pushf和popf

    pushf的功能是将标志寄存器的值入栈,popf是出栈标志寄存器。有了这两个命令,就可以直接访问标志寄存器了,如:

    mov ax,0
    push ax
    popf
     
    • 1
    • 2
    • 3
    标志寄存器在Debug中的表示

    Debug中-r查看寄存器信息,最后有一段表示,下面列出我们已知的寄存器在Debug里的表示:

    标志值1的标记值0的标记
    ofOVNV
    sfNGPL
    zfZRNZ
    pfPEPO
    cfCYNC
    dfDNUP

    内中断

    内中断的产生

    任何一个通用CPU都拥有执行完当前正在执行的指令后,检测到从CPU发来的中断信息,然后立即去处理中断信息的能力。这里的中断信息是指几个具有先后顺序的硬件操作,当CPU出现下面请看时会产生中断信息,相应的中断信息类型码(供CPU区分来源,是字节型,共256种)如下:

    • 除法错误,如执行div指令出现除法溢出 0
    • 单步执行 1
    • 执行into指令 4
    • 执行int指令 指令执行的int n后面的n就是一个字节型立即数,即为中断类型码
    中断处理和中断向量表

    CPU接收到中断信息之后,往往要对中断信息进行处理,而如何处理使我们编程决定的。而CPU通过中断向量表来根据中断类型找到处理程序的入口地址(CS:IP)也称为中断向量。

    中断向量表中存放着不同的中断类型对应的中断向量(处理程序的入口地址),中断向量表存放在内存中,8086PC指定必须放在内存地址0处,从0000:0000到0000:03FF的1024个单元存放中断向量表,每个表项占两个字,四个字节。

    CPU会自动根据中断类型找到对应的中断向量并设置CS和IP的值,这个过程称为中断过程,步骤如下:

    1. (从中断信息中)取得中断类型码
    2. 标志寄存器的值入栈(暂存)pushf
    3. 设置标志寄存器第8位TF和第9位IF的值为0 TF=0,IF=0
    4. CS内容入栈 push cs
    5. IP内容入栈 push ip
    6. 在中断向量表中找到对应的CS和IP值并设置 (ip)=(N*4),(cs)=(N*4+2)

    这么做的目的是,在中断处理之后还要回复CPU的现场(各个寄存器的值),所以先把那些入栈。

    中断处理程序和iret指令

    运行中的CPU随时都可能接收到中断信息,所以CPU随时都可能执行中断程序,执行的步骤:

    1. 保存用到的寄存器
    2. 处理中断
    3. 回复用到的寄存器
    4. 用iret返回

    iret的指令功能是:pop ip pop cs popf(前面说到了,这三个寄存器的入栈是硬件自动完成的,所以iret是和硬件自动完成的步骤配合使用的)。

    以处理0号除法溢出中断为例,我们想要编写除法溢出的中断处理程序需要解决如下几步问题:

    1. 编写程序
    2. 找到一段没有使用的内存空间
    3. 将程序写入到内存
    4. 将内存中的程序的入口写入0号中断的向量表位置

    我们可以采取下面框架来完成这个过程:

    ···
    start do0安装程序
    设置中断向量表
    mov ax,4c00h
    int 21h
    
    do0 程序部分
    mov ax,4c00h
    int 21h
    ···
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    可以看出我们分成了两部分,第一部分称之为“安装”,第二部分是代码实现。安装部分的函数实现思路如下:

    设置es:di至项目的地址
    设置ds:si指向源地址
    设置cx为传输长度
    设置传输方向为正
    rep movsb
    设置中断向量表
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    实现如下:

    start:mov ax,cs
    mov ds,ax
    mov si,offset do0
    mov ax,0
    es,ax
    mov di,200h
    mov cx,offset do0end-fooset do0
    cld
    rep movsb
    ···
    do0:代码
    do0end:nop
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这里offset do0end-fooset do0的意思是do0到do0end的代码长度,-是编译器可以识别并运算的符号,也就是说编译器可以再编译时处理表达式,如8-4等。还要注意的是,假如代码部分要输出“owerflow!”的话,需要将输出的内容写在代码部分并写入选择的内存中,否则如果单单在这个安装程序开始开辟一段data段的话,是会在程序返回时被释放。如:

    do0:jmp short do0start
    db "overflow!"
    
    do0start:
    ···
    do0end:nop
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    单步中断

    当标志寄存器的TF标志位为1的时候,CPU会在执行一条语句之后将资源入栈,然后去执行单步中断处理程序,如Debug就是运行在单步中断的条件下的,它能让CPU每执行一条指令都暂停,然后我们可以查看CPU的状态。但CPU可以防止在运行单步中断处理程序的时候再发生中断然后又去调用单步中断处理程序……CPU可以将TF置零,这样就不会再中断了。CPU提供这个功能就是为了单步跟踪程序的执行。

    但需要注意的是,CPU并不会每次接收中断信息之后立即执行,在某些特定情况下它不会立即响应中断,如设置ss寄存器的时候如果接收到了中断信息,就不会响应。因为我们需要连续设置ss和ip的值,在debug中单步执行的时候也是,mov ss,ax和mov sp,0是在一步之内执行的,所以我们需要灵活使用这个特性,sp紧跟着ss执行,而不要在设置ss和sp之间插入其他指令。

    int指令

    int指令

    int指令也会引发中断,使用格式是int n,n就是int引发的中断类型码,int中断的执行过程:

    1. 获取类型码n
    2. 标志寄存器入栈,if=0,tf=0
    3. cs,ip入栈
    4. (ip)=(n*4),(cs)=(n*4+2)
    5. 执行n号中断的程序

    所以我们可以使用int指令调用任何一个中断的中断程序,如int 0调用除法溢出中断。一般情况下,系统将一些具有一定功能的小程序以中断的方式提供给程序调用,当然也可以自己编写,可以简称为中断例程。

    编写中断例程

    如编写中断7ch的中断例程,实现word型数据的平方,返回dx和ax中。求2*3456^2,代码:

    assume cs:code
    code segment
    start mov ax,3456
    int 7ch
    add ax,ax
    adc dx,dx
    mov ax,4c00h
    int 21h
    code ends
    end start
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    接下来写7ch的功能和安装程序,并修改7ch中断向量表:

    assume cs:code
    code segment
    start:mov ax,cs
    mov ds,ax
    mov si,offset sqr
    mov ax,0
    mov es,ax
    mov di,200h
    mov cx,offset sqrend-offset sqr
    cld
    rep movsb
    
    mov ax,0
    mov es,ax
    mov word ptr es:[7ch*4],200h
    mov word ptr es:[7ch*4+2],0
    
    mov ax,4c00h
    int 21h
    
    sqr:mul ax
    iret
    
    sqrend:nop
    code ends
    end start
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    编写7ch中断实现loop指令,主程序输出80个“!”:

    ···
    start mov ax,0b800h
    mov es,ax
    mov di,160*12
    mov bx,offset s-offset se
    mov cx,80
    s:mov byte ptr es:[di],'!'
    add di,2
    int 7ch
    se:nop
    ···
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    7ch实现部分:

    lp:push bp
    mov bp,sp
    dec cx
    jcxz lpret
    add [bp+2],bx
    lpret:pop bp
    iret
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    因为bx里面是需要专一的偏移地址,而使用bp的时候默认段寄存器是ss,所以add [bp+2],bx就可以实现将栈中的sp的值修改回s处,自行推导一下就ok。

    BIOS和DOS提供的中断例程

    系统ROM中存放着一套程序,称为BIOS,除此之外还有DOS都提供了一套可以供我们调用的中断例程,不同历程有不同的中断类型码,并且还能根据传入的参数不同而实现不同的功能,也就是说同一个类型码的中断例程可以实现很多不同功能,如int 10h是BIOS提供的包含了多个和屏幕输出相关子程序的中断例程。传参数如下面例子:

    ···
    mov ah,2 ;置光标
    mov bh,0 ;第0页
    mov dh,5 ;dh中放行号
    mov dl,12 ;dl中放列号
    int 10h
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    BIOS和DOS安装历程的过程是,开机后CPU一加电,自动初始化CS为0FFFFH,IP为0,而在这个地方有一个跳转指令,挑战到BIOS和系统检测初始化程序。在BIOS系统检测初始化程序中会设置中断向量表中的值。

    端口

    端口的概念

    各种存储器都要和CPU的地址线,数据线,控制线相连,在CPU看来,总线就是一个由若干个存储单元构成的逻辑存储器,称之为内存地址空间。除了各种存储器,通过总线和CPU相连的还有下面三种芯片:

    • 各种接口卡(如网卡显卡)上的接口芯片,他们控制接口卡工作
    • 主板上的接口芯片,CPU通过它们访问外部设备
    • 其他芯片,用来存储相关系统信息,或进行相应的输入输出

    上面的芯片中都有一种由CPU读写的寄存器,它们都和CPU的总线相连(通过各自的芯片),CPU对他们进行读写时候都通过控制线向他们所在的芯片发出端口读写指令。

    所以,对于CPU来说,将这些寄存器都当做端口,对他们进行统一编址,建立了一个端口地址空间,每一个端口拥有一个地址,所以CPU可以直接读取下面三个地方的数据:

    • CPU内部的寄存器
    • 内存单元
    • 端口
    端口的写

    因为端口所在的芯片和CPU通过总线相连,所以端口地址和内存地址一样通过地址总线传送,并且在PC系统中,CPU最多可以定位64KB个不同的端口,所以端口地址范围是0~65535。

    对端口的读写不能使用mov,push,pop等内存读写指令,端口的读写指令只有两个:in和out分别用于从端口读取数据和往端口写入数据。

    访问端口的步骤:

    1. CPU通过地址总线降低至信息60h发出
    2. CPU通过控制线发出端口读命令,选中端口所在芯片,并通知它要从中读取数据
    3. 端口所在芯片将目标端口中的数据通过数据线送入CPU

    注:在in和out指令中,只能通过ax或al来存放从端口中读入的数据或要发送到端口中的数据,且访问8位端口时,用al,访问16位端口用ax。

    对0~255以内的端口进行读写时:

    in al,20h
    out 20h,al
     
    • 1
    • 2

    对256~65535的端口进行读写时,需要将端口号写在dx中:

    mov dx,3f8h
    in al,dx
    out dx,al
     
    • 1
    • 2
    • 3
    CMOS RAM芯片

    PC中有一个叫做CMOS RAM的芯片,称为CMOS,有如下特征:

    • 包含一个实时钟和一个有128个存储单元的RAM存储器(早期的计算机64个字节)
    • 靠电池供电,关机后内部的实时钟仍可继续工作,RAM中的信息不丢失
    • 128个字节的RAM中,内部实时钟占用0~0dh单元保存时间信息,其余大部分单元用于保存系统配置信息,供系统启动时BIOS程序读取,BIOS也提供了相关的程序可以让我们在开机时配置CMOS中的系统信息。
    • 芯片内部有两个端口70h和71h,CPU通过这两个端口读写CMOS
    • 70h为地址端口,存放要访问CMOS单元的地址,71h为数据端口,存放从选定的单元中读取的数据,或写入的数据。

    所以可以看出,想要从CMOS中读取数据,应分两步,先将单元号送入70h,然后再从71h读出对应号的数据。

    shl和shr指令

    shl和shr是逻辑移位指令,shl是逻辑左移,功能为:

    1. 将一个寄存器或内存单元中的数向左移位
    2. 将最后移出的一位写入CF
    3. 最低位补0

    如:mov al,01001000b shl al,1执行结束后(al)=10010000b,CF=0。

    注:如果移动位数大于1,那么必须将移动位数写在cl中。

    mov al,01010001b
    mov cl,3
    shl al,cl
     
    • 1
    • 2
    • 3

    执行后(al)=10001000b,最后移出的一位是0,所以CF=0。可以看出左移操作相当于x=x*2。

    右移shr同理,最高位用0补充,移出的写入CF,若移动位数大于1,也要写在cl中,相当于x=x/2

    在CMOS中存放着当前时间的年月日时分秒,分别存在下面的单元内:

    024789

    每个信息使用一个字节存放,以BCD码的形式,BCD码是对0-9这几个数字使用二进制表示,如:

    0123456789
    0000000100100011010001010110011110001001

    如果要表示一个两位数如13,就是一个字节高四位是十位1的BCD码,低四位是个位3的BCD码,表示为00010011b。下面程序获取当前月份:

    ···
    mov al,8
    out 70h,al   ;要从8号单元读取数据,所以先将8号单元送入70h端口
    in al,71h    ;从71h端口拿数据
    
    mov ah,al    ;复制一下
    mov cl,4     
    shr ah,cl    ;ah右移四位,ah里面的就是月份的十位
    and al,00001111b  ;al里面剩下的就是月份的个位
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    外中断

    接口芯片和端口

    CPU除了需要拥有运算的能力,还要拥有I/O(输入输出)能力,我们键入一个字母,要能处理,所以我们需要面对的是:外部设备随时的输入和CPU何处得到外部设备的输入。

    外部设备拥有自己的芯片连接到主板上,这些芯片内部由若干寄存器,而CPU将这些寄存器当做端口访问,外设的输入或CPU向外设输出都是送给对应的端口然后再由芯片处理送给目标(CPU或外设)。

    外中断

    CPU提供外中断来处理这些如随时可能出现的来自外设的输入,在PC系统中,外中断源有以下两类:

    可屏蔽中断:CPU可以不响应的外部中断,CPU是否响应看标志寄存器IF的设置,如果IF=1,CPU执行完当前指令后响应中断,如果IF=0,则不响应。可屏蔽中断的执行步骤和内部中断类似:

    1. 获取中断类型码n(从外部通过总线输入)
    2. 标志寄存器入栈,IF=0,TF=0
    3. CS,IP入栈
    4. (IP)=(n*4),(CS)=(n*4+2)

    可见,将IF置零的原因是以免在处理中断程序的时候再发生中断。当然我们也可以选择处理,下面两个指令可以改变IF的值:sti,设置IF=1,cli,设置IF=0。

    不可屏蔽中断:CPU必须响应的外部中断,CPU检测到不可屏蔽中断后执行完当前指令立即响应中断。8086CPU中不可屏蔽中断的中断类型码固定位2,所以中断过程中不需要获取中断类型码,步骤:

    1. 标志寄存器入栈,IF=0,TF=0
    2. CS,IP入栈
    3. (IP)=(8),(CS)=(0AH)

    几乎所有由外设引发的外中断都是可屏蔽中断,如键盘输入,不可屏蔽中断通常是在系统中又必须处理的紧急情况发生时通知CPU的中断信息。

    PC键盘处理过程

    键盘上每个按键都相当于一个开关,按下就是开关接通,抬起就是开关断开。键盘上有一个芯片对键盘中每一个键盘的状态进行扫描,开关按下生成一个扫描码——通码,记录按下的按键位置,开关抬起也会产生一个扫描——断码,码记录松开的位置,都是送入60h端口。通码的第7位为0,断码第7位为1,也就是说断码=通码+80h。P247表。

    当键盘输入送达60h时,相关新品就会向CPU发送中断类型码为9的可屏蔽中断信息。CPU检测到该中断信息之后,如果IF=1,响应中断,引发中断过程并执行int9的中断例程。BIOS中int9的中断程序用来进行基本的键盘输入处理,步骤如下:

    1. 读出60h的扫描码
    2. 如果是字符的扫描码,将对应的字符的ASCII吗存入内存中的BIOS键盘缓冲区,如果是控制键(Ctrl)和切换键(CapsLock)扫描码,则将其转换为状态字(二进制位记录控制键和切换键状态的字节)写入内存中的存储状态字节的单元。
    3. 对键盘系统进行相关控制,如向新平发出应答

    BIOS中键盘缓冲区能存储15个键盘输入,每个键盘输入两字节,高位存放扫描码,低位存放字符。此外,0040:17单元存放键盘状态字节,记录了控制键和切换键的状态,记录信息如下:

    含义
    0右shift,1表示按下
    1左shift,1按下
    2Ctrl,1按下
    3Alt,1按下
    4ScrollLock状态,1表示指示灯亮
    5NumLock状态,1表示小键盘输入的是数字
    6CapsLock状态,1表示大写字母
    7Insert状态,1表示处于删除状态

    可以看书P276的一个改写int 9的中断例程。

    直接定址表

    描述单元长度的标号

    我们可以使用下面的标号来表示数据的开始:

    ···
    code segment
    a:db 1,2,3,4,5,6,7,8
    b:dw 0
    ···
    code ends
    ···
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    a,b都是代表对应数据的起始地址,但并不能判断数据的长度或类型。下面一段程序将a中的8个数累加存入b中:

    assume cs:code
    code segment
    a db 1,2,3,4,5,6,7,8
    b dw 0
    start mov si,0
    mov cx,8
    s:mov al,a[si]
    mov ah,0
    add b,ax
    inc si
    loop s
    mov ax,4c00h
    int 21h
    code ends
    end start
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    code段中a和b后并没有”:”号,这种写法同时描述内存地址和单元长度的标号。a描述了地址code:0和从这个地址开始后的内存单元都是字节单元,而b描述了地址code:8和从这个地址开始以后的内存单元都是字单元。所以b相当于CS:[8],a[si]相当于CS:0[si],使用这种标号,我们可以间接地访问内存数据。

    其它段中使用数据标号

    刚说的第一种标号即加”:”号的标号,只能使用在代码段中,不能在其他段中使用。如果想要在其它段中(如data段)使用标号可以使用第二种:

    assume cs:code,ds:data
    data segment
    a db 1,2,3,4,5,6,7,8
    b dw 0
    data ends
    ···
    start mov ax,data
    mov ds,ax
    mov si,0
    mov al,a[si]
    ···
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    如果想在代码段中直接使用数据标号访问数据,需要使用assume伪指令将标号所在段和一个寄存器联系起来,是让寄存器明白,我们要访问的数据在ds指向的段中,但编译器并不会真的将段地址存入ds中,我们做了如下假设之后,编译器在编译的时候就会默认ds中已经存放了data的地址,如下面的编译例子:

    mov al,a[si]
    编译为:mov al,[si+0]
     
    • 1
    • 2

    可以看出编译器默认了a[si]在ds所在的段中。所以我们需要手工指定ds指向data:

    mov ax,data
    mov ds,ax
     
    • 1
    • 2

    也可以这么使用:

    data segment
    a db 1,2,3,4,5,6,7,8
    b dw 0
    c a,b
    data ends
     
    • 1
    • 2
    • 3
    • 4
    • 5

    c处存放的是a和b的偏移地址,相当于c dw offset a,offset b。同理c dd a,b相当于c dw offset a,seg a,offset b,seg b即存的是a和b的段地址和偏移地址。

    直接定址表

    使用查表的方法编写相关程序,如输出一个字节型数据的16进制形式(子程序):

    showbyte jmp short show
    table db '0123456789ABCDEF'
    show:push bx
    push es
    mov ah,al
    she ah,1
    she ah,1
    she ah,1
    she ah,1 ;右移四位,位移子程序限制使用的寄存器数,只能这么移
    and al,00001111b
    mov bl,al
    mov bh,0
    mov ah,table[bx]  ;高四位作为相对于table的偏移,取得对应字符
    mov bx,0b800h
    mov es,bx
    mov es:[160*12+40*2],ah
    mov bl,al
    mov bh,0
    mov al,table[bx]
    mov es:[160*12+40*2+2],al
    pop es
    pop bx
    ret
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    可见我们直接使用需要的数值和地址的映射关系来寻找需要的数据。

    程序入口地址的直接定址表

    可以看书P296的例程,主要思想是,编写多个子程序实现不同功能,每个子程序有自己的标号,如sub1,sub2···等。将它们存在一个表中:

    table dw sub1,sub2,sub3,sub4
     
    • 1

    然后按照之前的方法使用如:

    setscreen:jmp short set
    table dw sub1,sub2,sub3,sub4
    set:push bx
    cmp ah,3
    ja sret
    mov bl,ah
    mov bh,0
    add bx,bx
    call word ptr table[bx]
    sret:pop bx
    ret
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    使用BIOS进行键盘输入和磁盘读写

    int 9中断例程对键盘输入的处理

    键盘处理依次按下A,B,C,D,E,shift_A,A的过程:

    我们知道,键盘有16字的缓冲区,可以存放15个按键的扫描码和对应的ASCII码值,如下:

    |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |
     
    • 1

    我们按下A时,引发键盘中断,CPU执行int 9中断例程,从60h端口读出A键通码,然后检测状态字,看是否有控制键或切换键按下,发现没有,将A的扫描码1eh和对应的ASCII码’a’61h写在缓冲区:

    |1e61|  |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |
     
    • 1

    然后BCDE同理:

    |1e61|3062|2e63|2064|1265|  |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |
     
    • 1

    在按下shift之后引发键盘中断,int 9程序接受了shift的通码之后设置0040:17处状态字第一位为1,表示左shift按下,接下来按A间,引发中断,int 9中断例程从60h端口督导通码之后检测状态字,发现左shift被按下,于是将A的键盘扫描码1eh和’A’的ASCII41h写入缓冲区:

    |1e61|3062|2e63|2064|1265|1e41| |   |   |   |   |   |   |   |   |   |   |   |   |   |   |
     
    • 1

    松开shift,0040:17第一位变回0,之后又按下A和之前一样。

    int 16h读取键盘缓冲区

    int 16h可以供程序员调用,编号为0的功能是从键盘缓冲区读一个键盘输入,(ah)=扫描码,(al)=ascii码。如:

    mov ah,0
    int 16h
    |3062|2e63|2064|1265|1e41|  |   |   |   |   |   |   |   |   |   |   |   |   |   |   |
     
    • 1
    • 2
    • 3

    执行后,缓冲区第一个没了,然后ah中是1eh,al中是61h。如果缓冲区为空的时候执行,那么会循环等待知道缓冲区有数据,所以int 16h的0号功能的步骤是:

    1. 检测键盘缓冲区是否有数据
    2. 没有则继续1
    3. 读取第一个单元的键盘输入
    4. 扫描码送ah,ascii码送al
    int 13h读写磁盘

    3.5寸软盘分为上下两面,每面80个磁道,每个磁道18个扇区,每个扇区512字节,共约1.44MB。磁盘的实际访问时磁盘控制器进行的,我们通过控制磁盘控制器来控制磁盘,只能以扇区为单位读写磁盘,每次需要给出面号,磁道号,和扇区号,面号和磁道号从0开始,扇区号从1开始。BIOS提供int 13h来实现访问磁盘,读取0面0道1扇区的内容到0:200的程序:

    mov ax,0
    mov es,ax
    mov bx,200h
    
    mov al,1   ;读取的扇区数
    mov ch,0   ;磁道号
    mov cl,1   ;扇区号
    mov dl,0   ;驱动器号,0开始,0软驱A,1软驱B,磁盘从80h开始,80h硬盘C,81h硬盘D
    mov dh,0   ;磁头号(软盘面号)
    mov ah,2   ;13h的功能号,2表示读扇区
    int 13h
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    es:bx指向接收数据的内存区。操作成功(ah)=0,(al)=读入的扇区数,操作失败(ah)=错误代码。将0:200的数据写入0面0道1扇区:

    mov ax,0
    miv es,ax
    mov bx,200h
    
    mov al,1   ;读取的扇区数
    mov ch,0   ;磁道号
    mov cl,1   ;扇区号
    mov dl,0   ;驱动器号
    mov dh,0   ;磁头号(软盘面号)
    mov ah,3   ;13h的功能号,3表示写扇区
    int 13h
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    es:bx指向写入磁盘的数据,操作成功(ah)=0,(al)=写入的扇区数,操作失败(ah)=错误代码

    展开全文
  • 汇编语言程序

    2016-11-03 20:18:40
    一、汇编语言程序格式及开发过程 汇编语言——用指令的助记符、符号地址、标号等符号书写程序的语言。 源程序—— 用汇编语言编写的程序。 汇编——把源程序翻译成机器语言程序的过程。 汇编程序——完成汇编...
  • .word 汇编指令

    千次阅读 2018-08-04 11:04:01
    我们在汇编文件中经常会见到.word指令,如下所示: label: .word express  这里的用法表示:在当前位置存放一个字,可能有些人会觉得就是放一个字word,这个要怎么看了,一般一个word是两个字节,跟CPU的型号...
  • 4.1.1 汇编语言程序的建立和汇编过程 4.2 汇编语言程序格式 4.2.1 汇编语言语句的基本格式 一个汇编语言源程序中有3种基本语句:指令语句、伪指令语句和宏指令语句(宏指令语句就是由若干条指令语句形成的语句) ...
  • 汇编语言基础知识

    千次阅读 多人点赞 2016-10-19 17:22:40
    1、什么是汇编语言,它在计算机语言中的地位?汇编语言是程序设计语言的基础语言,是唯一可以直接与计算机硬件打交道的语言 2、汇编语言源程序、汇编程序、汇编的关系? 3、汇编语言的特点 1) 汇编语言与机器指令...
  • ASM2015 汇编语言

    2015-10-25 14:35:59
    Masm for windows 集成实验环境是从事一线教学的大学教师针对汇编语言初学者的特点开发的一个简单易用的汇编语言学习与实验软件,支持32位与4位的WINDOWS 7,支持DOS的16/32位汇编程序和Windows下的32汇编程序(并...
  • 例如,下面的指令中,用word ptr指明了指令访问的内存单元是一个字单元。 mov word ptr ds: [0],1 inc word ptr [bx] inc word ptr ds: [o] add word ptr [bx],2 下面的指令中,用byte ptr指明了指令访问的内存单元...
  • 简明x86汇编语言教程

    2018-08-23 22:27:23
    简明x86汇编语言教程,学习汇编语言有很大帮助,是word文档,很清晰
  • 汇编语言

    千次阅读 2017-03-29 00:06:03
    第一个实验是编写汇编语言 题目要求:用PCSpim模拟器编写汇编语言程序设计。我做是实现将既包含在数组A中又包含在数组B中的无符号字数取出并存于内存中,其中数组A包含5个数,数组B包含10个数。如找不到相同的数则...
  • 1. 掌握汇编语言程序的上机步骤 (1 )用编辑程序(如EDIT、记事本等)编辑汇编语言源程序(建立.ASM文件) (2 )用MASM程序产生OBJ文件 (3 )用LINK程序产生EXE文件 (4 )程序的运行(用DEBUG或在DOS下直接运行) 2. 掌握...
  • 汇编语言的所有指令

    万次阅读 多人点赞 2017-11-11 09:43:15
    汇编语言中使用的操作数,可以是常数、寄存器、存储器、变量、标号活表达式,其中藏书、变量和标号是三种基本数据项。 ⑴常数必须是固定的值,没有属性,是确定的数据。 ⑵变量在程序运行中是可以修改的。所有的...
  • 汇编语言学习笔记(【汇编语言】小甲鱼零基础汇编) 目录 第〇章 课程资料 第一章 基础知识 第二章 寄存器(CPU工作原理) 第三章 寄存器(内存访问) 第四章 第一个程序 第五章 [BX]和loop指令 第六章 包含多个段...
  • 汇编语言程序格式

    2020-02-11 13:59:15
    不同的汇编程序由不同的汇编语言编程规定。目前支持Intel 8086/8088系列微机,常用的汇编程序有ASM、MASM、TASM、OPTASM等。 汇编语言语句种类及其格式 汇编语言的语句可以分为指令语句和伪指令语句 指令语句 每一条...
  • 汇编语言8086笔记

    千次阅读 多人点赞 2017-06-07 14:56:36
    为什么要学习汇编语言? 汇编语言是很多相关课程的重要基础,比如:操作系统、接口技术等。它是底层编程语言,是计算机系统提供给用户最快最有效的语言,也是能对硬件直接编程的语言。因此,对空间和时间要求很高的...
  • 汇编语言程序设计

    千次阅读 2016-11-02 20:48:22
    一、汇编语言程序格式及开发过程 汇编语言——用指令的助记符、符号地址、标号等符号书写程序的语言。 源程序—— 用汇编语言编写的程序。 汇编——把源程序翻译成机器语言程序的过程。 汇编程序——完成汇编任务的...
  • 高级汇编语言程序设计实验十一 学 院 专 业 姓 名 学 号 指导教师 一实验目的 掌握高 言技 的方法 掌握宏定 宏 用方法 熟悉宏定 中的参数形式 二实验准备 复 教材中有关宏 的内容 写一条宏指令 CLRB完成用空格符将一...
  • 从基础到中级左右,其中包含了汇编的指令集,和两套汇编语言word版本的练习题!
  • 王爽《汇编语言》笔记(详细)

    万次阅读 多人点赞 2019-03-24 19:53:32
    一、基础知识 1、指令 机器指令:CPU能直接识别并执行的二进制编码...汇编语言发展至今,有以下3类指令组成。 汇编指令:机器码的助记符,有对应的机器码。 伪指令:没有对应的机器码,由编译器执行,计算机并不执...
  • IBM-PC汇编语言-答案

    2010-05-31 10:09:35
    IBM-PC汇编语言-答案 IBM-PC 汇编语言 答案 word
  • 基础汇编语言展示 BYTE :BYTE数据定义 BYTE2 : BYTE定义字符串 DUP :DUP操作符分配空间赋初始值,DUP使用一个整数表达式作为计数器,为多个数据分配存储空间 WORD : WORD数据定义 DWORD : DWORD数据定义 DWORD还...
  • 计算机底层原理——汇编语言

    千次阅读 多人点赞 2020-08-19 12:51:57
    想要成为高级程序员,我们必须要学会汇编语言汇编语言是非常重要的计算机底层技术,一般用于底层的编写。不懂汇编的程序员算不上一个好的程序员,充其量是一个熟练使用某种语言的工程师,而编程高手一定要研究底层...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 31,243
精华内容 12,497
关键字:

word汇编语言