第十八篇 -- 在C++中嵌入汇编语言
基于C++宝典的学习
一、什么是汇编语言
汇编语言是一种功能很强的程序设计语言,也是利用了计算机所有硬件特性并能直接控制硬件的语言。在汇编语言中,用助记符(Memoni)代替操作码,用地址符号(Symbol)或标号(Label)代替地址码。这样用符号代替机器语言的二进制码,就把机器语言变成了汇编语言。
汇编语言比机器语言易于读写、调试和修改,同时也具有机器语言执行速度快、占用内存空间少等优点。但在编写复杂程序时,相对高级语言来说汇编语言代码量较大,而且汇编语言依赖于具体的机型,不能通用,因此不能直接在不同处理机型之间移植。虽然其移植性不好,但效率非常高,针对计算机特定硬件而编制的汇编语言程序,能准确地发挥计算机硬件的功能和特长,程序精炼而质量高,所以汇编语言至今仍是一种常用而强有力的底层开发语言。
二、汇编语言的特点
汇编语言指令使用一些具有相应含义的助忆符来表达的,所以,它要比机器语言容易掌握和运用。但因为要直接使用CPU资源,所以相对高级程序设计语言来说它又显得相对复杂。汇编语言程序归纳起来大概有以下几个主要特点。
1. 与硬件相关:汇编语言指令是指机器指令的一种符号表示,而不同类型的CPU有不同的机器指令系统,也就有不同的汇编语言,所以汇编语言程序与机器有着密切的关系。也就是说,不同型号的CPU之间是无法通用相同汇编代码的,因此导致汇编语言的移植性和通用性降低,这是汇编语言天生的缺陷。
2. 保持了机器语言的优点,具有直接和简捷的特点:正因为汇编语言有“与机器相关性”的特性,程序员用汇编语言编写程序时,可充分发挥自己的聪明才智,对机器内部的各种资源进行合理的安排,让它们始终处于最佳的使用状态,这样做的最终效果就是程序的执行代码短,执行速度快,所以,汇编语言是高效的程序设计语言。另外汇编语言可有效地访问、控制计算机的各种硬件设备,如磁盘、存储器、CPU、I/O端口等,实现资源利用的最大化。
3. 编写程序复杂:汇编语言是一种面向机器的语言,其汇编指令与机器指令基本上一一对应,所以,汇编指令也同机器指令一样既有功能单一、具体的特点。要想完成某件工作,就必须安排CPU的每步工作。另外,在编写汇编语言程序时,还要考虑具体机型的限制、汇编指令的细节和限制等。
4. 经常与高级语言配合使用,应用十分广泛:在某些情况下,比如直接操作CPU执行中断以实现线程调度、保存CPU寄存器以存储/恢复线程状态等,仅仅使用高级语言是完不成的,需要借助于汇编语言,但是仅使用汇编语言的话,大型程序恐怕需要付出比高级语言几倍的工作量,有时候也是没有必要的。因此,可以在高级语言里嵌入汇编语句,让仅仅一部分需要高效率的代码用汇编语言来完成,其余的框架搭建等用高级语言来完成,这样既保证了效率又降低了代码的复杂程度。这种配合使用在大型软件开发里经常遇到,应用十分广泛。
三、汇编语言的应用领域
汇编语言是面向机器的低级程序设计语言。它可以直接控制硬件的最下层,如寄存器、标志位、存储单元等,因而能充分发挥机器硬件的性能,具有其他高级语言不可替代的作用。汇编语言是计算机能够提供给用户的最快的、最有效的语言,也是能够利用计算机所有硬件特性并且能够直接控制硬件的唯一语言。也正因为汇编语言具有这些特性,在对于程序的控件和时间要求很高的场合,以及需要直接控制硬件的应用场合,使用汇编语言是必不可少的,它能够完成许多其他高级语言所无法完成的功能。比如Linux操作系统的内核,大多数代码是用C语言完成的,但是在某些关键的与硬件关系密切的部分,仍不可避免地使用了汇编语言。汇编语言试用的应用领域如下。
1. 要求执行效率高、反应快的领域:在操作系统内核、实时系统、智能仪器仪表的控制程序等领域中最主要的要求是效率高和反应快。汇编语言是直接作用于机器硬件的,它所设计的程序具有较小的时间复杂度和空间复杂度。而高级语言是面向问题来设计的,它所设计的程序运行时间长,并且占用存储空间大,在反应和效率上远远不如汇编语言。
2. 与硬件资源密切相关的软件开发领域:汇编语言是面向机器的,而高级语言是面向问题的,对于一些与硬件资源密切相关的软件也只能采用汇编语言。如外部设备的底层驱动程序、单片机控制、图像处理软件、加密解密算法等领域。
3. 受存储容量限制的应用领域:诸如家用电器的控制领域等,控制系统功能单一、行为简单,且受内存、CPU等硬件条件限制,需要使用汇编语言进行嵌入式控制。
不过,相比高级语言,汇编语言调试困难,工作量大,其不宜使用的领域有大型软件的整体开发、没有特殊要求的一般应用系统的开发等。
四、汇编语言的基本语法
下面以8086系统为例讲解汇编语言的基本语法和用法。
按功能分,8086系统的汇编指令包括数据传送指令、算数指令、逻辑指令、串处理指令、控制转移指令和处理机控制指令6大类。
》通用数据传送指令
8086系统有4类数据传送指令,以实现CPU的内部寄存器之间、CPU与存储器之间、CPU和I/O端口之间的数据传送。这4类指令是通用数据传送指令、累加器专用传送指令、地址传送指令和标志传送指令。通用数据传送指令中包括最基本的传送MOV(Move)指令、堆栈PUSH(Push onto the stack) 和POP(Pop from the stack) 指令、数据交换XCHG(Exchange)指令。
1. 最基本的传送指令MOV指令
MOV指令为双操作数指令,它将一个字节或一个字的操作数从源操作数复制至目的操作数,其语法格式为:
MOV DST,SRC
其中,SRC为源操作数,它可以是立即数、寄存器以及各种寻址的内存单元内容。DST为目的操作数,它可以是寄存器或者各种寻址的内存单元,不可以是立即数。
执行的操作:
(DST)<-(SRC)
对于最基本的传送指令的使用,有几点需要说明:
1. 目的数可以是通用寄存器、存储单元和段寄存器(但不允许用CS段寄存器)。
2. 立即数不能直接传送至段寄存器。
3. 不允许在两个存储单元间直接传送数据。
4. 不允许在两个段寄存器间直接传达信息。
以8086为例:
MOV CL, AL ;AL中的8位数据到CL
MOV DS, AX ;AX中的16位数据到DS
2. 堆栈操作指令
堆栈是按照后进先出(LIFO--Last In First Out) 原则组织的一段内存区域。在子程序调用和终端处理时,分别要保存返回地址和断点地址,在进入子程序和中断服务程序后,还通常需要保护通用寄存器原先的内容,子程序返回或中断处理返回主程序前,则要恢复通用寄存器原先的内容,并分别将返回地址或断点地址恢复到指令指针寄存器中。这些功能都要通过堆栈来实现。另外,在高级语言中除中断及子程序调用外,参数的传递也是靠堆栈来实现的。除返回地址和断点地址的保护及恢复操作是由CPU自动完成的以外,寄存器的保存、恢复以及参数的传递都需要由堆栈指令来完成。
堆栈指令包括入栈PUSH指令和出栈POP指令。
PUSH是入栈指令,完成将源操作数中的一个字推入(也称压入)堆栈的操作。其堆栈指针SP的值始终指向刚刚入栈的数据处,每进一个字,栈顶指针SP的值减2.其语法格式为:
PUSH SRC
其中SRC为源操作数,可以是除立即数之外的16位的寄存器或者内存字单元的内容(两个字节)。对于入栈PUSH指令的使用,需要说明的是:
1. 入栈的操作数除不允许用立即操作数外,可以为通用寄存器、段寄存器(全部)和存储器。
2. 入栈时高位字节先入栈,低位字节后入栈。
以8086为例:
PUSH AX ;CPU通用寄存器入栈 PUSH CS ;段寄存器入栈 PUSH [BX+DI] ;存储器单元入栈
POP是出栈指令,用于将SP所指的堆栈顶处的一个字取出(也称弹出),送至目的DST中,并且SP的值加2.其语法格式为:
POP DST
DST是目的操作数,可以是除立即数之外的16位的寄存器或者内存字单元的内容(两个字节)。对于出栈POP指令的使用,需要说明的是:
1. 出栈操作数除不允许用立即数和CS段寄存器外,可以为通用寄存器、段寄存器和存储器。
2. 执行POP SS指令后,堆栈区在存储区的位置要改变。
执行POP SP指令后,栈顶的位置要改变。
以8086为例:
POP AX ;CPU通用寄存器出栈 POP CS ;段寄存器出栈 POP [BX+DI] ;存储单元出栈
3.数据交换指令:XCHG
数据交换指令XCHG可在寄存器之间、寄存器与存储器之间进行数据交换,但不允许在两个存储单元之间执行交换过程,并且段寄存器和IP寄存器不能参与数据交换。此外,该指令的执行不影响标志位。其语法格式为:
XCHG OPR1, OPR2
上述数据交换指令实现操作数OPR1和OPR2中的一个字或一个字节的数据交换。对于数据交换指令的使用,需要说明的是:
1. 必须有一个操作数在寄存器中。
2. 不能与段寄存器交换数据。
3. 存储器与存储器之间不能交换数据。
以8086为例:
XCHG AL, BL ;AL和BL间进行字节交换 XCHG BX, CX ;BX与CX间进行字交换
》累加器专用传送指令
累加器专用传送指令包括I/O端口向CPU输入信息指令IN(Input)、CPU向端口发送信息指令OUT(Output)以及换码指令XLAT(Translate)。
在IBM PC机里,所有I/O端口与CPU之间的通信都由IN和OUT指令来完成。其中IN完成从I/O到CPU的信息传送,而OUT完成从CPU到I/O的信息传送。CPU只能用累加器(AL或AX)接收或发送消息。
1. 输入IN指令
输入指令完成将信息从I/O通过累加器传送到CPU的操作。该指令有长格式和短格式之分。长格式语法格式为:
IN AL, PORT(每次输入一字节内容。字节传送) IN AX, PORT(字传送)
执行的操作
(AL)<-(PORT)(字节) (AX)<-(PORT+1, PORT)(字)
短格式语法格式为:
IN AL, DX(字节) IN AX, DX(字)
执行的操作:
AL<-((DX))(字节) AX<-((DX)+1, DX)(字)
CPU与外设通信时,输入/输出通信必须经过特殊的端口进行。外部设备最多可有65536个端口,端口号(即外设的端口地址)为0000~FFFFh。其中前256个端口(0~FFH)可以直接在指令中指定,这就是长格式指令中的PORT,此时机器指令用另个字节表示,第二个字节就是端口号。所以用长格式时可以在指令中直接指定端口号,但只限于外设的前256个端口。当端口号大于255时,只能使用短格式指令。短格式指令要用寄存器DX来间接传送,此时DX的内容是端口号,而IN指令的作用是将DX中指示的端口号内的信息送至AL或AX,OUT指令的作用是将AL或AX的内容送至DX中的内容所指示的端口中。当所送内容是一个字时,必须使用连续的两个端口号,端口地址(即DX中的内容)只能取偶数。
总而言之,IN和OUT指令提供了字和字节两种使用方式,选用哪一种,则取决于外设端口宽度。8088是准16位机,外部数据总线是8位,因而只有字节传送指令,而8086还有字传送指令。以8086位例,将12位A/D变换器所得数字量输入,这时,A/D变换器应使用一个字端口,输入数据的程序为:
MOV DX, 02F0H ;设该端口为02F0H IN AX, DX
2. 输出OUT指令
输出OUT指令完成将信息从CPU通过累加器传送到I/O的操作。与输入IN指令同样的原因,输出OUT指令的语法格式也有长格式和短格式之分。
长格式语法格式为:
OUT PORT, AL(字节) OUT PORT, AX(字)
执行的操作:
(PORT)<-(AL)(字节) (PORT+1,PORT)<-(AX)(字)
短格式语法格式为:
OUT DX, AL(字节) OUT DX, AX(字)
执行的操作:
((DX))<-(AL)(字节) ((DX)+1, (DX))<-AX(字)
以8086为例:
OUT PORT, AL ;直接的字节输出,PORT规定与IN指令相同 OUT PORT, AX OUT DX, AL ;间接地字节输出 OUT DX, AX MOV AL, 05H OUT 27H, AL ;将字节05H传送到地址27H的端口
。。。
其他各种命令不赘述,不会可以查。直接进入汇编语言在C++中的应用
五、汇编语言在C++中的应用
》内联汇编的优点
因为在Visual C++中使用内联汇编不需要额外的编译器和联接器,且可以处理Visual C++中不能处理的一些事情,同时可以使用在C/C++中的变量,所以非常方便。
内联汇编代码不易于移植,如果你的程序打算在不同类型的机器(比如x86和Alpha)上运行,应当尽量避免使用内联汇编,这时可以使用MASM,因为MASM支持更方便的宏指令和数据指示符。
1. __asm语法
__asm关键字用来调用内联汇编,可以出现在任何合法的C或C++声明中。它不能单独出现,后面必须有汇编指令,可以是一条汇编指令、大括号括起来的一组代码,或者至少是大括号括起来的空代码。术语“__asm块”指的是任何单独的一条指令或一组指令,可以不包括在大括号里。
第一种语法格式:
__asm 汇编指令
第二种语法格式:
__asm
{
汇编指令列表
}
例如,下面的代码是一个简单的大括号里的__asm块:
__asm { mov al, 4 mov dx, 0xB008 out dx, al }
另外,在每一条汇编指令前加上__asm,与前面的方法是一样的作用。例如:
__asm mov al, 4 __asm mov dx, 0xB0008 __asm out dx, al
上面的两个例子所生成的代码是相同的,但是在括号里的__asm块这种方式更具优势,因为大括号可以使汇编指令很清楚地和C或C++代码分开,避免了无意义的__asm关键字重复。另外,大括号还可以避免引起歧义。如果想把C或C++代码和__asm块放在同一行,则必须把这个__asm块放在括号里。如果没有括号,编译器就不能确定汇编代码结束和C或C++代码起始的位置。
另外,由于大括号里的语句和一般的MASM语句格式一样,所以可以很方便地从现有的MASM源程序里复制。
不像C或C++中的"{}",__asm块中的"{}"不会影响C或C++变量的作用范围。同时,__asm块可以嵌套,嵌套也不会影响变量的作用范围。