为什么不用call,而是用ret“vba 调用函数”main函数?

1996人阅读
做了几个实验,简单学习了解一下函数调用的开销。
程序1&没有参数的函数调用:
#include &stdio.h&
void test()
int main(int argc, char *argv[])
用gcc -S得到程序1的汇编代码:
.globl test
test, @function
%esp, %ebp
test, .-test
.globl main
main, @function
%esp, %ebp
main, .-main
"GCC: (GNU) 4.5.1
(Red Hat 4.5.1-4)"
.note.GNU-stack,"",@progbits&
从上面汇编代码可以看出,对于没有参数函数调用的开销:
1. 调用函数和返回,所以需要执行一次call/ret指令对。
2. 函数内部,需要保护现场,所以需要把%ebp push到栈中,返回前,再pop出来。
3. 构造函数运行环境,将%ebp赋值为当前的栈顶%esp。
则没有参数函数调用开销是5个指令。
程序2-带参数函数调用:
#include &stdio.h&
void test(int a)
int main(int argc, char *argv[])
int a = 1;
用gcc -S得到程序2的汇编代码:
.globl test
test, @function
%esp, %ebp
$1, 8(%ebp)
test, .-test
.globl main
main, @function
%esp, %ebp
$1, -4(%ebp)
-4(%ebp), %eax
%eax, (%esp)
main, .-main
"GCC: (GNU) 4.5.1
(Red Hat 4.5.1-4)"
.note.GNU-stack,"",@progbits&
相比于没有参数函数调用的开销,带参数函数调用多2个指令,用于传递参数:
movl -4(%ebp), %eax
movl %eax, (%ebp)
每个参数的传递时都需要2个指令。
而如果是指针参数,则函数在使用时,还得需要2个指令。
这么看,函数调用的开销还挺大的。
所以,当一个函数很小且调用频繁时,应该用宏或内联函数进行替代。
另外,虽然函数调用有开销,但除非有特殊的必要,该用函数的地方还是应该使用函数,否则会严重降低代码的可读性和可维护性。
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:39981次
排名:千里之外
原创:26篇
(1)(1)(1)(1)(1)(9)(3)(1)(2)(6)(2)(2)(3)(1)温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!&&|&&
一名小小的软件工程师
LOFTER精选
阅读(428)|
用微信&&“扫一扫”
将文章分享到朋友圈。
用易信&&“扫一扫”
将文章分享到朋友圈。
历史上的今天
在LOFTER的更多文章
loftPermalink:'',
id:'fks_',
blogTitle:'_stdcall与_cdecl的区别(函数调用方式)',
blogAbstract:'1.
{elseif x.moveFrom=='iphone'}
{elseif x.moveFrom=='android'}
{elseif x.moveFrom=='mobile'}
${a.selfIntro|escape}{if great260}${suplement}{/if}
{list a as x}
推荐过这篇日志的人:
{list a as x}
{if !!b&&b.length>0}
他们还推荐了:
{list b as y}
转载记录:
{list d as x}
{list a as x}
{list a as x}
{list a as x}
{list a as x}
{if x_index>4}{break}{/if}
${fn2(x.publishTime,'yyyy-MM-dd HH:mm:ss')}
{list a as x}
{if !!(blogDetail.preBlogPermalink)}
{if !!(blogDetail.nextBlogPermalink)}
{list a as x}
{if defined('newslist')&&newslist.length>0}
{list newslist as x}
{if x_index>7}{break}{/if}
{list a as x}
{var first_option =}
{list x.voteDetailList as voteToOption}
{if voteToOption==1}
{if first_option==false},{/if}&&“${b[voteToOption_index]}”&&
{if (x.role!="-1") },“我是${c[x.role]}”&&{/if}
&&&&&&&&${fn1(x.voteTime)}
{if x.userName==''}{/if}
网易公司版权所有&&
{list x.l as y}
{if defined('wl')}
{list wl as x}{/list}Call stack _百度百科
特色百科用户权威合作手机百科
收藏 查看&Call stack本词条缺少概述、信息栏、名片图,补充相关内容使词条更完整,还能快速升级,赶紧来吧! 调用堆栈:调用堆栈是一个方法列表,按调用顺序保存所有在运行期被调用的方法。
栈:在函数调用时,第一个进栈的是主函数中函数调用后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
当发生函数调用的时候,栈空间中存放的数据是这样的:  1、调用者函数把被调函数所需要的参数按照与被调函数的形参顺序相反的顺序压入栈中,即:从右向左依次把被调函数所需要的参数压入栈;  2、调用者函数使用call指令调用被调函数,并把call指令的下一条指令的地址当成返回地址压入栈中(这个压栈操作隐含在call指令中);  3、在被调函数中,被调函数会先保存调用者函数的栈底地址(push ebp),然后再保存调用者函数的栈顶地址,即:当前被调函数的栈底地址(mov ebp,esp);  4、在被调函数中,从ebp的位置处开始存放被调函数中的局部变量和临时变量,并且这些变量的地址按照定义时的顺序依次减小,即:这些变量的地址是按照栈的延伸方向排列的,先定义的变量先入栈,后定义的变量后入栈;  所以,发生函数调用时,入栈的顺序为:  参数N  参数N-1  参数N-2  .....  参数3  参数2  参数1  函数返回地址  上一层调用函数的EBP/BP  局部变量1  局部变量2  ....  局部变量N
解释:  首 先,将调用者函数的入栈(push ebp),然后将调用者函数的栈顶指针ESP赋值给被调函数的EBP(作为被调函数的栈底,mov ebp,esp),此时,EBP寄存器处于一个非常重要的位置,该寄存器中存放着一个地址(原EBP入栈后的栈顶),以该地址为基准,向上(栈底方向)能 获取返回地址、参数值,向下(栈顶方向)能获取函数的局部变量值,而该地址处又存放着上一层函数调用时的EBP值;  一般而言,SS: [ebp+4]处为被调函数的返回地址,SS:[EBP+8]处为传递给被调函数的第一个参数(最后一个入栈的参数,此处假设其占用4字节内存)的 值,SS:[EBP-4]处为被调函数中的第一个局部变量,SS:[EBP]处为上一层EBP值;由于EBP中的地址处总是&上一层函数调用时的EBP 值&,而在每一层函数调用中,都能通过当时的EBP值&向上(栈底方向)能获取返回地址、参数值,向下(栈顶方向)能获取被调函数的局部变量值&;  如此递归,就形成了函数调用栈;  函数内局部变量布局示例:  #include &stdio.h&  #include &string.h&  struct C  {        };  int test2(int x, int y, int z)  {  printf(&hello,test2\n&);  return 0;  }  int test(int x, int y, int z)  {  int a = 1;  int b = 2;  int c = 3;  struct C  printf(&addr x = %u\n&,(unsigned int)(&x));  printf(&addr y = %u\n&,(unsigned int)(&y));  printf(&addr z = %u\n&,(unsigned int)(&z));  printf(&addr a = %u\n&,(unsigned int)(&a));  printf(&addr b = %u\n&,(unsigned int)(&b));  printf(&addr c = %u\n&,(unsigned int)(&c));  printf(&addr st = %u\n&,(unsigned int)(&st));  printf(&addr st.a = %u\n&,(unsigned int)(&st.a));  printf(&addr st.b = %u\n&,(unsigned int)(&st.b));  printf(&addr st.c = %u\n&,(unsigned int)(&st.c));  return 0;  }
int main(int argc, char** argv)  {  int x = 1;  int y = 2;  int z = 3;  test(x,y,z);  printf(&x = %d; y = %d; z = %d;\n&, x,y,z);  memset(&y, 0, 8);  printf(&x = %d; y = %d; z = %d;\n&, x,y,z);  return 0;  }  打印输出如下:  addr x =   addr y =   addr z =   addr a =   addr b =   addr c =   addr st =   addr st.a =   addr st.b =   addr st.c =   a = 1; b = 2; c = 3;  a = 0; b = 0; c = 3;  示例效果图:
该图中的局部变量都是在该示例中定义的;
这个图片中反映的是一个典型的函数调用栈的内存布局;
访问函数的局部变量和访问函数参数的区别:  局部变量总是通过将ebp减去偏移量来访问,函数参数总是通过将ebp加上偏移量来访问。对于32位变量而言,第一个局部变量位于ebp-4,第二个位于ebp-8,以此类推,32位局部变量在栈中形成一个逆序数组;第一个函数参数位于ebp+8,第二个位于ebp+12,以此类推,32位函数参数在栈中形成一个正序数组。    汇编代码示例(1):  ////////////////////////////////////////////////////////////////////  比如 我们有这样一个C函数  #include&stdio.h&  long test(int a,int b)  {  a = a + 1;  b = b + 100;  return a +  }  void main()  {  printf(&%d&,test());  }  写成32位汇编就是这样  ;//////////////////////////////////////////////////////////////////////////////////////////////////////  .386  .model flat,这里我们用stdcall 就是函数参数 压栈的时候从最后一个开始压,和被调用函数负责清栈  option casemap:区分大小写  includelib msvcrt.这里是引入类库 相当于 #include&stdio.h&了  printf PROTO C:DWORD,:VARARG ;这个就是声明一下我们要用的函数头,到时候 汇编程序会自动到msvcrt.lib里面找的了  ;:VARARG 表后面的参数不确定 因为C就是这样的printf(const char *, ...);  ;这样的函数要注意 不是被调用函数负责清栈 因为它本身不知道有多少个参数  ;而是有调用者负责清栈 下面会详细说明  .data  szTextFmt BYTE '%d',0 ;这个是用来类型转换的,跟C的一样,字符用字节类型  a dword 1000 ;假设  b dword 2000 ;处理数值都用双字 没有int 跟long 的区别  ;/////////////////////////////////////////////////////////////////////////////////////////  .code  _A:DWORD,B:DWORD  push ebp  mov ebp,esp  mov eax,dword ptr ss:[ebp+8]  add eax,1  mov edx,dword ptr ss:[ebp+0Ch]  add edx,100  add eax,edx  pop ebp  retn 8  _test endp  _main proc  push dword ptr ds:反汇编我们看到的b就不是b了而是一个[*****]数字 dword ptr 就是我们在ds(数据段)把[*****]  ;开始的一个双字长数值取出来  push dword ptr ds:跟她对应的还有 byte ptr ****就是取一个字节出来 比如这样 mov al,byte ptr ds:szTextFmt  ;就把 % 取出来 而不包括 d  call _test  假设push eax的地址是×××××  push offset szTextFmt  call printf  add esp,8  ret  _main endp  end _main  汇编代码示例(2):  研究函数的调用过程int bar(int c, int d){ int e = c +}int foo(int a, int b){ return bar(a, b);}int main(void){ foo(2, 3); return 0;}
如果在编译时加上-g选项(在第 10 章gdb讲过-g选项),那么用objdump反汇编时可以把C代码和汇编代码穿插起来显示,这样C代码和汇编代码的对应关系看得更清楚。反汇编的结果很长,以下只列出我们关心的部分。
$ gcc main.c -g$ objdump -dS a.out ... &bar&:int bar(int c, int d){
mov %esp,%ebp
sub $0x10,%esp int e = c + 804839a: 8b 55 0c
mov 0xc(%ebp),%edx 804839d: 8b 45 08
mov 0x8(%ebp),%eax
add %edx,%eax
mov %eax,-0x4(%ebp) b 45 fc
mov -0x4(%ebp),%eax} 80483a8: c9
leave 80483a9: c3
ret 080483aa &foo&:int foo(int a, int b){ 80483aa: 55
push %ebp 80483ab: 89 e5
mov %esp,%ebp 80483ad: 83 ec 08
sub $0x8,%esp return bar(a, b); b 45 0c
mov 0xc(%ebp),%eax
mov %eax,0x4(%esp) b 45 08
mov 0x8(%ebp),%eax 80483ba: 89 04 24
mov %eax,(%esp) 80483bd: e8 d2 ff ff ff
call 8048394 &bar&} 80483c2: c9
leave 80483c3: c3
&main&:int main(void){ d 4c 24 04
lea 0x4(%esp),%ecx
and $0xfffffff0,%esp 80483cb: ff 71 fc
pushl -0x4(%ecx) 80483ce: 55
push %ebp 80483cf: 89 e5
mov %esp,%ebp
sub $0x8,%esp foo(2, 3); 80483d5: c7 44 24 04 03 00 00 movl $0x3,0x4(%esp) 80483dc: 00 80483dd: c7 04 24 02 00 00 00 movl $0x2,(%esp) 80483e4: e8 c1 ff ff ff
call 80483aa &foo& return 0; 80483e9: b8 00 00 00 00
mov $0x0,%eax} 80483ee: 83 c4 08
add $0x8,%esp
pop %ecx d
pop %ebp d 61 fc
lea -0x4(%ecx),%esp 80483f6: c3
要查看编译后的汇编代码,其实还有一种办法是gcc -S main.c,这样只生成汇编代码main.s,而不生成二进制的目标文件。
整个程序的执行过程是main调用foo,foo调用bar,我们用gdb跟踪程序的执行,直到bar函数中的int e = c +语句执行完毕准备返回时,这时在gdb中打印函数栈帧。
(gdb) start...main () at main.c:1414 foo(2, 3);(gdb) sfoo (a=2, b=3) at main.c:99 return bar(a, b);(gdb) sbar (c=2, d=3) at main.c:33 int e = c +(gdb) disassemble Dump of assembler code for function bar:0x &bar+0&: push %ebp0x &bar+1&: mov %esp,%ebp0x &bar+3&: sub $0x10,%esp0x0804839a &bar+6&: mov 0xc(%ebp),%edx0x0804839d &bar+9&: mov 0x8(%ebp),%eax0x &bar+12&: add %edx,%eax0x &bar+14&: mov %eax,-0x4(%ebp)0x &bar+17&: mov -0x4(%ebp),%eax0x &bar+20&: leave 0x &bar+21&: ret End of assembler dump.(gdb) si0x int e = c +(gdb) si0x 3 int e = c +(gdb) si0x 3 int e = c +(gdb) si4(gdb) si5 }(gdb) bt#0 bar (c=2, d=3) at main.c:5#1 0x in foo (a=2, b=3) at main.c:9#2 0x in main () at main.c:14(gdb) info registers eax
0xbff1c440 -edx
0xb7fe6ff4 -esp
0xbff1c3f4 0xbff1c3f4ebp
0xbff1c404 0xbff1c404esi
0x513680edi
0x513376eip
0xx80483a8 &bar+20&eflags
0x200206 [ PF IF ID ]cs
0x73 115ss
0x7b 123ds
0x7b 123es
0x7b 123fs
0x33 51(gdb) x/20 $esp0xbff1c3f4: 0xxbff1c6f7 0xb7efbdae 0xxbff1c404: 0xbff1c414 0x 0xxxbff1c414: 0xbff1c428 0x 0xxxbff1c424: 0xbff1c440 0xbff1c498 0xb7ea484100xbff1c434: 0x 0xbff1c498 0xb7ea00001(gdb)
这里又用到几个新的gdb命令。disassemble可以反汇编当前函数或者指定的函数,单独用disassemble命令是反汇编当前函数,如果disassemble命令后面跟函数名或地址则反汇编指定的函数。以前我们讲过step命令可以一行代码一行代码地单步调试,而这里用到的si命令可以一条指令一条指令地单步调试。info registers可以显示所有寄存器的当前值。在gdb中表示寄存器名时前面要加个$,例如p $esp可以打印esp寄存器的值,在上例中esp寄存器的值是0xbff1c3f4,所以x/20 $esp命令查看内存中从0xbff1c3f4地址开始的20个32位数。在执行程序时,操作系统为进程分配一块栈空间来保存函数栈帧,esp寄存器总是指向栈顶,在x86平台上这个栈是从高地址向低地址增长的,我们知道每次调用一个函数都要分配一个栈帧来保存参数和局部变量,现在我们详细分析这些数据在栈空间的布局,
图中每个小方格表示4个字节的内存单元,例如b: 3这个小方格占的内存地址是0xbff1c420~0xbff1c423,我把地址写在每个小方格的下边界线上,是为了强调该地址是内存单元的起始地址。我们从main函数的这里开始看起:
foo(2, 3); 80483d5: c7 44 24 04 03 00 00 movl $0x3,0x4(%esp) 80483dc: 00 80483dd: c7 04 24 02 00 00 00 movl $0x2,(%esp) 80483e4: e8 c1 ff ff ff
call 80483aa &foo& return 0; 80483e9: b8 00 00 00 00
mov $0x0,%eax
要调用函数foo先要把参数准备好,第二个参数保存在esp+4指向的内存位置,第一个参数保存在esp指向的内存位置,可见参数是从右向左依次压栈的。然后执行call指令,这个指令有两个作用:
foo函数调用完之后要返回到call的下一条指令继续执行,所以把call的下一条指令的地址0x80483e9压栈,同时把esp的值减4,esp的值现在是0xbff1c418。
修改程序计数器eip,跳转到foo函数的开头执行。
现在看foo函数的汇编代码:
int foo(int a, int b){ 80483aa: 55
push %ebp 80483ab: 89 e5
mov %esp,%ebp 80483ad: 83 ec 08
sub $0x8,%esp
push %ebp指令把ebp寄存器的值压栈,同时把esp的值减4。esp的值现在是0xbff1c414,下一条指令把这个值传送给ebp寄存器。这两条指令合起来是把原来ebp的值保存在栈上,然后又给ebp赋了新值。在每个函数的栈帧中,ebp指向栈底,而esp指向栈顶,在函数执行过程中esp随着压栈和出栈操作随时变化,而ebp是不动的,函数的参数和局部变量都是通过ebp的值加上一个偏移量来访问,例如foo函数的参数a和b分别通过ebp+8和ebp+12来访问。所以下面的指令把参数a和b再次压栈,为调用bar函数做准备,然后把返回地址压栈,调用bar函数:
return bar(a, b); b 45 0c
mov 0xc(%ebp),%eax
mov %eax,0x4(%esp) b 45 08
mov 0x8(%ebp),%eax 80483ba: 89 04 24
mov %eax,(%esp) 80483bd: e8 d2 ff ff ff
call 8048394 &bar&
现在看bar函数的指令:
int bar(int c, int d){
mov %esp,%ebp
sub $0x10,%esp int e = c + 804839a: 8b 55 0c
mov 0xc(%ebp),%edx 804839d: 8b 45 08
mov 0x8(%ebp),%eax
add %edx,%eax
mov %eax,-0x4(%ebp)
这次又把foo函数的ebp压栈保存,然后给ebp赋了新值,指向bar函数栈帧的栈底,通过ebp+8和ebp+12分别可以访问参数c和d。bar函数还有一个局部变量e,可以通过ebp-4来访问。所以后面几条指令的意思是把参数c和d取出来存在寄存器中做加法,计算结果保存在eax寄存器中,再把eax寄存器存回局部变量e的内存单元。
在gdb中可以用bt命令和frame命令查看每层栈帧上的参数和局部变量,现在可以解释它的工作原理了:如果我当前在bar函数中,我可以通过ebp找到bar函数的参数和局部变量,也可以找到foo函数的ebp保存在栈上的值,有了foo函数的ebp,又可以找到它的参数和局部变量,也可以找到main函数的ebp保存在栈上的值,因此各层函数栈帧通过保存在栈上的ebp的值串起来了。
现在看bar函数的返回指令:
mov -0x4(%ebp),%eax} 80483a8: c9
leave 80483a9: c3
bar函数有一个int型的返回值,这个返回值是通过eax寄存器传递的,所以首先把e的值读到eax寄存器中。然后执行leave指令,这个指令是函数开头的push %ebp和mov %esp,%ebp的逆操作:
把ebp的值赋给esp,现在esp的值是0xbff1c404。
现在esp所指向的栈顶保存着foo函数栈帧的ebp,把这个值恢复给ebp,同时esp增加4,esp的值变成0xbff1c408。
最后是ret指令,它是call指令的逆操作:
现在esp所指向的栈顶保存着返回地址,把这个值恢复给eip,同时esp增加4,esp的值变成0xbff1c40c。
修改了程序计数器eip,因此跳转到返回地址0x80483c2继续执行。
地址0x80483c2处是foo函数的返回指令:
80483c2: c9
leave 80483c3: c3
重复同样的过程,又返回到了main函数。注意函数调用和返回过程中的这些规则:
参数压栈传递,并且是从右向左依次压栈。
ebp总是指向当前栈帧的栈底。
返回值通过eax寄存器传递。
这些规则并不是体系结构所强加的,ebp寄 存器并不是必须这么用,函数的参数和返回值也不是必须这么传,只是操作系统和编译器选择了以这样的方式实现C代码中的函数调用,这称为Calling Convention,Calling Convention是操作系统二进制接口规范(ABI,Application Binary Interface)的一部分。
新手上路我有疑问投诉建议参考资料 查看扫扫二维码,随身浏览文档
手机或平板扫扫即可继续访问
不用main函数的编程
举报该文档为侵权文档。
举报该文档含有违规或不良信息。
反馈该文档无法正常浏览。
举报该文档为重复文档。
推荐理由:
将文档分享至:
分享完整地址
文档地址:
粘贴到BBS或博客
flash地址:
支持嵌入FLASH地址的网站使用
html代码:
&embed src='/DocinViewer-4.swf' width='100%' height='600' type=application/x-shockwave-flash ALLOWFULLSCREEN='true' ALLOWSCRIPTACCESS='always'&&/embed&
450px*300px480px*400px650px*490px
支持嵌入HTML代码的网站使用
您的内容已经提交成功
您所提交的内容需要审核后才能发布,请您等待!
3秒自动关闭窗口

我要回帖

更多关于 vba 调用函数 的文章

 

随机推荐