编译器和库

当我们运行一个程序时,会执行什么?

目的:在本节中,我们将了解在编程时遇到的不同类型的文件。

源与程序

有两种类型的编程语言:

  1. 解释型语言:我们写的是人可读的源代码,我们直接执行它:计算机在遇到我们的源代码时逐行翻译。
  2. 编译语言:我们的整个源代码首先被编译成一个程序,然后我们再执行它。

解释型语言的例子有 Python、Matlab、Basic、Lisp。解释型语言有一些优势:通常我们可以在一个交互式环境中编写它们,使我们可以非常迅速地测试代码。它们还允许我们在运行时动态地构建代码。然而,所有这些灵活性是有代价的:如果一个源码行被执行两次,它就会被翻译两次。那么,在本书中,我们将侧重于编译语言,用 CFortran作为原型例子。

因此,现在我们已经区分了可读的源代码和不可读的、但可执行的程序代码。在本教程中,我们将了解从一个到另一个的翻译过程。进行这种翻译的程序被称为编译器,编译器内部发生的事情是计算机科学的另一个分支。可以这么说,本教程将是一本编译器的 “用户手册”。

二进制文件

程序可以会非常大,所以我们不想为每一个改动都重新编译所有的东西:通常我们会把我们的源文件分成多个文件,并分别编译它们。

首先,一个源文件可以被编译成一个目标文件,这有点像一个可执行文件的一部分:它本身什么都不做,但它可以和其他文件结合起来。它本身不做任何事情,但它可以与其他目标文件结合起来,形成一个可执行文件。

如果我们有一个目标文件的集合,而我们需要在一个以上的程序中使用,通常是一个好主意,即做一个库:一个可以用来形成可执行文件的目标文件包。

通常情况下,库是由专家编写的含有专门用途的代码,如线性代数操作。库重要到它们可以是用于商业,我们可以购买我们需要某种目的的专业代码。

现在我们将学习这些类型的文件是如何创建和使用的。

简单汇编

目的:在本节中,我们将学习可执行文件和目标文件。

编译器

我们把源代码变成程序的主要工具是编译器,编译器是专门针对一种语言的。我们对C语言使用不同的编译器,而对Fortran 使用不同的编译器,我们也可以为同一种语言有两个来自不同的供应商的编译器。例如,虽然许多人使用开源的 gccclang 编译器系列,而像英特尔和 IBM 这样的公司提供的编译器可能会在其处理器上提供更有效的代码处理器上提供更有效的代码。

编译单个文件

让我们从一个简单的程序开始,它的全部源代码都在一个文件中。

一个 hello.c文件:用我们喜欢的编译器编译这个程序;本教程中我们将使用gcc。但也可以根据需要使用我们自己的编译器,编译的结果是创建了一个文件 a.out,这就是可执行文件。

  1. %% gcc hello.c
  2. %% ./a.out
  3. hello world

我们可以用 -o 选项得到一个更合理的程序名称:

  1. %% gcc -o helloprog hello.c
  2. %% ./helloprog
  3. hello world

多个文件:编译和链接

现在我们转到一个不止一个源文件中的程序。

主程序:fooprog.c

  1. #include<stdlib.h>
  2. #include<stdio.h>
  3. extern void bar(chzar *);
  4. int main() {
  5. bar("hello world\n");
  6. return 0;
  7. }

子程序:fooprog.c

  1. #include<stdlib.h>
  2. #include<stdio.h>
  3. void bar(char *s) {
  4. printf("%s",s);
  5. return;
  6. }

和以前一样,我们可以用一个命令来运行程序。

Output

[compile/c] makeoneprogram:

  1. clang -o oneprogram fooprog.c foosub.c
  2. ./oneprogram
  3. hello world

然而,我们也可以分步进行,分别编译每个文件,然后把它们连接起来。

Output

[compile/c] makeseparatecompile:

  1. clang -o oneprogram fooprog.o foosub.o
  2. ./oneprogram
  3. hello world

-c 选项告诉编译器对源文件进行编译,并给出一个目标文件。第三条命令不止作为链接器,将目标文件连接成一个可执行文件。(对于分布在多个文件中的程序,总是存在编辑子程序定义的危险,然后忘记更新使用过的的地方。参见教程第22节,了解处理这个问题的方法)。

