提交 8cf91ef8 编写于 作者: W wizardforcel

ch1

上级 989a4db1
......@@ -52,3 +52,150 @@ int add(int x, int y) {
'__name__': '__main__', '__doc__': None, '__package__': None}
```
这段代码表明,变量的名称在程序运行期间储存在内存中(以及其它作为默认运行时环境一部分的值)。
在编译语言中,变量的名称只存在于编译时,而不是运行时。编译期为每个变量选择一个位置,并记录这些位置作为所编译程序的一部分[1]。变量的位置被称为“地址”。在运行期间,每个变量的值都储存在它的地址处,但是变量的名称完全不会储存(除非它们由于调试目的被编译器添加)。
> [1] 这只是一个简述,之后我们会深入了解更多细节。
## 1.3 编译过程
作为程序员,你应该对编译期间发生的事情有所认识。如果你理解了这个过程,它会帮助你解释错误信息,调试你的代码,以及避免常见的陷阱。
下面是编译的步骤:
1. 预处理:C是包含“预处理指令”的几种语言之一,它生效于编译之前。例如,`#include`指令使其它文件的源代码插入到指令所在的位置。
2. 解析:在解析过程中,编译器读取源代码,并构建程序的内部表示,称为“抽象语法树”(AST)。这一阶段的错误检测通常为语法错误。
3. 静态检查:编译器会检查变量和值的类型是否正确,函数调用是否带有正确数量和类型的参数,以及其它。这一阶段的错误检测通常为一些“静态语义”的错误。
4. 代码生成:编译器读取程序的内部表示,并生成机器码或字节码。
5. 链接:如果程序使用了定义在库中的值或函数,编译器需要找到合适的库并包含所需的代码。
6. 优化:在这个过程的几个时间点上,编译器可以修改程序来生成运行更快或占用更少空间的代码。大多数优化都是一些简单的修改,来消除明显的浪费。但是一些编译器会执行复杂的分析和修改。
通常当你运行`gcc`时,它会执行上述所有步骤,并且生成一份可执行文件。例如,下面是一个小型的C语言程序:
```c
#include <stdio.h>
int main()
{
printf("Hello World\n");
}
```
如果你把它保存在名为`hello.c`的文件中,你可以像这样编译并运行它:
```sh
$ gcc hello.c
$ ./a.out
```
通常,`gcc`将可执行代码储存在名为`a.out`的文件中(它原本代表汇编器的输出,即“assembler output”)。第二行运行了这个可执行文件。`./`前缀告诉shell在当前目录中寻找它。
使用`-o`选项来为可执行文件提供一个更好的名字,通常是个不错的注意。
```sh
$ gcc hello.c -o hello
$ ./hello
```
## 1.4 目标代码
`-c`选项告诉`gcc`编译程序并生成机器码,但是不链接它们或生成可执行文件:
```sh
$ gcc hello.c -c
```
执行结果是名为`hello.o`的文件,其中`o`代表“目标代码”(object code),它就是编译后的程序。目标代码并不是可执行代码,但是它可以链接到可执行文件中。
`nm` UNIX命令可以读取目标文件并生成关于它所定义和所使用的名称的信息。例如:
```sh
$ nm hello.o
0000000000000000 T main
U puts
```
输出显示,`hello.o`定义了`main`名称,并使用了`puts`函数,它代表“输出字符串”(put string)。在这个例子中,`gcc`通过将`printf`替换掉执行了优化,它是一个复杂的大型函数。而`puts`相对来说比较简单。
你可以使用`-O`选项来控制`gcc`优化的程度。通常,它执行非常细微的优化,可以使调试更加容易。`-O1`选项会开启最为普通和安全的优化。更高的数值开启需要长时间编译的高级优化。
理论上,优化除了加速运行之外,不应改变程序的行为。但是如果你的程序中有微妙的bug,你可能会发现,优化会使bug出现或消失。在开发新的代码时,关闭优化通常是一个不错的注意。一旦程序正常运行并通过了适当的测试,你可以开启优化,并确保测试仍然能够通过。
## 1.5 汇编代码
`-c`选项类似。`-S`告诉`gcc`编译程序并生成汇编代码,它通常为机器代码的可读形式。
```sh
$ gcc hello.c -S
```
执行结果是名为`hello.s`的文件,它可能看起来是这样:
```asm
.file "hello.c"
.section .rodata
.LC0:
.string "Hello World"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $.LC0, %edi
call puts
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu/Linaro 4.7.3-1ubuntu1) 4.7.3"
.section .note.GNU-stack,"",@progbits
```
`gcc`通常为你所运行的机器生成代码,所以对我来说它生成x86汇编代码,运行在Intel、AMD和许多其它处理器上面。如果你运行在不同的架构上,你会看到不同的代码。
## 1.6 预处理
在编译过程中再往前退一步,你可以使用`-E`选项来只运行预处理器:
```sh
$ gcc hello.c -E
```
执行结果就是预处理器的输出。这个例子中,它含有来自`stdio.h`的被包含代码,和`stdio.h`所包含的所有文件,还有这些文件所包含的所有文件,以及其它。在我的机器上,共计800行代码。因为几乎每个C语言程序都会包含`stdio.h`,这800行代码经常会被编译。如果你像大多数C程序那样也包含了`stdlib.h`,结果会变成多于1800行代码。
## 1.7 理解错误
既然我们知道了编译过程的步骤,理解错误消息就变得十分容易。例如,如果`#include`指令中出现了一个错误,你会从预处理器处得到一个错误:
```
hello.c:1:20: fatal error: stdioo.h: No such file or directory
compilation terminated.
```
如果有语法错误,你会从编译器处得到一个错误:
```
hello.c: In function 'main':
hello.c:6:1: error: expected ';' before '}' token
```
如果你使用了没有在任何标准库中定义的函数,你会从链接器处得到一个错误:
```
/tmp/cc7iAUbN.o: In function `main':
hello.c:(.text+0xf): undefined reference to `printff'
collect2: error: ld returned 1 exit status
```
`id`是UNIX链接器的名称,这样命名是因为“装载”(loading)是编译过程中的另一个步骤,它和链接关系密切。
一旦程序运行起来,C会执行非常少的运行时检测,所以你会看到极少的运行时错误。如果你发生了除零错误,或者执行了其它非法的浮点操作,你会得到“浮点数异常”。而且,如果你尝试读写内存的不正确位置,你会得到“段错误”。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册