在网上找了下vc内联汇编相关的知识。发现主要就那么一篇文章。看着也是不太懂。而且有几处在vc里实验并不是那么回事。自己研究了一番,在原有基础上改了些内容。能达到更容易理解的目的。跟大家分享一下。同时感谢原创作者。(因为这篇文章我是在你的起初上改的)
在 Visual C++ 中使用内联汇编
一、内联汇编的优缺点
使用内联汇编可以在 C/C++ 代码中嵌入汇编语言指令,而且不需要额外的汇编和连接步骤。在 Visual C++ 中,内联汇编是内置的编译器,因此不需要配置诸如 MASM 一类的独立汇编工具。内联汇编代码可以使用 C/C++ 变量和函数,因此它能非常容易地整合到 C/C++ 代码中。它能做一些对于单独使用 C/C++ 来说非常笨重或不可能完成的任务。所以非常方便。内联汇编主要用于如下场合:
1.使用汇编语言写函数;
2.对速度要求非常高的代码;
3.设备驱动程序中直接访问硬件;
4.编写"Naked" Call的初始化和结束代码。
//(."Naked",大概意思就是不需要C/C++的编译器(自作聪明)生成的函数初始化和收尾代码,请参看MSDN的"Naked Functions"的说明)
内联汇编代码不易于移植,如果你的程序打算在不同类型的机器上运行,应当尽量避免使用内联汇编。这时候你可以使用MASM,因为MASM支持更方便的的宏指令和数据指示符。
(**注意**:MASM是微软公司开发的汇编开发环境,是一堆伪指令的合集,而内联汇编用的是纯汇编,尽量不要用伪指令)
二、内联汇编关键字
在Visual C++使用内联汇编用到的是__asm关键字,这个关键字有两种使用方法:
1.简单__asm块
__asm
{
MOV AL, 2
MOV DX, 0xD007
OUT AL, DX
}
2.在每条汇编指令之前加__asm关键字
__asm MOV AL, 2
__asm MOV DX, 0xD007
__asm OUT AL, DX
因为__asm关键字是语句分隔符,因此你可以把汇编指令放在同一行:
__asm MOV AL, 2 __asm MOV DX, 0XD007 __asm OUT AL, DX
显然,第一种方法和C/C++的风格很一致,并且有很多其它优点,因此推荐使用第一种方法。
不象在C/C++中的"{}",__asm块的"{}"不会影响C/C++变量的作用范围。同时,__asm块可以嵌套,嵌套也不会影响变量的作用范围。
为了与低版本的 Visual C++ 兼容,_asm 和 __asm 具有相同的意义。
三、在__asm块中使用汇编语言
1.内联汇编指令集
内联汇编完全支持的Intel x86指令集,允许使用MMX指令(浮点运算相关的指令)。不支持的指令可以使用_EMIT伪指令定义(_EMIT伪指令说明见下文)。 (相当于直接用_EMIT 可以写机器码)
2.MASM表达式
内联汇编可以使用MASM中的表达式(MASM 表达式是指用来计算一个数值或一个地址的操作符和操作数的组合)。比如: MOV EAX, 1。
3.数据指示符和操作符
虽然__asm块中允许使用C/C++的数据类型和对象,但它不能用MASM指示符和操作符定义数据对象。这里特别指出,__asm块中不允许MASM中的定义指示符: DB、DW、DD、DQ、DT和DF,也不允许DUP和THIS操作符。MASM结构和记录也不再有效,内联汇编不接受STRUC、RECORD、WIDTH或者MASK。
4.EVEN和ALIGN指示符
尽管内联汇编不支持大多数MASM指示符,但它支持EVEN和ALIGN,当需要的时候,这些指示符在汇编代码里面加入NOP(空操作)指令使标号对齐到特定边界。这样可以使某些处理器取指令时具有更高的效率。
5.MASM宏指示符
内联汇编不是宏汇编,不能使用MASM宏指示符(MACRO、REPT、IRC、IRP和ENDM)和宏操作符(<>、!、&、%和.TYPE)。
6.段说明
必须使用寄存器来说明段,跨越段必须显式地说明,如ES:[BX]。
7.注释
可以使用C/C++的注释。 也可以使用汇编语言的注释,即“;”
8._EMIT伪指令
_EMIT伪指令相当于MASM中的DB,但一次只能在当前代码段(.text 段)中定义一个字节,比如:
__asm
{
JMP _CodeOfAsm
_EMIT 0x00 //定义混合在代码段的数据
_EMIT 0x01
_CodeOfAsm:
//这里是代码
_EMIT 0x90 ; NOP指令
}
9寄存器使用
一般来说,不能假定某个寄存器在 __asm 块开始的时候有已知的值。寄存器的值将不能保证会从 __asm 块保留到另外一个 __asm 块中。
如果一个函数声明为 __fastcall 调用方式,则其参数将通过寄存器而不是堆栈来传递。这将会使 __asm 块产生问题,因为函数无法被告知哪个参数在哪个寄存器中。如果函数接收了 EAX 中的参数并立即储存一个值到 EAX 中的话,原来的参数将丢失掉。另外,在所有声明为 __fastcall 的函数中,ECX 寄存器是必须一直保留的。为了避免以上的冲突,包含 __asm 块的函数不要声明为 __fastcall 调用方式。
* 提示:如果使用 EAX、EBX、ECX、EDX、ESI 和 EDI 寄存器,你不需要保存它。但如果你用到了 DS、SS、SP、BP 和标志寄存器,那就应该用 PUSH 保存这些寄存器。
* 提示:如果程序中改变了用于 STD 和 CLD 的方向标志,必须将其恢复到原来的值。
下面来解释下 _fastcall调用方式:
__fastcall :属于一种调用约定,是一种快速调用方式。 规定将前两个(或若干个)参数由寄存器传递,其余参数还是通过堆栈传递(从右到左)。 不同编译器编译的程序规定的寄存器不同。在Intel 386平台上,使用ECX和EDX寄存器。 所以如果使用__fastcall方式将无法用作跨编译器的接口。
四、在__asm块中使用C/C++语言元素
1. 可用的 C/C++ 元素
C/C++与汇编可以混合使用,在内联汇编可以使用C/C++的变量和很多其它C/C++的元素。在__asm块中可以使用以下C/C++元素:
符号,包括标号、变量和函数名;
常量,包括符号常量和枚举型(enum)成员;
宏定义和预处理指示符;
注释,包括"/**/"和"//";
类型名,包括所有MASM中合法的类型
typedef名称, 像PTR、TYPE、特定的构成员或枚举成员这样的通用操作符。
可以使用C/C++或ASM的基数计数法(比如: 0x100和100H是相等的)。
2. 操作符使用
__asm块中不能使用像<<一类的C/C++操作符。C/C++和MASM通用的操作符,比如"*"和"[]"操作符,都被认为是汇编语言的操作符。举个例子:
int array[10];
__asm MOV array[6], BX //array+6(byte) = EBX (错误的)
array[6] = 0;
* 小技巧: 内联汇编中,你可以使用TYPE操作符使作其与C一致。比如,下面两条语句是一样的:
__asm MOV array[6 * TYPE int ], 0 //array[6] = EBX (正确的)
array[6] = 0;
3. C/C++ 符号使用
在 __asm 块中可以引用所有在作用范围内的 C/C++ 符号,包括变量名称、函数名称和标号。但是不能访问 C++ 类的成员函数。
下面是在内联汇编中使用 C/C++ 符号的一些限制:
* 每条汇编语句只能包含一个 C/C++ 符号。
* 在 __asm 块中引用函数必须先声明。否则,编译器将不能区别 __asm 块中的函数名和标号。
* 在 __asm 块中不能使用对于 MASM 来说是保留字的 C/C++ 符号(不区分大小写)。MASM 保留字包含指令名称(如 PUSH是指令名称,在c/c++里可以当成变量,但不能在asm块中当变量)和寄存器名称(如 ESI)等。
* 在 __asm 块中不能识别结构和联合标签。由于内联汇编中用的是纯汇编,所以没有struct概念。对于struct的表达尽量用指针偏移来表示。例如
struct first_type
{
char *weasel;
int same_name;
};
struct second_type
{
int wonton;
long same_name;
};
如果按下面声明变量:
struct first_type hal;
struct second_type oat;
那么,下面的实例:
__asm
{
MOV EBX, OFFSET hal (错误)
Mov EBX,dowrd ptr [hal] (正确)//hal的地址,转换为dword指针
MOV ECX, [EBX]hal.same_name ; 取 same_name(不提倡)也可以 MOV ECX, [EBX+hal.same_name]
MOV ESI, [EBX].weasel ; 取 weasel (不提倡)
MOV ESI, dword ptr [EBX+0] ;取 same_name (提倡)
MOV ESI, dword ptr [EBX+4] ; 取 weasel (提倡)
}
可以不受限制地访问C++成员变量,但是不能调用C++的成员函数。
* 内联汇编能通过变量名直接引用C/C++的变量。__asm块中可以引用任何符号,包括变量名。例如:
int iVar = 0;
__asm MOV EAX, iVar
4 用内联汇编写函数
C/C++ 函数一般用堆栈来传递参数,所以上面的函数中需要通过堆栈位置来访问它的参数(在 MASM 或其它一些汇编工具中,也允许通过名称来访问堆栈参数和局部堆栈变量)。
下面的程序是使用内联汇编写的:
// PowerC.c
#include <Stdio.h>
int GetPowerC(int iNum, int iPower);
int main()
{
printf("3 times 2 to the power of 5 is %d\n", GetPowerC( 3, 5));
}
int GetPowerC(int iNum, int iPower)
{
__asm
{
MOV EAX, iNum ; Get first argument
MOV ECX, iPower ; Get second argument
SHL EAX, CL ; EAX = EAX * (2 to the power of CL)
}
// 默认的函数返回值都是放到 EAX中
}
使用内联汇编写的 GetPowerC 函数可以通过参数名称来引用它的参数。由于 GetPowerC 函数没有执行 C 的 return 语句,所以编译器会给出一个警告信息,我们可以通过#pragma warning 禁止生成这个警告。
该函数没有使用naked call。用的是默认的调用约定__cdecl。
__cdecl是C和C++程序的默认调用约定:参数通过堆栈来传递,从右向左依次入栈,由调用者平衡堆栈。
可以将 int GetPowerC(int iNum, int iPower),改成: int __cdecl GetPowerC(int iNum, int iPower)。加个断点,F5运行断下后,按“ALT+F8”打开反汇编窗口,没有任何变化,和没加__cdecl一样,说明默认调用约定就是__cdecl
类似的调用约定还有:
__stdcall(和__cdecl 的区别是函数自己来平衡堆栈,内部多了个ret指令)
__fastcall的调用约定是:第一个参数通过ECX传递,第二个参数通过EDX传递,第三个参数起从右向左依次入栈,由被调用者平衡堆栈
这里重点说一下c++:
类成员函数的默认调用约定是:参数通过堆栈来传递,从右向左依次入栈,由被调用者平衡堆栈栈,this指针通过ECX传递。除了this指针,其他都和__stdcall相同。
所以我们只要看到call调用前,某个地址传递给了ECX,就可以知道十有八九调用的是一个类成员函数。
值得注意的是VC编译器默认使用ECX传递this指针,但是Borland C++编译器却是用EAX,不同的编译器处理的方式不一样。
内联汇编的其中一个用途是编写 naked 函数的初始化和结束代码。对于一般的函数,
编译器会自动帮我们生成函数的初始化(构建参数指针和分配局部变量等)和结束代码(
平衡堆栈和返回一个值等)。使用内联汇编,我们可以自己编写干干净净的函数。当然,
此时我们必须自己动手做一些有关函数初始化和扫尾的工作。例如:
void __declspec(naked) MyNakedFunction()
{
// Naked functions must provide their own prolog.
__asm
{
PUSH EBP
MOV ESP, EBP
SUB ESP, __LOCAL_SIZE
}
.
.
.
// And we must provide epilog.
__asm
{
POP EBP
RET
}
}
5定义 __asm 块为 C/C++ 宏
使用 C/C++ 宏可以方便地把汇编代码插入到源代码中。但是这其中需要额外地注意
,因为宏将会扩展到一个逻辑行中。
为了不会出现问题,请按以下规则编写宏:
* 使用括号把 __asm 块包围住;
* 把 __asm 关键字放在每条汇编指令之前;
* 使用经典 C 风格的注释(“/* comment */”),不要使用汇编风格的注释(“; c
omment”)或单行的 C/C++ 注释(“// comment”);
举个例子,下面定义了一个简单的宏:
#define PORTIO __asm \
/* Port output */ \
{ \
__asm MOV AL, 2 \
__asm MOV DX, 0xD007 \
__asm OUT DX, AL \
}
乍一看来,后面的三个 __asm 关键字好像是多余的。其实它们是需要的,因为宏将被
扩展到一个单行中:
__asm /* Port output */ {__asm MOV AL, 2 __asm MOV DX, 0xD007 __asm
OUT DX, AL}
从扩展后的代码中可以看出,第三个和第四个 __asm 关键字是必须的(作为语句分隔
符)。在 __asm 块中,只有 __asm 关键字和换行符会被认为是语句分隔符,又因为定义
为宏的一个语句块会被认为是一个逻辑行,所以必须在每条指令之前使用 __asm 关键字。
括号也是需要的,如果省略了它,编译器将不知道汇编代码在哪里结束,__asm 块后
面的 C/C++ 语句看起来会被认为是汇编指令。
同样是由于宏展开的原因,汇编风格的注释(“; comment”)和单行的 C/C++ 注释
(“// commen”)也可能会出现错误。为了避免这些错误,在定义 __asm 块为宏时请使
用经典 C 风格的注释(“/* comment */”)。
和 C/C++ 宏一样 __asm 块写的宏也可以拥有参数。和 C/C++ 宏不一样的是,__asm
宏不能返回一个值,因此,不能使用这种宏作为 C/C++ 表达式。
不要不加选择地调用这种类型的宏。比如,在声明为 __fastcall 的函数中调用汇编
语言宏可能会导致不可预料的结果
五、转跳
可以在C里面使用goto调到__asm块中的标号处,也可以在__asm块中转跳到__asm块里面和外面的标号处。__asm块内的标号是不区分大小写的(指令、指示符等也是不区分大小写的)。例:
void func()
{
goto C_Dest; /* 合法 */
goto c_dest; /* 错误 */
goto A_Dest; /* 合法 */
goto a_dest; /* 合法 */
__asm
{
JMP C_Dest ; 合法
JMP c_dest ; /* 错误 */
JMP A_Dest ; 合法
JMP a_dest ; 合法
a_dest: ; __asm 标号
}
C_Dest: /* C的标号 */
return;
}
不要使用函数名称当作标号,否则将使其跳到函数执行而不是标号处。如下所示:
; 错误: 使用函数名作为标号
JNE exit
.
.
.
exit:
; 下面是更多的ASM代码
美元符号$用于指定当前位置,如下所用,常用于条件跳转:
JNE $+5 ; 下面这条指令的长度是5个字节
JMP farlabel
;$+5,跳到了这里
.
.
.
farlabel:
六、内联汇编例子,调用Windows函数
内联汇编调用C/C++函数必须自己清除堆栈,下面是一个调用C/C++函数例子:
#include <stdio.h>
char szformat[] = "%s %s\n";
char szHello[] = "Hello";
char szWorld[] = " world";
void main()
{
__asm
{
MOV EAX, OFFSET szWorld
PUSH EAX
MOV EAX, OFFSET szHello
PUSH EAX
MOV EAX, OFFSET szformat
PUSH EAX
CALL printf
//内联汇编调用C函数必须自己清除堆栈
//用不使用的EBX寄存器清除堆栈,或ADD ESP, 12
POP EBX
POP EBX
POP EBX
}
}
注意:函数参数是从右向左压栈。
不能够访问C++中的类成员函数,但是可以访问extern "C"函数。
如果调用Windows API函数,则不需要自己清除堆栈,因为API的返回指令是RET n,会自动清除堆栈
比如下面的例子:
#include <windows.h>
char szAppName[] = "API Test";
void main()
{
char szHello[] = "Hello, world!";
__asm
{
PUSH MB_OK
PUSH OFFSET szAppName ; 全局变量用OFFSET
LEA EAX, szHello ; 局部变量用LEA
PUSH EAX
PUSH 0
CALL DWORD PTR [MessageBoxA] ; 注意这里,不是CALL MessageBoxA
}
}
一般来说,在Visual C++中使用内联汇编是为了提高速度,因此这些函数调用尽可能用C/C++写。
七、在 Visual C++ 工程中使用独立汇编
内联汇编代码不易于移植,如果你的程序打算在不同类型的机器(比如 x86 和 Alpha)上运行,你可能需要在不同的模块中使用特定的机器代码。这时候你可以使用 MASM(Microsoft Macro Assembler),因为 MASM 支持更多方便的宏指令和数据指示符。
这里简单介绍一下在 Visual Studio 中调用 MASM 编译独立汇编文件的步骤。
在 Visual C++ 工程中,添加按 MASM 的要求编写的 .asm 文件。在解决方案资源管理器中,右击这个文件,选择“属性”菜单项,在属性对话框中,点击“自定义生成步骤”,设置如下项目:
命令行:ML.exe /nologo /c /coff "-Fo$(IntDir)\$(InputName).obj" "$(InputPath)"
输出:$(IntDir)\$(InputName).obj
如果要生成调试信息,可以在命令行中加入“/Zi”参数,还可以根据需要生成 .lst 和 .sbr 文件。
如果要在汇编文件中调用 Windows API,可以从网上下载 MASM32 包(包含了 MASM汇编工具、非常完整的 Windows API 头文件/库文件、实用宏以及大量的 Win32 汇编例子等)。相应地,应该在命令行中加入“/I X:\MASM32\INCLUDE”参数指定 Windows API 汇编头文件(.inc)的路径。MASM32 的主页是:http://www.masm32.com,里面可以下载最
新版本的 MASM32 包。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)