J..*_*..S 5 c gcc built-in function-definition
我们stdio.h在 C 程序中包含头文件以使用内置库函数。我曾经认为这些头文件包含我们可能在程序中使用的内置函数的函数定义。但很快就发现并非如此。
当我们打开这些头文件(例如 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 *));`
Run Code Online (Sandbox Code Playgroud)
(来源:https : //www.gnu.org/software/m68hc11/examples/stdio_8h-source.html)
然后我被告知,也许函数定义必须在我们检查的头文件中包含的头文件之一中,所以我相信了一段时间。从那以后,我查看了很多头文件,但从未找到一个函数定义。
我最近读到内置函数的函数定义不是直接提供的,而是以某种特殊的方式给出的。这是真的?如果是这样,内置函数的函数定义存储在哪里?由于头文件只有它们的原型,它们是如何被引入我们的程序的?
编辑:请注意,我将头文件的内容作为示例显示。我的问题不是关于_EXFUN宏。
“原型”通常指的是函数的声明——您可以在头文件中找到它。在这种情况下,原型构建是由_EXFUN()宏来辅助的,并且通过预处理将得到充分揭示。以下命令将stdio.h通过预处理器并将结果输出到 stdout:
gcc -E -x c /dev/null -include stdio.h
Run Code Online (Sandbox Code Playgroud)
如果您仔细查看输出,您将找到预期的原型(用作下面的示例),我的系统给出:
extern int printf (const char *__restrict __format, ...);
extern int vfprintf (FILE *__restrict __s, const char *__restrict __format,
__gnuc_va_list __arg);
Run Code Online (Sandbox Code Playgroud)
我最近读到内置函数的函数定义不是直接提供的,而是以某种特殊的方式给出的。这是真的?
是的,通过图书馆。如果您正在寻找该函数的实现,那么您将需要查看相应函数的源代码。在本例中,stdio.h它属于“C 标准库”的一个变体 - libc,或者在我的例子中为 glibc。
头文件几乎不应该包含实现细节,而应该只包含需要共享的struct、enum、和函数原型的定义。typedef
如果您正在寻找printf()(作为示例)的实现/源代码,那么您将需要查看该库的源代码。
您的工具链不太可能随源代码一起提供,它可能会包含库(*.a和*.so)和头文件(*.h)。一些包管理器和库有两个与之关联的包 - 例如:mylibrary和mylibrary-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");
}
Run Code Online (Sandbox Code Playgroud)
输出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)
[...]
Run Code Online (Sandbox Code Playgroud)
如需进一步阅读,请参阅此处: https: //www.technovelty.org/linux/plt-and-got-the-key-to-code-sharing-and-dynamic-libraries.html