我简要介绍了C程序的构建块,然后检查了通常隐藏在gcc调用后面的构建步骤。
传统的编译语言,例如C和C ++,是组织在源文件中的,每个源文件通常被“编译”为一个“对象文件”。每个源文件都是一个“翻译单元” - 在处理所有包含指令之后。 (因此,一个翻译单元通常由多个文件组成,并且同一个包含文件通常出现在多个翻译单元中 - 文件和翻译单元在严格意义上具有n:m关系。但实际上,可以说“翻译单元”是C文件。)
要将单个源文件编译为对象文件,需要向编译器传递-c标志:
gcc -c myfile.c
这将在相同目录中创建myfile.o或myfile.obj。
对象文件包含机器代码和数据(以及可能的调试信息,但我们在此忽略)。机器代码包含函数,数据以变量的形式出现。对象文件中的函数和变量都有名称,称为“符号”。编译器通常通过在程序中添加下划线或类似字符来转换变量和函数名称,在C ++中生成的(“缠绕”)名称包含有关类型和函数参数的信息。
某些符号,例如全局变量和普通函数的名称,可从其他对象文件中使用;它们被“导出”。
一个符号可以(仅略微简化)被认为是一个地址别名:对于函数,名称是跳转目标地址的别名;对于变量,名称是可读取和写入程序的内存位置的地址别名。
您的文件help.c包含函数herp的代码。在C中,默认情况下函数具有“外部链接”,它们可以从其他翻译单元中使用。它们的名称 - “符号” - 被导出。
在现代C中,使用在不同翻译单元中定义的名称的源文件必须声明该名称。这告诉编译器如何处理它以及在源代码中可以以哪些方式语法上使用它(例如调用函数,分配给变量,索引数组)。编译器生成从此“符号地址”读取或跳转到该“符号地址”的代码;连接器的工作是将所有这些符号地址替换为指向最终可执行文件中的现有数据和代码的“实际”内存位置,以便跳转和内存访问落在所需位置。
在使用某个名称(函数、变量)的文件中,该名称的声明可以是“手动”的,比如
void herp();
,直接出现在你的文件中,在第一次使用之前。不过,更典型的情况是,在定义了其他翻译单元可以使用的名称的翻译单元中,这些名称在头文件中声明,即你的
helper.h
。使用的翻译单元通过
#include
指令使用头文件中“罐装”的声明。这里没有任何魔法;一个包含指令只是将包含文件文本插入到文件中,就好像它是直接写到文件中一样。没有任何区别。特别地,
包含头文件并不会告诉链接器链接相应的源文件。原因很简单:链接器从不知道包含的文件,因为这一信息在编译成目标文件时被抹掉了。
这意味着,在你的情况下,必须编译
help.c
,并告诉链接器将其与程序的其余部分(即从
main.c
的编译中获得的代码)结合起来。
关于如何执行此操作的讨论要更加困难,因为这个过程非常普遍,以至于典型的C编译器集成了编译和链接阶段:
gcc -o myprog help.c main.c
只需执行创建可执行文件
myprog
所需的所有操作。
当我们说“编译器”时,比如指的是
gcc
,我们通常实际上指的是“编译器驱动程序”,它从命令行接收命令和文件,并执行必要的步骤,以达到所需的结果,例如从源文件生成可执行程序。对于gcc而言,
真正的编译器是
cc1
,它生成一个汇编文件,必须使用
as
将其汇编成一个目标文件。在编译源文件之后,gcc使用适当的选项调用链接器,生成可执行文件。
以下是详细描述这些阶段的示例会话:
$ ls
Makefile help.c help.h main.c
$ /lib/gcc/x86_64-pc-cygwin/7.4.0/cc1 main.c
main
Analyzing compilation unit
Performing interprocedural optimizations
<*free_lang_data> <visibility> <build_ssa_passes> <opt_local_passes> <targetclone> <free-inline-summary> <emutls> <whole-program> <inline>Assembling functions:
<materialize-all-clones> <simdclone> main
Execution times (seconds)
phase setup : 0.00 ( 0%) usr 0.00 ( 0%) sys 0.00 (22%) wall 1184 kB (86%) ggc
TOTAL : 0.00 0.00 0.01 1374 kB
$ ls
Makefile help.c help.h main.c main.s
$ /lib/gcc/x86_64-pc-cygwin/7.4.0/cc1 help.c
herp
Analyzing compilation unit
Performing interprocedural optimizations
<*free_lang_data> <visibility> <build_ssa_passes> <opt_local_passes> <targetclone> <free-inline-summary> <emutls> <whole-program> <inline>Assembling functions:
<materialize-all-clones> <simdclone> herp
Execution times (seconds)
phase setup : 0.01 (100%) usr 0.00 ( 0%) sys 0.00 (33%) wall 1184 kB (86%) ggc
TOTAL : 0.01 0.00 0.01 1370 kB
$ ls
Makefile help.c help.h help.s main.c main.s
我们现在有两个汇编文件,main.s和help.s,可以使用汇编器as将它们汇编成目标文件。但是让我们快速查看一下help.s文件:
$ cat help.s
.file "help.c"
.text
.globl some_variable
.data
.align 4
some_variable:
.long 1
.text
.globl herp
.def herp; .scl 2; .type 32; .endef
.seh_proc herp
herp:
pushq %rbp
.seh_pushreg %rbp
movq %rsp, %rbp
.seh_setframe %rbp, 0
.seh_endprologue
nop
popq %rbp
ret
.seh_endproc
.ident "GCC: (GNU) 7.4.0"
即使我们对汇编语言一无所知,也可以清楚地识别出符号
some_variable
和
herp
,这些是汇编标签。
啊,我忘记在help.c中添加了一个变量定义:
$ cat help.c
#include "help.h"
int some_variable = 1;
void herp() {}
我们可以使用汇编器
as
来组装汇编文件:
$ as main.s -o main.o
$ ls
Makefile help.c help.h help.s main.c main.o main.s
$ as help.s -o help.o
$ ls
Makefile help.c help.h help.o help.s main.c main.o main.s
现在我们有两个目标文件。我们可以使用实用程序
nm
("
name
mangling")来查看哪些符号被导出("extern")或需要("undefined"):
$ nm --extern-only help.o
0000000000000000 T herp
0000000000000000 D some_variable
$ nm --extern-only main.o
U __main
U herp
"T"表示符号位于“text”部分,其中包含代码;“D”是数据部分,“U”代表“未定义”。 (未定义的
__main
是
gcc和/或cygwin的怪癖。)
这里是问题的根源:除非将main.o与一个定义该未定义符号的对象文件配对,否则链接器无法“解析”名称并且无法生成跳转。没有跳转目标。
现在我们可以将两个对象文件链接到可执行文件中。Cygwin要求我们链接cygwin.dll;对此我们很抱歉。
$ ld main.o help.o /bin/cygwin1.dll -o main
$ ls
Makefile help.c help.h help.o help.s main* main.c main.o main.s
这就是全部内容。需要补充的是,该程序无法正常运行。它不会结束,也不会对Ctrl-C做出反应;我可能错过了一些Gnu或Windows构建细节,这些细节由gcc为我们完成。
啊,Makefile。Makefile由目标定义和这些目标的依赖项组成:一行
main: help.o main.o
指定一个名为“main”的目标,其依赖于两个.o文件。
Makefile通常还包含规则,指定如何生成目标。但是Make具有内置规则;它知道您调用编译器从.c文件生成.o文件(并自动考虑此依赖项),并且它知道您将o文件链接在一起以生成依赖于它们的目标,前提是目标与其中一个.o文件具有相同的名称。
因此,我们不需要任何规则:我们只需定义非隐式依赖项。您的项目的整个Makefile可简化为:
$ cat Makefile
CC=gcc
main: help.o main.o
help.o: help.h
main.o: help.h
CC=gcc
指定要使用的C编译器。CC是内置的make变量,用于指定C编译器(CXX将指定C++编译器,例如g ++)。
让我们看一下:
$ make
gcc -c -o main.o main.c
gcc -c -o help.o help.c
gcc main.o help.o -o main
$ ls
Makefile help.c help.h help.o main.c main.exe* main.o
依赖关系是否正常工作?
$ make
make: 'main' is up to date.
$ touch main.c
$ make
gcc -c -o main.o main.c
gcc main.o help.o -o main
$ touch help.h
$ make
gcc -c -o main.o main.c
gcc -c -o help.o help.c
gcc main.o help.o -o main
看起来很不错:在触摸单个源文件后,make仅编译该文件;但是,如果触摸到两个文件都依赖的头文件,则make会编译两个文件。无论如何链接都需要完成。