51、高级语言接口与汇编代码的使用

高级语言接口与汇编代码的使用

1. 内联汇编代码概述

内联汇编代码是直接插入到高级语言程序中的汇编语言源代码,大多数 C 和 C++ 编译器都支持这一特性。以在 32 位保护模式下使用平面内存模型的 Microsoft Visual C++ 为例,内联汇编代码是编写外部模块汇编代码的直接替代方案。

1.1 优缺点

  • 优点 :简单性是其主要优势,因为无需担心外部链接问题、命名问题和参数传递协议。
  • 缺点 :缺乏可移植性。当高级语言程序需要为不同目标平台编译时,这会成为一个问题。例如,在 Intel Pentium 处理器上运行的内联汇编代码在 RISC 处理器上无法运行。虽然可以通过在程序源代码中插入条件定义来为不同目标系统启用不同版本的函数,但维护仍然是个难题。而外部汇编语言程序的链接库则可以轻松替换为针对不同目标机器设计的类似链接库。

1.2 __asm 指令

在 Visual C++ 中,__asm 指令可以放在单个语句的开头,也可以标记一组汇编语言语句(称为 asm 块)的开始。语法如下:

__asm 语句
__asm {
  语句 1
  语句 2
  ...
  语句 n
}

注意,“asm” 前面有两个下划线字符。

1.3 注释

在 asm 块中的任何语句之后都可以放置注释,可以使用汇编语言语法或 C/C++ 语法。Visual C++ 手册建议避免使用汇编器风格的注释,因为它们可能会干扰在单个逻辑行上展开的 C 宏。以下是允许的注释示例:

mov  esi,buf     ; 初始化索引寄存器
mov  esi,buf    // 初始化索引寄存器
mov  esi,buf    /* 初始化索引寄存器 */

1.4 特性

编写内联汇编代码时可以进行以下操作:
- 使用 x86 指令集中的大多数指令。
- 使用寄存器名称作为操作数。
- 按名称引用函数参数。
- 引用在 asm 块外部声明的代码标签和变量(这很重要,因为局部函数变量必须在 asm 块外部声明)。
- 使用包含汇编器风格或 C 风格基数表示法的数字字面量。例如,0A26h 和 0xA26 是等效的,都可以使用。
- 在诸如 inc BYTE PTR [esi] 的语句中使用 PTR 运算符。
- 使用 EVEN 和 ALIGN 指令。

1.5 限制

编写内联汇编代码时不能进行以下操作:
- 使用数据定义指令,如 DB(BYTE)和 DW(WORD)。
- 使用汇编器运算符(PTR 除外)。
- 使用 STRUCT、RECORD、WIDTH 和 MASK。
- 使用宏指令,包括 MACRO、REPT、IRC、IRP 和 ENDM,或宏运算符(<>、!、&、% 和 .TYPE)。
- 按名称引用段(但是,可以使用段寄存器名称作为操作数)。

1.6 寄存器值

不能对 asm 块开始时的寄存器值做任何假设,因为寄存器可能已被 asm 块之前执行的代码修改。Microsoft Visual C++ 中的 __fastcall 关键字会使编译器使用寄存器传递参数,为避免寄存器冲突,不要同时使用 __fastcall 和 __asm。

一般来说,可以在内联代码中修改 EAX、EBX、ECX 和 EDX,因为编译器不期望这些值在语句之间保留。但是,如果修改的寄存器过多,可能会使编译器无法完全优化同一过程中的 C++ 代码,因为优化需要使用寄存器。虽然不能使用 OFFSET 运算符,但可以使用 LEA 指令检索变量的偏移量。例如:

lea esi,buffer

1.7 长度、类型和大小运算符

可以在内联汇编器中使用 LENGTH、SIZE 和 TYPE 运算符。LENGTH 运算符返回数组中的元素数量。TYPE 运算符根据其目标返回以下值之一:
- C 或 C++ 类型或标量变量使用的字节数。
- 结构使用的字节数。
- 对于数组,单个数组元素的大小。

SIZE 运算符返回 LENGTH * TYPE。以下程序示例展示了内联汇编器为各种 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
}

1.8 文件加密示例

下面是一个读取文件、加密文件并将输出写入另一个文件的简短程序。

1.8.1 TranslateBuffer 函数
void TranslateBuffer( char * buf, 
     unsigned count, unsigned char eChar )
{
__asm {
mov
esi,buf
mov
ecx,count
mov
al,eChar
L1:
xor
[esi],al
inc
esi
loop
L1
}     // asm
}

