分类: LINUX
2011-07-20 17:03:36
在理解一个源代码是如何成为可执行文件时,我简单的回顾下硬件层面、操作系统层面的知识。
开机启动
一 BIOS扫描基本设备,cpu、memory、display etc,从硬盘启动,读盘面1磁道1扇区1的内容进入内存,这段内容是操作系统引导程序
二 cpu的任务是计算,不同的cpu制定了一套instruction set,通过调用指令集,输入数据,输出数据
三 DMA directly memory access 主存,数据存储介质,读写速率高,满足对cpu的数据输入输出
四 操作系统 负责用户资源(进程、内存、文件系统、硬件设备etc)的管理、调度、安全
以上是我们在开机后做的工作,大量的、复杂的工作,而我们确实享受者。
我们开始编写源代码,然后编译执行。我们感觉到自己非常厉害,其实我们仅仅做了一点点东西。
1 编译器
源代码通过编译器变成汇编文件。
编译器做的主要工作就是根据语言,词法、语法分析,将面向对象的、面向过程的高级语言翻译成汇编语言。
我认为编译器应该是建立在操作系统上的,因为不同的cpu的汇编语言存在差异,所以编译器无法跨硬件平台,需要与操作系统匹配。
(java的编译与c c++的编译,我们称之为传统的编译,是不同的,java的编译是生成字节代码,也就是JVM能够读懂的代码,这是一种中间代码。)
在编译的过程中,所有的全局变量在内存中的标识是虚拟地址,而不是我们在开发过程中定义的名称。例如int a = 1;这里的a在汇编代码中就不存在了,取而代之的是一个地址。在汇编文件中有一个符号表,它指明了这个地址的名称为a,以及其他信息,用于以后的debug。由于并非是可执行文件(在可执行文件中所有变量、调用的地址才能真正确定),这些地址是未确定的,所以对于这些数据(变量、函数)有relocation table,需要在最后的链接过程中对全局变量、函数做relocation
2 汇编器
通过汇编器,将之前的汇编代码,转行成机器语言。但格式并非是纯执行代码。
这个时候生成目标文件,文件有不同的段组成,head、text、date、symbol、string、relocat等等
3 linker 链接
linker 就是将目标文件合并,符号解析、重定向。
合并,就是多个obj组合为一个,一个lib或者elf执行文件
重定向,由于地址程序执行代码的地址可以确定了(多亏了操作系统的虚拟内存,每个程序的虚拟内存空间地址都是一样的),之前我们无法确定地址的变量、函数以及由于合并而受到影响的变量、函数都可以给一个地址了。
符号解析,前面的地址都变化了,符号表中内容要更新
4 loader 加载
最后我们运行程序,加载之前linker好的elf文件。
在内存中画一片空间,几个重要的区域。静态code文件区,全局变量区,heap区,stack区。
stack区:是程序运行的动态执行流。我们平时看的程序在做些什么,就打threaddump、processdump实际上就是看stack中的内容。
如果这个时候符号表中的内容在gcc编译时保留了,那么我们可以看到详细的系统调用过程。在linux中我们用lstack和strace可以看程序的stack。
heap区:这个是在程序中deveploer制定分配的区间,用来保存数据,c是面向过程的,所以对内存的分配比较容易理解,nmap函数,分配多少字节等等,最后还要释放这些空间,否则会出现内存泄露。对于面向对象语言java,可能了解起来就比较麻烦,因为java编译器为我们做了一个面向对象到面向过程的转换。但是不变的是我们实例化一个对象,就必须nmap一个区间给这个实力。对于这个实例的attribute属性进行保存,而method方法则无需,因为他是执行流,也就是code,通一个类的所有的实例的method是一样的,在静态code区有method的执行代码。
stack和heap的关系:当然stack中持有了大量heap的reference,比如java,两个类的执行流一样,但是reference不同,只想heap不同的区域,代表不同的实例。
stack中要注意的就是push and pop,他是函数调用、参数传递的主要手段。
动态链接:动态链接库是通过loader来做重定向。在加载时我们并不将库放入内存,而是在运行时通过虚拟内存将一份代码映射到多个程序中。