首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >你这个程序员懂函数调用么

你这个程序员懂函数调用么

作者头像
码农UP2U
发布2026-03-16 18:31:57
发布2026-03-16 18:31:57
950
举报
文章被收录于专栏:码农UP2U码农UP2U

网上比较多的话题是副业,周围的人聊的比较多的也是副业。网上有很多副业,甚至有陪跑;但是真正适合自己的、可以复制的只能自己去尝试;尝试之前要用心听他们怎么说,更要用心思考是否适合自己;适合自己的才能坚持做下去,否则没有结果,也会很痛苦,毕竟和别人学习做副业是要教学费的。当然了,我并不认为这是割韭菜,因为我们从小学习都是教学费的,学这么多年都不能保证有工作,学个副业就真的能拿到结果?理性对待就好!

分享个不错的资源,喜欢的保存吧!

安卓系统定制从入门到实践:

https://pan.baidu.com/s/1yNNf5vYRdxF-oE6_Z9Dh7g?pwd=hr54

Linux设备驱动入门到高级进阶:

https://pan.baidu.com/s/1d8YgFKzwqoeXPYsUZ40YDQ?pwd=1nft

本篇文章聊聊 C 语言的函数调用!

0x01:函数的地位

函数在结构化程序设计当中起着举足轻重的作用,它很好的完成了代码复用,让程序员在管理源码上有了长足的进步。我隐约记得,在第一次软件危机之前就有了结构化程序设计的概念,在第一次软件危机之后,结构化程序设计被大力发展吧。不过软件设计没有银弹,说着容易,实际不容易!

结构化程序设计中必不可少的部分就是函数,函数之间主要就是调用和被调用的关系。即使在面向对象的今天,函数依然存在。在面向对象中函数变成了类的成员函数,或者是类的方法,从设计的本质来说,只是将函数可操作的数据进行了范围的限制,语言的语法那么要求,编译器就那样实现!

无论是函数,还是类方法,作为程序员,我们的想法是让别的程序员给我们写了,然后我们来调用一下就行了。为什么 Python 很受欢迎呢,库多啊!调用的更丝滑了。从这可以看出,函数、类等这些能复用的模块,给我们带来了很多方便。

在我们学习 C 语言的时候,函数是在学完控制结构才开始讲解的。但是仔细想想,其实在程序一开始就已经带入了,毕竟函数是 C 语言的基本单位。刚开始学习写代码时,我们会在主函数 main 中写简单的代码,学习基础的诸如 数据类型、控制结构 等内容;但是,输入输出时也会调用 printf 和 scanf 这类函数。这样,其实在刚入门的时候,其实已经涉及到了函数的定义和调用,所以写 C 语言程序从最开始就离不开函数。你看什么 PHP、JavaScript、BASIC 直接就可以开始写代码,入门这些语言的时候,就不用先定义个主函数吧!

0x02:完成函数调用的汇编指令

其实函数的调用并不是有了高级语言才有的,在汇编语言的层面就已经有了这种思想。对于函数的调用,汇编语言层面就有相应的指令,比如 call 指令和 ret 指令等。试想,没有这样的指令支持,高级语言怎么搞?

call 指令和 ret 指令是成对出现的,当然了,也不一定成对出现。但是高级语言的编译器生成的汇编指令几乎都是成对出现的吧。如果是手写汇编的话,非要搞些骚操作,那就另当别论了。当然很多保护机制也不会成对出现,因为它的目的就是要降低可读性,增加阅读代码成本,从而保护软件,比如 保护壳 和 用来代码虚拟化的虚拟机。

其实吧,不单单是函数的调用,各种语法到汇编层面都是那样了,就是面向对象吧,也是编译器实现了面向对象的语法,从而有了各种限制,最终的结果不也是汇编、也是二进制么。

0x03:高级语言函数调用的原理

高级语言的函数在调用时有调用约定,调用约定有 stdcall、cdecl、fastcall、pascal、thiscall 之类的。本篇文章只讨论 C 语言的调用吧,其它的语言,其实思想也是类似的吧。