该函数使用 __asm 块定义语句,遍历字符数组并将每个字符与预定义值进行异或运算。内联语句可以引用函数参数、局部变量和代码标签。

1.8.2 C++ 模块
// ENCODE.CPP - Copy and encrypt a file.
#include <iostream>
#include <fstream>
#include "translat.h"
using namespace std;
int main( int argcount, char * args[] ) 
{
// 从命令行读取输入和输出文件。
if( argcount < 3 ) {
cout << "Usage: encode infile outfile" << endl;
return -1;
}
const int BUFSIZE = 2000;
char buffer[BUFSIZE];
unsigned int count;
// 字符计数
unsigned char encryptCode;
cout << "Encryption code [0-255]? ";
cin >> encryptCode;
ifstream infile( args[1], ios::binary );
ofstream outfile( args[2], ios::binary );
cout << "Reading" << args[1] << "and creating" 
<< args[2] << endl;
while (!infile.eof() )
{
infile.read(buffer, BUFSIZE);
count = infile.gcount();
TranslateBuffer(buffer, count, encryptCode);
outfile.write(buffer, count);
}
return 0;
}

该程序从命令行读取输入和输出文件的名称,在循环中调用 TranslateBuffer 函数,从文件中读取数据块,对其进行加密,并将转换后的缓冲区写入新文件。

1.8.3 头文件

translat.h 头文件包含 TranslateBuffer 函数的单个原型:

void TranslateBuffer(char * buf, unsigned count, 
                      unsigned char eChar);
1.8.4 过程调用开销

在调试器的反汇编窗口中查看该程序时,可以看到调用和返回过程涉及的开销。以下是将三个参数压入堆栈并调用 TranslateBuffer 的语句:

; TranslateBuffer(buffer, count, encryptCode)
mov
al,byte ptr [encryptCode] 
push
eax  
mov
ecx,dword ptr [count] 
push
ecx  
lea
edx,[buffer] 
push
edx  
call
TranslateBuffer (4159BFh) 
add
esp,0Ch 

TranslateBuffer 的反汇编代码如下:

push
ebp  
mov
ebp,esp 
sub
esp,40h 
push
ebx  
push
esi  
push
edi 
; 内联代码开始
mov
esi,dword ptr [buf] 
mov
ecx,dword ptr [count] 
mov
al,byte ptr [eChar] 
L1:
  xor
byte ptr [esi],al 
  inc
esi  
  loop L1 (41D762h)
; 内联代码结束
pop
edi  
pop
esi  
pop
ebx  
mov
esp,ebp 
pop
ebp  
ret

如果关闭调试器反汇编窗口中的显示符号名称选项,将参数移动到寄存器的三个语句将显示为:

mov
esi,dword ptr [ebp+8] 
mov
ecx,dword ptr [ebp+0Ch] 
mov
al,byte ptr [ebp+10h] 

编译器被指示生成调试目标,这是适合交互式调试的未优化代码。如果选择发布目标,编译器将生成更高效(但更难阅读)的代码。

1.8.5 省略过程调用

TranslateBuffer 函数中的六个内联指令总共需要 18 条指令来执行。如果该函数被调用数千次,所需的执行时间可能会很可观。为避免这种开销,可以将内联代码插入调用 TranslateBuffer 的循环中,创建一个更高效的程序:

while (!infile.eof() )
{
   infile.read(buffer, BUFSIZE );
   count = infile.gcount();
   __asm {
       lea esi,buffer
       mov ecx,count
       mov al,encryptCode
   L1:
       xor [esi],al
       inc  esi
       Loop L1
   } // asm
    outfile.write(buffer, count);
}

1.9 内联汇编代码相关问题回顾

  • 内联汇编代码与内联 C++ 过程有何不同?
  • 内联汇编代码相对于使用外部汇编语言程序有什么优势?
  • 至少展示两种在内联汇编代码中放置注释的方法。
  • (是/否):内联语句可以引用 __asm 块外部的代码标签吗?

2. 32 位汇编语言代码与 C/C++ 的链接

在创建设备驱动程序和嵌入式系统代码时,通常需要将 C/C++ 模块与用汇编语言编写的专用代码集成。汇编语言在直接硬件访问、位映射以及对寄存器和 CPU 状态标志的低级访问方面表现出色。虽然用汇编语言编写整个应用程序会很繁琐,但可以用 C/C++ 编写主应用程序,仅用汇编语言编写用 C 编写会很麻烦的代码。

