
网上比较多的话题是副业,周围的人聊的比较多的也是副业。网上有很多副业,甚至有陪跑;但是真正适合自己的、可以复制的只能自己去尝试;尝试之前要用心听他们怎么说,更要用心思考是否适合自己;适合自己的才能坚持做下去,否则没有结果,也会很痛苦,毕竟和别人学习做副业是要教学费的。当然了,我并不认为这是割韭菜,因为我们从小学习都是教学费的,学这么多年都不能保证有工作,学个副业就真的能拿到结果?理性对待就好!
分享个不错的资源,喜欢的保存吧!
安卓系统定制从入门到实践:
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 语言的调用吧,其它的语言,其实思想也是类似的吧。
所谓调用约定就是传参怎么传,借助什么传,比如参数是从左往右传,还是从右往左传,是通过栈传递,还是通过寄存器传递,如果是栈传递在函数调用完成后是由哪方进行平栈,返回值怎么返回之类的。
简单说,函数调用有几个关键点吧,如何调用的,如何返回的,如何传参的。差不多就这些吧。至于平栈,那是编译器根据调用约定去生成的吧。如果单单是高级语言,那么函数调用就类似这样。
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 这个变量。没有任何异议!
来简单的看下它的反汇编代码
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 函数的反汇编代码。
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 手册中给出的图吧。

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