《程序员自我修养》读书总结十一Author: Once Day Date: 2026年2月5日一位热衷于Linux学习和开发的菜鸟试图谱写一场冒险之旅也许终点只是一场白日梦…漫漫长路有人对你微笑过嘛…全系列文章可参考专栏: 书籍阅读_Once-Day的博客-CSDN博客参考文章:《程序员的自我修养》读书笔记 | Zachary’s blog《程序员的自我修养》阅读笔记 - T0fV404 - 博客园读书笔记《程序员的自我修养》 - 楷哥 - 博客园文章目录《程序员自我修养》读书总结十一11. 运行库11.1 入口函数和程序初始化11.2 C/C运行库11.3 运行库与多线程11.4 C全局构造与析构11. 运行库11.1 入口函数和程序初始化对于大多数 C/C 开发者而言main函数似乎是程序执行的起点。然而实际上在main被调用之前操作系统已经将控制权交给了运行库提供的入口函数Entry Point由该函数完成一系列关键的初始化工作后才会跳转至用户编写的main函数。同样地当main返回之后入口函数还需要负责资源的清理与进程的退出。这一前后包裹main函数的过程构成了程序完整的生命周期。在glibc中程序的真正入口是_start该符号由链接器默认指定为可执行文件的起始地址。_start的主要职责是从栈上提取由内核压入的argc、argv和envp等参数随后调用__libc_start_main。在这个函数内部依次完成以下工作设置线程相关的数据结构、注册fini终止清理函数通过__cxa_atexit、执行init段中的全局初始化函数最终调用main(argc, argv, envp)。当main返回后其返回值作为参数传递给exit触发通过atexit注册的清理函数链并最终通过_exit系统调用终止进程。整个流程可概括如下_start → __libc_start_main → __libc_init_first / __init → main(argc, argv, envp) → exit() → _exit()MSVC的运行库采用了类似但略有不同的结构。以控制台程序为例链接器默认的入口函数为mainCRTStartup。该函数首先调用操作系统 API 获取命令行字符串和环境变量再自行解析为argc/argv的形式——这与glibc直接从内核栈上获取参数的方式不同。随后mainCRTStartup完成堆初始化_heap_init、I/O 初始化_ioinit、全局变量初始化_initterm等步骤再调用main函数。对于Windows下的图形界面程序对应的入口函数则是WinMainCRTStartup其调用的用户函数为WinMain。对比项glibcLinuxMSVCWindows入口符号_startmainCRTStartup参数获取方式内核压栈_start直接读取调用GetCommandLineA等 API 解析核心初始化函数__libc_start_mainmainCRTStartup内部序列堆初始化brk/mmap由malloc首次调用触发显式调用_heap_init退出路径exit→_exitexit→ExitProcess运行库的 I/O 初始化是入口函数中不可或缺的一环。在用户代码能够使用printf或scanf之前运行库需要建立起标准输入stdin、标准输出stdout和标准错误stderr三个流与操作系统文件描述符之间的绑定关系。在glibc中这三个FILE结构体作为全局对象在库内部静态定义并在初始化阶段与文件描述符0、1、2完成关联。MSVC则通过_ioinit函数初始化一个内部的文件句柄表将操作系统层面的句柄映射到 C 运行库的文件描述符体系中。此外I/O 初始化还涉及缓冲区的配置。标准流默认采用行缓冲stdout或无缓冲stderr策略而普通文件流则为全缓冲模式。这些缓冲区并非在初始化阶段一次性分配而通常在首次执行读写操作时按需创建以避免不必要的内存开销。这种延迟初始化lazy initialization的设计在运行库的多个模块中均有体现是一种常见的性能优化策略。11.2 C/C运行库C/C 运行库C Runtime Library简称CRT是支撑 C/C 程序运行的基础设施其职责远不止提供标准函数的实现。从宏观角度看CRT涵盖了两大核心功能一是构建程序的运行环境包括入口函数中执行的启动与退出逻辑二是提供语言标准所规定的库函数实现。前者确保程序在进入main之前拥有可用的堆、I/O 通道和全局状态后者则为开发者提供字符串处理、数学运算、文件操作等基本能力。两者共同构成了从裸操作系统接口到高级语言编程模型之间的桥梁。按功能划分CRT所实现的模块大致可归纳为以下几类模块类别典型内容说明启动与退出_start、mainCRTStartup、atexit、exit程序生命周期的入口和出口标准函数string.h、math.h、stdlib.h字符串、数学、类型转换等通用工具I/O 函数printf、scanf、fopen、fread格式化与文件 I/O 操作堆管理malloc、free、calloc、realloc动态内存分配与释放语言实现变长参数、setjmp/longjmp、多字节字符支撑语言特性所需的底层机制调试支持assert、内存泄漏检测、栈保护开发阶段的诊断与安全辅助其中堆管理模块是CRT中最复杂的部分之一。malloc的实现通常需要在用户态维护空闲链表或内存池结构仅在现有空间不足时才通过系统调用如Linux下的brk/mmapWindows下的HeapAlloc向操作系统申请新的内存页。调试支持模块则在Release和Debug版本之间差异显著例如MSVC的Debug CRT会在malloc分配的内存块前后填充哨兵字节0xFD并在free时将已释放的内存填充为0xDD以便检测越界访问和使用已释放内存等常见错误。C 语言标准库的历史可以追溯到 1970 年代。早期的 C 语言并不具备正式的库规范各个UNIX系统各自提供风格迥异的函数集。1989 年ANSI发布了 C89 标准即ANSI C首次对标准库的接口进行了统一定义确立了stdio.h、stdlib.h、string.h等 15 个标准头文件。此后C99标准新增了stdbool.h、stdint.h、complex.h等头文件将标准头文件扩展到 24 个。C11则进一步引入了threads.h、stdatomic.h等多线程与原子操作相关的接口使标准库的覆盖面从单线程环境延伸到了并发编程领域。每一次标准的演进都对运行库的实现提出了新的要求。C89标准库按功能可大致分为以下几组以stdio.h和stdlib.h为代表的 I/O 与通用工具函数以string.h和ctype.h为代表的字符串与字符处理函数以math.h和float.h为代表的数学与浮点支持以setjmp.h、signal.h、stdarg.h为代表的语言机制支撑以及locale.h、time.h、errno.h等系统相关的辅助设施。这些头文件仅定义了接口契约具体的实现则由各平台的运行库自行完成因此同一段符合标准的 C 代码在不同运行库上的行为理论上一致但性能特征和内部细节可能存在较大差异。在Linux平台上glibcGNU C Library是最主流的 C 运行库实现。glibc不仅实现了ISO C标准规定的全部接口还提供了大量POSIX扩展和GNU扩展例如getopt_long、asprintf以及对Linux系统调用的直接封装。glibc以动态链接库libc.so.6的形式存在于几乎所有主流Linux发行版中其内部采用了高度优化的汇编实现如memcpy、strlen等热点函数并针对不同处理器架构提供了专门的优化路径。除glibc外嵌入式领域常用的替代方案包括musl、uClibc-ng等它们以更小的体积和更简洁的实现换取了在资源受限环境下的适用性。MSVC的运行库则与Windows操作系统深度绑定。历史上MSVC CRT以版本号命名的 DLL 形式发布如msvcrt.dll、msvcr120.dll不同版本的Visual Studio编译出的程序依赖不同版本的运行库 DLL这在部署时常常引发版本兼容问题。自Visual Studio 2015起微软对CRT进行了重大重构将其拆分为通用 CRTUniversal CRT即ucrtbase.dll和编译器专属的vcruntime两部分。ucrtbase.dll作为Windows操作系统组件随系统更新分发解决了长期存在的版本碎片化问题。两者的核心差异在于设计哲学和生态定位。glibc遵循POSIX标准强调跨UNIX系统的可移植性其源代码公开允许社区审计和贡献。MSVC CRT则优先保证与Windows API的紧密协作提供了如_beginthread、_CrtDumpMemoryLeaks等平台专属扩展。在链接方式上glibc几乎总是动态链接而MSVC提供了静态链接/MT和动态链接/MD两种选项供开发者选择。尽管两者的外部接口均遵循ISO C标准但在错误处理如errno的线程安全实现方式、内存分配策略、以及对未定义行为的处理倾向上仍存在不少细微差别这也是跨平台开发中需要特别注意的地方。11.3 运行库与多线程多线程编程的正确性在很大程度上取决于对数据可见性的理解——哪些数据是线程私有的哪些是线程间共享的。一般而言每个线程拥有三类私有资源栈、线程局部存储Thread Local Storage简称TLS以及寄存器上下文。栈是最直观的私有空间每个线程在创建时由操作系统或运行库分配独立的栈区域Linux下pthread默认分配 8MB 栈空间Windows默认为 1MB。函数内的局部变量、参数和返回地址均保存在各自线程的栈上天然不存在竞争问题。寄存器上下文则在线程切换时由操作系统负责保存和恢复包括通用寄存器、程序计数器PC/RIP、栈指针SP/RSP以及浮点/向量寄存器等确保线程恢复执行时的状态与被挂起时完全一致。与私有资源相对线程间共享的数据范围相当广泛。全局变量和函数内的静态变量static局部变量存储在进程的.data或.bss段中所有线程均可直接访问和修改。堆上通过malloc或new分配的内存同样是共享的——只要一个线程持有某块堆内存的指针其他线程同样可以通过该指针进行读写。此外程序的代码段.text在所有线程间共享且只读而通过fopen等函数打开的文件描述符也属于进程级资源多个线程对同一文件的并发读写如果缺乏同步保护将导致数据交错和不一致。这些共享数据正是多线程程序中竞态条件和数据竞争的根源。资源类型归属典型示例栈线程私有局部变量、函数调用链寄存器线程私有RIP、RSP、XMM0等TLS线程私有errno、strtok内部状态全局/静态变量线程共享.data、.bss段中的变量堆内存线程共享malloc/new分配的内存文件描述符线程共享fopen打开的FILE*代码段线程共享.text段正因为存在大量共享资源运行库本身必须具备线程安全性否则即使用户代码编写正确仍可能在库函数内部触发竞争。早期的 C 运行库在设计时并未考虑多线程场景许多函数使用了内部静态缓冲区来保存中间状态典型的例子包括strtok、asctime、gmtime等——它们在连续调用之间通过静态变量维持上下文在多线程环境下极易产生数据覆盖。为此POSIX标准引入了带_r后缀的可重入版本如strtok_r、gmtime_r要求调用方自行提供缓冲区。MSVC则采用了不同的策略从Visual Studio 2005开始其CRT默认将大部分涉及静态状态的函数改为使用TLS存储中间结果从而在不改变 API 签名的前提下实现了线程安全。errno的线程安全化是运行库多线程改造中最具代表性的案例。在单线程时代errno是一个简单的全局整型变量。进入多线程时代后如果两个线程先后调用了可能失败的库函数后一个线程的errno赋值会覆盖前一个线程尚未读取的错误码。现代CRT的解决方案是将errno定义为一个宏展开后实际调用一个返回线程私有存储指针的函数。以glibc为例errno被定义为(*__errno_location())该函数返回当前线程TLS区域中errno变量的地址从而使每个线程拥有独立的errno副本。在链接层面MSVC历史上曾将运行库分为单线程版本/MLlibc.lib和多线程版本/MTlibcmt.lib。单线程版本省略了锁操作以获得更高性能但在多线程程序中使用会导致未定义行为。自Visual Studio 2005起微软移除了单线程CRT所有程序统一使用多线程版本彻底消除了因误选链接选项导致的隐患。glibc则始终以单一版本覆盖单线程和多线程场景线程相关的支持通过链接libpthread.so-lpthread来启用。线程局部存储的实现机制因平台和使用方式而有所不同。从语言层面看C11标准引入了_Thread_local关键字C11则提供了thread_local关键字GCC的扩展语法为__threadMSVC使用__declspec(thread)。当编译器遇到这些声明时会将对应变量放入可执行文件的.tdata已初始化或.tbss未初始化段中。在运行时操作系统为每个线程分配该段的独立副本。以x86-64 Linux为例TLS变量的访问通过FS段寄存器加偏移量完成典型的访问指令形如mov eax, fs:[offset]几乎不产生额外的性能开销。// GCC / Clang__threadinttls_var0;// C11 标准_Thread_localinttls_var20;// MSVC__declspec(thread)inttls_var30;除编译期的静态TLS外运行库还提供了运行期动态分配TLS槽位的 API。POSIX系统下为pthread_key_create/pthread_setspecific/pthread_getspecificWindows下对应TlsAlloc/TlsSetValue/TlsGetValue。动态TLS的实现通常依赖于线程控制块TCB中的一个指针数组每个槽位对应数组中的一个索引。与静态TLS直接通过段寄存器寻址不同动态TLS需要经过函数调用和间接寻址性能略逊但胜在灵活——特别是在动态链接库DLL/SO中由于加载时机不确定动态TLS往往是更为稳妥的选择。11.4 C全局构造与析构C 允许在全局作用域或命名空间作用域定义具有构造函数的对象这些对象必须在main函数执行之前完成构造并在main返回之后按逆序析构。这一语义看似简单但其实现需要编译器与运行库的紧密配合。核心挑战在于编译器在编译每个翻译单元时能够确定本单元中有哪些全局对象需要构造但无法得知最终链接时所有翻译单元的全局对象集合。因此编译器和链接器需要一种协作机制将分散在各目标文件中的构造函数指针汇集到统一的数据结构中供运行库在启动阶段遍历调用。在glibc与GCC的协作体系中这一机制通过.init_array和.fini_array两个特殊的ELF段来实现。当编译器遇到一个全局 C 对象时会为其生成一个包装函数通常以编译器内部命名规则标识该函数内部调用对象的构造函数。随后编译器在目标文件的.init_array段中写入一个指向该包装函数的指针。链接阶段链接器将所有目标文件的.init_array段合并为一个连续的函数指针数组。运行库在__libc_start_main中调用__libc_csu_init该函数遍历合并后的.init_array数组依次调用每个函数指针从而完成所有全局对象的构造。编译器生成: .init_array: [ __static_init_func1, __static_init_func2, ... ] .fini_array: [ __static_fini_func1, __static_fini_func2, ... ] 链接器合并所有 .o 的 .init_array → 最终可执行文件中的连续数组 运行时: __libc_csu_init() → 遍历 .init_array → 逐一调用构造包装函数析构过程的实现则依赖于__cxa_atexit函数。当每个全局对象的构造包装函数被调用时在执行完构造函数之后会紧接着调用__cxa_atexit注册该对象对应的析构函数。__cxa_atexit的原型为int __cxa_atexit(void (*func)(void*), void* arg, void* dso_handle)其中dso_handle参数用于标识该析构函数所属的共享库这在动态库被dlclose卸载时尤为关键——运行库能够根据此标识仅调用属于该库的析构函数而不影响其他模块。当main返回或exit被调用时运行库以后进先出的顺序遍历__cxa_atexit注册的函数列表确保析构顺序与构造顺序严格相反从而满足 C 标准对全局对象生命周期的要求。exit()main().init_arrayglibc CRT操作系统exit()main().init_arrayglibc CRT操作系统_start → __libc_start_main__libc_csu_init 遍历 .init_array构造全局对象 __cxa_atexit 注册析构调用 main(argc, argv, envp)main 返回调用 exit()按 LIFO 顺序调用 __cxa_atexit 注册的析构函数_exit 终止进程值得一提的是在较早的GCC版本和ELF规范中全局构造与析构使用的是.ctors/.dtors段配合_init/_fini函数的方案。.ctors段本质上也是一个函数指针数组但其遍历逻辑由crtbegin.o和crtend.o中的辅助代码负责。现代GCC和glibc已经全面转向.init_array/.fini_array方案后者在语义上更清晰且支持优先级属性——开发者可通过__attribute__((init_priority(N)))指定构造函数的调用顺序链接器会据此对.init_array中的条目进行排序。MSVC的全局构造与析构机制在原理上类似但采用了不同的段命名和组织方式。MSVC编译器将全局对象的构造函数指针放置在以.CRT$XCU命名的PE/COFF段中。PE格式的链接器会将名称前缀相同的段按字母序合并因此.CRT$XCA、.CRT$XCU、.CRT$XCZ最终形成一个连续区域——XCA和XCZ分别由CRT启动代码定义为数组的起始和结束哨兵XCU则是用户目标文件贡献的构造函数指针。启动函数mainCRTStartup内部调用_initterm函数该函数接收起始和结束指针作为参数遍历区间内所有非空的函数指针并依次调用// MSVC CRT 中 _initterm 的简化实现typedefvoid(__cdecl*PVFV)(void);void__cdecl_initterm(PVFV*pfbegin,PVFV*pfend){for(;pfbeginpfend;pfbegin){if(*pfbegin!NULL){(**pfbegin)();}}}MSVC的析构处理同样依赖atexit机制。每个全局对象在构造完成后通过atexit注册其析构函数。与glibc的__cxa_atexit携带dso_handle参数不同MSVC的atexit不直接关联模块信息因此在涉及 DLL 动态加载和卸载的场景中需要更谨慎地管理全局对象的生命周期。MSVC同样保证atexit注册的函数以LIFO顺序调用析构顺序与构造顺序相反。此外MSVC将初始化函数指针段进一步细分为.CRT$XIC 初始化如浮点环境设置和.CRT$XCC 构造确保 C 层面的初始化始终先于 C 全局对象构造执行。对比项glibc / GCCMSVC构造函数指针段.init_array.CRT$XCU段合并方式链接器合并同名段PE按段名字母序合并遍历调用函数__libc_csu_init_initterm析构注册机制__cxa_atexit含dso_handleatexit优先级控制init_priority属性段名字母序如XCL先于XCU旧方案.ctors/.dtors无明显历史迁移需要注意的是C 标准并未规定不同翻译单元之间全局对象的构造顺序——这就是著名的静态初始化顺序问题Static Initialization Order Fiasco。如果两个分别定义在不同.cpp文件中的全局对象存在依赖关系其构造顺序取决于链接器处理目标文件的先后这在不同构建配置下可能产生不同的结果。常用的规避方案是采用函数内静态局部变量即Meyers Singleton利用 C11 保证的线程安全局部静态初始化语义将构造时机推迟到首次使用时从而消除跨翻译单元的顺序依赖。Once Day也信美人终作土不堪幽梦太匆匆......如果这篇文章为您带来了帮助或启发不妨点个赞和关注(◕‿◕)感谢您的阅读与支持~~~