How kernel, compiler, and C library work together

来自osdev
Zhang3讨论 | 贡献2022年2月18日 (五) 04:57的版本
跳到导航 跳到搜索

内核

内核是操作系统的核心。 在传统设计中,它负责内存管理,I/O,中断处理以及其他各种事情。 即使像MicrokernelExokernel这样的一些现代设计,将上面这些的部分服务移动到了用户空间,但这在本文的范围内并不重要。

内核通过一组系统调用使其对外服务可用; 它们的调用方式以及它们的作用因内核而异。

C库

主要文章:参看 C LibraryCreating a C Library

当你开始处理你的内核时,最开始你会遇到一个问题:你没有一个可用的C库。 你必须自己提供一切,除了编译器本身提供的一些基础功能。 你还必须移植现有的C库或自己编写一个。

C库实现标准C函数 (即 <stdlib.h>, <math.h>, <stdio.h>等头文件中声明的东西),并以二进制形式提供它们,适合与用户空间应用程序链接。

除了标准C函数(如ISO标准中所定义)之外,你的C库还可能需要(而且通常确实)实现更多的功能,这些功能可能由某些标准定义,也可能不由某些标准定义。 例如,标准C库对联网只字不提。 对于类Unix系统,POSIX标准定义了C库的预期内容; 其他系统则可能根本不同。

应该注意的是,为了实现其功能,C库一定会调用内核函数。 因此,对于你自己的操作系统,你当然可以选择一个现成的C库,然后针对你的操作系统重新编译它- 但这需要你告诉库如何调用你的内核函数,同时内核能提供这些函数的实际实现。

Library Calls中提供了一个更详细的示例,或者,你可以使用现有的C Library创建你自己的C库

编译器/汇编器

汇编器的任务是获取 (纯文本) 源代码并将其转换为 (二进制) 机器代码; 更准确地说,它将源代码转换为 object(目标) 代码,其中包含其他信息,例如符号名称,重定位信息等。

编译器的任务是获取更高级的语言源代码,然后或者直接将其转换为目标代码,或者(就像GCC一样)将其转换为汇编程序源代码,并在最后一步调用汇编程序。

生成的目标代码还尚未包含任何被调用的标准函数代码。 如果你 包含d,例如 <stdio.h> 并使用 printf(),目标代码将仅包含一个 “引用”,说明名为 printf() 的函数 (并采用 const char * 和许多未命名的参数作为参数) 必须链接到目标代码才能接收完整的可执行文件。

有些编译器在内部已认为可以直接使用标准库函数,即使你还没有包含头文件或使用此名称的函数,这些函数能直接成为对象文件的引用,例如memset()memcpy()。 但你必须向链接器提供这些函数的实现,否则链接将失败。 GCC独立环境只期望函数 memset()memcpy()memcmp()memmove(),以及 libgcc 库。 一些高级操作(例如32位系统上的64位除法)可能涉及编译器内部函数。(译者注:这里指64位除法是编译器封装出来给高级语言的,并不是CPU指令能提供的功能) 对于GCC,这些函数驻留在libgcc中。 该库的内容与你使用的操作系统无关,并且不会因任何类型的许可证问题而污染你的编译内核。

链接器

链接器获取编译器/汇编器生成的目标代码,并将其链接到C库(包括libgcc.a或你提供的任何其它链接库)。 这可以通过两种方式来完成: 静态和动态。

静态链接

静态链接时,链接器在编译器/汇编程序执行完之后被调用。 链接器获取目标代码,检查它是否有未解决的引用,并检查是否可以从可用的库中解析出这些引用。 然后将这些库中的二进制代码添加到可执行文件中;在这个过程之后,可执行文件是完整的,也就是说,当运行它时,除了内核之外,不需要任何东西。

静态链接不利的一面是,可执行文件可能会变得相当大,库中的相同代码会在磁盘和内存中反复复制。

动态链接

动态链接时,链接器在加载可执行文件期间被调用。 然后针对系统中当前存在的库解析目标代码中未解析的引用。 这使得磁盘上的可执行文件要小得多,并允许一些在内存中节省空间的策略,例如 共享库 (请参见下文)。

但从另一方面来说,可执行文件得依赖于它引用的库的存在; 如果系统没有这些库,则无法运行可执行文件。

共享库

共享库是一种跨多个可执行文件共享动态链接库的流行策略。 这意味着,不是将库的二进制文件附加到可执行映像,而是调整可执行文件中的引用,以便所有可执行文件引用所需库的相同内存表示形式。

这需要一些技术技巧。 首先,库必须完全没有任何状态(指静态或全局数据),或者执行共享库必须能为每个可执行文件提供单独的状态。 这在多线程系统中变得更加棘手,在多线程系统中,一个可执行文件可能同时具有多个控制流。

其次,在虚拟内存环境中,通常不可能以相同的虚拟内存地址向系统中的所有可执行文件提供一个库。 要在任意虚拟地址访问库代码,需要库代码位置独立(position independent) (这可以通过为GCC编译器设置-pic命令行选项来实现)。 这需要二进制格式 (重定位表) 支持该功能,并且可能导致某些体系结构上的代码效率稍低。

ABI - 应用程序二进制接口

系统的ABI定义了库函数调用和内核系统调用的实际执行方式。 这些规则包括参数是在堆栈上还是在寄存器中传递,函数入口点在库中的位置以及其他此类问题。

当使用静态链接时,运行可执行文件的操作系统内核使用和可执行文件当初被构建时一样的ABI,这样可执行文件就可以正常被内核运行; 当使用动态链接时,可执行文件也依赖于库的ABI保持不变。

未解析符号

链接器执行的阶段,你会遇到一些你不知道的东西被添加了,而这些东西不是你的环境提供的。然后你就会得到Unresolved Symbols(未解析符号)的错误提示。 链接器提示的可能是关于alloca()memcpy()或其他几个引用的错误信息。 这通常表明你的工具链或命令行选项未以编译你自己的OS内核正确设置- 或者你正在使用尚未在C库/运行时环境中实现的功能! 如果你没有使用交叉编译器,没有libgcc库,或者没有memcpy、memmove、memset和memcmp的实现,那么你肯定会遇到麻烦。

其他符号,如_udiv* or __builtin_saveregs,也在libgcc中可用。 如果你遇到缺少这些符号的错误提示,请记住你需要与libgcc链接。