如何在C语言中实现stdarg

7
出于好奇心,我想编写用于替换标准C库中某些函数的最小代码。到目前为止,我已完成了printf()strlen()strcpy()memcpy()memset()等函数的替代,但当我尝试使用printf函数时,我不知道如何实现stdarg.h!有什么方法可以实现吗?
它是否使用宏或实际函数?
如果需要回答,我正在使用32位x86上的gccclang编译器。

3
为什么不查看您所偏爱的编译器提供的“头文件”?请注意,这取决于具体实现。任何全面的答案都会太泛泛了,请更加具体明确。 - too honest for this site
@Olaf 我已经在gcc、clang和glibc的源代码中找了两个小时,但我甚至没找到一个看起来像实现的。 - sadljkfhalskdjfh
1
我认为重新实现仅期望在托管环境中使用的函数,就像你迄今所做的那样,是一个很好的练习。然而,实现来自应该即使在独立环境中也可用的头文件的功能,即 float.hiso646.hlimits.hstdarg.hstdbool.hstddef.hstdint.h,并不是一个很好的练习,因为这些的实现更可能依赖于编译器或架构特定的行为,并且可能不具有可移植性。 - icktoofay
1
特别是,stdarg.h 可能是最难决定重新实现的东西之一,仅次于 setjmp.h。这两个都需要了解编译器、架构和其他实现的低级细节,并且根据架构和其他因素,可能需要至少部分地使用汇编语言而不是纯 C 来实现。 - icktoofay
1
我喜欢他这样学习。下一步可以改进那些标准函数,因为许多旧的C标准函数相对于今天的标准和最佳实践而言设计得非常糟糕。如果你真的想要使用你的工作成果,我甚至建议你立即开始这个改进步骤,跳过重新实现的步骤。 - BitTickler
显示剩余10条评论
4个回答

7

在使用cdecl调用规范的32位x86架构中,参数会被传递到栈中:

^ higher addresses (lower on the stack)
|
| caller local variables
| ...
| argument 3
| argument 2
| argument 1
| return address
| saved EBP (usually)
| callee local variables
|
v lower addresses (higher on the stack)

你可以将 va_list 实现为指针。 va_start 可以使用传递给它的参数地址并添加该参数的大小以移动到下一个参数。 va_arg 可以访问指针并将其向下移动到下一个参数。 va_copy 只能复制指针值。 va_end 不需要执行任何操作。
另一方面,如果你不使用 cdecl(也许你使用 fastcall),你不是 32 位,你不是 x86,那么这种方法就行不通了; 你可能需要对寄存器进行调整而不仅仅是指针值。即使如此,也无法保证它能正常工作,因为你依赖于未定义的行为;举一个潜在问题的例子,内联可能会破坏一切。这就是为什么头文件只将其typedef为编译器内置 - 在 C 中实现这是没有希望的,你需要编译器的支持。甚至不要让我开始实现setjmplongjmp...

由于这几乎不可能在没有编写编译器的情况下“正确”实现(而且我也不知道如何做),所以最好的解决方案是与libgcc进行链接吗? - sadljkfhalskdjfh
2
这个答案是错误的;它假设了古老的编译器技术,其中参数对象的地址实际上将是它们在堆栈上传递的地址(或者应该是的)。没有理由需要这样。即使在像i386这样的架构中,外部函数的参数是通过堆栈传递的,编译器完全可以将它们移动到单独的本地存储中,并使&产生指向该新存储的指针。它还可以执行内联/LTO,在这种情况下,参数可能根本不会存在于堆栈上。 - R.. GitHub STOP HELPING ICE
1
@R..:我理解,这就是为什么我在最后一段指出即使满足了所有前提条件,它也是完全不可靠的。 - icktoofay
好的。顺便说一下,虽然我说了“错误”,但并不是我将您的答案投了反对票。 - R.. GitHub STOP HELPING ICE
不是未定义的,而是实现定义的。但是你忽略了对齐问题。如果声明这只是一个简化的例子来传达思想,那么答案就可以接受。你不能依赖于未定义的东西。提到“inline”是一个好点。然而,编译器可以自由地忽略“inline”,因此也可以忽略可变参数函数中的“inline”。 - too honest for this site
显示剩余4条评论

4
在C语言中,无法实现stdarg.h宏,您需要像__builtin_va_arg等编译器内置函数,GCC和兼容的编译器提供此功能,或者使用与您的编译器相应的等效物。即使您知道特定目标的参数传递规则(如icktoofay答案中的i386),在C语言中也无法访问该内存。仅仅对va_start传递的地址进行指针运算是无效的,这会导致未定义行为。但即使C语言允许这种算术运算,也不能保证最后一个命名参数的地址实际上对应于在调用约定期间作为栈的一部分传递的位置;编译器可能已选择将其移动到其堆栈帧中的其他位置(可能是为了获得额外的对齐或数据局部性)。

这取决于体系结构的PCS/ABI。宏可以很好地关注对齐,因为它们知道类型。早期的实现实际上确实使用了宏来处理这个问题。例如,我在68000上有这些(如果我没记错的话,这是gcc 1.x版本,但不确定)。而重新排序会至少破坏不同编译单元之间的调用。 - too honest for this site
@Olaf:这与ABI无关。即使您知道args是通过堆栈传递的,您也不知道&argN是它传递的堆栈插槽的地址。在函数体内,argN只是一个普通的自动存储对象。编译器可以自由地将其放置在任何位置(只要它从调用约定放置的任何位置复制原始值),这纯粹是函数如何编译的内部考虑,而不是ABI。 - R.. GitHub STOP HELPING ICE
啥?标准实现只是通过当前参数的大小增加指针,对齐并获得下一个条目的地址。这通常是PCS和ABI的一部分。请参阅当前的ARM标准(AAPCS,包括基本的C/C++ ABI)。而且,除非编译器可以证明该函数不会从其他地方调用,否则它不是免费的。这只适用于“静态”的情况。 - too honest for this site
后者不受ABI的控制。有人应该告诉ARM。 - too honest for this site
当然,如果编译器提供了 stdarg.h,它必须满足自己的 stdarg.h 的假设,这可能会对编译器的操作施加限制。这就是为什么好的编译器使用类似于 __builtin_va_arg 的东西:这样它们就不会对自己施加任何限制。然而,OP 的问题是关于自己编写 stdarg.h。这是一件你只凭知道 ABI 就无法完成的事情。你还需要对编译器在 函数内部 所做和不做的其他假设。 - R.. GitHub STOP HELPING ICE
显示剩余5条评论

2

您可以在这里看到如何实现va宏的示例。该头文件用于VC++,每个处理器架构都有不同的实现。这些宏似乎不特定于Microsoft编译器。在GCC和Clang中,va宏指的是编译器内置的函数。


2

1992年,CALC源代码在comp.sources.unix上发布了一份实现。

这是一个shar存档文件,所以忽略其中的X

X * Copyright (c) 1992 David I. Bell
X * Permission is granted to use, distribute, or modify this source,
X * provided that this copyright notice remains intact.

X/*
X * SIMULATE_STDARG
X *
X * WARNING: This type of stdarg makes assumptions about the stack
X *             that may not be true on your system.  You may want to
X *            define STDARG (if using ANSI C) or VARARGS.
X */
X
Xtypedef char *va_list;
X#define va_start(ap,parmn) (void)((ap) = (char*)(&(parmn) + 1))
X#define va_end(ap) (void)((ap) = 0)
X#define va_arg(ap, type) \
X    (((type*)((ap) = ((ap) + sizeof(type))))[-1])

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接