背景
从事 android 开发四年有余,应用开发做得越久,就越有“知其然不知其所以然”的感觉,于是乎,过去的大半年,我几乎一有时间就去啃《Linux内核完全剖析-基于0.12内核》,接近1000页的 linux 系统源码,读的过程可谓是五味杂陈,终于在2016春节跟它作了一个了断。虽不能说对 linux 的底层实现已了然于心,但阅读这本书确实起到了醍醐灌顶的功效,在很大程度上打通了我在技术上的任督二脉。
有人会问,为什么是0.12版本?
答案很简单:
麻雀虽小,五脏俱全。
恕我才疏学浅,就像我更喜欢看 2.3版本的 android framework 源码一样,就是因为简单。linux 发展至今20余年,历经茫茫多的版本迭代,但其中的设计思想却是历久弥新,跟动辄几百万行的最新版本源码相比,先选一个软柿子捏捏,何乐而不为呢。
接下来我会把书上看到的知识点结合我自己的理解,写成一个系列发布出来,也算是对自己过去一段时间学习成果的整理和总结。
可变参数列表的使用
可变参数列表在很多语言中都有对应的语法支持,比如 Java
、python
。C 语言虽然是一门低级语言, 但是也支持可变参数列表, 而且它的可变参数列表实现得简单又不失巧妙,其中最著名的就是几个用来格式化字符串输出的 C 标准函数:
printf
, 直接把输出送到标准输出句柄stdout
cprintf
, 把输出送到控制台fprintf
, 把输出送到文件句柄sprintf
, 把输出送到以null
结尾的字符串中
可变参数列表的实现
下面就以 sprintf
函数说明在 C 标准库函数中是如何实现可变参数列表的。在说明 sprintf
的实现之前,非常有必要温习一下 C 函数调用在操作系统中是如何实现的,后面你们就知道为什么这里说这个很重要了。
C 函数调用机制
学过计算机的人都有一个模糊的印象(如果能把这里的事情说明白,那你基础一定很扎实),函数调用是通过栈来实现的,基本上就是一个入栈出栈的过程。大家可能又知道,C 的函数调用是传值调用,意思就是说 C 函数中用到的参数只是函数调用时传入参数的一个副本,所以要修改某个变量,就必须传入对应变量的地址指针。
那么为什么是这样的?且看下面这幅图
上面这幅图描绘了一个典型的函数调用栈内存结构,其中栈帧是指单个函数调用所使用的内存部分。每个栈帧的起始位置保存在寄存器 ebp
中。当在 C 程序中做函数调用的时候,比如函数 A 调用函数 B,在 A 的代码逻辑中会把 B 函数用到的实参压入栈中,实参所在的内存部分实则属于 A 函数的栈帧部分(参数1到参数n),所以图中参数1到参数n部分其实是在函数 B 被调用之前被复制到内存中的,即所谓的副本。
问题来了,我们知道C 程序里仅仅是一个简单的函数调用,那么这部分工作是由谁来完成的呢?