首页
社区
课程
招聘
[旧帖] [原创]在 Visual C++ 中使用内联汇编 0.00雪花
发表于: 2012-10-19 11:15 3492

[旧帖] [原创]在 Visual C++ 中使用内联汇编 0.00雪花

2012-10-19 11:15
3492
在网上找了下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期)

收藏
免费 0
支持
分享
最新回复 (3)
雪    币: 120
活跃值: (18)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
全面,好,学习
2012-10-19 14:27
0
雪    币: 207
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
为什么我一个星星都没有?
2012-10-19 17:04
0
雪    币: 37
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
学习了,正好需要
2012-10-19 17:57
0
游客
登录 | 注册 方可回帖
返回
//