为什么我可以 link 而不包括 ctype.h

Why am I able to link without including ctype.h

我正在使用 TDM-GCC 4.9.2 64 位。我想知道第一种情况下 isdigit 的实现是什么,以及 为什么它能够 link.

#include<stdio.h>
//#include<ctype.h>
int main()
{
    printf("%d %d\n",isdigit(48),isdigit(48.4));
    return 0;
}

默认情况下,GCC 使用允许隐式声明的 C90 标准(带有 GNU 扩展 (reference))。你的情况的问题是你有两个调用 isdigit 有两个不同的参数,这可能会使编译器在创建函数的隐式声明时感到困惑,并且它可能选择 int isdigit(double) 是安全的边。这当然是函数的错误原型,这意味着当在 运行 时调用库函数时,它将使用错误的参数调用,并且您将有 未定义的行为 .

当你包含 <ctype.h> 头文件时,有一个正确的原型,然后编译器知道 isdigit 接受一个 int 参数并且可以转换 double 文字 48.4 到调用的整数 48


至于为什么要链接,是因为虽然这些函数可以实现为宏,但这不是必需的。 的一个要求是那些功能,至少在C11标准中(我目前没有任何旧版本可用)必须知道当前的语言环境使它们作为宏的实现更加困难,而作为普通库函数则更加容易。由于标准库始终处于链接状态(除非您以其他方式告诉 GCC),因此这些函数将可用。

首先 #include 语句与 linking 没有任何关系。请记住,C 中前面带有 # 的任何内容都是针对预处理器的,而不是编译器或 linker。

但是那是说函数必须 linked 不是吗?

让我们分步执行这些步骤。

$ gcc -c -Werror --std=c99 st.c 
st.c: In function ‘main’:
st.c:5:22: error: implicit declaration of function ‘isdigit’ [-Werror=implicit-function-declaration]
     printf("%d %d\n",isdigit(48),isdigit(48.4));
                      ^
cc1: all warnings being treated as errors

如您所见,gcc 的 lint(静态分析器)正在运行!

无论我们将继续忽略它...

$ gcc -c  --std=c99 st.c 
st.c: In function ‘main’:
st.c:5:22: warning: implicit declaration of function ‘isdigit’ [-Wimplicit-function-declaration]
     printf("%d %d\n",isdigit(48),isdigit(48.4));

这次只是一个警告。现在我们在当前目录中有一个目标文件。让我们检查一下...

$ nm st.o 
                 U isdigit
0000000000000000 T main
                 U printf

如您所见,printfisdigit 都被列为未定义。所以代码必须来自某个地方,不是吗?

让我们继续 link 它 ...

$ gcc st.o
$ nm a.out | grep  'printf\|isdigit'
                 U isdigit@@GLIBC_2.2.5
                 U printf@@GLIBC_2.2.5

如您所见,情况略有改善。因为 isdigitprintf 不像在 st.o 时那样无助的孤独者。您可以看到 GLIBC_2.2.5 提供了这两个功能。但是 GLIBC 在哪里?

好吧,让我们再检查一下最终的可执行文件...

$ ldd a.out 
        linux-vdso.so.1 =>  (0x00007ffe58d70000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb66f299000)
        /lib64/ld-linux-x86-64.so.2 (0x000055b26631d000)

AHA...有那个 libc 。所以结果是,虽然你没有给出任何说明,但 linker 默认使用 3 个库 linking,其中之一是 libc,它包含两个 printfisdigit.

您可以通过以下方式查看 linker 的默认行为:

$gcc -dumpspec
*link:
%{!r:--build-id} %{!static:--eh-frame-hdr} %{!mandroid|tno-android-ld:%{m16|m32|mx32:;:-m elf_x86_64}                    %{m16|m32:-m elf_i386}                    %{mx32:-m elf32_x86_64}   --hash-style=gnu   --as-needed   %{shared:-shared}   %{!shared:     %{!static:       %{rdynamic:-export-dynamic}       %{m16|m32:-dynamic-linker %{muclibc:/lib/ld-uClibc.so.0;:%{mbionic:/system/bin/linker;:/lib/ld-linux.so.2}}}       %{m16|m32|mx32:;:-dynamic-linker %{muclibc:/lib/ld64-uClibc.so.0;:%{mbionic:/system/bin/linker64;:/lib64/ld-linux-x86-64.so.2}}}       %{mx32:-dynamic-linker %{muclibc:/lib/ldx32-uClibc.so.0;:%{mbionic:/system/bin/linkerx32;:/libx32/ld-linux-x32.so.2}}}}     %{static:-static}};:%{m16|m32|mx32:;:-m elf_x86_64}                    %{m16|m32:-m elf_i386}                    %{mx32:-m elf32_x86_64}   --hash-style=gnu   --as-needed   %{shared:-shared}   %{!shared:     %{!static:       %{rdynamic:-export-dynamic}       %{m16|m32:-dynamic-linker %{muclibc:/lib/ld-uClibc.so.0;:%{mbionic:/system/bin/linker;:/lib/ld-linux.so.2}}}       %{m16|m32|mx32:;:-dynamic-linker %{muclibc:/lib/ld64-uClibc.so.0;:%{mbionic:/system/bin/linker64;:/lib64/ld-linux-x86-64.so.2}}}       %{mx32:-dynamic-linker %{muclibc:/lib/ldx32-uClibc.so.0;:%{mbionic:/system/bin/linkerx32;:/libx32/ld-linux-x32.so.2}}}}     %{static:-static}} %{shared: -Bsymbolic}}

另外两个库是什么?

请记住,当您深入研究 a.out 时,printfisdigit 仍显示为 U,这意味着未知。换句话说,没有 memory 地址与这些符号相关联。

实际上这就是魔法所在。这些库实际上是在运行时加载的,而不是像旧系统那样在 link 时间内加载。

它是如何实现的?好吧,它有一个相关的行话,比如 lazy linking。它的作用是,当进程调用一个函数时,如果没有内存地址(TEXT 部分),它会生成一个 Trap(类似于高级语言术语中的异常,当控制权移交给语言时引擎)。内核拦截这样的 Trap 并将其交给动态加载程序,动态加载程序加载库和 returns 关联的内存地址给调用进程。

有多种理论原因,为什么懒惰地做事比事先做事更好。我想这是一个全新的话题,我们将在其他时间讨论。