在我尝试从kernel中深入了解TCP IP协议栈时,遇到了难题。
我选择的是linux-2.4.20 kernel,理由如下:
1. 首先是<TCP/IP Architecture, Design, And Implementation In Linux>一书采用的是该版本,会有按图索骥的效果。
2. 第二个理由,正如书中所说的,TCP/IP协议栈在2.4内核中就已经基本成型,而根据我实际对比,2.4.20内核与4.4.1内核在TCP/IP实现的框架上大体是相同的,区别是2.6 kernel以后完全将VFS中的各组件namespace化,另外是一些高版本内核引入的措施(比如对比net/socket.c中sock_create函数)。
3. 第三个理由是,linux-2.4 kernel的代码还没有开始爆炸,适合初学者入门,也适合我这样学力不足的人。
采用linux-2.4 kernel的不足之处在于,版本较老,想亲自动手实验,需要做一些兼容性的准备。
我搜到了这样一篇帖子, 收益颇深。解决了我的疑问,用我最能接受的方式,先从kernel启动的函数说起,然后调用到我能看到的net/socket.c中的函数;然后又通过修改kernel源码添加标记,打印运行log来标志函数执行;然后通过讲解module_init注册的静态模块是如何加进内核可执行文件里的,然后编译出linux.map文件,进一步确定函数执行顺序。这个方式让我非常容易接受,也很感慨写博客的人功力之深,通篇干货没有废话;更感慨的是,这个帖子写于2001左右,当时进行kernel修改还是比较容易的事情,现在的kernel代码越来越庞大,初学者为此望而却步,很难入手;新人难以入门的问题,近年来也多有讨论。
那我就先把原作者的文章翻译过来,再继续下一步工作吧。
先说结论
- kernel启动时,第一个与network有关的函数是sock_init(),用来向kernel注册sock文件系统并挂载,以及加载其他模块,比如netfilter
- loopback设备随后被初始化,因为该设备比较简单。drivers/net/loopback.c
- dummy 和 Ethernet 设备随后被初始化
- TCP/IP协议栈是在inet_init()中初始化的
- Unix Domain Socket是在af_unix_init()中初始化的。1~5步按时间顺序排列。
Linux Kernel 2.4的入口函数
1.经过基本硬件设置后,启动代码(定义在head.S中)调用 /init/main.c 的 start_kernel()函数
|
|
2.sock_init()调用过程,向系统注册sock文件系统并挂载
sock_init()将向系统注册sock文件系统
do_initcalls()中循环调用所有MODULE_INIT()的模块,包括系统中的inet_init和af_unix_init,至于如何关联起来的,稍后会有介绍。
|
|
3.sock_init()的内容
欢迎信息printk()
清空协议栈数组,此时系统中没有任何协议
|
|
4.do_initcalls()中的调用函数
|
|
同时也修改loopback_init函数
|
|
重新编译内核,替换并重启,dmesg的输出结果为:
|
|
5.initcalls的实现机制
首先我们可以看到每个module都有使用module_init宏。
|
|
__init宏和 module_init宏在 include/linux/init.h 中定义
|
|
* init.h中的#define MODULE是在Makefile中的 -DMODULE 设置的,表示可以动态添加MODULE
* 目前 CONFIG_INET (/arch/i386/defconfig) 不是 可选module (M),而是静态编译进内核的(y)。静态模块由init.h中的#ifndef MODULE块预处理,而可动态加载的模块(M)则会调用 #else //MODULE 后的初始化代码
* 所以经过预编译后,inet_init()函数将由上述代码的#ifndef MODULE 预处理为
|
|
- 这个扩展过程意味着:
- inet_init()函数的代码段text code将编译进kernel可执行文件的***.text.init***段中,这种机制的目的是kernel启动,注册模块后能够释放所占用的内存
- 预处理后的***__initcall_inet_init***作为inet_init()函数的入口,将被存储在kernel可执行文件的 .initcall.init 段中。
- 注意这个宏定义是static类型的,所以我们并不能确定这个宏定义的结果是否在kernel的全局符号表中。(只有全局变量才在符号表中)
- 为了能够一探究竟,移除该宏定义的static标志,然后***_initcall*** ***这些入口就是全局变量了,然后我们就能在内核编译后的符号表中看到这些入口函数。
- 注意如果这些入口函数不是static作用域后,会导致一些链接错误,原因是命名冲突,比如netfilter中有类似命名
- Let’s hack the kernel!!!
5.如何从内部观察linux kernel
* linux kernel只是一个ELF可执行目标文件,和/bin/ls之类的可执行文件没有区别
* 所以作为kernel ELF文件,vmlinux可以通过nm、objdump和readelf等工具观察
* 默认情况下,linux kernel的顶层Makefile编译成功后将生成System.map文件,以方便调试,而这个文件不过是一个符号表。所以我向这个编译添加"--cref -Map linux.map"选项,可以生成一个包含更多信息的符号表
|
|
* make vmlinux编译kernle源码,生成vmlinux和linux.map,通过objdump -h vmlinux查看各段信息
* 可以看到***.text.init***段和***.initcall.init***段
|
|
* 如下是linux.map的文件内容
* __initcall_start 和 __initcall_end 定义了***.initcall.init***段的起始和结束,并出现在do_initcalls()函数中
* 之前将__initcall()宏的static关键字去掉了,所以__initcall_***这些入口函数地址,比如__initcall_inet_init就全局可见了,我们可以在文件中看到内核启动过程
* 内核开发者们总是喜欢用grep等工具来找某个函数在哪个文件中,而我们在linux.map中可以看到每个函数在哪个模块中,只需要less linux.map就可以了
|
|
linux.map中的信息可以帮助我们和dmesg的输出信息对照起来,可以看到内核中每个我们感兴趣的静态模块的加载顺序。
以上工作都是基于linux-2.4内核实现的,新版本内核该如何实现呢?