2.1 调用约定

C/C++ 程序按参数列表中从右到左的顺序传递参数。函数返回后,调用程序负责将堆栈恢复到之前的状态。可以通过向堆栈指针添加等于参数大小的值,或者从堆栈中弹出足够数量的值来实现。

在汇编语言源代码中,需要在 .MODEL 指令中指定 C 调用约定,并为从外部 C/C++ 程序调用的每个过程创建原型。例如:

.586
.model flat,C
IndexOf PROTO,
srchVal:DWORD, arrayPtr:PTR DWORD, count:DWORD

2.2 函数声明

  • C 程序 :在声明外部汇编语言过程时,使用 extern 限定符。例如:
extern long IndexOf( long n, long array[], unsigned count );
  • C++ 程序 :如果过程将从 C++ 程序调用,添加 “C” 限定符以防止 C++ 名称修饰:
extern "C" int IndexOf( long n, long array[], unsigned count );

名称修饰是标准的 C++ 编译器技术,涉及用额外字符修改函数名称,以指示每个函数参数的确切类型。在支持函数重载(多个具有相同名称但参数列表不同的函数)的任何语言中都需要此技术。从汇编语言程序员的角度来看,名称修饰的问题在于,C++ 编译器在生成可执行文件时会告诉链接器查找修饰后的名称,而不是原始名称。

2.3 IndexOf 示例

2.3.1 C++ 实现
long IndexOf( long searchVal, long array[], unsigned count )
{
for(unsigned i = 0; i < count; i++) {
if( array[i] == searchVal )
return i;
}
return -1;
}

该函数用于在数组中线性搜索整数的第一个匹配实例。如果搜索成功,返回匹配元素的索引位置;否则返回 -1。

2.3.2 汇编语言实现

将汇编语言代码放在名为 IndexOf.asm 的源文件中,该文件将编译成名为 IndexOf.obj 的目标代码文件。使用 Visual Studio 编译和链接调用的 C++ 程序和汇编语言模块。C++ 项目可以使用 Win32 Console 作为输出类型。

; IndexOf function      (IndexOf.asm)

.586
.model flat,C
IndexOf PROTO,
srchVal:DWORD, arrayPtr:PTR DWORD, count:DWORD

.code
;-----------------------------------------------
IndexOf PROC USES ecx esi edi,
srchVal:DWORD, arrayPtr:PTR DWORD, count:DWORD
;
; 对 32 位整数数组进行线性搜索,
; 查找特定值。如果找到该值,
; 则在 EAX 中返回匹配的索引位置; 
; 否则,EAX 等于 -1。
;-----------------------------------------------
NOT_FOUND = -1

mov  eax,srchVal    ; 搜索值
mov  ecx,count      ; 数组大小
mov  esi,arrayPtr   ; 数组指针
mov  edi,0          ; 索引

L1: cmp  [esi+edi*4],eax
je   found
inc  edi
loop L1

notFound:
mov  al,NOT_FOUND
jmp  short exit

found:
mov  eax,edi

exit:
ret   
IndexOf ENDP
END

在汇编语言代码的第 25 - 28 行,测试循环小而高效,尽量在多次执行的循环中使用最少的指令。

2.3.3 C++ 测试程序
#include <iostream>
#include <time.h>
#include "indexof.h"
using namespace std;

int main()  {
  // 用伪随机整数填充数组。
  const unsigned ARRAY_SIZE = 100000; 
  const unsigned LOOP_SIZE = 100000;
  char* boolstr[] = {"false","true"};

  long array[ARRAY_SIZE]; 
  for(unsigned i = 0; i < ARRAY_SIZE; i++)
    array[i] = rand();

  long searchVal;
  time_t startTime, endTime;

  cout << "Enter an integer value to find: ";
  cin >> searchVal;
  cout << "Please wait...\n";

  // 测试汇编语言函数。
  time( &startTime );
  int count = 0;

  for( unsigned n = 0; n < LOOP_SIZE; n++)
    count = IndexOf( searchVal, array, ARRAY_SIZE );

  bool found = count != -1;

  time( &endTime );
  cout << "Elapsed ASM time: " << long(endTime - startTime) 
       << " seconds. Found = " << boolstr[found] << endl;

  return 0;
}

