前言
之前就对linux上附加的一些小工具像netstat、ps、top等的功能比较感兴趣,虽然知道本质上还是去解析vfs中的信息,但对其细节比较好奇,这边做下源码分析,同时也为后续对其它相对于netstat更复杂的程序/项目的分析做下铺垫,如sysdig、osquery、linux kernel等等;这块spoock江城子师傅已经写过类似的文章,个人觉得写的很好,要点都get到了,作为读者受益匪浅,贴下链接致敬下引路人:https://blog.spoock.com/2019/05/26/netstat-learn/。
下面所做分析可能无法面面俱到,非核心的函数和功能分析将被drop。
调试环境
此次调试是在vscode下使用gdbserver launch远程进行的,调试端的launch.json配置如下,被调端gdbserver挂住就好了
1 | { |
源码分析
2286行之前大量操作用在判断程序启动参数上,并给当作参数标志位使用的变量置位,以及其它一些初始化操作
1 | #if I18N |
prg_cache_load
1 | #if HAVE_AFINET |
2286行之后,首先需要引起注意的是#if HAVE_AFINET
这句判断,需要了解的是,socket套接字协议类型是种类繁多的,官方将不同种类和功能的socket根据协议分类到不同的socket协议族中,如AF_BLUETOOTH、AF_NETLINK、AF_INET、AF_INET6等,这些不同协议族中的socket有的是用于进程间通信,有的是用于和硬件通信,有的是用于网络通信等等,不同类型功能各不相同,所以需要分类去显示,#if HAVE_xxxx
也就是用于netstat去分类显示用的。
其次比较重要的是prg_cache_load这个函数,先贴下
1 | static void prg_cache_load(void) |
这部分代码主要用来初始化保存pid/cmdline-inode之间对应关系的缓存链表,看上去很长,其实逻辑很清晰,首先if (prg_cache_loaded || !flag_prg) return;
一句通过判断prg_cache是否已经加载以及netstat -p参数是否置位,来决定是否进入此分支;一旦进入此分支,则利用readdir进行二次循环,第一次拿到/proc下pid,第二次解析出/proc/pid/fd下的inode以及拿到cmdline等数据;最后,利用prg_cache_add(inode, finbuf, "-")
将解析出的inode及findbuf放入prg_node链表中,此处的finbuf是pid和cmdline的拼接,第三个参数scon为”-“,后面可知由于proc下pid具有瞬时特性,pid数据可能存在丢失,一旦netstat未解析出网络数据对应的pid,则用”-“在pid/program一栏进行代替。
看下prg_cache_add,如下,与之联动对链表进行增、查、删操作的还有prg_cache_get和prg_cache_get_con以及prg_cache_clear,一并贴下:
1 | static void prg_cache_add(unsigned long inode, char *name, const char *scon) |
tcp_info
Prg_load之后正式进入解析流程,首先依旧是判断socket协议族类型,不同协议族进入不同分支进行处理,这边主要关注HAVE_AFINET
协议,HAVE_AFINET协议族内部又包含不同socket类型,具体包括tcp、udp、raw_info等,此处主要分析tcp,因为流程都类似。
1 | if HAVE_AFINET |
1 | static int tcp_info(void) |
tcp_info调用了INFO_GUTS6
,INFO_GUTS6有六个参数,前两个是lib/pathnames.h中定义的宏,分表vfs下的tcp4/6存储文件:
1 |
剩下的tcp_do_one看上去像函数指针,猜测是在INFO_GUTS6中进行了调用,跟进INFO_GUTS6
:
1 | #define INFO_GUTS6(file,file6,name,proc,prot4,prot6) \ |
这边是用的宏去定义函数,可以看到其根据数据包ipv4/ipv6协议类型分设了两条分支,这边只看ipv4,进入INFO_GUTS1:
1 | #define INFO_GUTS1(file,name,proc,prot) \ |
同样是一个宏,首先利用proc_fopn获取/proc/net/tcp文件句柄,然后fgets读取文件中的每一行到8192字节的buffer中,(proc)(lnr++, buffer,prot);
调用了INFO_GUTS6传入的tcp_do_one函数指针,将/proc/net/tcp中的每一行传入作为tcp_do_one的参数进行循环处理,猜测是对buffer进行内容进行解析。
tcp_do_one
首先贴下代码:
1 | static void tcp_do_one(int lnr, const char *line, const char *prot) |
几个点,首先解析/proc/net/tcp文件中的每一行数据,将每个字段的数据赋值到不同变量:
1 | num = sscanf(line,"%d: %64[0-9A-Fa-f]:%X %64[0-9A-Fa-f]:%X %X %lX:%lX %X:%lX %lX %d %d %lu %*s\n", |
其次针对local/remote address进行解析,将解析出的字符数组转为hex整形数据:
1 | sscanf(local_addr, "%X", &localaddr->sin_addr.s_addr); |
再之后,adore_do_one函数,对addr及port进行操作:
1 | addr_do_one(local_addr, sizeof(local_addr), 22, ap, &localsas, local_port, "tcp"); |
跟进:
1 | static void addr_do_one(char *buf, size_t buf_len, size_t short_len, const struct aftype *ap, |
发现其addr和port进行拼接,比较有意思的一点是利用short_len对addr和port拼接后的长度进行限制,若长度超过23,且-W参数对应变量置位,则对addr进行截断显示。
最后对addr:port拼接后的数据及前面在/proc/net/tcp中解析出的字段进行输出:
1 | printf("%-4s %6ld %6ld %-*s %-*s %-11s",prot, rxq, txq, (int)netmax(23,strlen(local_addr)), local_addr, (int)netmax(23,strlen(rem_addr)), rem_addr, _(tcp_state[state])); |
剩下的最后一个 finish_this_one
函数,跟进分析后发现其主要是在以上的基础输出外检测变量是否置位,即参数使用情况来输出指定类型的数据栏目:
1 | static void finish_this_one(int uid, unsigned long inode, const char *timers) |
其中flag_exp、flag_prg、flag_selinux、flag_opt分别对应-e、-p、-Z、-o:
1 | case 'A': |
具体参数对应含义:
1 | static void usage(int rc) |
else
剩下的就是af_inet协议族下其它类型的socket解析及输出以及其它协议族的解析输出,类似tcp_info,尾部使用prg_cache_clear清理缓存链表,不再赘述。
总结
代码逻辑是非常清晰的,但是调试过程中发现mac下的ide的debug功能实在不是太友好,无法查看/修改整块的内存,数据的显示也和其本身格式不一致,以及单步过程中遇到了可视化界面中的光标位置和真实elf执行流程不匹配的问题,总体下来,感觉不是很灵活的样子,不止是vscode,clion也有这种问题;
正如spoock师傅所说的,netstat工具在网络进程频繁开关socket通道的环境中性能压力是很大的,联想到之前写的网络日志采集demo中有模块是复用了部分netstat源码,但是总体效果不是非常好,原因无他,遍历proc这块的io性能压力比较大,若放到实际业务环境中,如openstack这种大流量和进程频繁操作的云环境,业务影响之大可想而知;