glibc源码分析-1:构建过程
简介
glibc(GNU C Library)是Linux采用的C语言运行库,它提供了ISO C11/POSIX.1-2008等标准定义的一系列API,几乎被所有应用所引用。阅读其源码能够提升对计算机底层组件的认知层次,修炼内功。
不同于python这类解释型语言可以直接在解释器中运行程序,C作为编译型语言需要将源码编译链接成机器码后才能执行(关于编译链接的具体机制可以参看我之前写的介绍文章:理解C语言-3:编译链接)。在glibc这样的超大型项目中,编译过程尤为复杂:不仅需要考虑不同平台间的不同配置项,还需要通过awk/sed等文本处理程序生成各种项目依赖文件。如果贸然扎进源码分析某个模块,很容易陷入不知道某个具体符号来自哪里的困境,铩羽而归。
GNU Make负责完成如此负责的编译,本文就通过分析Make的调试信息,解析编译glibc的具体过程,引出glibc源代码的目录结构,为后续分析glibc中的具体模块打下基础。
构建流程
构建环境
本文所准备的构建环境是一个安装有CentOS-7-x86_64-Minimal-1908.iso的VirtualBox虚拟机,构建的源代码版本为glibc-2.31,具体的编译指令如下:
1 | wget https://ftp.gnu.org/gnu/glibc/glibc-2.31.tar.gz |
其中configure和make命令都是GNU构建体系的重要成员:
- configure脚本负责侦测平台环境,确认平台特定的参数,并生成相关文件。
执行完*./configure*,你可以看到它生成的文件:config.h(由config.h.in作为模版生成)
定义了项目源文件可能使用的宏定义(平台相关)。config.make(由config.make.in作为模版生成)
定义了make过程中依赖的一系列变量,包括构建使用的具体程序/构建涉及的目录/构建参数/平台相关(这部分和config.h内容有部分重叠) 等等。
在我的构建环境中,configure确认配置为x86_64-pc-linux-gnu(machine-vendor-os)。并据此确认config-sysdir变量的值。Makefile(由Makefile.in作为模版生成)
由make程序执行的目标文件。为了加速后续make的编译,可以修改该文件内容,取消PARALLELMFLAGS注释,开启并行编译。config.status
这是一个shell脚本,完成生成上述3个文件的具体任务。可以单独调用该脚本重新生成某个特定文件。config.log(本次configure的日志)
config.cache(本次configure的缓存,可选)
- make命令按照指定的Makefile执行具体构建任务。
简单来说,Makefile包含了两方面的信息:构建目标间的依赖关系(决定了本次构建该更新哪些文件)以及某个目标具体的构建步骤(shell指令)。详细指南可以参见GNU Make手册。
构建目标和源代码布局
构建目标
glibc作为C语言运行库主要是以共享库的方式存在,因此libc.so是主要的构建目标。
在正式构建libc.so之前,glibc会提前构建出不同的lib类型:
- libc.a
使用-DPIE生成后缀为.o的目标文件,这些文件只能被链接成最终可执行文件。 - libc_pic.a
使用-DPIC -DSHARED生成后缀为.os的目标文件,这些文件可以链接成动态库文件。 - libc_nonshared.a
使用-DPIC -DLIBC_NONSHARED=1生成后缀为.oS的目标文件,这类代码只包括静态函数(static-only routines)。
针对这些不同的目标文件类型(.o/.os/.oS),glibc会先在子目录(下文源代码布局中将详细阐述)中依次构建。子目录所包含的Makefile会定义本目录涉及的source,并针对目标文件类型进行相应剪裁,也可能由sysdeps下的Makefile根据目标系统进行增减。
源代码布局
glibc源代码的顶级子目录主要分成以下几类:
include目录
包含库所包含的头文件,make install将会把它们安装到目标平台中。bits目录
和平台相关的头文件sysdeps目录
包含所有与平台相关的特定源代码。这个目录是glibc能在各平台间移植的关键,其包含的子目录布局遵照如下内部约定:依照维度划分代码
这些维度包含操作系统(os)/平台(platform)/字长(wordsize)/浮点计算(FPU)等,不同的维度间通过父子目录进行组合,例如:特定于操作系统的代码:sysdeps/unix/sysv/linux/
.[ch]
特定于操作系统以及平台的代码:sysdeps/unix/sysv/linux/powerpc/.[ch]
特定于字长的代码:sysdeps/unix/sysv/linux/powerpc/powerpc[32|64]/.[ch]
特定于平台的代码:sysdeps/powerpc/.[ch]
特定于FPU的代码:sysdeps/powerpc/powerpc32/fpu/.[ch] 目录间存在依赖关系
首先如果configure确认需要包含某个sysdeps子目录,则暗示该子目录的每一层父目录都应该被包含进来。
其次任意一个子目录还可以通过包含一个Implies文件说明其需要依赖不在自身路径中的其它sysdpes目录,例如unix/Implies中包含posix,因此点那个unix被包含时,posix也应一并考虑。
这两种层次使得目录间的依赖关系形成一张图。
上文提到过,执行*./configure的过程中就会确认构建目标需要依赖哪些sysdeps目录并写入变量config-sysdir中。这里就来描述下configure*脚本是如何作出选择的:
- 首先判断本次构建的目标config,以本文构建环境x86_64-pc-linux-gnu举例:
- machine: x86_64/64(注意configure会调用sysdeps/*/preconfigure中的判断逻辑最终确定)
- vendor:pc
- os:linux-gnu
- base-os:/unix/sysv(由os推算出)
- 将每个值扩展为一个子序列
- machine(在每一个层级考虑加入fpu和multiarch的组合)
/x86_64/64/fpu/multiarch /x86_64/64/fpu /x86_64/64/multiarch /x86_64/64 /x86_64/fpu/multiarch /x86_64/fpu /x86_64/multiarch /x86_64 - vendor
pc - os-try
linux-gnu /linux - base-os
/unix/sysv /unix
- 按照machine/base-os/vendor/os-try/machine的顺序得到上述子序列的笛卡尔积,查找源代码sysdeps下是否存在相应目录,由此得到满足构建环境要求的一组sysdeps目录sysnames
需要注意的是这里的每一个部分都可以取空字符串,保证遍历到所有可能。 - 遍历sysnames,按照以下逻辑处理其中每一个路径,得到最终的sysdir
- 将本路径输出至sysdir
- 检查本路径下是否存在Implies,如果存在,将其包含的每一个路径加入sysnames,如果不存在,将本路径的父路径加入sysnames
- 将sysdeps/generic加在最后
generic子目录服务于两个目标 - 作为所有平台特定实现的索引:如果索引不存在,make不会查找对应的平台对应实现
- 作为找不到对应平台特定实现时的兜底
通用模块目录
不依赖平台的模块源代码分布在不同的顶级子目录中,这些子目录由sysd-sorted文件中的sorted-subdirs变量定义。这些子目录包括:
Id | 目录名 | 备注 |
---|---|---|
1 | csu | |
2 | iconv | |
3 | locale | |
4 | localedata | |
5 | iconvdata | |
6 | assert | |
7 | ctype | |
8 | intl | |
9 | catgets | |
10 | math | |
11 | setjmp | |
12 | signal | |
13 | stdlib | |
14 | stdio-common | |
15 | libio | |
16 | dlfcn | |
17 | nptl | |
18 | malloc | |
19 | string | |
20 | wcsmbs | |
21 | timezone | |
22 | time | |
23 | dirent | |
24 | grp | |
25 | pwd | |
26 | posix | |
27 | io | |
28 | termios | |
29 | resource | |
30 | misc | |
31 | socket | |
32 | sysvipc | |
33 | gmon | |
34 | gnulib | |
35 | wctype | |
36 | manual | |
37 | shadow | |
38 | gshadow | |
39 | po | |
40 | argp | |
41 | rt | |
42 | conform | |
43 | debug | |
44 | mathvec | |
45 | support | |
46 | crypt | |
47 | nptl_db | |
48 | inet | |
49 | resolv | |
50 | nss | |
51 | hesiod | |
52 | sunrpc | |
53 | nis | |
54 | nscd | |
55 | login | |
56 | elf |
通用目录间可以通过Depend文件指定依赖关系,同时被sysdeps子目录中的Subdirs文件设置为该sysdeps子目录的依赖。
scripts
系统构建时会用到的脚本。benchtests
基准测试相关的脚本。htl/hurd/mach/posix/soft-fp
make关键机制
这一节介绍几个make的特色机制,了解之后才能更好地理解glibc的构建行为。
Makefile重制机制
Makefile是包含make规则的文件,可以被make命令直接调用或者通过include指令组织成层次结构。
以make命令直接调用的Makefile为入口,每当遇到include指令,make都会读取对应的Makefile文件。
当所有Makefile文件读取完成之后,make反向遍历读取的Makefile,查看是否存在对应规则,并决定是否需要重新生成对应的Makefile。
一旦任何Makefile发生了重制,make会回到整个命令的起点,重新开始整个流程。
有了这个机制,可以通过*-include foo读取一个当时并不存在,但可以被重制的Makefile。这里的’-‘确保如果该文件不存在,make*也不会报错退出。
相同目标的多条规则
如果制定了同一个目标的多条规则,make会把所有规则的依赖收集到一起来判断目标是否需要更新,但只会执行最后一条规则定义的构建操作。
延时求值
在读取Makefile过程中,并不是所有变量和规则都是马上求值的。make运行过程分成两趟:
- 第一趟负责读入所有Makefile,初始化所有变量和规则,并根据规则形成目标间的依赖关系
- 第二趟根据依赖关系决定该更新哪些目标
官方把第一趟中会求值的目标称为immediate,第二趟才求值的目标称为deferred,并提供了如下说明:
1 | immediate = deferred |
构建过程
明确了源代码的布局和make特色机制之后,是时候来看看glibc构建过程的细节了(为了简化,暂时忽略install/clean等目标)。
图1展示了通过加上*–debug来得到的make*细节。
图中的图标说明如下:
- 红色五角星:make过程中的关键点
- 脸色五角星:流程中涉及Makefile重制的部分
- 数字标示:重制过程的顺序,其中(a-b)代表第a轮的第b项
构建过程中的技巧
o-iterator.mk
在Makefile中有大量操作需要针对不同的目标后缀(.o/.os/.oS)重复执行,项目使用o-iterator来提炼循环:
1 | o := $(firstword $(object-suffixes-left)) |
通过以下方式调用循环:
1 | define o-iterator-doit |
标记完成
构建过程中大量使用各种标记记录某个文件已自动生成:
变量标记
例如在自动生成的sysd-rules(Makefile)中包含:1
sysd-rules-done := t
之后Makefile就可以通过ifndef sysd-rules-done来控制后续流程。
stamp标记
- 后缀标记
例如stdio_lim.st来标记和检测stdio_lim.h是否已经构建成功。 - 文件标记
在构建子目录时,通过stamp.o/.os/.oS来标示相应的文件是否已经构建成功,同时文件中包含相应的源代码列表。
- 后缀标记
编译模版
具体编译命令的模版在Makerules中,这里以编译csu/init-first.o为目标举例:
1 | gcc init-first.c -c -std=gnu11 -fgnu89-inline -g -O2 -Wall -Wwrite-strings -Wundef -Werror -fmerge-all-constants -frounding-math -fno-stack-protector -Wstrict-prototypes -Wold-style-definition -fmath-errno -fno-stack-protector -DSTACK_PROTECTOR_LEVEL=0 -ftls-model=initial-exec -I../include -I/root/source/glibc-2.31/build3/csu -I/root/source/glibc-2.31/build3 -I../sysdeps/unix/sysv/linux/x86_64/64 -I../sysdeps/unix/sysv/linux/x86_64 -I../sysdeps/unix/sysv/linux/x86/include -I../sysdeps/unix/sysv/linux/x86 -I../sysdeps/x86/nptl -I../sysdeps/unix/sysv/linux/wordsize-64 -I../sysdeps/x86_64/nptl -I../sysdeps/unix/sysv/linux/include -I../sysdeps/unix/sysv/linux -I../sysdeps/nptl -I../sysdeps/pthread -I../sysdeps/gnu -I../sysdeps/unix/inet -I../sysdeps/unix/sysv -I../sysdeps/unix/x86_64 -I../sysdeps/unix -I../sysdeps/posix -I../sysdeps/x86_64/64 -I../sysdeps/x86_64/fpu/multiarch -I../sysdeps/x86_64/fpu -I../sysdeps/x86/fpu/include -I../sysdeps/x86/fpu -I../sysdeps/x86_64/multiarch -I../sysdeps/x86_64 -I../sysdeps/x86 -I../sysdeps/ieee754/float128 -I../sysdeps/ieee754/ldbl-96/include -I../sysdeps/ieee754/ldbl-96 -I../sysdeps/ieee754/dbl-64/wordsize-64 -I../sysdeps/ieee754/dbl-64 -I../sysdeps/ieee754/flt-32 -I../sysdeps/wordsize-64 -I../sysdeps/ieee754 -I../sysdeps/generic -I.. -I../libio -I. -D_LIBC_REENTRANT -include /root/source/glibc-2.31/build3/libc-modules.h -DMODULE_NAME=libc -include ../include/libc-symbols.h -DTOP_NAMESPACE=glibc -o /root/source/glibc-2.31/build3/csu/init-first.o -MD -MP -MF /root/source/glibc-2.31/build3/csu/init-first.o.dt -MT /root/source/glibc-2.31/build3/csu/init-first.o |
对应的编译模版为:
1 | (CC) $< -c $(CFLAGS) $(CPPFLAGS) $(OUTPUT_OPTION) $(compile-mkdep-flags) |
变量之间的对应关系如下:
Id | 变量名 | 值 | 备注 |
---|---|---|---|
1 | $(CC) | gcc | 来自config.make |
2 | $(CFLAGS) | -std=gnu11 -fgnu89-inline -g -O2 | 来自Makeconfig |
3 | $(CPPFLAGS) | -Wall -Wwrite-strings -Wundef -Werror -fmerge-all-constants -frounding-math -fno-stack-protector -Wstrict-prototypes -Wold-style-definition -fmath-errno -fno-stack-protector -DSTACK_PROTECTOR_LEVEL=0 -ftls-model=initial-exec -I../include -I/root/source/glibc-2.31/build3/csu -I/root/source/glibc-2.31/build3 -I../sysdeps/unix/sysv/linux/x86_64/64 -I../sysdeps/unix/sysv/linux/x86_64 -I../sysdeps/unix/sysv/linux/x86/include -I../sysdeps/unix/sysv/linux/x86 -I../sysdeps/x86/nptl -I../sysdeps/unix/sysv/linux/wordsize-64 -I../sysdeps/x86_64/nptl -I../sysdeps/unix/sysv/linux/include -I../sysdeps/unix/sysv/linux -I../sysdeps/nptl -I../sysdeps/pthread -I../sysdeps/gnu -I../sysdeps/unix/inet -I../sysdeps/unix/sysv -I../sysdeps/unix/x86_64 -I../sysdeps/unix -I../sysdeps/posix -I../sysdeps/x86_64/64 -I../sysdeps/x86_64/fpu/multiarch -I../sysdeps/x86_64/fpu -I../sysdeps/x86/fpu/include -I../sysdeps/x86/fpu -I../sysdeps/x86_64/multiarch -I../sysdeps/x86_64 -I../sysdeps/x86 -I../sysdeps/ieee754/float128 -I../sysdeps/ieee754/ldbl-96/include -I../sysdeps/ieee754/ldbl-96 -I../sysdeps/ieee754/dbl-64/wordsize-64 -I../sysdeps/ieee754/dbl-64 -I../sysdeps/ieee754/flt-32 -I../sysdeps/wordsize-64 -I../sysdeps/ieee754 -I../sysdeps/generic -I.. -I../libio -I. -D_LIBC_REENTRANT -include /root/source/glibc-2.31/build3/libc-modules.h -DMODULE_NAME=libc -include ../include/libc-symbols.h -DTOP_NAMESPACE=glibc | 来自Makeconfig |
4 | $(OUTPUT_OPTION) | -o /root/source/glibc-2.31/build3/csu/init-first.o | 来自Makerules |
5 | $(compile-mkdep-flags) | -MD -MP -MF /root/source/glibc-2.31/build3/csu/init-first.o.dt -MT /root/source/glibc-2.31/build3/csu/init-first.o | 来自Makerules |
特别需要关注得是libc-symbols.h,其中定义了编译需要的基础宏定义(config.h也是在这里被include)。
总结
学习C语言有点像玩任天堂的游戏:上手简单,精通困难。其中一个很大的原因就是构建系统的及其繁杂:因为源码本身也可能是构建系统通过shell创建的。这种所见非所得加深了阅读源码的难度,更有甚者,很多源码中的符号竟然是引用了gcc这个编译器提供的功能,难怪IDE表示无能为力。
本篇作为glibc源码系列的第一篇,希望为大家建立一个big picture,也为后续研究铺平道路。