每个目标文件都定义了一些程序名称,并使用了一些在其中未定义的其他名称,但这些名称将在其他目标文件或系统库中被定义,但在其他目标文件或系统库中会被定义。使用 nm 命令来显示这些:

  1. [c:264] nm foosub.o
  2. 0000000000000000 T _bar
  3. U _printf

T的行表示已定义的程序;带U的行表示已使用但未在本文件中定义的程序。在这种情况下,printf是一个在链接器阶段提供的系统程序。

目的:在本节中,我们将学习Unix中的库。

如果我们写了一些子程序,并且我们想与其他人分享它们(或是售卖它们),那么交出单个目标文件是不方便的。相反,解决方案是将它们合并成一个库。首先,我们看一下归档工具 ar 使用的静态库,一个静态库被链接到我们的可执行文件中,成为它的一部分可能会导致可执行文件变大;接下来我们会了解到共享库,它不存在这个问题。

创建一个包含我们的库的目录(取决于我们的库是什么,这可以是一个系统的目录,如 /usr/bin ),并在那里创建库文件。

Output

[compile/c] makestaticlib:

  1. Making static library
  2. for o in foosub.o ; do \
  3. ar cr libs/libfoo.a ${o} ; \
  4. done

nm 命令告诉我们库中有什么,就像它对对象文件所做的那样,但现在它还告诉我们哪些对象文件在库中:

  1. %% nm ../lib/libfoo.a
  2. ../lib/libfoo.a(foosub.o):
  3. 00000000 T _bar
  4. U _printf

该库可以通过明确给出其名称或指定一个库的路径来链接到我们的可执行文件中:

Output

[compile/c] makestaticlib:

  1. for o in foosub.o ; do \
  2. ar cr libs/libfoo.a ${o} ; \
  3. done
  4. Linking to static library
  5. clang -o staticprogram fooprog.o -Llibs -lfoo
  6. .. note the size of the program
  7. -rwxr-xr-x 1 eijkhout staff 8464 Jan 23 13:07 staticprogram
  8. .. running:
  9. hello world

虽然使用起来比较复杂,但共享库有几个优点。例如,由于它们没有被链接到可执行文件中,而只是在运行时被加载,所以它们会导可执行文件更小。它们不是用 ar 创建的,而是通过编译器创建的:

Output

[compile/c] makedynamiclib:

  1. Making shared library
  2. clang -o libs/libfoo.so -shared foosub.o

我们可以再次使用 nm 命令:

  1. %% nm ../lib/libfoo.so
  2. ../lib/libfoo.so(single module):
  3. 00000fc4 t \__dyld_func_lookup _
  4. 0000000 t \_mh_dylib_header
  5. 00000fd2 T _bar U _printf
  6. 00001000 d dyld__mach_header
  7. 00000fb0 t dyld_stub_binding_helper*

共享库实际上并没有被链接到可执行文件中;相反,可执行文件需要的信息是在执行时找到库的位置。一种方法是用 LD_LIBRARY_PATH 来做到这一点:

Output

[compile/c] dynamicprogram:

  1. Linking to shared library
  2. clang -o libs/libfoo.so -shared foosub.o
  3. clang -o dynamicprogram fooprog.o -Llibs -lfoo
  4. .. note the size of the program
  5. -rwxr-xr-x 1 eijkhout staff 8432 Jan 23 13:07 dynamicprogram
  6. .. note unresolved link to a library
  7. make[3]: ldd: No such file or directory
  8. make[3]: [run_dynamicprogram] Error 1 (ignored)
  9. .. running by itself:
  10. hello world
  11. .. running with updated library path:
  12. LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:./libs ./dynamicprogram
  13. hello world

另一个解决方案是让路径包含在可执行文件中:

  1. %% gcc -o foo fooprog.o -L../lib -Wl,-rpath,‘pwd‘/../lib -lfoo
  2. %% ./foo
  3. hello world*

现在的链接行包含了库的路径两次:

  1. 一次是-L指令,这样链接器就可以解析所有的引用,以及

  2. 另一次是使用链接器指令 -Wl,-rpath,‘pwd’/…/lib将路径存储到可执行文件中,以便在运行时可以找到它。

    使用 ldd 命令来获取关于我们的可执行文件使用了哪些共享库的信息。(在Mac OS X上,使用 otool -L代替)。