该程序首先用伪随机值初始化数组,提示用户输入要查找的值,然后多次调用 IndexOf 函数进行搜索,并记录搜索所用的时间。在一台相当快的计算机上测试时,循环在 6 秒内执行了 100,000 * 100,000 = 100 亿次迭代,约为每秒 16.7 亿次循环迭代。需要注意的是,程序重复了 100,000 次过程调用开销(压入参数、执行 CALL 和 RET 指令),过程调用会导致相当多的额外处理。

2.4 调用 C 和 C++ 函数

可以编写调用 C 和 C++ 函数的汇编语言程序,原因如下:
- C 和 C++ 具有丰富的输入输出库,输入输出更加灵活,在处理浮点数时特别有用。
- 两种语言都有广泛的数学库。

在调用标准 C 库(或 C++ 库)中的函数时,必须从 C 或 C++ 的 main() 过程启动程序,以便运行库初始化代码。

2.4.1 函数原型

从汇编语言代码调用的 C++ 函数必须使用 “C” 和 extern 关键字定义。基本语法如下:

extern "C" 函数名( 参数列表 )
{ ... }

例如:

extern "C" int askForInteger( )
{
cout << "Please enter an integer:";
//...
}

为了方便,可以将多个函数原型分组在一个块中,这样在单个函数实现中可以省略 extern 和 “C”:

extern "C" { 
int askForInteger();
int showInt( int value, unsigned outWidth );
// 等等
}
2.4.2 汇编语言模块

如果汇编语言模块将调用 Irvine32 链接库中的程序,要注意它使用以下 .MODEL 指令:

.model flat, STDCALL

虽然 STDCALL 与 Win32 API 兼容,但与 C 程序使用的调用约定不匹配。因此,在声明汇编模块要调用的外部 C 或 C++ 函数时,必须在 PROTO 指令中添加 C 限定符:

INCLUDE Irvine32.inc
askForInteger PROTO C
showInt PROTO C, value:SDWORD, outWidth:DWORD

C 限定符是必需的,因为链接器必须将函数名称和参数列表与 C++ 模块导出的函数匹配。此外,汇编器必须使用 C 调用约定生成正确的代码,以便在函数调用后清理堆栈。

汇编语言程序被 C++ 程序调用时也必须使用 C 限定符,以便汇编器使用链接器可以识别的命名约定。例如:

SetTextOutColor PROC C, 
color:DWORD
.
.
SetTextOutColor ENDP

如果汇编代码调用其他汇编语言程序,C 调用约定要求在每次过程调用后从堆栈中移除参数。

如果汇编语言代码不调用 Irvine32 程序,可以让 .MODEL 指令使用 C 调用约定:

; (不要包含 Irvine32.inc)
.586
.model flat,C

此时不再需要在 PROTO 和 PROC 指令中添加 C 限定符:

askForInteger PROTO
showInt PROTO, value:SDWORD, outWidth:DWORD
SetTextOutColor PROC, 
color:DWORD
.
.
SetTextOutColor ENDP
2.4.3 函数返回值

C++ 语言规范未提及代码实现细节,因此 C 和 C++ 函数返回值没有标准化的方式。编写调用这些语言中函数的汇编语言代码时,需要查看编译器文档以了解其函数返回值的方式。以下是一些可能的情况:
| 返回值类型 | 返回方式 |
| ---- | ---- |
| 整数 | 可以在单个寄存器或寄存器组合中返回。 |
| 函数返回值空间 | 调用程序可以在堆栈上预留空间,函数在返回前将返回值插入堆栈。 |
| 浮点值 | 通常在函数返回前压入处理器的浮点堆栈。 |

Microsoft Visual C++ 函数返回值的方式如下:
| 返回值类型 | 返回寄存器或方式 |
| ---- | ---- |
| bool 和 char 值 | 在 AL 中返回。 |
| short int 值 | 在 AX 中返回。 |
| int 和 long int 值 | 在 EAX 中返回。 |
| 指针 | 在 EAX 中返回。 |
| float、double 和 long double 值 | 分别以 4 字节、8 字节和 10 字节的值压入浮点堆栈。 |

综上所述,通过合理使用内联汇编代码以及实现汇编语言与 C/C++ 的链接,可以充分发挥汇编语言和高级语言的优势,提高程序的性能和可维护性。在实际开发中,需要根据具体需求和场景选择合适的方法。

2.5 调用 C 和 C++ 函数的流程图

