汇编与C互操作

来源:互联网 发布:办公必备软件 编辑:程序博客网 时间:2024/05/22 10:50
汇编与C互操作

Binhua Liu

download2代码下载

document_thumb前言

    本文介绍3种汇编和C互操作的方法:

    C语言内嵌汇编代码

    C语言调用汇编过程

    汇编调用C语言过程

    本文开始前,我们先介绍如何配置我们的编译环境:

    1)打开VS2008,创建一个VC++空工程。

    2)在Solution Explorer上右击工程名,选择Custom Build Rules…,在弹出的对话框上勾选Microsoft Macro Assembler。

    3)在Solution Explorer上右击工程名,选择属性,在弹出的对话框上选择Configuration Properties->Linker->System->SubSystem,选择Console (/SUBSYSTEM:CONSOLE)

    4)如果需要创建汇编源文件,在Solution Explorer上右击工程名->Add->New Item,在打开的对话框上选择C++ File(.cpp),命名为***.asm。

document_thumbC语言内嵌汇编代码

    在C语言中可以通过__asm伪指令插入汇编代码,语法如下

__asm 汇编指令

    或者:

__asm{汇编指令汇编指令...}

 

    我发现,VC编译器不会为在__asm段中用到的寄存器保存/恢复状态,这意味着什么呢?比如在__asm中使用了ECX寄存器,在函数前缀和后缀中很可能没有保存/恢复ECX的代码(除非函数的C代码部分用到了ECX),那么对于调用者来说,ECX寄存器的状态在调用该函数后被破坏了,所以我们最好自己在__asm段中保存/恢复用到的寄存器,看下面的例子:

int AddNumber(int number1,int number2){__asm{push ecxmov ecx,number1add ecx,number2mov eax,ecxpop ecx}}

 

document_thumbC语言调用汇编过程

    在进入正题前,我们需要先来简单讲解一下C语言的名字修饰规则。在C/C++编译时,VC编译器会把函数名根据一定规则进行修饰应用到生成的对象文件、列表文件等文件中。在链接时,链接器只知道函数的修饰名并用它进行链接。函数的修饰规则根据函数采用的调用约定不同而不同,我们介绍2中常用的调用约定的名字修饰:

    C语言的__cdecl调用约定:在函数名前加下划线”_”。

    C语言的__stdcall调用约定:在函数名前加下划线”_”,在后面加@,后面跟参数的字节数总和,如_GetStdHandle@4。

    C++的名字修饰规则和C完全不同,我们这里不再具体说明,大家可以参考MSDN的介绍。

    为什么要提到名字修饰呢?我们来考虑下面这种情形:在C中声明了采用__cdecl调用规范的外部过程AddNumber,这个外部过程是在汇编源文件中定义的。由于C语言采用了名字修饰,在生成的对象文件中AddNumber的修饰名为_AddNumber。然而,在汇编源文件中,过程没有使用名字修饰(我们也可以为汇编使用名字修饰,稍后再涉及),生成的对象文件中函数名依然为AddNumber,显然,这时候链接出错。解决的办法很简单,在汇编代码中,把函数名命名为_AddNumber即可。看下面的例子:

//main.cpp#include "iostream"using namespace std;extern "C" int __cdecl AddNumber(int number1,int number2);int main(){int i=AddNumber(1,3);cout<<i<<endl;return 0;}

 

;AddNumber.asmTITLE Add Number.386.MODEL flat.code_AddNumber PROCpush ebpmov ebp,espmov eax,[ebp+8]add eax,[ebp+12]pop ebpret_AddNumber ENDPEND

 

    在main.cpp文件中,对引用的外部汇编过程AddNumber进行声明使用了extern “C”修饰符,这是由于VC编译器默认使用C++编译器,那就就会使用C++的名字修饰规则。extern “C”告诉编译器,AddNumber是一个C函数,编译时使用C的名字修饰规则。声明中的__cdecl可以省略,因为VC编译默认就是使用__cdecl调用约定的。

    如果AddNumber采用__stdcall调用规范,则在main.cpp函数中声明为extern "C" int __stdcall AddNumber(int number1,int number2); AddNumber.asm中函数名修改为_AddNumber@8,函数返回指令修改为ret 8,代码如下:

;AddNumber.asmTITLE Add Number.386.MODEL flat.code_AddNumber@8 PROCpush ebpmov ebp,espmov eax,[ebp+8]add eax,[ebp+12]pop ebpret 8_AddNumber@8 ENDPEND

 

    事实上,汇编代码也可以使用C调用约定和名字修饰规则,在代码中把模式定义为.MODEL flat,C,或者.MODEL flat,stdcall,汇编将使用__cdecl或者__stdcall调用约定和对应的修饰名。这样,在汇编代码中,我们只需要使用和C相同的原始的函数名称即可,而不用手工在源代码中使用修饰名了,代码如下(main.cpp代码不变):

