如何在C中实现标准

如何在C中实现标准,c,header-files,C,Header Files,出于好奇,我希望为标准C库中的一些函数编写最小替换。到目前为止,我已经完成了printf(),strlen(),strcpy(),memcpy(),memset(),等等。。。但是当我尝试使用printf函数时,我不知道如何实现stdarg.h!我有什么办法可以做到这一点 它使用宏还是实际函数 我在32位x86上使用了gcc或clang,如果这有助于更容易回答的话。在使用cdecl调用约定的32位x86上,参数在堆栈上传递: ^更高的地址(堆栈中较低的地址) | |调用者局部变量 | ... |

出于好奇,我希望为标准C库中的一些函数编写最小替换。到目前为止,我已经完成了
printf()
strlen()
strcpy()
memcpy()
memset()
,等等。。。但是当我尝试使用printf函数时,我不知道如何实现
stdarg.h
!我有什么办法可以做到这一点

它使用宏还是实际函数


我在32位x86上使用了
gcc
clang
,如果这有助于更容易回答的话。

在使用cdecl调用约定的32位x86上,参数在堆栈上传递:

^更高的地址(堆栈中较低的地址)
|
|调用者局部变量
| ...
|论据3
|论据2
|论据1
|回信地址
|保存的EBP(通常)
|被叫局部变量
|
v较低的地址(堆栈上较高的地址)
您可以将
va_list
实现为指针
va_start
可以获取传递给它的参数的地址,并添加该参数的大小以移动到下一个参数
va_arg
可以访问指针并将其跳到下一个参数
va_copy
只需复制指针值即可
va_end
无需执行任何操作


另一方面,如果您没有使用cdecl(可能您使用的是fastcall),您不是32位,您不是x86,那么这将不起作用;您可能需要处理寄存器,而不仅仅是指针值。即使如此,它也不能保证有效,因为你依赖于未定义的行为;作为一个潜在问题的例子,内联可能会毁掉一切。这就是为什么头文件仅仅是
typedef
s它到一个用C实现的内置编译器是没有希望的,你需要编译器的支持。甚至不要让我开始实现
setjmp
longjmp

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

在C中无法实现
stdarg.h
宏;您需要GCC和兼容编译器提供的编译器内置程序,如
\uuuu builtin\u va\u arg
,或编译器的等效程序


即使您知道正在使用的特定目标的参数传递约定(如icktoofay的答案中的i386),在C中也无法访问该内存。仅对传递给
va_start
的地址执行指针运算是无效的;它会导致未定义的行为。但是,即使C允许该算法,也不能保证最后一个命名参数的地址实际上与作为调用约定的一部分在堆栈上传递的位置相对应;编译器可以选择将其移动到堆栈框架中的其他位置(可能是为了获得额外的对齐或数据位置)。

1992年发布的CALC源代码中有一个实现

这是来自shar存档,因此忽略
X
s

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])

为什么不直接查看首选编译器提供的头文件呢?请注意,这是特定于实现的。任何全面的回答都过于宽泛。请说得更具体些。@Olaf我已经在gcc、clang和glibc的源代码中查找了2个小时,但我找不到一个看起来像是实现的源代码。我认为重新实现只在托管环境中使用的功能是一个很好的练习,正如您到目前为止所做的那样;然而,从即使在独立环境中也应该可用的头实现功能,即
float.h
iso646.h
limits.h
stdarg.h
stdbool.h
stddef.h
,以及
stdint.h
,并不是一个很好的练习,因为它们的实现更可能依赖于编译器或特定于体系结构的行为,并且可能不可移植。特别是,
stdarg.h
可能是决定重新实现的最困难的事情之一,只有
setjmp.h
可以与之匹敌。这两者都需要了解编译器、体系结构和其他实现的底层知识,并且根据体系结构和其他因素,可能需要至少部分地在汇编中实现,而不是纯C。我喜欢他以这种方式学习。下一步可能是改进这些标准函数,因为与当今的标准和最佳实践相比,许多旧的c标准函数设计得相当糟糕。如果你真的想使用你的作品的结果,我甚至建议你立即开始这一改进步骤,跳过重新实现。因为如果不编写编译器(我不知道如何),这实际上是不可能“正确”实现的,仅使用libgcc链接是最好的解决方案吗?@AnonymousShadow:首先尝试不包含
libgcc
的链接;由于
va.*
是引用内置函数的宏,它可能不会添加任何外部函数引用,因此您可能根本不需要链接任何额外的函数。也就是说,如果你在
libgcc
定义的符号中得到未定义的符号错误,是的,我想你需要把它链接进去;它假设了一种古老的编译器技术,其中参数对象的地址实际上就是它们在堆栈上传递(或本来会传递)的地址。没有理由这样。即使在像i386这样的ARCH上,外部函数的参数在堆栈上传递,编译器也完全可以自由地将它们移动到单独的本地存储,并让
&
生成指向该新存储的指针。它还可以自由地执行内联/LTO,在这种情况下,参数可能永远不会存在