Visual C++ __asm伪指令:C语言/C++内嵌汇编语言代码
内嵌汇编代码 (inline assembly code) 是指直接插入高级语言程序中的汇编源代码。大多数 C 和 C++ 编译器都支持这一功能。
本节将展示如何在运行于 32 位保护模式,并采用平坦内存模式的 Microsoft Visual C++ 中编写内嵌汇编代码。其他高级语言编译器也支持内嵌汇编代码,但其语法会发生变化。
内嵌汇编代码是把汇编代码编写为外部模块的一种直接替换方式。编写内嵌代码最突岀的优点就是简单性,因为不用考虑外部链接,命名以及参数传递协议等问题。
但使用内嵌汇编代码最大的缺点是缺少兼容性。高级语言程序针对不同目的平台进行编译时,这就成了一个问题。比如,在 Intel Pentium 处理器上运行的内嵌汇编代码就不能在 RISC 处理器上运行。
一定程度上,在程序源代码中插入条件定义可以解决这个问题,插入的定义针对不同目标系统可以启用函数的不同版本。不过,容易看出,维护仍然是个问题。另一方面,外部汇编过程的链接库容易被为不同目标机器设计的相似链接库所代替。
一般情况下,可以在内嵌代码中修改 EAX、EBX、ECX 和 EDX,因为编译器并不期望在语句之间保留这些寄存器值。但是,如果修改的寄存器太多,那么编译器就无法对同一过程中的 C++ 代码进行完全优化,因为优化要用到寄存器。
虽然不能使用 OFFSET 运算符,但是用 LEA 指令也可以得到变量的偏移地址。比如,下面的指令将 buffer 的偏移地址送入 ESI:
SIZE 运算符返回 LENGTH*TYPE 的值。下面的程序片段演示了内嵌汇编程序对各种 C++ 类型的返回值。
Microsoft Visual C++ 内嵌汇编程序不支持 SIZEOF 和 LENGHTOF 运算符。
本节将展示如何在运行于 32 位保护模式,并采用平坦内存模式的 Microsoft Visual C++ 中编写内嵌汇编代码。其他高级语言编译器也支持内嵌汇编代码,但其语法会发生变化。
内嵌汇编代码是把汇编代码编写为外部模块的一种直接替换方式。编写内嵌代码最突岀的优点就是简单性,因为不用考虑外部链接,命名以及参数传递协议等问题。
但使用内嵌汇编代码最大的缺点是缺少兼容性。高级语言程序针对不同目的平台进行编译时,这就成了一个问题。比如,在 Intel Pentium 处理器上运行的内嵌汇编代码就不能在 RISC 处理器上运行。
一定程度上,在程序源代码中插入条件定义可以解决这个问题,插入的定义针对不同目标系统可以启用函数的不同版本。不过,容易看出,维护仍然是个问题。另一方面,外部汇编过程的链接库容易被为不同目标机器设计的相似链接库所代替。
__asm 伪指令
在 Visual C++ 中,伪指令 __asm 可以放在一条语句之前,也可以放在一个汇编语句块(称为 asm 块)之前。语法如下:
__asm statement
__asm {
statement-1
statement-2
....
statement-n
}
提示:在“asm”的前面有两个下划线。
注释
注释可以放在 asm 块内任何语句的后面,使用汇编语法或 C/C++ 语法。Visual C++ 手册建议不要使用汇编风格的注释,以防与 C 宏混淆,因为 C 宏会在单个逻辑行上进行扩展。下面是可用注释的例子:
mov esi,buf ; initialize index register
mov esi,buf // initialize index register
mov esi,buf /* initialize index register */
特点
编写内嵌汇编代码时允许:- 使用 x86 指令集内的大多数指令。
- 使用寄存器名作为操作数。
- 通过名字引用函数参数。
- 引用在 asm 块之外定义的代码标号和变量。(这点很重要,因为局部函数变量必须在 asm 块的外面定义。)
- 使用包含在汇编风格或 C 风格基数表示法中的数字常数。比如,0A26h 和 0xA26 是等价的,且都能使用。
- 在语句中使用 PTR 运算符,比如 inc BYTE PTR[esi]。
- 使用 EVEN 和 ALIGN 伪指令。
限制
编写内嵌汇编代码时不允许:- 使用数据定义伪指令,如 DB(BYTE)和 DW(WORD)。
- 使用汇编运算符(除了 PTR 之外)。
- 使用 STRUCT、RECORD, WIDTH 和 MASK。
- 使用宏伪指令,包括 MACRO、REPT、IRC、IRP 和 ENDM,以及宏运算符(<>、!、&、% 和 .TYPE)。
- 通过名字引用段。(但是,可以用段寄存器名作为操作数。)
寄存器值
不能在一个 asm 块开始的时候对寄存器值进行任何假设。寄存器有可能被 asm 块前面的执行代码修改。Microsoft Visual C++ 的关键字 __fastcall 会使编译器用寄存器来传递参数,为了避免寄存器冲突,不要同时使用 __fastcall 和 __asm。一般情况下,可以在内嵌代码中修改 EAX、EBX、ECX 和 EDX,因为编译器并不期望在语句之间保留这些寄存器值。但是,如果修改的寄存器太多,那么编译器就无法对同一过程中的 C++ 代码进行完全优化,因为优化要用到寄存器。
虽然不能使用 OFFSET 运算符,但是用 LEA 指令也可以得到变量的偏移地址。比如,下面的指令将 buffer 的偏移地址送入 ESI:
lea esi,buffer
长度、类型和大小
内嵌汇编代码还可以使用 LENGTH、SIZE 和 TYPE 运算符。LENGTH 运算符返回数组内元素的个数。按照不同的对象,TYPE 运算符返回值如下:- 对 C 或 C++ 类型以及标量变量,返回其字节数。
- 对结构,返回其字节数。
- 对数组,返回其单个元素的大小。
SIZE 运算符返回 LENGTH*TYPE 的值。下面的程序片段演示了内嵌汇编程序对各种 C++ 类型的返回值。
Microsoft Visual C++ 内嵌汇编程序不支持 SIZEOF 和 LENGHTOF 运算符。
使用 LENGTH、TYPE 和 SIZE 运算符
下面程序包含的内嵌汇编代码使用 LENGTH、TYPE 和 SIZE 运算符对 C++ 变量求值。每个表达式的返回值都在同一行的注释中给出:struct Package { long originZip; // 4 long destinationZip; // 4 float shippingPrice; // 4 }; char myChar; bool myBool; short myShort; int myInt; long myLong; float myFloat; double myDouble; Package myPackage; long double myLongDouble; long myLongArray[10]; __asm { mov eax,myPackage.destinationZip; mov eax,LENGTH myInt; // 1 mov eax,LENGTH myLongArray; // 10 mov eax,TYPE myChar; // 1 mov eax,TYPE myBool; // 1 mov eax,TYPE myShort; // 2 mov eax,TYPE myInt; // 4 mov eax,TYPE myLong; // 4 mov eax,TYPE myFloat; // 4 mov eax,TYPE myDouble; // 8 mov eax,TYPE myPackage; // 12 mov eax,TYPE myLongDouble; // 8 mov eax,TYPE myLongArray; // 4 mov eax,SIZE myLong; // 4 mov eax,SIZE myPackage; // 12 mov eax,SIZE myLongArray; // 40 }