【问题标题】:Function definitions of built-in functions in CC中内置函数的函数定义
【发布时间】:2017-08-04 20:32:51
【问题描述】:

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

【问题讨论】:

标签: c gcc built-in function-definition


【解决方案1】:

“原型”通常被称为函数的声明 - 这是您可以在头文件中找到的内容。在这种情况下,原型构造由_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。

头文件几乎不应该包含实现细节,而应该只包含structenumtypedef 的定义以及需要共享的函数原型。

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

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

在我的例子中(如上所述),库是 glibc:

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

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


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

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

静态链接

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

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

  • 运行时: 当你的二进制文件被加载时,它已经有一个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

【讨论】:

  • “原型”通常被称为函数的定义不,应该是声明"A function definition in C programming consists of a function header and a function body." 参见the C Standard6.7 声明6.9.1 函数定义 函数的定义 IS 实现.
  • 感谢@AndrewHenle - 已修复
  • @Attie 如果您正在寻找函数的实现,那么您将需要查看源代码 但是我在我的计算机中哪里可以找到这些源代码?他们必须在机器上编译代码,对吗?我正在使用 Ubuntu。
  • @J...S 他们很可能不在您的计算机上。工具链往往随库(*.a*.so)和头文件一起提供。如果您想要这些来源,则必须单独找到它们。你会经常发现系统有mylibrarymylibrary-dev 包——前者包含库二进制文件,后者包含头文件。软件包很少包含源代码。
  • gnu.org/software/m68hc11/m68hc11_install.html '使用源码安装需要你获取各种 GNU 软件包的源码,应用代表 68HC11/68HC12 端口的补丁并编译整套。'
【解决方案2】:

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

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-06-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多