在内核 space 中实现可变参数的可移植方法?

Portable way to implement variadic arguments in kernel space?

我想知道是否可以在 C 或汇编中实现可变参数宏。

我希望至少 va_start() 是一个 C 宏,但看起来这可能是不可能的。我看到了不同问题的其他答案,说在 C 中是不可能的,因为你必须依赖未定义的行为。

就上下文而言,我正在编写一个内核,我不想依赖任何特定的 C89 编译器或类 unix 汇编器。使用任何 C 编译器构建源代码对于项目来说都很重要。保持简单是另一个目标,不幸的是,在某些架构(amd64 ABI)上支持可变参数之类的东西似乎很复杂。

我知道 __builtin_va_start(v,l)、__builtin_va_arg(v, l) 等宏存在,但它们仅适用于特定的编译器?

现在我有用汇编 (i386 ABI) 编写的内核 printf(, ...) 和 panic(, ...) 例程,它们设置 va_list (指向第一个 va 参数的指针stack) 并将其传递给 vprintf(, va_list) 然后使用 va_arg() 宏(用 C 编写)。这不依赖于任何未定义或实现定义的行为,但我希望所有宏都用 C 编写。

既然你突出了内核space,你想要一个用户space函数对吗它是通过某种 内核调用 实现的并且是可变的? 这有点问题;作为典型的内核入口点,将控制流转移到 内核堆栈 。您的 va_start()、va_arg() 实现必须知道如何遍历用户的堆栈,并可能映射寄存器保存区域的位进入向量。

一个更简单的方法是让用户函数:

int ufunc(char *fmt, ...) {
    va_list v;
    int n;
    va_start(v, fmt);
    n = __ufunc(fmt, v);
    va_end(v);
    return n;
}

并在内核中实现__ufunc。传统上,这就是 execlexecv 函数家族如何合作创建方便的接口,但只使用一个内核调用。

尽管如此,您的内核仍然需要做一些处理用户堆栈的工作。例如,我可以为您的调用制作一个 va_list 值,使内核读取一些私有数据。但是,如果您能够对 va_list 指向某个有效的地方进行排序,并且您使用 va_arg() 提供的值进行的任何处理也是有效的,那么您会能够使用标准编译器提供的实现。

请注意,如果用户程序使用与内核不同的调用约定,您可能需要做一些工作。例如,Microsoft 忽略了为 amd64 发布的 ABI,因此这可能会导致问题。

总结:只需 #include <stdarg.h> 并像往常一样使用 va_start 和朋友。 standard-conformant C 编译器将支持这一点,即使没有我们通常认为的“C 库”,它可以完美地用于必须 运行 在没有 OS 支持的裸机上的内核中。这也是最便携的解决方案,并且避免了需要架构、编译器或 ABI-dependent 解决方案。


当然,在编写内核时,您习惯于不使用库设施,例如 <stdio.h><stdlib.h> 甚至 <string.h> 中的函数(printf, malloc, strcpy,等),或者必须自己编写。但是 <stdarg.h> 属于不同的类别。它的功能可以在没有 OS 支持或大量库代码的情况下由编译器提供,并且在某种意义上更像是 compiler/language 的一部分而不是“库”。

从C标准的角度来看,有两种符合的实现(见C17第4节,“符合”)。应用程序程序员主要考虑 符合要求的托管实现 ,它必须提供 printf 等等。但是对于内核或嵌入式代码或裸机上 运行 的任何其他内容,您需要的是 符合标准的独立实现 (我将简称为 CFI)。非正式地说,这就是没有“标准库”的“只是编译器”。但是有一些标准 headers 的内容 CFI 仍然必须支持,<stdarg.h> 就是其中之一。其他的是 <limits.h><stddef.h><stdint.h> 之类的东西,主要是常量、宏和 typedef。

(这种区别一直存在到 C89,同样保证 <stdarg.h> 可用。)

如果您的内核将使用任何 CFI 构建,那几乎就是内核可移植性的黄金标准。事实上,在某些时候 hard-pressed 不使用更多 compiler-specific 特性会更好(例如,内联汇编非常有用)。但是 <stdarg.h> 不一定是其中之一;您实际上并没有放弃使用它的任何可移植性。您可以期望它受到针对任何给定体系结构的任何可用编译器的支持,其中包括交叉编译器(将配置为针对目标使用正确的 header)。例如,对于 GNU 系统,<stdarg.h> 附带 gcc 编译器本身,而不是 glibc 标准库。

作为进一步的保证,直到最近,Linux 内核本身正是以这种方式使用 <stdarg.h>。 (大约一个月前有一个 commit 来创建他们自己的 <linux/stdarg.h> 文件,它只是 copy-pastes 来自旧版本的 gcc <stdarg.h> 并将宏定义为他们的 gcc-specific __builtin 版本。Linux 无论如何只支持使用 gcc 构建,所以这不会伤害他们。但我最好的猜测是这是出于许可原因而完成的 - 提交消息强调他们复制了GPL 2 版本 - 而不是基于任何技术。)


相比之下,在汇编中编写可变参数函数自然会将您绑定到特定的体系结构,如果您想移植到另一种体系结构,它们将是另一回事。并尝试从 C 访问堆栈上的可变参数,使用像 arg = *((int *)&fixed_arg + 1) 这样的技巧,是 (a) ABI-dependent, (b) 仅对于实际在堆栈上传递参数的 ABI 才有可能,这些除了 x86-32 之外,天数并不多,并且 (c) 是未定义的行为,可能会被某些编译器“错误编译”。最后,像 __builtin_va_start 这样的东西严格来说是 compiler-dependent (在这种情况下是 gcc 和 clang),使用 <stdarg.h> 也不会更糟,因为 gcc 的 <stdarg.h> 只包含像 [=33= 这样的宏].