“How kernel, compiler, and C library work together”的版本间差异

来自osdev
跳到导航 跳到搜索
(创建页面,内容为“==内核== 内核是操作系统的核心。 在传统设计中,它负责内存管理、I/O、中断处理和其他各种事情。 即使像Microkernels或exockernels这样的一些现代设计将这些服务中的一些移动到了用户空间,但这在本文档的范围内并不重要。 内核通过一组系统调用使其服务可用;它们的调用方式以及它们所做的工作因内核而异。 ==C 库== :''Main Articles: See C Libr…”)
 
 
(未显示同一用户的4个中间版本)
第1行: 第1行:
==内核==
==内核==


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


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


==C 库==
==C库==


:''Main Articles: See [[C Library]], [[Creating a C Library]]''
:''主要文章:参看 [[C Library]][[Creating a C Library]]''


首先,当您开始处理内核时,您没有可用的C库。 您必须自己提供所有内容,除了编译器本身提供的一些片段。 You will also have to port an existing C library or write one yourself.
当你开始处理你的内核时,最开始你会遇到一个问题:你没有一个可用的C库。 你必须自己提供一切,除了编译器本身提供的一些基础功能。 你还必须移植现有的C库或自己编写一个。


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


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


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


[[Library Calls]]中提供了更详细的示例,或者,您可以使用现有的[[C Library]]或[[Creating A C Library | create your own C Library]]。
[[Library Calls]]中提供了一个更详细的示例,或者,你可以使用现有的[[C Library]]或[[Creating A C Library |创建你自己的C库]]。


==Compiler / Assembler==
==汇编器和编译器==


汇编程序获取(明文)源代码并将其转换为(二进制)机器代码;更准确地说,它将源代码转换为“对象”代码,其中包含诸如符号名称、重新定位信息等附加信息。
汇编器的任务是获取 (纯文本) 源代码并将其转换为 (二进制) 机器代码; 更准确地说,它将源代码转换为 ''object(目标)'' 代码,并且目标代码中还包含其他信息,例如符号名称,重定位信息等。


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


生成的对象代码“不”包含调用的标准函数的任何代码。如果您<tt>包括<tt>d,例如<tt><stdio。h> </tt>并且使用<tt>printf()</tt>,目标代码将仅包含一个“引用”,说明名为<tt>printf()</tt>的函数(并将<tt>常量char*</tt>和许多未命名参数作为参数)必须链接到目标代码才能接收完整的可执行文件。
生成的目标代码还''尚未''包含任何被调用的标准函数代码。 例如你<tt>include</tt> <tt><stdio.h></tt> 并且使用<tt>printf()</tt>,这样得到的目标代码将仅包含一个''引用'',说明名为 <tt>printf()</tt> (并且采用 <tt>const char *</tt> 等其它参数作为参数清单)的函数必须链接到目标代码才能接收完整的可执行文件。


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


==Linker==
==链接器==


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


===静态链接===
===静态链接===


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


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


===动态链接===
=== 动态链接 ===


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


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


===共享库===
=== 共享库 ===


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


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


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


===ABI - 应用程序二进制接口===
===ABI(Application Binary Interface)- 应用程序二进制接口===


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


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


===未解析符号===
===“未解析符号(Unresolved Symbols)“错误 ===


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


其他符号,如_udiv*或u builtin_saveregs,在[[libgcc]]中提供。 如果您发现缺少这些符号的错误,请记住您需要链接libgcc。
其他符号,如 _udiv* 或 __builtin_saveregs,也在[[libgcc]]中可用。 如果你遇到缺少这些符号的错误提示,请记住你需要与libgcc链接。

2022年2月18日 (五) 14:16的最新版本

内核

内核是操作系统的核心。 在传统设计中,它负责内存管理,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一样)将其先转换为汇编源代码,并在最后调用汇编器。

生成的目标代码还尚未包含任何被调用的标准函数代码。 例如你include <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(Application Binary Interface)- 应用程序二进制接口

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

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

“未解析符号(Unresolved Symbols)“错误

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

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