操作系统思考 第一章 编译

第一章 编译

作者:Allen B. Downey

原文:Chapter 1 Compilation

译者:飞龙

协议:CC BY-NC-SA 4.0

1.1 编译语言和解释语言

人们通常把编程语言描述为编译语言或者解释语言。前者的意思是程序被翻译成机器语言,之后由硬件执行;而后者的意思是程序被软件解释器读取并执行。例如,C被认为是编译语言,而Python被认为是解释语言。但是二者之间的界限并不总是那么明显。

首先,许多语言既可以编译执行也可以解释执行。例如,存在C的解释器,和Python的编译器。其次,类似Java的语言混合了这两种方法,它先把程序编译成中间语言,之后在解释器中执行转换后的程序。Java使用了一种叫做“Java 字节码”的中间语言,它类似于机器语言,但是由软件解释器执行,即Java虚拟机(JVM)。

所以,编译执行或解释执行并不是语言的内在特征。尽管如此,在编译语言和解释语言之间有一些普遍的差异。

1.2 静态类型

许多解释语言都支持动态类型,但是编译语言通常限制为静态类型。在静态类型的语言中,你可以通过观察程序,来分辨出每个变量都指向哪种类型。在动态类型的语言中,直到运行起来,你才能知道变量的类型。通常,“静态”指那些在编译时发生的事情,而“动态”指在运行时发生的事情。

例如,在Python中你可以像这样编写函数:

def add(x, y):
    return x + y

观察这段代码,你不能分辨出xy所指向的类型。这个函数在运行时可能会调用数次,每次都接受不同类型的值。任何支持加法操作的值都是有效的,任何其它类型的值都会引发异常,或者“运行时错误”。

C中你可以像这样编写同样的函数:

int add(int x, int y) {
    return x + y;
}

函数的第一行包含了参数及返回值的“类型声明”:xy都声明为整数,这意味着我们可以在编译时检查加法操作对该类型是否合法(是的)。返回值也声明为整数。

由于这些类型声明,当函数在程序其它位置调用时,编译器就可以检查所提供的参数是否具有正确类型,以及返回值是否使用正确。

这些检查在程序开始运行之前发生,所以可以更快地找到错误。更重要的是,程序永远不会运行的一部分中也可以找到错误。而且,这些检查不必发生于运行期间,这也是编译语言通常快于解释语言的原因之一。

编译时的类型声明也会节省空间。在动态语言中,变量的名称在程序运行时储存在内存中,并且它们通常可由程序访问。例如,在Python中,内建的locals函数返回含有变量名称和值的字典。下面是Python解释器中的一个示例:

>>> x = 5
>>> print locals()
{‘x‘: 5, ‘__builtins__‘: <module ‘__builtin__‘ (built-in)>,
‘__name__‘: ‘__main__‘, ‘__doc__‘: None, ‘__package__‘: None}

这段代码表明,变量的名称在程序运行期间储存在内存中(以及其它作为默认运行时环境一部分的值)。

在编译语言中,变量的名称只存在于编译时,而不是运行时。编译器为每个变量选择一个位置,并记录这些位置作为所编译程序的一部分[1]。变量的位置被称为“地址”。在运行期间,每个变量的值都储存在它的地址处,但是变量的名称完全不会储存(除非它们由于调试目的被编译器添加)。

[1] 这只是一个简述,之后我们会深入了解更多细节。

1.3 编译过程

作为程序员,你应该对编译期间发生的事情有所认识。如果你理解了这个过程,它会帮助你解释错误信息,调试你的代码,以及避免常见的陷阱。

