4

我正在尝试为 Raspberry Pi 制作一个操作系统(没什么大不了的,只是为了好玩),虽然我可以用汇编语言编写它,但这比用 C 语言编写要困难得多。我想知道是否(以及为什么如果我不能,则不会)我可以将 C 库(文件)包含在操作系统中,这样我就不必重写它们了。因为库本身是用 C 编写的,所以它不会起作用吗?

4

1 回答 1

7

不,您必须将 C 库移植到您的操作系统,因为该库具有挂钩到操作系统细节的“存根”。C 标准要求某些标头以独立模式存在,您始终可以使用该模式。但是像 printf 这样的库函数必须自己实现或通过填写存根来移植。看看newlib看看你必须做的工作。它至少需要一个具有系统调用接口(用于读取、写入等)的工作内核。这些将取决于您的操作系统中可用的功能(例如,文件系统)。摘自常见问题解答:

  1. 将 newlib 移植到新平台需要执行哪些步骤?

一个基本端口需要更改一些文件并添加一些目录。

  1. 为您的平台添加一个子目录到 newlib/libc/machine 目录

    在此目录中,您需要有一个 setjmp/longjmp 实现。这是必需的,因为 setjmp/longjmp 通常是汇编程序。查看 libc/machine/fr30 目录并复制/修改其中的文件。

  2. 编辑 newlib/libc/include/machine/ieeefp.h

    这为您的平台定义了 ieee 字节序。编译器应该定义一些标识你的机器的东西。在某些情况下,字节序可能是编译器选项,因此除了平台标识符之外,您可能还需要检查另一个定义。请参阅文件中的示例。

  3. 编辑 newlib/libc/include/machine/setjmp.h

    您需要指定 setjmp 缓冲区特性以匹配您的 setjmp/longjmp 实现。这只是 setjmp 缓冲区的大小。有关示例,请参见文件。

  4. 编辑 newlib/libc/include/sys/config.h

    这根据需要有各种定义。大多数情况下,它定义了一些最大值。有些默认值可能适用于您的平台,在这种情况下您无需执行任何操作。

  5. 编辑configure.host

    您需要添加配置,以便 newlib 可以识别它。您应该通过 machine_dir 变量为您的平台指定新的机器目录。如果需要,您可以添加特殊的 newlib 编译标志。sys_dir 用于操作系统的东西,所以你不需要改变它。较旧的平台使用 sys_dir 来实现系统调用,但这是不正确的,并且是历史上的麻烦事。syscall_dir 是一种选择,但我建议默认指定 syscall_dir=syscalls。阅读 newlib/libc/include/reent.h 中的注释以了解选择的说明。

  6. 将平台子目录添加到 libgloss

    您需要为您的平台添加一个 bsp。这是 newlib 和所需的任何链接器脚本所需的最小系统调用集。这因板而异(也可以是模拟器)。有关示例,请参见 mn10300 或 fr30。您将需要编辑 configure.in 并重新生成配置,以便它构建您的新文件。默认情况下,您会得到 libnosys,它为您提供一组默认的系统调用存根。大多数存根只是返回失败。您仍然需要提供 __exit 例程。这可以像生成异常以停止程序一样简单。

  7. 可能覆盖头文件

    如果您需要覆盖任何默认机器头文件,您可以将机器目录添加到 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 库绝对不是编写内核时采取的第一步。

于 2014-10-24T05:15:50.633 回答