VS写汇编程序002:用汇编语言写函数

       汇编语言在运行效率上有优势,通过精心设计的汇编程序,其执行效率会比C语言高,但是程序难写难调试。使用汇编程序编写大型程序很具有挑战性,不太可能全部使用汇编。为兼顾开发成本和程序执行效率,C语言和汇编混合编程为上好的选择,即在重视性能的模块使用汇编,在其他部分使用C语言。那么能不能用汇编语言编写函数让C语言调用呢?答案是肯定能!

      在VS上配置好汇编语言开发环境的基础上,下面就一个简单的例子来探讨一下如何在VS中使用汇编语言编写函数,并将函数编译为obj或lib文件。

一、问题描述

         使用汇编语言编写一个Add函数,来实现两个整数的相加,并将函数编译为输出为lib文件。该lib文件可以被汇编、C语言和C++引用。Add函数的C语言定义如下:

int Add(int x,int y)
{
    return x+y;
}

二、汇编语言编写函数要注意的问题

(1)函数名称

         函数输出到lib文件时,其名称并非原来的样子,而是会增加一些修饰符,比如前下划线,后缀等。函数名称修饰符跟程序语言、和调用约定有关。(这里的程序语言指的是C语言和C++,调用约定指的是cdecl和stdcall)。汇编语言写的函数必须按照有关规定将函数名称进行修饰,才能被C/C++调用。下面按照调用约定给Add函数命名。

cdecl调用约定:这是C语言的函数调用约定,函数名会被下划线修饰:_Add

stdcall调用约定:这个约定有两种风格,如果是C语言的stdcall约定,那么函数名前要加上下划线,后面要加标号@和参数字节数。Add函数有两个4字节的参数,参数总字节数为8字节,那么C的stdcall就是_Add@8;如果是C++语言的stdcall约定,那么前缀加问号“?”,后缀加“@@YG”以及一些参数标识,详细可以参考这篇博文,C++的stdcall约定下的函数名为“?Add@@YGHH@Z”。本文不写C++调用约定为stdcall风格的函数。

(2)参数传递

         参数传递跟调用约定有关,对于C语言来说,不管是cdecl约定还是stdcall约定,其参数都是通过堆栈来传递的,且进栈的方式是从右到左。传递参数是主调函数要做的事,而编写被调函数并不用关心参数是如何传进来的,只需考虑如何访问传进来的参数即可。如何访问堆栈中的数据呢?一般通过ebp寄存器来访问。为了快速存取堆栈中的数据,系统通常要进行字节对齐,一次压入或弹出堆栈的字长应是4字节,即使这个参数是小于4字节的。换句话说,就是1字节的参数,也要占用4字节的堆栈空间。

(3)堆栈平衡

         函数执行完毕,栈顶指针应该恢复到调用前的值。由于参数和局部变量都保存在栈空间,这会使栈顶指针发生改变,为了堆栈平衡,就要对堆栈进行清理。如果被调函数存在局部变量,那么被调函数自身一定要进行处理。对于参数,如果是stdcall调用约定,则由被调函数清理,如果是cdecl调用约定,则由主调函数处理。恢复栈顶指针方法有很多,可以使用add/sub指令对esp进行加减,或者ret XX指令对esp进行操作。

扫描二维码关注公众号,回复: 15214985 查看本文章

三、例子

(1)编写cdecl调用约定的函数

.386
.model flat//内存模式为平坦模式
.code
_Add proc
    push ebp//保存基址指针寄存器的值
    mov ebp,esp//将栈指针赋予ebp,用来访问栈中的参数
    mov eax,[ebp+8]//取出第一参数x
    mov ebx,[ebp+12]//取出第二个参数y
    add eax,ebx
    mov esp,ebp//恢复栈指针
    pop ebp//恢复基址指针
    ret//函数返回
_Add endp//函数结束
end _Add//将程序入口点设为_Add
    


        基本上,不管什么样的函数,函数的开始前两行代码都是【push ebp】和【mov ebp,esp】,函数结束前两行都是【mov esp,ebp】和【pop ebp】。

       访问保存在栈中的参数,需要知道它们存放的位置。esp是栈指针寄存器,存放的是栈顶指针,它指示这些参数的存放位置。但是又不能直接通过esp访问参数,原因有二,一是因为esp总是指向栈顶,也应该指向栈顶,改变其指向会造成堆栈混乱;二是,push/pop指会使堆栈增长或收缩,从而影响esp的值,这不利于获取参数。利用ebp作为中间寄存器访问参数是很有必要的,ebp可以改变,也可以不变,改变时也不影响esp的指向。

        通过[ebp+8]取得第一个参数,[ebp+12]取得第二个参数。为什么要将地址加8才能取得第一个参数呢?可以从函数的调用过程来分析这个问题,下图是参数在栈中的存储位置示意图。

       在call指令执行之前,先要将参数从左到右压入堆栈。参数传递 完了之后,执行call指令,这个call指令完成了两件事:一是将call指令之后第一条指令的地址压入堆栈,二是将IP(指令指针)指向被调函数的入口点。进入到函数内部时,第一条指令是【push ebp]】,这引起了堆栈增长,第二条指令【mov ebp,esp】,将栈顶指针赋予ebp。从上图可知,第一个参数X,距离栈顶8字节,第二个参数Y距离栈顶12字节。由于栈的增长方向与地址增长的方向刚好相反,所以获取第一个参数X是[ebp+8],第二个参数Y是[ebp+12]。

      函数的收尾工作:因为调用约定是cdecl,清理堆栈的任务交给主调函数,在被调函数中不需要也不能清理堆栈。在返回之前,仅仅是回复一下ebp和esp寄存器的值即可。于是执行【mov esp,ebp】【pop ebp】。

(2)编写stdcall调用约定的函数

.386
.model flat
.code
_Add@8 proc
    push ebp
    mov ebp,esp
    mov eax,[ebp+8]
    mov ebx,[ebp+12]
    add eax,ebx
    mov esp,ebp
    pop ebp
    ret 8//函数清理8个字节的参数,并返回
_Add@8 endp
end _Add@8

      stdcall调用约定的函数与cdecl调用约定的函数大部分都是一样的,不一样的地方是函数名称和堆栈清理部分。由于stdcall调用约定的函数需要自己清理堆栈,所以在函数返回时,需要清理栈中的两个参数。【ret 8】表示清理8字节的堆栈,然后返回。

四、VS编译汇编程序

汇编程序写好后如下图所示:

在“生成”菜单中的“编译”,或者右击汇编源文件选择“编译”,即可编译汇编程序、编译完成后会生成obj文件。为了防止调用该函数时出现意想不到的麻烦,最好将“Use Safe  Exception handler”设为“否”。操作如下图:

在VS中编译的汇编程序只能生成obj文件,如果需要生成lib文件,则需要利用VS的lib.exe工具进行操作。在生成obj文件之后,利用命令行启动lib.exe,来生成lib文件。为了方便,可以写一个批处理文件来完成这个工作。批处理文件如下:

批处理文件与obj文件放在一个文件夹里,点击bat文件即可生成lib文件。如果有多个obj文件,可以用通配符“*”来代表文件名。

猜你喜欢

转载自blog.csdn.net/qq_28249373/article/details/85205806