下面是编译的步骤:

  1. 预处理:C是包含“预处理指令”的几种语言之一,它生效于编译之前。例如,#include指令使其它文件的源代码插入到指令所在的位置。
  2. 解析:在解析过程中,编译器读取源代码,并构建程序的内部表示,称为“抽象语法树”(AST)。这一阶段的错误检测通常为语法错误。
  3. 静态检查:编译器会检查变量和值的类型是否正确,函数调用是否带有正确数量和类型的参数,以及其它。这一阶段的错误检测通常为一些“静态语义”的错误。
  4. 代码生成:编译器读取程序的内部表示,并生成机器码或字节码。
  5. 链接:如果程序使用了定义在库中的值或函数,编译器需要找到合适的库并包含所需的代码。
  6. 优化:在这个过程的几个时间点上,编译器可以修改程序来生成运行更快或占用更少空间的代码。大多数优化都是一些简单的修改,来消除明显的浪费。但是一些编译器会执行复杂的分析和修改。

通常当你运行gcc时,它会执行上述所有步骤,并且生成一份可执行文件。例如,下面是一个小型的C语言程序:

#include <stdio.h>
int main()
{
    printf("Hello World\n");
}

如果你把它保存在名为hello.c的文件中,你可以像这样编译并运行它:

$ gcc hello.c
$ ./a.out

通常,gcc将可执行代码储存在名为a.out的文件中(它原本代表汇编器的输出,即“assembler
output”)。第二行运行了这个可执行文件。./前缀告诉shell在当前目录中寻找它。

使用-o选项来为可执行文件提供一个更好的名字,通常是个不错的注意。

$ gcc hello.c -o hello
$ ./hello

1.4 目标代码

-c选项告诉gcc编译程序并生成机器码,但是不链接它们或生成可执行文件:

$ gcc hello.c -c

执行结果是名为hello.o的文件,其中o代表“目标代码”(object
code),它就是编译后的程序。目标代码并不是可执行代码,但是它可以链接到可执行文件中。

nm UNIX命令可以读取目标文件并生成关于它所定义和所使用的名称的信息。例如:

$ 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编译程序并生成汇编代码,它通常为机器代码的可读形式。

$ gcc hello.c -S

执行结果是名为hello.s的文件,它可能看起来是这样:

        .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选项来只运行预处理器:

$ 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会执行非常少的运行时检测,所以你会看到极少的运行时错误。如果你发生了除零错误,或者执行了其它非法的浮点操作,你会得到“浮点数异常”。而且,如果你尝试读写内存的不正确位置,你会得到“段错误”。

时间: 2024-10-12 15:37:34

操作系统思考 第一章 编译的相关文章

操作系统原理 第一章第二章复习

操作系统复习 第一章 操作系统概述 基本概念 吞吐量:单位时间内系统能处理的工作量. 进程:正在动态执行的程序 实时操作系系统:实时计算.计算的正确性不仅依赖于系统计算的逻辑结果,还依赖于产生这个结果的时间一类的计算. 操作系统的特征:现代操作系统大多支持多任务,具有并发.共享.虚拟.异步的特征. 单道批处理系统 特性:自动性.顺序性.单道性. 作业独占CPU和内存. 多道批处理系统 特性: 多道性.无序性.调度性.复杂性. 优点:提高CPU的利用率.提高内存和I/O设备的利用率.增加系统吞吐量

编译技术图示(第一章 编译概述)

编译技术图示(第一章 编译概述) 源程序——>机器代码 分析(前端):分成小部分,找出小部分属性,包括:词法分析.语法分析.语义分析.中间代码生成 合成(后端),包括:中间代码优化.目标代码生成.目标代码优化 1.词法分析:识别.删除单词符号.词法检查 输入:源代码,输出:二元式<单词类别,单词属性> 2.语法分析 输出:抽象语法树(AST),从下向上看 3.语义分析 有限,大部分都是类型(运算)检查 4.中间代码生成 eg:x=a+b —> t1=a+b x=t1 为什么分为两个

操作系统思考 第九章 线程

第九章 线程 作者:Allen B. Downey 原文:Chapter 9 Threads 译者:飞龙 协议:CC BY-NC-SA 4.0 当我在2.3节提到线程的时候,我说过线程就是一种进程.现在我会更仔细地解释它. 当你创建进程时,操作系统会创建一块新的地址空间,它包含text段.static段.和堆区.它也会创建新的"执行线程",这包括程序计数器和其它硬件状态,以及运行时栈. 我们目前为止看到的进程都是"单线程"的,也就是说每个地址空间中只运行一个执行线程

