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
2
3
4
5
6
wget https://ftp.gnu.org/gnu/glibc/glibc-2.31.tar.gz
tar -zxvf glibc-2.31.tar.gz
cd glibc-2.31
mkdir build && cd build
../configure --prefix=/tmp
make

其中configuremake命令都是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*脚本是如何作出选择的:

    1. 首先判断本次构建的目标config,以本文构建环境x86_64-pc-linux-gnu举例:
    • machine: x86_64/64(注意configure会调用sysdeps/*/preconfigure中的判断逻辑最终确定)
    • vendor:pc
    • os:linux-gnu
    • base-os:/unix/sysv(由os推算出)
    1. 将每个值扩展为一个子序列
    • 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
    1. 按照machine/base-os/vendor/os-try/machine的顺序得到上述子序列的笛卡尔积,查找源代码sysdeps下是否存在相应目录,由此得到满足构建环境要求的一组sysdeps目录sysnames
      需要注意的是这里的每一个部分都可以取空字符串,保证遍历到所有可能。
    2. 遍历sysnames,按照以下逻辑处理其中每一个路径,得到最终的sysdir
    • 将本路径输出至sysdir
    • 检查本路径下是否存在Implies,如果存在,将其包含的每一个路径加入sysnames,如果不存在,将本路径的父路径加入sysnames
    1. 将sysdeps/generic加在最后
      generic子目录服务于两个目标
    2. 作为所有平台特定实现的索引:如果索引不存在,make不会查找对应的平台对应实现
    3. 作为找不到对应平台特定实现时的兜底
  • 通用模块目录
    不依赖平台的模块源代码分布在不同的顶级子目录中,这些子目录由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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
immediate = deferred
immediate ?= deferred
immediate := immediate
immediate ::= immediate
immediate += deferred or immediate
immediate != immediate
define immediate
deferred
endef
define immediate =
deferred
endef
define immediate ?=
deferred
endef
define immediate :=
immediate
endef
define immediate ::=
immediate
endef
define immediate +=
deferred or immediate
endef
define immediate !=
immediate
endef

构建过程

明确了源代码的布局和make特色机制之后,是时候来看看glibc构建过程的细节了(为了简化,暂时忽略install/clean等目标)。
图1展示了通过加上*–debug来得到的make*细节。
图1 glibc的make流程

图中的图标说明如下:

  • 红色五角星:make过程中的关键点
  • 脸色五角星:流程中涉及Makefile重制的部分
  • 数字标示:重制过程的顺序,其中(a-b)代表第a轮的第b项

构建过程中的技巧

o-iterator.mk

在Makefile中有大量操作需要针对不同的目标后缀(.o/.os/.oS)重复执行,项目使用o-iterator来提炼循环:

o-iterator.mk
1
2
3
4
o := $(firstword $(object-suffixes-left))
object-suffixes-left := $(filter-out $o,$(object-suffixes-left)) // shift one suffix each time

$(o-iterator-doit)

通过以下方式调用循环:

1
2
3
4
5
define o-iterator-doit
do something with $o
endef
object-suffixes-left := $(all-object-suffixes) // reset to all suffixes
include o-iterator.mk

标记完成

构建过程中大量使用各种标记记录某个文件已自动生成:

  • 变量标记
    例如在自动生成的sysd-rules(Makefile)中包含:

    1
    sysd-rules-done := t

    之后Makefile就可以通过ifndef sysd-rules-done来控制后续流程。

  • stamp标记

    1. 后缀标记
      例如stdio_lim.st来标记和检测stdio_lim.h是否已经构建成功。
    2. 文件标记
      在构建子目录时,通过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,也为后续研究铺平道路。

参考阅读

  1. GCC 在线文档
  2. glibc
  3. GNU coding standard
  4. GNU make
  5. GNU autoconf