我们在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
宏。
答案 0 :(得分:3)
'原型'通常被称为函数的声明 - 这是您将在头文件中找到的。在这种情况下,原型构造由_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。
头文件几乎不应包含实现细节,而应仅包含struct
,enum
,typedef
和需要共享的函数原型的定义。
如果您正在寻找printf()
的实现/来源(作为示例),那么您需要查看该库的源代码。
您的工具链不太可能附带源代码,它可能包含库(*.a
和*.so
)和头文件(*.h
)。某些包管理器和库有两个与之关联的包 - 例如:mylibrary
和mylibrary-dev
。在这种情况下,前者通常包含库二进制文件,而后者将包含头文件,以便您可以在应用程序中使用库 - 两个包通常都不包含源。
it在我的情况下(如上所述),库是glibc:
如果您对printf()
感兴趣,那么您需要查看stdio-common/printf.c
:
这当然只是vfprintf()
的一个薄包装。
正是在这一点上你开始意识到一些库非常大而且复杂......你可以花很多时间试图看到“通过”宏来找到你的目标函数,这恰好在{{1} }:
它们如何被带入我们的程序,因为头文件只有它们的原型?
“编译”应用程序的最后一步是“链接”。有两种类型:
机器代码取自stdio-common/vfprintf.c
个文件 - 静态库。
这些文件只是包含目标文件(*.a
)的存档(参见ar(1)
),而目标文件又包含机器代码。
编译时间: 特定功能的实际机器代码将复制到二进制文件中。
运行时:
加载二进制文件后,它已经有*.o
函数的副本。完成工作。
机器代码取自printf()
个文件 - 静态库或'DLL' - 动态链接库。
这些文件本身就是二进制文件,包含一组符号或可以使用的入口点。
编译时间: 链接器将确保您正在调用的函数存在于共享库中,并记下它们需要在运行时链接。
运行时:
加载二进制文件时,它有一个需要链接的“符号”列表,以及可以找到它们的位置。
此时,调用动态链接器(我为*.so
)。简单来说,动态链接器将在应用程序执行之前“连接”所有共享库函数。实际上,这可以推迟到实际访问符号为止。
作为另一个扩展......你必须要小心 - 编译器通常会优化昂贵的操作。
以下对/lib/ld-linux.so.2
的简单使用可能会优化为对printf()
的调用:
puts()
#include <stdio.h>
void main(void) {
printf("Hello World\n");
}
的输出:
objdump -d ${MY_BINARY}
如需进一步阅读,请参阅此处:https://www.technovelty.org/linux/plt-and-got-the-key-to-code-sharing-and-dynamic-libraries.html
答案 1 :(得分:2)
我最近读到内置函数的函数定义不是直接提供的,而是以某种特殊方式给出的。这是真的吗?
这可能是真的,具体取决于您使用的编译器和编译器设置。但是我们应该稍微补充一下。
首先,您需要了解有许多C库,其中库是与您的程序分开编译的函数集合。您在源代码中包含库附带的头文件(.h),以便编译器知道您在说什么。在编译代码之后,它与其使用的库链接,使得这些库函数的定义可用于您的程序。在大多数情况下,如果要查看如何编写库中定义的函数,则需要查看该库的源代码。包括库中的函数是标准的东西 - 它不符合“一些特殊的方式”,因为它并不是那么特别。然而...
C语言库中的一些函数在C代码中无处不在,因此编译器有自己的优化版本是有意义的。根据您指定的编译器选项,编译器可以替换标准函数,如printf()
,malloc()
,fputs()
,isascii()
以及其他具有相应函数的函数。您可以找到GCC here的“内置”函数列表,以及允许或禁止使用它们的编译器标志的说明。这些函数 以“特殊方式”定义,因为它们从编译器中获得特殊处理,如果要更改它们,则必须重新编译编译器本身。很高兴知道标准库函数可以通过这种方式进行优化,但在编写代码的正常过程中,您不应该担心它。