网络操作系统习题第一章

1.什么是网络操作系统?网络操作系统具有哪些基本功能? 答:   除了实现单机操作系统全部功能外,还具备管理网络中的共享资源,实现用户通信以及方便用户使用网络等功能,是网络的心脏和灵魂. 网络操作系统是网络用户与计算机网络之间的接口,是计算机网络中管理一台或多台主机的软硬件资源.支持网络通信.提供网络服务的程序集合. 功能(1)共享资源管理 (2)网络通信 (3)网络服务 (4)网络管理 (5)互操作能力 2.网络操作系统具有哪些特征? (1)客户/服务器模式 (2)32位操作系统 (3)抢先式

Orange&#39;s 自己动手写操作系统 第一章 十分钟完成的操作系统 U盘启动 全记录

材料: 1 nasm:编译汇编源代码,网上很多地方有下 2  WinHex:作为windows系统中的写U盘工具,需要是正版(full version)才有写的权限,推荐:http://down.liangchan.net/WinHex_16.7.rar 步骤: 1 编译得到引导程序的机器代码.用命令行编译汇编源代码:name boot.asm -o boot.bin,其中boot.bin文件产生在命令行的当前目录中. 2 将引导程序写入到U盘引导盘的第一个扇区的第一个字节处(后),即主引导区.

第一章 Android系统的编译和移植实例

第一章 Android系统的编译和移植实例 这一章节主要介绍了Android系统的编译和移植技术,作为建立在Linux内核的基础上的Android操作系统,它的编译和移植不论在过程还是技术方面都和嵌入式Linux非常相似. 首先要准备一套可以正常运行Linux系统的一套开发版,需要在其移植Android系统,并能够正常运行. 移植的主要过程为: 1.下载Android Linux 内核 2.安装交叉工具链 3.移植Android Linux 内核支持的平台 4.安装Android SDK 5.获

操作系统第一章总结/

第一章 操作系统概述 操作系统功能:计算机系统资源的管理者,用户和计算机硬件系统之间的接口,可用做扩充机器. 操作系统是一种系统软件. 操作系统特征并发,共享,虚拟,异步. 命令接口:用户利用这些操作命令来组织和控制作业的执行. 程序接口(系统调用,广义指令):编程人员使用它们请求操作系统服务. 系统调用是操作系统提供给应用程序使用内核功能的接口. 系统中缓存全部由操作系统管理,用户不可见. 操作系统管理:处理机管理,存储器管理,文件管理,设备管理,用户接口. 多道批与单道相比,优点是cpu利用

第一章 操作系统引论

知识框架 主导:PV操作 核心:(1)进程管理,存储管理,文件管理,I/O管理 (2)多处理机,多媒体 (408中没有) 概念:进程,线程,死锁,中断,DMA等 1.1  操作系统的目标和作用 操作系统的目标与应用环境有关. (1)在查询系统中所用的OS,希望能提供良好的人机交互性: (2)对于应用于工业控制.武器控制以及多媒体环境下的OS,要求其具有实时性: (3)对于微机上配置的OS,则更看重的是其使用的方便性: 1.1.1  操作系统的目标 1.  方便性---用户 2.  有效性---系

学习第一章 Android系统的编译和移植实例后的心得体会

说起来,去年在岳老师的带领下就接触了嵌入式系统的编译和移植.而现在我们又开始接触Android系统的编译和移植.第一章主要介绍安卓系统的编译和移植技术.其实安卓和嵌入式非常相似. 安卓 移植涉及的主要过程大致分为六步:1.下载安卓linux内核. 2.安装交叉工具链.3.移植安卓linux内核支持EZ6410平台.4.安装安卓SDK.4.获得安卓根文件系统.5.设置系统环境,完成安卓正常启动.虽然步骤不多,但是涉及了很多东西.在开始内核移植之前,先完成工具链的搭建.在移植过程中会发现硬件差异.差