我正在尝试为 Raspberry Pi 制作一个操作系统(没什么大不了的,只是为了好玩),虽然我可以用汇编语言编写它,但这比用 C 语言编写要困难得多。我想知道是否(以及为什么如果我不能,则不会)我可以将 C 库(文件)包含在操作系统中,这样我就不必重写它们了。因为库本身是用 C 编写的,所以它不会起作用吗?
1 回答
不,您必须将 C 库移植到您的操作系统,因为该库具有挂钩到操作系统细节的“存根”。C 标准要求某些标头以独立模式存在,您始终可以使用该模式。但是像 printf 这样的库函数必须自己实现或通过填写存根来移植。看看newlib看看你必须做的工作。它至少需要一个具有系统调用接口(用于读取、写入等)的工作内核。这些将取决于您的操作系统中可用的功能(例如,文件系统)。摘自常见问题解答:
- 将 newlib 移植到新平台需要执行哪些步骤?
一个基本端口需要更改一些文件并添加一些目录。
为您的平台添加一个子目录到 newlib/libc/machine 目录
在此目录中,您需要有一个 setjmp/longjmp 实现。这是必需的,因为 setjmp/longjmp 通常是汇编程序。查看 libc/machine/fr30 目录并复制/修改其中的文件。
编辑 newlib/libc/include/machine/ieeefp.h
这为您的平台定义了 ieee 字节序。编译器应该定义一些标识你的机器的东西。在某些情况下,字节序可能是编译器选项,因此除了平台标识符之外,您可能还需要检查另一个定义。请参阅文件中的示例。
编辑 newlib/libc/include/machine/setjmp.h
您需要指定 setjmp 缓冲区特性以匹配您的 setjmp/longjmp 实现。这只是 setjmp 缓冲区的大小。有关示例,请参见文件。
编辑 newlib/libc/include/sys/config.h
这根据需要有各种定义。大多数情况下,它定义了一些最大值。有些默认值可能适用于您的平台,在这种情况下您无需执行任何操作。
编辑configure.host
您需要添加配置,以便 newlib 可以识别它。您应该通过 machine_dir 变量为您的平台指定新的机器目录。如果需要,您可以添加特殊的 newlib 编译标志。sys_dir 用于操作系统的东西,所以你不需要改变它。较旧的平台使用 sys_dir 来实现系统调用,但这是不正确的,并且是历史上的麻烦事。syscall_dir 是一种选择,但我建议默认指定 syscall_dir=syscalls。阅读 newlib/libc/include/reent.h 中的注释以了解选择的说明。
将平台子目录添加到 libgloss
您需要为您的平台添加一个 bsp。这是 newlib 和所需的任何链接器脚本所需的最小系统调用集。这因板而异(也可以是模拟器)。有关示例,请参见 mn10300 或 fr30。您将需要编辑 configure.in 并重新生成配置,以便它构建您的新文件。默认情况下,您会得到 libnosys,它为您提供一组默认的系统调用存根。大多数存根只是返回失败。您仍然需要提供 __exit 例程。这可以像生成异常以停止程序一样简单。
可能覆盖头文件
如果您需要覆盖任何默认机器头文件,您可以将机器目录添加到 newlib/libc/machine/ 该子目录中的头文件将覆盖在 newlib/libc/include/machine 中找到的默认值。您可能不需要这样做。
这假设您已经处理了将新配置添加到顶级目录文件的操作。
现在linux是一个不同的动物。它是一个拥有大量系统调用的操作系统。如果您查看 newlib/libc/sys/linux 目录,您会发现那里有许多系统调用(例如,参见 io.c)。有一组为特定平台定义的基本系统调用宏。对于 x86,您会发现这些宏定义在 newlib/libc/sys/linux/machine/i386/syscall.h 文件中。目前,Linux 支持仅适用于 x86。要添加另一个平台,必须为新平台提供 syscall.h 文件,此外还需要移植一些其他特定于平台的文件。
对于 newlib,请查看syscall 文档页面,其中列出了您需要实现的内容以及最小实现包含的内容。您很快就会意识到,sbrk
如果您还没有实现内存管理,那么类似的东西将变得毫无意义。到移植 C 库时,您可能最终已经编写了大部分内核。
_exit
退出程序而不清理文件。如果您的系统不提供此功能,最好避免与需要它的子例程(退出、系统)链接。
close
关闭一个文件。最小实现:
int close(int file) { return -1; }
environ
指向环境变量列表及其值的指针。对于最小的环境,这个空列表就足够了:
char *__env[1] = { 0 }; char **environ = __env;
execve
将控制权转移到新进程。最小实现(对于没有进程的系统):
#include <errno.h> #undef errno extern int errno; int execve(char *name, char **argv, char **env) { errno = ENOMEM; return -1; }
fork
创建一个新进程。最小实现(对于没有进程的系统):
#include <errno.h> #undef errno extern int errno; int fork(void) { errno = EAGAIN; return -1; }
fstat
打开文件的状态。为了与这些示例中的其他最小实现保持一致,所有文件都被视为字符特殊设备。所需的 sys/stat.h 头文件分布在该 C 库的 include 子目录中。
#include <sys/stat.h> int fstat(int file, struct stat *st) { st->st_mode = S_IFCHR; return 0; }
getpid
进程 ID;这有时用于生成不太可能与其他进程冲突的字符串。最小实现,对于没有进程的系统:
int getpid(void) { return 1; }
isatty
查询输出流是否为终端。为了与仅支持输出到标准输出的其他最小实现保持一致,建议使用此最小实现:
int isatty(int file) { return 1; }
kill
发出信号。最小实现:
#include <errno.h> #undef errno extern int errno; int kill(int pid, int sig) { errno = EINVAL; return -1; }
link
为现有文件建立一个新名称。最小实现:
#include <errno.h> #undef errno extern int errno; int link(char *old, char *new) { errno = EMLINK; return -1; }
lseek
在文件中设置位置。最小实现:
int lseek(int file, int ptr, int dir) { return 0; }
open
打开一个文件。最小实现:
int open(const char *name, int flags, int mode) { return -1; }
read
从文件中读取。最小实现:
int read(int file, char *ptr, int len) { return 0; }
sbrk
增加程序数据空间。由于 malloc 和相关函数依赖于此,因此有一个有效的实现很有用。对于独立系统,以下内容就足够了;它利用了 GNU 链接器自动定义的符号 _end。
caddr_t sbrk(int incr) { extern char _end; /* Defined by the linker */ static char *heap_end; char *prev_heap_end; if (heap_end == 0) { heap_end = &_end; } prev_heap_end = heap_end; if (heap_end + incr > stack_ptr) { write (1, "Heap and stack collision\n", 25); abort (); } heap_end += incr; return (caddr_t) prev_heap_end; }
stat
文件的状态(按名称)。最小实现:
int stat(char *file, struct stat *st) { st->st_mode = S_IFCHR; return 0; }
times
当前进程的时间信息。最小实现:
int times(struct tms *buf) { return -1; }
unlink
删除文件的目录条目。最小实现:
#include <errno.h> #undef errno extern int errno; int unlink(char *name) { errno = ENOENT; return -1; }
wait
等待子进程。最小实现:
#include <errno.h> #undef errno extern int errno; int wait(int *status) { errno = ECHILD; return -1; }
write
写入文件。libc 子例程将使用此系统例程输出到所有文件,包括标准输出——因此,如果您需要生成任何输出,例如到串行端口进行调试,您应该使您的最小写入能够做到这一点。以下最小实现是一个不完整的示例;它依赖于一个 outbyte 子例程(未显示;通常,您必须从硬件制造商提供的示例中用汇编程序编写它)来实际执行输出。
int write(int file, char *ptr, int len) { int todo; for (todo = 0; todo < len; todo++) { outbyte (*ptr++); } return len; }
有关移植 newlib 所需步骤的更全面概述,请参阅osdev.org。虽然我建议首先阅读网站上与编写内核有关的其他教程,因为移植 C 库绝对不是编写内核时采取的第一步。