;AddNumber.asmTITLE Add Number.386.MODEL flat,C.codeAddNumber PROC,number1:SDWORD,number2:SDWORDmov eax,number1add eax,number2retAddNumber ENDPEND

  

    显然,这种方法最简单,不用去关心名字修饰规则。但是,通过了解名字修饰在其中的作用,我们可以知其然,也知其所以然。

 

document_thumb汇编调用C语言过程

    如果我们采取这样的调用方式:C主函数->汇编过程->C过程,那比较简单,我们只要处理好名字修饰的问题就可以了。本节主要讲解如何通过汇编主函数调用C过程,这种情况更复杂些,我们希望解决下面这些问题:

    1)如何能在汇编主函数中能够调用C语言的库函数?

    2)我们希望汇编主函数能够调用__stdcall和__cdelc两种调用约定的函数,那么我们只能把模式声明为.MODEL flat,而不是.MODEL flat,C,或者.MODEL flat,stdcall。

    3)还有就是C语言运行时的入口点函数的问题,我们下面再讨论。

     让我们先来看看代码:

;main.asmTITLE main .386.MODEL flat.datainput BYTE "the result is %d", 00HEXTRN_printf:PROCEXTRN   _AddNumber:PROC.code_mainPROCpush ebpmov ebp,esppush 5push 4call _AddNumberadd esp,8pushEAXpushOFFSET inputcall_printfaddesp, 8pop ebpret_mainENDPEND

 

//AddNumber.cppint AddNumber(int number1,int number2){return number1+number2;}

 

    以上代码除了前言中的配置外,还要进行以下配置才能运行:

    1)在AddNumber.cpp上右击属性,在打开的对话框上选择C/C++->Advanced->Compile as,把Compile as C++ code修改为 Compile as C code。这就使用C而不是C++的名字修饰规则来编译AddNumber.cpp。

    2)在工程上右击属性,在打开的对话框上选择Configuration Properties->Linker->Command Line,在文本框中添加:msvcrtd.lib,这样,程序链接时将链接C运行时库,就能使用C语言的库函数了。其实,这一步也可以省略,如果我们查看编译器用AddNumber.cpp生成的汇编代码,你会发现这样的语句:INCLUDELIB MSVCRTD。由于cpp源文件都会默认添加对C运行时库的引用,这就是为什么这步可以省略。但是,如果工程中没有任何引用C运行时库的源文件,而我们又希望使用C语言的库函数,那么就需要这一步配置。

    现在,我们来研究我们上面的代码。我们通过”EXTRN”来声明外部过程,如C语言库函数printf,由于名字修饰,因此这里声明为_printf。

    这里最关键的是程序的入口点问题。使用C运行时库的程序(也就是一般的C/C++程序),实际的程序入口点为mainRTSetup,然后mainRTSetup调用main函数或者wmain函数(如果是窗口程序而非控制台,则为WinMain或者wWinMain)。我们的工程由于使用了C运行时库,再考虑到名字修饰,因此主函数名必须是_main或者_wmain。在汇编工程中,我们会在源文件中用“END _main”结尾来标识_main为入口点函数,但是这个工程中_main并不是入口点函数,所以需要以END结尾。同样的道理,汇编工程中我们会以语句INVOKE ExitProcess,0来结束入口点函数,而这里_main函数则不需要。相反的,由于_main函数结束后会返回到mainRTSetup函数,因此_main应该加上必要的前缀和后缀来保存/恢复寄存器,而汇编工程的入口点函数我们不保存/恢复寄存器也没有关系。

    或许,仍然有办法在使用C运行时库的情况下直接把_main直接设置为入口点函数,但是我的尝试都失败了,就不赘述。

document_thumbTroubleShooting

    作者在写本文时也遇到了许多问题,或许大家还会遇到一些其他问题,如果你的汇编源文件不能通过编译或链接,尝试这个办法:首先,写一个C语言版的源文件来替代汇编源文件,例如先用AddNumber.cpp取代AddNumber.asm,然后确保整个工程能够编译并运行。再右击AddNumber.cpp,打开属性对话框,选择C/C++->Output Files->Assembler Output,选择Assembly With Source Code (/FAs)。编译AddNumber.cpp,然后在AddNumber.cpp目录的debug子目录中你可以发现一个叫做AddNumber.asm的文件,这是编译器为AddNumber.cpp生成的汇编源文件,可以参考这个文件对你的汇编代码进行排错。

 

document_thumb声明

  本文为Binhua Liu原创作品。本文允许复制,修改,传递,但不允许用于商业用途。转载请注明出处。本文发表于2010年10月6日。

0 0
原创粉丝点击