- 如果没有
#include<ctype.h>
,以下程序会输出 1 和 0。 - 使用包含,它输出 1 和 1。
我正在使用 TDM-GCC 4.9.2 64 位。我想知道isdigit
在第一种情况下的实现是什么,以及为什么它能够链接.
#include<stdio.h>
//#include<ctype.h>
int main()
{
printf("%d %d\n",isdigit(48),isdigit(48.4));
return 0;
}
默认情况下,GCC 使用允许隐式声明的 C90 标准(带有 GNU 扩展(参考))。您的案例的问题是您有两个isdigit
使用两个不同参数的调用,这可能会使编译器在创建函数的隐式声明时感到困惑,并且它可能会选择int isdigit(double)
安全的一面。这当然是函数的错误原型,这意味着当库函数在运行时被调用时,它会被错误的参数调用,并且你会有未定义的行为。
当你包含<ctype.h>
头文件时,会有一个正确的原型,然后编译器知道它isdigit
需要一个int
参数,并且可以将double
文字转换48.4
为整数48
以进行调用。
至于为什么要链接,这是因为虽然这些功能可以实现为宏,但这不是必需的。要求是这些函数,至少在 C11 标准中(我目前没有任何旧版本可用),必须了解当前的语言环境,这将使它们作为宏的实现更加困难,而且像普通的库函数一样容易。并且由于标准库始终是链接的(除非您另外告诉 GCC),因此这些功能将可用。
首先#include
声明与 . 没有任何关系linking
。请记住,任何带有#
in-front in 的东西C
都适用于预处理器,而不是编译器或链接器。
但这就是说必须链接功能不是吗?
让我们在不同的步骤中执行这些步骤。
$ 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
如您所见,两者都printf
被isdigit
列为未定义。所以代码必须来自某个地方,不是吗?
让我们继续链接它...
$ gcc st.o
$ nm a.out | grep 'printf\|isdigit'
U isdigit@@GLIBC_2.2.5
U printf@@GLIBC_2.2.5
正如你所看到的,情况略有改善。作为isdigit
并且printf
不是像他们一样无助的孤独者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)
啊哈……就是这样libc
。所以事实证明,虽然你没有给出任何指令,但链接器默认链接了 3 个库,其中一个是libc
同时包含printf
和的库isdigit
。
您可以通过以下方式查看链接器的默认行为:
$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
两者都显示出来,因为这意味着未知。换句话说,没有与这些符号相关的地址。printf
isdigit
U
memory
实际上,这就是魔法所在。这些库实际上是在运行时加载的,而不是像旧系统那样在链接时加载。
它是如何实施的?好吧,它有一个相关的术语,比如惰性链接。它的作用是,当进程调用一个函数时,如果没有内存地址(TEXT 部分),它会生成一个Trap
(在高级语言术语中类似于异常,当控制权移交给语言引擎时)。内核拦截这些Trap
并将其交给动态加载程序,该加载程序加载库并将相关的内存地址返回给调用者进程。
有多种理论原因,为什么懒惰地做事比事先做事要好。我想这是一个全新的话题,我们将在其他时间讨论。