所谓调用约定就是传参怎么传,借助什么传,比如参数是从左往右传,还是从右往左传,是通过栈传递,还是通过寄存器传递,如果是栈传递在函数调用完成后是由哪方进行平栈,返回值怎么返回之类的。

简单说,函数调用有几个关键点吧,如何调用的,如何返回的,如何传参的。差不多就这些吧。至于平栈,那是编译器根据调用约定去生成的吧。如果单单是高级语言,那么函数调用就类似这样。

代码语言:javascript
复制
int fun(int x, int y)
{
    return x + y;
}

int main()
{
    int num = fun(1, 2);
    printf("%d\r\n", num);
}

这个例子足以简单到任何人都能看懂了。把 1 和 2 传递给 fun 函数,fun 函数返回了 1 + 2 的和,并赋值给了 num 这个变量。没有任何异议!

  1. 先来说一下,1 和 2 是如何传递给 fun 这个函数呢?是通过内存传递(传参一般使用栈)。方式很简单,把 1 和 2 拷贝一份,这份拷贝在 fun 函数可以使用的范围内即可。
  2. 那么如何调用函数呢,当 1 和 2 拷贝完成后,代码转到 fun 函数去执行即可。
  3. fun 函数执行完以后呢,接着执行调用函数的下一条 printf 语句即可,那么 fun 函数执行完以后是如何返回到 printf 继续执行的呢?其实在步骤 1 之后,也就是传参之后,需要把返回地址在内存也保存好(返回地址也在栈中),这样在函数执行完成返回的时候去内存中找到要返回的地址,接着那里进行执行就可以了。
  4. 参数呢,对于 VS 来说,通过 eax 寄存器返回即可。

来简单的看下它的反汇编代码

代码语言:javascript
复制
 push        2  
 push        1  
 call        009D1127  
 add         esp,8  
 mov         dword ptr [ebp-8],eax  
 mov         eax,dword ptr [ebp-8]  
 push        eax  
 push        9D6B30h  
 call        009D131B

上面代码中,1 ~ 3 行是对 fun 函数的调用,参数是 2 和 1 分别入栈,参数的传递顺序是从右往左;

第 4 行是平栈,也就是平栈在调用方完成;

7 ~ 9 行是调用 printf 函数,第 8 行是格式化字符串的地址,第 7 行是 eax 寄存器,eax 寄存器的值来源于 [ebp - 8],而 [ebp - 8] 的值来源于 eax。这么看是不是很搞笑?eax 赋值给 [ebp - 8] 时是在 call 指令之后,也就是调用 fun 函数之后,那么看下 fun 函数的反汇编代码。

代码语言:javascript
复制
 push        ebp  
 mov         ebp,esp  
 sub         esp,0C0h  
 push        ebx  
 push        esi  
 push        edi  
 lea         edi,[ebp+FFFFFF40h]  
 mov         ecx,30h  
 mov         eax,0CCCCCCCCh  
 rep stos    dword ptr es:[edi]  
 mov         eax,dword ptr [ebp+8]  
 add         eax,dword ptr [ebp+0Ch]  
 pop         edi  
 pop         esi  
 pop         ebx  
 mov         esp,ebp  
 pop         ebp  
 ret 

关键看 11 ~ 12 行代码,[ebp + 8] 是参数 x,[ebp + 0ch] 是参数 y,x 赋值给 eax 寄存器,eax 寄存器再和 [ebp + 0ch] 相加,这样就完成了 x + y 的运算,并且结果保存在 eax 寄存器中。

在第 18 行返回之前,eax 寄存器的值都没有改变过。

返回到 main 函数后,也就是第 1 段代码的第 5 行,把 eax 寄存器的值赋值给了 [ebp - 8] 这个栈的位置中,那么相当于 fun 函数的返回值保存到了 [ebp - 8] 中,那么此时说明,fun 函数的返回值是由 eax 寄存器返回的。

当然了,最重要的是我们看到了成对出现的 call 和 ret 指令,它们虽然是汇编指令,但是它们的确是诸多高级语言对函数调用的底层指令。

0x04:最后

总得有个结尾,结尾看下来看看 call 和 ret 指令在 intel 手册中给出的图吧。

image.png
image.png

都到这里了,点个赞、关注一下吧!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-06-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 源代码010 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档