目录
接上一篇文章: 指令——计算机的语言(part 1)
1.1 翻译并执行程序
程序翻译层次图如下:
首先高级语言比如说C,会被编译器编译成汇编语言,然后汇编器把汇编语言编译成目标文件,机器语言。然后链接器会将程序需要库文件和之前汇编器编译出来的把目标文件链接在一起生成可执行文件。然后加载器将可执行文件加载到内存合适的位置,然后处理器就可以执行该程序了。
1.1.1 编译器
将高级语言翻译成机器能懂的汇编语言,高级语言编写的程序比汇编语言简洁的多,所以程序员效率更高。
1.1.2 汇编器
因为汇编语言对于高级语言也是一种接口,所以汇编语言能处理一些机器语言指令的一些变种,就像这些变种是他自己的语言一样。硬件不需要实现这些指令,然而他们在汇编语言总的存在简化了程序转变和编程,这些指令被称为伪指令。
汇编语器的主要任务是将汇编语言转换成目标文件(object file),因此汇编器必须处理所有标号对应的地址。汇编器将分支和数据传输指令中用到的标号都放入一个符号表中。这个表由标号和地址成对构成。
UNIX系统中目标文件通常包括以下6个不同的部分:
- 目标文件头:描述目标文件其他部分的大小和位置
- 正文段:包含机器语言代码
- 静态数据段:包含在程序生命周期内分配的数据
- 重要定位信息:包含了程序加载进内存时依赖的绝对地址的指令和数据
- 符号表:包含未定义的符号标记,如外部引用
- 调试信息:包含一份说明目标模块如何编译的简明描述,这样调试器能将机器指令关联到C源文件,并使数据结构也变的可读。
1.1.3 链接器
到目前为止,任何一行程序的修改都需要重新编译和汇编整个程序。然而全部重新编译是对计算机资源的严重浪费,尤其是对于一个标准库文件,没有任何改变的情况下,每次修改代码都要重新编译。另一种方式是只编译和汇编每个函数,这样使得代码的改变只用编译和汇编有改动的函数,其他的不需要重新编译,这样就节省了一大部分计算机资源。这个方法需要一个新的系统工具:链接器。它可以把所有独立编译的机器语言程序拼接在一起。
链接器的工作分为三个步骤:
- 将代码和数据模块象征性的放入内存中
- 决定数据和指令标签的地址
- 修补内部和外部引用
链接器使用每个目标模块中的重要定位信息和符号表来解析所有未定义标签。如果外部引用都解析完了,链接器会决定每个模块将要占用的内存的位置。因为所有模块都是独立编译的,所有编译器不清楚这些模块在内存中的位置,那么当程序被放到内存中的时候就需要链接器来决定各个模块的绝对地址,即于寄存器无关的所有地址都必须重新定位用来反映真实的地址。
1.1.4 加载器
可执行文件存在磁盘中之后,操作系统可以将其读到内存中并启动执行它。在UNIX系统中,加载器工作步骤如下:
- 读取可执行文件的文件头来确定正文段和数据段的大小
- 为正文和数据段创建一个足够大的内存空间
- 将可执行文件的指令和数据复制到内存中
- 把主程序的参数(如果有)复制到栈顶
- 初始化寄存器,将栈指针指向第一个空位置
- 跳转到启动例程,它会将参数复制到参数寄存器并调用程序的main函数,当main函数返回时,启动例程会调用exit终止程序。
大概总结一下,就是确定可执行文件的内容,大小,然后分配内存空间,接着复制机器代码和参数进入内存,最后初始化寄存器并执行程序。
1.1.5 动态链接库
原本调用按上面的方法调用静态库是最快捷的调用方式,但是会有以下缺点:
- 库会成为可执行代码的一部分,这样在更新或发布一些新版本的时候,导致静态链接库依然是旧版本。
- 在程序运行时,虽然并不是所有库文件的内容都会被用到,但是整个库需要被加载到内存中,实际上程序只是使用了其中的一小部分,这样会造成资源的浪费。
基于以上的缺点,就出现了动态链接库的概念,只有在程序运行的时候这些库才会被链接加载,这样就减少了程序的大小。