2021-01-23Bash源码分析(一)
作者简介:15年通讯底层研发经验,熟悉linux/vxworks等实时操作系统的内核原理和实现,在虚拟化的openstack,kubernetes,docker等领域也初有涉猎。
摘要:本文讲述当下留下的linux的bash的源代码,通过代码分析和单步调试解析bash的运行流程,适合喜欢研究linux原理的高级用户。分析的源代码来自gnu的开源项目https://git.savannah.gnu.org/git/bash.git,例子是作者自己编写,可以随意引用。
1 引言
Bash这个程序作为一个linux的用户,用的实在太频繁了,但一般局限于会用就结束了,一直没机会研究bash本身的原理。因工作需要,调试一个bash的cpu冲高问题,趁此机会对bash的源码做了一些研究,希望能对大家有点帮助。
2 linux的各种主流shell介绍
现在一般使用的shell有sh,bash和csh这几种,我们这里主要说的是bash,其他shell的源代码逻辑也差不多。
3 bash使用到的主要数据结构介绍
3.1 COMMAND
COMMAND是所有数据结构的纲,从这里可以看出一个bash实际能执行的语句有14种,分别位for,case,while,fi,connection,simple_com,function,group,select,arith,cond,arith_for,subshell,coproc,其中select,arith,cond,arith_for这4个命令需要打开对应的编译开关之后才能执行。 除了下面的这个union外,另外几个属性分别对应命令类型,行号和执行环境控制参数。其中控制参数有很多,每个控制参数占用一个bit位,包括是否启动子shell,是否忽略exit值等。
这些flag可以在bash启动shell脚本时设置,或者在shell脚本内部调用set指令来设置,一般用户不怎么关注,高阶用户可以看看:
3.2 FOR_COM
FOR_COM对应的shell语句是for name in map_list; do action; done 从结构体定义可以看出,除了和COMMAND相同的flags和行号外,for语句是有一个变量名,一个列表和一个递归的COMMAND组成的,实际for循环执行过程中也是将列表中的每个元素拿出来赋值给变量名,并执行action中的脚本段。 从这里的flags,可以看出,每条命令的flags是可以单独设置的,本条命令设置的控制参数可以不影响其他命令的控制参数。
3.3 CASE_COM
对照下面的脚本,可以看出,先判断一个变量,变量判断晚走到复合语句clauses,注意clause最终实现的时候是一个单向链表,链表中每个元素由一个样式的列表和一个执行体action来组成。
3.4 WHILE_COM
WHILE_COM比较简单,主体有两部分组成,判断条件和执行体。上篇文章调试过程中导致CPU冲高到100%的例子就是用的WHILE_COM,不过例子中的WHILE_COM的判断条件留空,相当于永远为true。
3.5 IF_COM
IF_COM由3段组成,条件判断,条件为true时的执行体和条件为false时的执行体,其中false情况下的执行体可以为空。
从IF_COM的定义看,3部分都可以是复杂的COMMAND结构,所以嵌套起来也可以做的非常复杂,例如可以在test部分通过执行脚本,依靠脚本的返回值来判断是应该执行true_case还是false_case。
3.6 CONNECTION
CONNECTION由4个属性组成:ignore字段对应其他命令结构种的flags,但对连接命令实际上没有用first对应第一条命令,second对应第二条命令,connector对应两条命令之间的连接符。难道只能两个命令一起用,不能多余两个命令一起调用?显然不是,一个CONNECTION对应一个连接符连接起来的两段命令,每段命令又可以是一个CONNECTION,这样就形成了级联的效果。 CONNECTION有3种:AND_AND对应“&&”,表示first执行返回结果为0的时候执行second;OR_OR对应“||”,表示first执行返回结果为非0的时候执行second;分号对应的connector还是分号,表示无论first执行结果是0还是非0,都执行second。是不是有点像C语言里面的&&,||和;?
上面的三个例子别真的执行,后果很严重(:))。第一条表示删除$dir对应值的目录中的所有文件;第二条表示$dir不存在的时候删除当前目录下面的所有文件;第三条表示,如果$dir存在就删除$dir目录下的所有文件,如果不存在就删除当前目录下面的所有文件。
3.7 SIMPLE_COM
SIMPLE_COM按字面意思就是简单命令,结构体由四部分组成,通用的flags,行号line,命令队列WORD_LIST,重定向队列REDIRECT。 WORD_LIST队列比较容易理解。一堆命令的集合:
其中单个命令还可以独立设置flags:
REDIRECT就是重定向的意思,这里也有很多种重定向,结构体的各个属性的含义:next,组成重定向链表的指针;redirector,重定向的源;rflags,重定向时使用的私有flags;flags,打开重定向目标文件时的flags;instruction,重定向的实际功能指令,这个又有很多种,下面会详细描述;redirectee,重定向的目的文件描述符或者文件名;here_doc_eof,本地文件。
从REDIRECTEE的定义看,它既可以是一个文件描述符,例如0表示标准输入,1表示标准输出,2表示错误输出,也可以是一个文件名。
Bash支持二十种不同的重定向,后面会根据bash的源代码来一一解释一下具体内容(bash源代码的注释对重定向的含义理解也有很多帮助):
先来五种输出的重定向:普通输出,强制输出,错误和标准输出,叠加输出,错误和标准叠加输出。统一说一下几个概念,标准输出就是2级stdout,错误输出就是3级sdterr,强制的意思是文件存在的情况下会被先清空,再增加,叠加输出的意思是原有内容后面再增加。
再来九种输入和输出重定向:普通输入重定向,后台执行,输入和输出同时重定向,去掉空格的输入重定向,输入重定向,字符串作为输入,关闭重定向源(怎么还有这种应用场景?),复制输入,复制输出。 紧接着六种输入输出的重定向分别为输入剪切,输出剪切,字符指向的输入剪切和字符指向的输出剪切,字符指向的输入复制和字符指向的输出复制。
Bash的重定向真是博大精深!!
3.8 FUNCTION_DEF
FUNCTION_DEF由五部分组成,通用flags,起始行号,函数名,解析之后的函数执行体,如果函数定义在文件中,最后会有文件名。函数的定义也可以有入参,入参的提取和文件执行时类似的,都是走$1,$2类似的形式获得的。
3.9 GROUP_COM
GROUP_COM是个什么鬼?通过分析group的处理函数,发现group原来就是多个命令组成的命令段,一般用{}包围起来,从group_command_nesting变量的变化看,group是支持多层嵌套的。
3.10 SELECT_COM
SELECT_COM并不是每个版本的bash都存在,可以通过在bash里面敲help来确定其是否存在。下面这个bash 4.2.46的版本中是打开了select的开关的:
SELECT_COM的作用是为了生成一个简单的菜单,用户通过选择菜单来让系统执行对应的命令,常见的SELECT_COM是时区配置时使用的。
具体分析/usr/bin/tzselect源码时发现,为了做到各个shell之间的兼容,这个脚本写的比想象中要复杂的多。 首先要是一下版本号的记录:
跳过紧接着的注释,然后是版本兼容性判断,使用帮助:
然后很无聊的定义了一个完全不用的变量IFS,但用来定义IFS的newline后面倒是用过。 为了规避bug,还要把PS3清空。
终于进正题了,先选择大洲或者大洋:
根据大洲或者大洋,通过awk汇总对应的国家列表:
二级select,选择国家:
再次祭出awk,通过国家汇总时区列表:
第三重select,选择时区:
计算好时区之后,出现第四重select,确认是否要修改:
还要判断一下当前是cshell还是其他shell,指导用户将当前的时区改到shell的启动脚本里面去(老大你写了这么多代码,不能自动把这句加进去么?还是要手动加:))。
3.11 ARITH_COM
ARITH_COM也是需要开关打开的,不过当前默认用的bash都是支持的,这个命令的意思是算术表达式,算术表达式要用(())包起来,要不然bash会不知道你想当做算术表达式使用,如果执行“echo 1+1”会怎么样?bash认为它是一个文本,直接将文本本身显示出来了。
加上(())之后echo还是失败的:
再加一个$之后终于正常了:
实际操作的时候,发现[]包起来的算术表达式也能用,但不加$的时候不会报错,加了会触发求值:
通过查看代码,发现,只有(())是算术表达式:
而[]只是为了兼容posix.2d9的一种算术替换规则,也就是说$[1+1]是直接替换成了2,而(())还需要走到算术表达式求值过程(是不是有点饶)。