哈喽,我是子牙,一个很卷的硬核男人
好久没更新了,因为要上VIP课。而且又是第一期,为了口碑,需要不断的打磨课程。总归功夫不负有心人,收获了大量的好评,贴一小段。
今天得空给大家分享一篇文章,聊聊可变参数的底层实现原理。文章很硬核,消化需谨慎
什么是可变参数
如果你写过C代码,一定用过printf
printf函数与我们接触过的大多数函数不同的是,理论上它可以支持任意个参数。像这种支持任意个参数的函数,称之为可变参数
那可变参数是如何实现的呢?我们来看看它的函数原型,秘密就是那三个点
语法糖的支持需要编译系统与运行系统共同支持,我们这里看到的是编译系统对可变参数的支持,那运行系统是如何支持的呢?这就牵扯到很多知识点了,如果你理解了这些,你的内功会提供巨大截
这些知识点包括:
- 对执行流的理解。你得了解对于需要传参的函数,编译器编译生成的代码长啥样子,这个学汇编才能知晓
- 对内存的理解。即你脑海中得有内存图,准确地说是栈图,你得清楚你传的所有的参数,在栈中是如何存储的,也是需要学汇编才能知道
- 对指针的理解。能够熟练操控指针,移到合适的位置,拿到需要的值
你木有这些知识也木有关系,我尽可能讲得简单一点,让你能够理解答案。至于动手能力,那这些相关知识需要你自己去补。你也可以加入我的手写操作系统小班习得
编译器如何编译函数
为什么要了解这个?因为编译器会根据不同的调用约定,需要对传参生成不同的代码
调用约定有哪些呢:__cdecl、__stdcall、__fastcall,不同的调用约定,传参方式、平栈方式都不同。比如__cdecl,它是C语言默认的调用约定,它会按照从右向左的顺序将参数压栈。平栈你没学过汇编可能不理解,我就不解释了。我们后面讲的内容都是基于__cdecl,剩下两个调用约定我就不讲了,感兴趣的自己找资料看看
可变参数只有在__cdecl、__stdcall这两种调用约定下才有意义,你知道为什么吗?答案在后面
贴个编译器编译生成的传参代码感受一下。你可能看不懂汇编,但是push是压栈你肯定知道
执行流栈
前面我们已经讲了编译系统对可变参数的支持,接下来咱们看看这些参数在内存栈中是如何存储的。说个前提,32位机,意味着程序以4字节为一个单位使用系统栈
为什么要铺垫这么多,因为没有这些,代码从何写起呢?现在咱们已经有了这张内存图,就可以写代码了
代码怎么写
现在printf有七个参数,这个是我们用肉眼看出来的,那计算机是怎么知道的呢?通过解析第一个参数得出来的,就是一个%s加六个%d。这是实现可变参数的第一个重点,需要知道有多少个参数个数。
同时通过第一个参数,我们可以得到参数开始的内存地址0xe4,加上我们知道栈是按4字节为一个单位使用。了解指针的都知道,有这两个信息,我们就可以取到所有的参数。
代码如下
但是我们用可变参数,是这样子的
你肯定猜到了,其实就是把指针的参照封装起来了
第10行很容易理解,就是将指针移动到第一个参数所在的位置,即0xe8
第12行业容易理解,就是指针用完了清空
最难理解的是第11行,代码为什么这么写呢?我们想一下,我们把0xe8处的值,即第二个参数取到,我们要去第三个参数,是不是需要移动指针,我们可以按照p+0、p+1去做,那有没有更简单的方式,将移动指针与取值合为一步?这就是这个代码的意义,一行代码做了两件事情
比如我现在取第二个参数,即0xe8中的值,取完指针应该变成0xec。这一步只要达到这两个效果即可,写法可以有很多,我提供的是一种写法。讲到这里,第11行的代码你看懂了吗?
结语
我是子牙老师,喜欢钻研底层,深入研究Windows、Linux内核、JVM。如果你也喜欢研究底层,欢迎关注我的公众号【硬核子牙】