本文由 marsCatXDU_李经纬 翻译,如有谬误,还请大神们多多指教
原文地址:wiki.osdev.org/How_kernel,…
翻译的原文版本:23 December 2019, 10:56.
内核 (Kernel)
内核是操作系统的核心。在传统的设计中,内核负责内存管理、I/O 、中断管理等工作。
在微内核(Microkernel)、外内核(Exokernel)等现代的设计中,部分上述工作被迁移到了用户空间,这些情况不在本文的讨论范围内。
所有的内核都要通过系统调用(system calls)来提供服务,不同的内核的系统调用及返回方式不尽相同。
C 库(C Library)
更多内容见:C库wiki.osdev.org/C_Library ,创建 C库wiki.osdev.org/Creating_a_…
当我们开发自己的内核时,若想让系统支持 C 就必须添加 C 库,因为 C 语言程序的正常运行需要 C 库的支持(添加 C 库,可以移植已有的或自己写一个新的)。C 库实现标准的 C 函数(比如那些声明在 <stdlib.h>,<math.h>,<stdio.h> 等头文件中的函数)并提供了可在用户空间链接的对应二进制文件。
除了 IOS 标准中定义的标准 C 函数之外,C 库一般还会实现标准外的更多功能(比如 C 标准中完全没有提过的网络相关功能),例如在各种类UNIX 系统中的 POSIX 标准就定义了 C 库中需要包含的其他东西。需要注意,不同的操作系统提供的库可能会相当不一样。
C 库想要实现功能就必须使用系统调用函数,所以如果想要做一个支持 C 库的操作系统,就必须在系统中实现系统调用,并告诉 C 库应如何使用这些系统调用。
更多关于库函数调用的内容可见( wiki.osdev.org/Library_Cal… )C库wiki.osdev.org/C_Library ,创建C库 wiki.osdev.org/Creating_a_…
编译器 / 汇编器
汇编器能够将输入的纯文本源码文件转化为二进制格式的机器码:即将源代码转化为目标代码并添加上符号名、重定位信息等额外的信息。
编译器能够将高级语言的代码直接转换为目标文件,也可以先将高级语言转化为汇编语言,然后再调用汇编器将汇编语言转为目标文件。
但,编译器或汇编器生成的目标文件此时并没有包含任何 C 标准库中函数的代码,也就是说此时的代码并不完整,当然也就无法正常运行:举例来说,假如我们在代码中使用了 <stdio.h> 中的 printf() 并将其编译为了一个目标文件,那么在生成的目标文件中其实只有对 printf() 函数的引用。如果想要运行程序,还需要将这个函数的实现代码链接到目标文件中,才能将这个目标文件转为一个可以运行的可执行目标文件。
一些编译器会在编译器的内部使用库函数,这种情况下,即使我们没有 include 相应的头文件而且也没有调用这些头文件中声明的函数,目标文件还是有可能会引用 memset()、memcpy() 这些函数。这种情况下就必须为链接器提供包含这些函数实现的库了,否则链接器就会报错。GCC 的独立环境(freestanding environment)只需要 memset(),memcpy()、memcmp() 、memmove() 函数和 libgcc 库,一些高级的操作(比如 32 位系统中的 64 位除法)也许会用到编译器内部函数。在 GCC 中,这些函数实现在 libgcc 中。该库的内容与其所在的操作系统无关,也不会因为授权问题等影响编译后的内核。
C 库有两种: hosted 和 freestanding。
Hosted:提供完整的 C 标准库,用于用户环境编程; Freestanding:只有一些包含了定义和类型的头文件可用,用于内核编程
编译器默认使用 Hosted 。可以使用
-ffreestanding参数让编译器改用 freestandingGCC 包含的 Freestanding 头有:
<float.h>, <iso646.h>, <limits.h>, <stdalign.h>, <stdarg.h>, <stdbool.h>, <stddef.h>, <stdinth>, <stdnoreturn.h>以及 CPUID、SSE 等
链接器
链接器将编译器或汇编器生成的目标代码与 C 库(比如 libgcc.a 或者我们提供的其他什么库)进行链接。
链接方法有两种:静态链接和动态链接
静态链接
当进行静态链接时,链接器会在汇编完成后开始工作。链接器会检查目标代码中未解析的引用,并尝试用已有的库来解析这些引用,将具体的二进制代码从库中添加到目标代码中,生成可执行文件。使用静态链接生成的可执行文件只需要内核即可运行,而不需要其他的支持。
这种链接方式的坏处在于:二进制代码的直接复制会让可执行文件越来越大,而且会导致完全一样的库函数代码在磁盘和内存中被多个程序多次拷贝,浪费空间。
动态链接
使用动态链接的程序会在程序加载时启动链接器。目标文件中的未解析引用在此时才会和当前系统中的库链接。这种链接方式大大减小了可执行文件占用的磁盘空间,而且支持使用共享库等方法来节约内存空间、
动态链接听起来不错,但这样一来,可执行文件就无法在没有库的系统中运行了。
共享库
共享库可以让多个可执行文件动态链接。所有用到同一个库的可执行文件都可以引用内存中的同一个库中的代码,而不需要每个可执行文件都拷贝一份一模一样的代码到自身的代码中
实现共享库还需要解决一些问题,比如:共享库要么不能有其自身的状态(因为多个程序要共享使用同一段内存,所以库不能有静态数据或全局数据),要么为每个程序提供一个独立的状态。这在多线程系统中更加难处理——该类系统中,一个可执行文件可能会有多个控制流,让问题更加复杂。
在虚拟内存环境中,在所有程序的同一个虚拟内存地址提供共享库通常是做不到的。想要能在任何虚拟内存地址都能访问库代码就需要 PIC(Position Independent Code ,位置无关代码),我们可以在 GCC 中使用 -PIC 参数来创建 pic 库。该技术需要二进制格式的支持(也就是要包含重定位表),且会在一些架构中降低代码的运行效率
ABI - Application Binary Interface
系统的 ABI 定义了使用库函数调用和系统调用的具体方式,包含该怎样用栈或寄存器来传参、如何定位库中的函数入口点等。
使用静态链接生成的可执行文件会使用和其运行系统内核相同的 ABI ;而在动态链接中,可执行文件的 ABI 会与其实际使用的库的 ABI 保持一致
未解析符号
在链接阶段,我们通常会发现一些在我们不知情的情况下添加进程序且没有被环境提供的内容,比如 alloca(),memcpy() 等。这种情况的出现,通常意味着我们的工具链或命令行参数设置不正确,或者使用了库 / 运行时环境中没有实现的功能。如果链接错误并提示没有找到符号,可以检查一下是否有忘记链接的库。