C 中内置函数的功能定义

7
我们在C程序中包含像stdio.h这样的头文件,以使用内置的库函数。我曾经认为这些头文件包含了我们在程序中可能使用的内置函数的函数定义。但很快发现并非如此。
当我们打开这些头文件(例如stdio.h)时,它们只包含函数原型,我看不到函数定义。我看到的是这样的东西:
00133 int     _EXFUN(printf, (const char *, ...));
00134 int     _EXFUN(scanf, (const char *, ...));
00135 int     _EXFUN(sscanf, (const char *, const char *, ...));
00136 int     _EXFUN(vfprintf, (FILE *, const char *, __VALIST));
00137 int     _EXFUN(vprintf, (const char *, __VALIST));
00138 int     _EXFUN(vsprintf, (char *, const char *, __VALIST));
00139 int     _EXFUN(vsnprintf, (char *, size_t, const char *, __VALIST));
00140 int     _EXFUN(fgetc, (FILE *));
00141 char *  _EXFUN(fgets, (char *, int, FILE *));
00142 int     _EXFUN(fputc, (int, FILE *));
00143 int     _EXFUN(fputs, (const char *, FILE *));
00144 int     _EXFUN(getc, (FILE *));
00145 int     _EXFUN(getchar, (void));
00146 char *  _EXFUN(gets, (char *));
00147 int     _EXFUN(putc, (int, FILE *));
00148 int     _EXFUN(putchar, (int));
00149 int     _EXFUN(puts, (const char *));`

(来源:https://www.gnu.org/software/m68hc11/examples/stdio_8h-source.html
然后有人告诉我,也许函数定义必须在我们检查的头文件中包含的头文件之一中,所以我相信了一段时间。从那时起,我查看了很多头文件,但从未找到单个函数定义。
最近我读到内置函数的函数定义不是直接提供的,而是以某种特殊方式给出的。这是真的吗?如果是这样,内置函数的函数定义存储在哪里?它们如何被引入我们的程序,因为头文件只有它们的原型?
编辑:请注意,我只是展示了头文件的内容作为一个示例。我的问题不是关于_EXFUN宏的。

经过预处理后,它们包含了正确的定义。 - StoryTeller - Unslander Monica
Google 链接器,这个过程被称为链接。 - Selçuk Cihan
可能是Meaning of "char *_EXFUN(index,(const char *, int));"的重复问题。 - StoryTeller - Unslander Monica
2个回答

4

“原型”通常指的是函数的声明 - 这是您将在头文件中找到的内容。在这种情况下,原型构建受到_EXFUN()宏的帮助,并将通过预处理完全显示出来。 以下命令将通过预处理将stdio.h传递并将结果输出到stdout:

gcc -E -x c /dev/null -include stdio.h

如果你浏览输出,你会找到期望的原型(以下用作示例),我的系统会给出:
extern int printf (const char *__restrict __format, ...);

extern int vfprintf (FILE *__restrict __s, const char *__restrict __format,
       __gnuc_va_list __arg);

我最近读到,内置函数的函数定义不是直接提供的,而是以某种特殊方式给出。这是真的吗?
是的,通过库。如果你正在寻找函数的实现,则需要查看所需函数的源代码。在这种情况下,stdio.h 是由 C 标准库的变体 libc 拥有的,或者在我的情况下是 glibc。
头文件几乎永远不包括实现细节,而应该只包含需要共享的结构、枚举、typedef 和函数原型的定义。
如果你正在寻找 printf() 的实现/源代码(例如),则需要查看库的源代码。
很可能您的工具链不会附带源代码,而是包括库文件(*.a*.so)和头文件(*.h)。某些软件包管理器和库与两个相关的软件包 - 例如:mylibrarymylibrary-dev。在这种情况下,前者通常包含库二进制文件,而后者包含头文件,以便您可以在应用程序中使用该库 - 两个软件包通常都不包含源代码。

在我的情况下(如上所述),库是glibc:

如果您对printf()感兴趣,那么您需要查看stdio-common/printf.c

这当然只是对vfprintf()的简单封装。在这一点上,您开始意识到某些库非常庞大和复杂... 您可能需要花费相当多的时间来尝试“穿透”宏以找到您的目标函数,该函数恰好位于stdio-common/vfprintf.c中:
“它们是如何被引入我们的程序中的,因为头文件只有它们的原型?”
“编译”应用程序的最后一步是“链接”。有两种类型:

静态链接

机器代码来自于*.a文件 - 静态库。这些文件只是包含目标文件(*.o)的存档文件(参见ar(1)),而这些目标文件本身包含机器代码。
  • 编译时: 特定函数的实际机器代码被复制到您的二进制文件中。

  • 运行时: 当您的二进制文件被加载时,它已经拥有printf()函数的副本。工作完成。

动态链接

机器代码来自于*.so文件 - 静态库或“DLLs”-动态链接库。这些文件本身就是二进制文件,包含一组符号或可使用的入口点。
  • 编译时: 连接器仅确保您调用的函数存在于共享库中,并记录下需要在运行时链接它们。

  • 运行时: 当您的二进制文件被加载时,它会有一些需要链接的 '符号' 列表,以及它们所在的位置。 此时将调用动态链接器 (/lib/ld-linux.so.2 对我而言)。简单地说,动态链接器将在应用程序执行之前“连接”所有的共享库函数。实际上,这可以推迟到访问符号时才发生。


作为又一个扩展...你必须小心 - 编译器通常会优化掉昂贵的操作。
下面这个简单使用 printf() 的例子可能会被优化为调用 puts()
#include <stdio.h>

void main(void) {
    printf("Hello World\n");
}

objdump -d ${MY_BINARY}的输出:

[...]

000000000040052d <main>:
  40052d:       55                      push   %rbp
  40052e:       48 89 e5                mov    %rsp,%rbp
  400531:       bf c4 05 40 00          mov    $0x4005c4,%edi
  400536:       e8 d5 fe ff ff          callq  400410 <puts@plt>
  40053b:       5d                      pop    %rbp
  40053c:       c3                      retq
  40053d:       0f 1f 00                nopl   (%rax)

[...]

如需进一步阅读,请参见此处:https://www.technovelty.org/linux/plt-and-got-the-key-to-code-sharing-and-dynamic-libraries.html


2
一个'原型'通常被称为函数的定义。不,那将是'声明'。C编程中的函数定义由'函数头'和'函数体'组成。请参阅C标准中的'6.7 声明'和'6.9.1 函数定义'。函数的'定义'就是'实现'。 - Andrew Henle
感谢 @AndrewHenle - 已修复。 - Attie
@Attie 如果你正在寻找函数的实现,那么你需要查看源代码。但是在我的电脑上我应该在哪里可以找到这些源代码呢?它们肯定在编译代码的机器上吧?我正在使用Ubuntu操作系统。 - J...S
@J...S,它们很可能不在你的电脑上。工具链通常会随着库(*.a*.so)和头文件一起发货。如果你想要源代码,你必须单独找到它们。你经常会发现系统有mylibrarymylibrary-dev包 - 前者包含库二进制文件,后者包含头文件。包含源代码是非常罕见的。 - Attie
使用源代码进行安装需要获取各种GNU软件包的源代码,应用代表68HC11/68HC12端口的补丁并编译整个集合。 - Attie
https://www.gnu.org/software/m68hc11/m68hc11_src.html - 不幸的是,许多下载链接似乎已经失效了... - Attie

2
最近我读到内置函数的功能定义不是直接提供的,而是以某种特殊方式给出。这是真的吗?
这可能取决于你使用的编译器和编译器设置。但我们需要回顾一下。
首先,你需要了解有许多C库,其中库是从你的程序分别编译的函数集合。你在源代码中包含与库一起提供的头文件(.h),以便编译器知道你在说什么。在编译你的代码之后,它会与它使用的库链接,使得那些库函数的定义可用于你的程序。在大多数情况下,如果你想看看库中定义的函数是如何编写的,你需要查看该库的源代码。包含来自库的函数是标准的东西——它并不算“某种特殊方式”,因为它并不是那么特殊。然而...
C标准库中的一些函数在C代码中被如此广泛地使用,以至于编译器具有自己优化版本的意义。根据您指定的编译器选项,编译器可能会用其自己相应的函数替换标准函数,例如printf()malloc()fputs()isascii()等等。您可以在这里找到GCC的“内置”函数列表,以及允许或禁止使用它们的编译器标志的描述。这些函数是以“特殊方式”定义的,因为它们获得了编译器的特殊处理,如果您想要更改它们,您必须重新编译编译器本身。了解标准库函数可能以这种方式进行优化是很好的,但在编写代码的正常过程中不应该担心这个问题。

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