5

我们在 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宏无关。

4

2 回答 2

4

一个“原型”通常被称为函数的声明——这是你可以在头文件中找到的。在这种情况下,原型构建是由_EXFUN()宏辅助的,并且将通过预处理充分展现。以下命令将stdio.h通过预处理器并将结果输出到标准输出:

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。

头文件几乎不应该包含实现细节,而应该只包含需要共享的 、 、 和函数原型的struct定义enumtypedef

如果您正在寻找printf()(作为示例)的实现/源代码,那么您将需要查看库的源代码。

您的工具链不太可能随源代码一起提供,它可能包括库(*.a*.so)和头文件(*.h)。一些包管理器和库有两个与之关联的包 - 例如:mylibrarymylibrary-dev. 在这种情况下,前者通常包含库二进制文件,而后者将包含头文件,以便您可以在应用程序中使用该库 - 两个包通常都不包含源代码。

它就我而言(如上所述),库是 glibc:

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

这当然只是一个薄薄的包装vfprintf()。正是在这一点上,您开始意识到一些库非常庞大和复杂......您可以花费相当多的时间尝试“通过”宏来找到您的目标函数,它恰好位于stdio-common/vfprintf.c


由于头文件只有它们的原型,它们是如何被带入我们的程序的?

“编译”应用程序的最后步骤之一是“链接”。有两种类型:

静态链接

机器代码取自*.a文件 - 静态库。这些文件只是ar(1)包含目标文件( )的档案(参见 ),*.o而目标文件又包含机器代码。

  • 编译时间:特定函数的实际机器代码被复制到您的二进制文件中。

  • 运行时:当你的二进制文件被加载时,它已经有一个printf()函数的副本。任务完成。

动态链接

机器代码取自*.so文件 - 静态库,或“DLL” - 动态链接库。这些文件本身就是包含一组符号或可以使用的入口点的二进制文件。

  • 编译时间:链接器将确保您调用的函数存在于共享库中,并记下它们需要在运行时链接。

  • 运行时:当你的二进制文件被加载时,它有一个需要链接的“符号”列表,以及在哪里可以找到它们。此时,将/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

于 2017-03-14T13:31:30.747 回答
2

我最近读到,内置函数的函数定义不是直接提供的,而是以某种特殊的方式给出的。这是真的?

这可能是真的,这取决于您使用的编译器和编译器设置。但我们应该稍微备份一下。

首先,您需要了解有许多 C 库,其中库是与您的程序分开编译的函数的集合。您在源代码中包含库附带的头文件 (.h),以便编译器知道您在说什么。编译代码后,它会与它使用的库链接,从而使这些库函数的定义可用于您的程序。在大多数情况下,如果您想了解库中定义的函数是如何编写的,您需要查看该库的源代码。包括来自库的函数是标准的东西——它不符合“某种特殊方式”的条件,因为它并不是那么特别。然而...

C 标准库中的一些函数在 C 代码中使用得非常普遍,编译器有自己的优化版本是有意义的。根据您指定的编译器选项,编译器可能会用自己的相应函数替换标准函数,如printf()malloc()、和其他函数。您可以在此处找到 GCC 的“内置”函数列表,以及允许或禁止使用它们的编译器标志的描述。这些功能fputs()isascii()以“特殊方式”定义,因为它们从编译器得到特殊处理,如果你想改变它们,你必须重新编译编译器本身。很高兴知道标准库函数可能会以这种方式进行优化,但这不是您在编写代码的正常过程中应该担心的事情。

于 2017-03-14T15:33:43.323 回答