32位汇编语言与C/C++的链接及相关编程实践
1. 乘法表示例
我们将编写一个简单的应用程序,该程序会提示用户输入一个整数,使用位运算将其乘以2的升幂(从2^1到2^10),并显示每个乘积,同时在前面填充空格。这里使用C++进行输入输出,汇编语言模块会调用三个用C++编写的函数,程序由一个C++模块启动。
1.1 汇编语言模块
汇编语言模块包含一个名为
DisplayTable
的函数,它调用一个名为
askForInteger
的C++函数来获取用户输入的整数,然后使用循环将名为
intVal
的整数左移并通过调用
showInt
函数显示结果。
; ASM function called from C++
INCLUDE Irvine32.inc
; External C++ functions:
askForInteger PROTO C
showInt PROTO C, value:SDWORD, outWidth:DWORD
newLine PROTO C
OUT_WIDTH = 8
ENDING_POWER = 10
.data
intVal DWORD ?
.code
;---------------------------------------------
SetTextOutColor PROC C,
color:DWORD
;
; Sets the text colors and clears the console
; window. Calls Irvine32 library functions.
;---------------------------------------------
mov
eax,color
call
SetTextColor
call
Clrscr
ret
SetTextOutColor ENDP
;---------------------------------------------
DisplayTable PROC C
;
; Inputs an integer n and displays a
; multiplication table ranging from n * 2^1
; to n * 2^10.
;----------------------------------------------
INVOKE askForInteger
; call C++ function
mov
intVal,eax
; save the integer
mov
ecx,ENDING_POWER
; loop counter
L1:
push
ecx
; save loop counter
shl
intVal,1
; multiply by 2
INVOKE showInt,intVal,OUT_WIDTH
INVOKE newLine
; output CR/LF
pop
ecx
; restore loop counter
loop
L1
ret
DisplayTable ENDP
END
在
DisplayTable
过程中,在调用
showInt
和
newLine
之前必须对
ECX
进行压栈和出栈操作,因为Visual C++函数不会保存和恢复通用寄存器。
askForInteger
函数将结果返回在
EAX
寄存器中。
DisplayTable
在调用C++函数时不一定要使用
INVOKE
,也可以使用
PUSH
和
CALL
指令,例如调用
showInt
的代码如下:
push OUT_WIDTH
; push last argument first
push intVal
call showInt
; call the function
add
esp,8
; clean up stack
必须遵循C语言调用约定,即参数以相反的顺序压入栈中,调用者负责在调用后从栈中移除参数。
1.2 C++测试程序
下面是启动程序的C++模块,其入口点是
main()
,确保执行所需的C++语言初始化代码。它包含外部汇编语言过程和三个导出函数的函数原型:
// main.cpp
// Demonstrates function calls between a C++ program
// and an external assembly language module.
#include <iostream>
#include <iomanip>
using namespace std;
extern "C" {
// external ASM procedures:
void DisplayTable();
void SetTextOutColor(unsigned color);
// local C++ functions:
int askForInteger();
void showInt(int value, int width);
}
// program entry point
int main()
{
SetTextOutColor( 0x1E );
// yellow on blue
DisplayTable();
// call ASM procedure
return 0;
}
// Prompt the user for an integer.
int askForInteger()
{
int n;
cout << "Enter an integer between 1 and 90,000:";
cin >> n;
return n;
}
// Display a signed integer with a specified width.
void showInt( int value, int width )
{
cout << setw(width) << value;
}
1.3 项目构建
将C++和汇编语言模块都添加到Visual Studio项目中,然后从“项目”菜单中选择“生成解决方案”。
1.4 程序输出
当用户输入90000时,乘法表程序的示例输出如下:
Enter an integer between 1 and 90,000: 90000
180000
360000
720000
1440000
2880000
5760000
11520000
23040000
46080000
92160000
1.5 Visual Studio项目属性
如果使用Visual Studio构建集成C++和汇编语言并调用Irvine32库的程序,需要更改一些项目设置。以
Multiplication_Table
程序为例,操作步骤如下:
1. 从“项目”菜单中选择“属性”。
2. 在窗口左侧的“配置属性”下,选择“链接器”。
3. 在右侧面板的“附加库目录”中输入
c:\Irvine
。
4. 点击“确定”关闭“公共属性页”窗口。
这样Visual Studio就可以找到Irvine32库了。
2. 调用C库函数
C语言有一个标准化的函数集合,称为标准C库。这些函数同样适用于C++程序,因此也适用于与C和C++程序关联的汇编语言模块。汇编语言模块在调用每个C函数时必须包含其原型,通常可以通过访问C++编译器提供的帮助系统找到C函数原型,在从程序中调用这些函数之前,必须将C函数原型转换为汇编语言原型。
2.1 printf函数
printf
函数的C/C++语言原型如下,第一个参数是指向字符的指针,后面可以跟可变数量的参数:
int printf(
const char *format [, argument]...
);
汇编语言中的等效原型将
char *
改为
PTR BYTE
,并将可变长度参数列表改为
VARARG
类型:
printf PROTO C, pString:PTR BYTE, args:VARARG
2.2 scanf函数
scanf
函数用于从标准输入(键盘)输入字符、数字和字符串,并将输入值赋给变量,其原型如下:
scanf PROTO C, format:PTR BYTE, args:VARARG
2.3 使用printf函数显示格式化实数
编写格式化和显示浮点值的汇编语言函数并不容易,我们可以利用C库的
printf
函数。在Visual C++ .NET中设置这样的程序的步骤如下:
1. 在Visual C++中创建一个Win32控制台程序,创建一个名为
main.cpp
的文件,并插入一个调用
asmMain
的
main
函数:
extern "C" void asmMain( );
int main( )
{
asmMain( );
return 0;
}
-
在与
main.cpp相同的文件夹中,创建一个名为asmMain.asm的汇编语言模块,其中应包含一个使用C调用约定声明的asmMain过程:
; asmMain.asm
.386
.model flat,stdcall
.stack 2000
.code
asmMain PROC C
ret
asmMain ENDP
END
-
汇编
asmMain.asm(但不链接),生成asmMain.obj。 -
将
asmMain.obj添加到C++项目中。 -
构建并运行项目。如果修改了
asmMain.asm,则再次汇编并重新构建项目后再运行。
程序正确设置后,就可以在
asmMain.asm
中添加调用C/C++语言函数的代码。例如,以下汇编语言代码在
asmMain
中通过调用
printf
打印一个
REAL8
类型的值:
.data
double1 REAL8 1234567.890123
formatStr BYTE "%.3f",0dh,0ah,0
.code
INVOKE printf, ADDR formatStr, double1
对应的输出为:
1234567.890
传递给
printf
的格式字符串与C++中的略有不同,不能嵌入转义字符(如
\n
),而必须插入ASCII码(
0dh, 0ah
)。
2.4 多个参数
printf
函数可以接受可变数量的参数,因此可以在一次函数调用中轻松格式化和显示两个数字:
TAB = 9
.data
formatTwo BYTE "%.2f",TAB,"%.3f",0dh,0ah,0
val1 REAL8 456.789
val2 REAL8 864.231
.code
INVOKE printf, ADDR formatTwo, val1, val2
对应的输出为:
456.79 864.231
2.5 使用scanf函数输入实数
可以调用
scanf
从用户输入中获取浮点值,示例代码如下:
.data
strSingle BYTE "%f",0
strDouble BYTE "%lf",0
single1 REAL4 ?
double1 REAL8 ?
.code
INVOKE scanf, ADDR strSingle, ADDR single1
INVOKE scanf, ADDR strDouble, ADDR double1
必须从C或C++启动程序中调用汇编语言代码。传递给
printf
的浮点参数应声明为
REAL8
类型,虽然也可以传递
REAL4
类型的值,但需要一些巧妙的编程技巧。
3. 目录列表程序
我们将编写一个简短的程序,该程序会清屏、显示当前磁盘目录,并要求用户输入一个文件名(你可能希望扩展此程序以打开并显示所选文件)。
3.1 C++存根模块
C++模块只包含一个对
asm_main
的调用,因此可以称其为存根模块:
// main.cpp
// stub module: launches assembly language program
extern "C" void asm_main();
// asm startup proc
void main()
{
asm_main();
}
3.2 汇编语言模块
汇编语言模块包含函数原型、几个字符串和一个
fileName
变量。它两次调用
system
函数,分别传递“cls”和“dir”命令,然后调用
printf
显示文件名提示,调用
scanf
让用户输入文件名。由于它不调用Irvine32库,因此可以将
.MODEL
指令设置为C语言约定:
; ASM program launched from C++ (asmMain.asm)
.586
.MODEL flat,C
; Standard C library functions:
system PROTO, pCommand:PTR BYTE
printf PROTO, pString:PTR BYTE, args:VARARG
scanf PROTO, pFormat:PTR BYTE,pBuffer:PTR BYTE, args:VARARG
fopen PROTO, mode:PTR BYTE, filename:PTR BYTE
fclose PROTO, pFile:DWORD
BUFFER_SIZE = 5000
.data
str1 BYTE "cls",0
str2 BYTE "dir/w",0
str3 BYTE "Enter the name of a file:",0
str4 BYTE "%s",0
str5 BYTE "cannot open file",0dh,0ah,0
str6 BYTE "The file has been opened",0dh,0ah,0
modeStr BYTE "r",0
fileName BYTE 60 DUP(0)
pBuf DWORD ?
pFile DWORD ?
.code
asm_main PROC
; clear the screen, display disk directory
INVOKE system,ADDR str1
INVOKE system,ADDR str2
; ask for a filename
INVOKE printf,ADDR str3
INVOKE scanf, ADDR str4, ADDR filename
; try to open the file
INVOKE fopen, ADDR fileName, ADDR modeStr
mov
pFile,eax
.IF eax == 0
; cannot open file?
INVOKE printf,ADDR str5
jmp quit
.ELSE
INVOKE printf,ADDR str6
.ENDIF
; Close the file
INVOKE fclose, pFile
quit:
ret
; return to C++ main
asm_main ENDP
END
scanf
函数需要两个参数:第一个是指向格式字符串(“%s”)的指针,第二个是指向输入字符串变量(
fileName
)的指针。
以下是相关操作的流程图:
graph TD;
A[开始] --> B[清屏并显示磁盘目录];
B --> C[提示输入文件名];
C --> D[获取用户输入的文件名];
D --> E[尝试打开文件];
E --> F{文件是否打开成功};
F -- 是 --> G[显示文件已打开信息];
F -- 否 --> H[显示无法打开文件信息];
G --> I[关闭文件];
H --> I;
I --> J[结束];
4. 相关总结与思考
通过上述内容,我们了解了如何将32位汇编语言代码与C/C++程序进行链接,以及如何调用C库函数。在实际编程中,需要注意以下几点:
- 当从一种语言的程序调用汇编语言过程时,两种语言共享的标识符必须兼容,并且过程中使用的段名必须与调用程序兼容。
- 编写过程时,需要使用高级语言的调用约定来确定如何接收参数,调用约定会影响栈指针是由被调用过程还是调用程序恢复。
- 在Visual C++中,
__asm
指令用于在C++源程序中编写内联汇编代码。
- 调用标准C(C++)库函数时,需要创建一个包含
main()
函数的存根程序,从
main()
调用汇编语言模块中的启动过程,汇编语言模块可以调用C标准库中的任何函数。
以下是一些复习问题和编程练习,帮助我们进一步巩固所学知识:
4.1 复习问题
- 当高级语言程序调用汇编语言编写的过程时,调用程序和过程是否必须使用相同的内存模型?
- 为什么在从C和C++程序调用汇编语言过程时,大小写敏感性很重要?
- 语言的调用约定是否包括过程对某些寄存器的保存?
-
(是/否):
EVEN和ALIGN指令是否都可以在内联汇编代码中使用? -
(是/否):
OFFSET运算符是否可以在内联汇编代码中使用? -
(是/否):是否可以在内联汇编代码中使用
DW和DUP运算符定义变量? -
使用
__fastcall调用约定时,如果内联汇编代码修改了寄存器,可能会发生什么? -
除了使用
OFFSET运算符,还有其他方法将变量的偏移量移动到索引寄存器中吗? -
将
LENGTH运算符应用于32位整数数组时返回什么值? -
将
SIZE运算符应用于长整数数组时返回什么值? -
标准C
printf()函数的有效汇编语言PROTO声明是什么? -
调用以下C语言函数时,参数
x是先压入栈还是最后压入栈?
void MySub( x, y, z );
-
在从C++调用的过程的
extern声明中,“C”指定符的作用是什么? - 为什么在从C++调用外部汇编语言过程时,名称修饰很重要?
- 通过互联网搜索,列出一些C/C++编译器使用的优化技巧。
4.2 编程练习
- 数组与整数相乘 :编写一个汇编语言子程序,将一个双字数组乘以一个整数。编写一个C/C++测试程序,创建一个数组,将其传递给子程序,并打印结果数组的值。
-
最长递增序列
:编写一个汇编语言子程序,接收两个输入参数:数组的偏移量和数组的大小。它必须返回整数序列中最长递增序列的长度。例如,在数组
[ -5, 10, 20, 14, 17, 26, 42, 22, 19, -5 ]中,最长严格递增序列从索引3开始,长度为4({ 14, 17, 26, 42 })。从C/C++程序中调用该子程序,创建数组,传递参数,并打印子程序返回的值。 - 三个数组求和 :编写一个汇编语言子程序,接收三个等长数组的偏移量,将第二个和第三个数组的值加到第一个数组中。返回时,第一个数组将包含所有新值。编写一个C/C++测试程序,创建数组,将其传递给子程序,并打印第一个数组的内容。
-
素数程序
:编写一个汇编语言过程,如果传递到
EAX寄存器中的32位整数是素数,则返回1;如果不是素数,则返回0。从高级语言程序中调用该过程,让用户输入一系列整数,并让程序为每个整数显示是否为素数的消息。建议在第一次调用该过程时使用埃拉托斯特尼筛法初始化一个布尔数组。 -
LastIndexOf过程
:修改
IndexOf过程,将函数命名为LastIndexOf,让它从数组末尾向后搜索。返回第一个匹配值的索引,如果未找到匹配项,则返回 -1。
通过这些复习问题和编程练习,我们可以更好地掌握32位汇编语言与C/C++的链接以及相关编程技巧,进一步提升编程能力。
以上就是关于32位汇编语言与C/C++链接及相关编程实践的详细介绍,希望对大家有所帮助。在实际应用中,我们可以根据具体需求灵活运用这些知识,实现更高效、更复杂的程序。
32位汇编语言与C/C++的链接及相关编程实践
5. 16位MS - DOS编程基础
在早期的计算机系统中,16位MS - DOS编程有着重要的地位。下面将介绍MS - DOS和IBM - PC相关的一些基础知识。
5.1 MS - DOS和IBM - PC概述
IBM的PC - DOS是第一个在使用Intel 8088处理器的IBM个人计算机上实现实地址模式的操作系统,后来发展成了Microsoft MS - DOS。实地址模式也被称为16位模式,因为地址是由16位值构成的。为了确保与相关程序的完全兼容,建议安装早期版本的Windows,如Windows 98,也可以使用软件工具在计算机上创建虚拟机进行实验。
5.2 内存组织
在MS - DOS环境下,内存组织有其特定的方式。但具体的内存组织细节这里暂不详细展开,后续可以根据实际需求进一步深入研究。
5.3 重定向输入 - 输出
重定向输入 - 输出是MS - DOS编程中的一个重要功能。它允许程序将输入从标准输入(通常是键盘)重定向到文件,或者将输出从标准输出(通常是屏幕)重定向到文件。例如,在命令行中可以使用
>
符号将程序的输出重定向到一个文件,使用
<
符号将文件内容作为程序的输入。
5.4 软件中断
软件中断是MS - DOS编程中实现系统功能调用的重要方式。通过执行特定的中断指令,程序可以请求操作系统提供各种服务,如输入输出、文件操作等。在MS - DOS中,常用的中断是
INT 21h
,它提供了丰富的功能调用。
5.5 INT指令
INT
指令用于触发软件中断。例如,
INT 21h
就是调用MS - DOS的功能服务。在汇编语言中,可以使用
INT
指令来调用这些服务,实现各种操作。
5.6 16位程序编码
编写16位MS - DOS程序时,需要注意一些特殊的编码规则。例如,地址的表示方式、寄存器的使用等都与32位编程有所不同。以下是一个简单的16位汇编程序示例:
.MODEL SMALL
.STACK 100H
.DATA
msg DB 'Hello, MS - DOS!', 0DH, 0AH, '$'
.CODE
MAIN PROC
MOV AX, @DATA
MOV DS, AX
LEA DX, msg
MOV AH, 09H
INT 21H
MOV AH, 4CH
INT 21H
MAIN ENDP
END MAIN
这个程序的功能是在屏幕上显示
Hello, MS - DOS!
,然后退出程序。
6. MS - DOS函数调用(INT 21h)
INT 21h
是MS - DOS中非常重要的一个中断,它提供了众多的功能调用,涵盖了输出、输入、日期时间等多个方面。
6.1 部分输出函数
-
显示字符
:使用
AH = 02H功能调用可以在屏幕上显示一个字符。例如:
MOV DL, 'A'
MOV AH, 02H
INT 21H
这段代码会在屏幕上显示字符
A
。
-
显示字符串
:使用
AH = 09H
功能调用可以显示一个以
$
结尾的字符串。例如前面示例中的显示
Hello, MS - DOS!
的代码。
6.2 “Hello World”程序示例
以下是一个完整的“Hello World”程序示例,使用
INT 21h
的功能调用:
.MODEL SMALL
.STACK 100H
.DATA
hello_msg DB 'Hello World!', 0DH, 0AH, '$'
.CODE
MAIN PROC
MOV AX, @DATA
MOV DS, AX
LEA DX, hello_msg
MOV AH, 09H
INT 21H
MOV AH, 4CH
INT 21H
MAIN ENDP
END MAIN
这个程序的执行流程如下:
graph TD;
A[开始] --> B[初始化数据段寄存器];
B --> C[加载字符串地址到DX];
C --> D[设置功能号AH为09H];
D --> E[调用INT 21h显示字符串];
E --> F[设置功能号AH为4CH];
F --> G[调用INT 21h退出程序];
G --> H[结束];
6.3 部分输入函数
-
读取字符
:使用
AH = 01H功能调用可以从键盘读取一个字符。例如:
MOV AH, 01H
INT 21H
MOV BL, AL
这段代码会从键盘读取一个字符,并将其存储在
BL
寄存器中。
-
读取字符串
:使用
AH = 0AH
功能调用可以从键盘读取一个字符串。需要预先定义一个缓冲区来存储输入的字符串。
6.4 日期/时间函数
INT 21h
还提供了一些日期和时间相关的功能调用。例如,
AH = 2AH
可以获取当前日期,
AH = 2CH
可以获取当前时间。以下是获取当前日期的示例代码:
MOV AH, 2AH
INT 21H
MOV AL, DH ; 年的高8位
MOV BL, DL ; 年的低8位
MOV CL, CH ; 月
MOV DL, DH ; 日
6.5 相关总结
通过
INT 21h
的各种功能调用,我们可以实现丰富的输入输出、日期时间等操作。在使用这些功能调用时,需要注意每个功能调用的参数设置和返回值的处理。
7. 标准MS - DOS文件I/O服务
在MS - DOS环境下,文件的输入输出操作也是非常重要的。下面介绍一些标准的MS - DOS文件I/O服务。
7.1 创建或打开文件(716Ch)
使用特定的功能调用可以创建或打开一个文件。在汇编语言中,需要设置相应的参数并调用中断来完成操作。例如:
MOV AH, 716Ch
; 设置其他参数
INT 21H
具体的参数设置需要根据实际需求进行调整。
7.2 关闭文件句柄(3Eh)
当文件使用完毕后,需要关闭文件句柄以释放系统资源。可以使用
AH = 3Eh
功能调用来关闭文件句柄。示例代码如下:
MOV AH, 3Eh
; 设置文件句柄
INT 21H
7.3 移动文件指针(42h)
在文件操作中,有时需要移动文件指针到指定的位置。可以使用
AH = 42h
功能调用来实现。例如:
MOV AH, 42h
; 设置移动方式和偏移量
INT 21H
7.4 获取文件创建日期和时间
可以通过特定的功能调用获取文件的创建日期和时间。具体的功能调用和参数设置需要根据实际情况进行调整。
7.5 示例:读取和复制文本文件
以下是一个读取和复制文本文件的示例代码:
.MODEL SMALL
.STACK 100H
.DATA
input_file DB 'input.txt', 0
output_file DB 'output.txt', 0
buffer DB 100 DUP(?)
handle_in DW ?
handle_out DW ?
.CODE
MAIN PROC
MOV AX, @DATA
MOV DS, AX
; 打开输入文件
MOV AH, 3Dh
MOV AL, 0 ; 只读模式
LEA DX, input_file
INT 21H
MOV handle_in, AX
; 打开输出文件
MOV AH, 3Dh
MOV AL, 1 ; 只写模式
LEA DX, output_file
INT 21H
MOV handle_out, AX
; 读取文件内容并复制
READ_LOOP:
MOV AH, 3Fh
MOV BX, handle_in
MOV CX, 100
LEA DX, buffer
INT 21H
CMP AX, 0
JE END_READ
MOV AH, 40h
MOV BX, handle_out
MOV CX, AX
LEA DX, buffer
INT 21H
JMP READ_LOOP
END_READ:
; 关闭文件
MOV AH, 3Eh
MOV BX, handle_in
INT 21H
MOV AH, 3Eh
MOV BX, handle_out
INT 21H
MOV AH, 4CH
INT 21H
MAIN ENDP
END MAIN
这个程序的执行流程如下:
graph TD;
A[开始] --> B[初始化数据段寄存器];
B --> C[打开输入文件];
C --> D[打开输出文件];
D --> E[进入读取循环];
E --> F{是否读取到文件末尾};
F -- 否 --> G[读取文件内容];
G --> H[将读取内容写入输出文件];
H --> E;
F -- 是 --> I[关闭输入文件];
I --> J[关闭输出文件];
J --> K[退出程序];
K --> L[结束];
7.6 读取MS - DOS命令尾
在MS - DOS中,可以读取命令行输入的参数。通过特定的功能调用,可以获取命令尾的信息,从而实现根据不同的命令行参数执行不同的操作。
7.7 示例:创建二进制文件
以下是一个创建二进制文件的示例代码:
.MODEL SMALL
.STACK 100H
.DATA
binary_file DB 'binary.bin', 0
data DB 1, 2, 3, 4, 5
handle DW ?
.CODE
MAIN PROC
MOV AX, @DATA
MOV DS, AX
; 打开二进制文件
MOV AH, 3Dh
MOV AL, 2 ; 创建并只写模式
LEA DX, binary_file
INT 21H
MOV handle, AX
; 写入数据
MOV AH, 40h
MOV BX, handle
MOV CX, 5
LEA DX, data
INT 21H
; 关闭文件
MOV AH, 3Eh
MOV BX, handle
INT 21H
MOV AH, 4CH
INT 21H
MAIN ENDP
END MAIN
8. 总结与展望
通过对32位汇编语言与C/C++的链接以及16位MS - DOS编程的学习,我们掌握了多种编程技巧和方法。在32位编程中,学会了如何将汇编语言代码与C/C++程序结合,调用C库函数实现复杂的功能。在16位MS - DOS编程中,了解了MS - DOS的基本原理、内存组织、中断调用和文件I/O操作等知识。
在实际应用中,我们可以根据具体的需求选择合适的编程方式。例如,对于性能要求极高的部分可以使用汇编语言进行优化,而对于界面和高级功能的实现可以使用C/C++。未来,随着计算机技术的不断发展,虽然16位MS - DOS编程的应用场景逐渐减少,但其中的一些原理和方法仍然具有重要的参考价值。同时,32位汇编语言与C/C++的结合编程在嵌入式系统、驱动开发等领域仍然有着广泛的应用前景。
以下是一个简单的对比表格,展示32位编程和16位MS - DOS编程的一些特点:
| 编程类型 | 地址模式 | 功能调用方式 | 应用场景 |
| ---- | ---- | ---- | ---- |
| 32位编程 | 32位地址 | 调用C库函数、使用高级语言调用约定 | 现代操作系统下的应用程序开发、性能优化 |
| 16位MS - DOS编程 | 16位地址 | 软件中断(如INT 21h) | 早期计算机系统、嵌入式系统开发 |
无论是32位编程还是16位MS - DOS编程,都需要不断地实践和学习,才能更好地掌握相关知识和技能,实现更高效、更复杂的程序。
超级会员免费看
15万+

被折叠的 条评论
为什么被折叠?