下面是一个 mermaid 格式的流程图,展示了汇编语言模块调用 C 和 C++ 函数的基本流程:

graph TD
    A[汇编语言模块开始] --> B[检查是否调用 Irvine32 程序]
    B -- 是 --> C[使用 .model flat, STDCALL]
    B -- 否 --> D[使用 .model flat, C]
    C --> E[PROTO 指令添加 C 限定符]
    D --> F[PROTO 指令无需 C 限定符]
    E --> G[调用 C/C++ 函数]
    F --> G
    G --> H[根据 C 调用约定清理堆栈]
    H --> I[汇编语言模块结束]

2.6 调用 C 和 C++ 函数的注意事项

  • 初始化问题 :在调用标准 C 库(或 C++ 库)中的函数时,必须从 C 或 C++ 的 main() 过程启动程序,以确保库初始化代码能够正常运行。否则,可能会出现一些未定义的行为,例如输入输出函数无法正常工作等。
  • 调用约定匹配 :要确保汇编语言模块和 C/C++ 程序使用的调用约定一致。如果不一致,可能会导致参数传递错误、堆栈不平衡等问题。例如,当汇编语言模块使用 STDCALL 调用约定,而 C 程序使用 C 调用约定时,就需要进行相应的调整。
  • 函数原型声明 :从汇编语言代码调用 C++ 函数时,要正确使用 “C” 和 extern 关键字定义函数原型,避免 C++ 名称修饰带来的问题。同时,将多个函数原型分组在一个块中可以简化代码,但要注意在函数实现时的语法规范。

3. 总结与应用建议

3.1 内联汇编代码与链接汇编代码的对比

对比项 内联汇编代码 32 位汇编语言代码与 C/C++ 链接
优点 简单性,无需处理外部链接、命名和参数传递协议问题 可将汇编语言的优势(如直接硬件访问)与 C/C++ 的高级特性结合,提高程序性能
缺点 缺乏可移植性,维护困难 需要处理调用约定、名称修饰等问题
适用场景 简单的代码片段,对可移植性要求不高的情况 对性能要求较高,需要进行直接硬件访问或位操作的场景

3.2 应用建议

  • 选择合适的方法 :根据具体的需求和场景选择使用内联汇编代码还是将汇编语言代码与 C/C++ 进行链接。如果只是需要在高级语言程序中插入一些简单的汇编指令来完成特定任务,内联汇编代码是一个不错的选择;如果需要实现复杂的汇编功能,并且对性能有较高要求,建议使用链接的方式。
  • 注意调用约定和名称修饰 :在进行汇编语言代码与 C/C++ 的链接时,要特别注意调用约定和名称修饰的问题,确保函数能够正确调用和返回。
  • 优化代码性能 :在使用内联汇编代码时,要注意寄存器的使用和代码的优化,避免过多修改寄存器导致编译器无法优化 C++ 代码。在进行汇编语言与 C/C++ 链接时,要尽量减少过程调用开销,提高程序的执行效率。

3.3 未来发展趋势

随着计算机技术的不断发展,高级语言和汇编语言在软件开发中的应用也在不断变化。未来,可能会有更多的工具和技术来简化汇编语言与高级语言的集成,提高开发效率。同时,对于性能要求极高的领域,如嵌入式系统、游戏开发等,汇编语言仍然会发挥重要作用。开发者需要不断学习和掌握新的技术,以适应不断变化的市场需求。

3.4 实际案例分析

以下是一个简单的实际案例,展示了如何根据具体需求选择合适的方法。假设我们要开发一个图像处理程序,需要对图像进行一些复杂的位操作和颜色转换。由于这些操作对性能要求较高,我们可以使用汇编语言来实现这些核心功能,然后将其与 C++ 程序进行链接。同时,在一些简单的输入输出和用户交互部分,我们可以使用 C++ 的高级特性来实现,这样可以充分发挥两种语言的优势,提高程序的性能和可维护性。

graph TD
    A[图像处理程序需求] --> B[选择实现方法]
    B -- 复杂位操作和颜色转换 --> C[使用汇编语言实现核心功能]
    B -- 输入输出和用户交互 --> D[使用 C++ 高级特性实现]
    C --> E[将汇编语言代码与 C++ 程序链接]
    D --> E
    E --> F[完成图像处理程序]

通过以上的分析和建议,希望能够帮助开发者更好地理解和应用高级语言接口与汇编代码,在实际开发中取得更好的效果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值