程序的前世今生

 

程序的前世今生——一段高级源代码通过编译器或解释器的处理,才能得以运行。

 

 一、编译

编译器Compiler),是一种电脑程序,它会将用某种编程语言写成的源代码(原始语言),转换成另一种编程语言(目标语言)。

它主要的目的是将便于人编写,阅读,维护的高级计算机语言所写作的源代码程序,翻译为计算机能解读、运行的低阶机器语言的程序,也就是可执行文件。编译器将原始程序(Source program)作为输入,翻译产生使用目标语言(Target language)的等价程序。源代码一般为高阶语言 (High-level language), 如 Pascal、C、C++、C# 、Java 等,而目标语言则是汇编语言或目标机器的目标代码(Object code),有时也称作机器代码(Machine code)。 一个现代编译器的主要工作流程如下: 源代码 (source code) → 预处理器 (preprocessor) → 编译器 (compiler) → 汇编程序 (assembler) → 目标代码 (object code) → 链接器 (Linker) → 可执行文件 (executables)

 

有一些编译器输出的代码,将运行于与编译器所在相同类型的计算机和操作系统之上,这种编译器叫做本地编译器。输出可以运行于不同的平台之上的编译器,叫做交叉编译器

 

二、链接

链接器英语Linker),是一个程序,将一个或多个由编译器汇编器生成的目标文件外加链接为一个可执行文件

An illustration of the linking process. Object files and static libraries are assembled into a new library or executable.

 

目标文件是包括机器码和链接器可用信息的程序模块。简单的讲,链接器的工作就是解析未定义的符号引用,将目标文件中的占位符替换为符号的地址。链接器还要完成程序中各目标文件的地址空间的组织,这可能涉及重定位工作。

 

(1)  静态链接和动态链接

在编译Linux程序时,我们经常会看到动态链接和静态链接这两个术语。这两个术语中是我Linux的共享函数库(shared libraries)相关的。共享函数库就象Windows系统里的.dll文件,它里面包含有很多程序常用的函数。为了方便程序开发和减少程序的冗余,程序当中就不用包含每个常用函数的拷贝,只是在需要时调用系统中共享函数库中常函数功能即可。这种方式我们称之为动态链接(Dynamically Linked)。但有时为了程序调试方便或其它原因,我们不希望叫程序去调用共享函数库的函数,而是在函数代码直接链接入程序代码中,也就是说,在程序本身拥有一份共享函数库中函数的副本。这种方式我们称之为静态链接(Statically Linked)。

(2)  以linux gcc为依托详解静态链接与动态链接
在使用GCC编译程序时,只需加上-shared选项即可,这样生成的执行程序即为动态链接库。
例如有文件:hello.c x.h main.c

编译:gcc hello.c -fPIC -o libhello.so


其中-fPIC选项的作用是:表示编译为位置独立的代码,不用此选项的话编译后的代码是位置相关的,
所以动态载入时是通过代码拷贝的方式来满足不同的调用,而不能达到真正的代码段共享的目的.


将main.c与hello.so动态库
gcc main.c -L. -lhello -o main


Linux动态链接库

1.创建hello.so动态库

#include <stdio.h>
void hello(){
	printf("hello world\n");
}
编译:gcc -fPIC -shared hello.c -o libhello.so

2.hello.h头文件

void hello();

3.链接动态库

#include <stdio.h>
#include "hello.h"

int main(){
	printf("call hello()");
	hello();
}
编译:gcc main.c -L. -lhello -o main
这里-L的选项是指定编译器在搜索动态库时搜索的路径,告诉编译器hello库的位置。"."意思是当前路径.


3.编译成够后执行./main,会提示:

In function `main':
 
main.c:(.text+0x1d): undefined reference to `hello'
collect2: ld returned 1 exit status
这是因为在链接hello动态库时,编译器没有找到。
解决方法:
sudo cp libhello.so /usr/lib/
这样,再次执行就成功输入:
call hello()

 

Linux静态链接库

文件有:main.c、hello.c、hello.h
1.编译静态库hello.o: 

gcc hello.c -o hello.o  #这里没有使用-shared

2.把目标文档归档

ar -r libhello.a hello.o  #这里的ar相当于tar的作用,将多个目标打包。
程序ar配合参数-r创建一个新库libhello.a,并将命令行中列出的文件打包入其中。这种方法,如果libhello.a已经存在,将会覆盖现在文件,否则将新创建。

3.链接静态库

gcc main.c -lhello -L. -static -o main
这里的-static选项是告诉编译器,hello是静态库。
或者:

gcc main.c libhello.a -L. -o main
这样就可以不用加-static

4.执行./main

输出:call hello()

 

三、运行

程序经过编译,链接得到一个可执行文件,这个可执行文件被加载到内存后就可得以运行。

 

可执行文件在逻辑上包含:

(1) 代码段:存放源程序中的可执行语句序列

(2) 静态数据段:存放全局变量、静态对象、符号表等

(3) 堆栈段:供给函数和线程使用

说明:堆和自由存储空间不属于程序,而是属于操作系统,但是应用程序可以通过动态内存分配指令来获取它们的使用权。

 

可执行文件物理上就是二进制形成的文件,包含:

(1) 指令

(2) 地址

(3) 数据

源代码中的标识符(类名、函数名、变量名)、类型定义、语句都会在“编译时被转换成二进制程序中的指令、地址和数据。

因此,通过名字直接引用一个变量、对象及其成员,这样的代码在编译和链接完成之后,在运行时都被转换成了通过变量、对象及其成员的地址(及内存单元的地址)进行访问。

 

程序的运行:存储程序控制原理

本质上,任何一个程序都是由待处理的数据和一系列处理他们的指令组成的,这些指令通过内存地址来访问待处理的数据。

程序的任何复杂操作最终都被转换为简单的加法运算让计算机执行:

(1) 把内存操作数的地址通过数据总线(DB)传递到CPU寄存器中

(2) CPU指示将它送到地址总线(AB)上,接着内存单元的数据就会流入CPU的寄存器中;

(3) 然后取第二个操作数,最后执行加法运算。

函数调用就是先将函数的首地址放入CPU寄存器中,然后将CPU指令指针修改为这个寄存器中的值,CPU从寄存器提取下一条指令时就可以获取到函数的第一条指令,这样就实现了函数的跳转。

 

四、其他

解释器英语:Interpreter),又译为直译器,是一种电脑程序,能够把高级编程语言一行一行直接转译运行。解释器不会一次把整个程序转译出来,只像一位“中间人”,每次运行程序时都要先转成另一种语言再作运行,因此解释器的程序运行速度比较缓慢。它每转译一行程序叙述就立刻运行,然后再转译下一行,再运行,如此不停地进行下去。

解释器的好处是它消除了编译整个程序的负担,但也会让运行时的效率打了折扣。相对地,编译器并不运行程序或原代码,而是一次将其翻译成另一种语言,如机器码,以供多次运行而无需再经编译。其制成品无需依赖编译器而运行,程序运行速度比较快。

跳转至: 导航 搜索

解释器英语:Interpreter),又译为直译器,是一种电脑程序,能够把高级编程语言一行一行直接转译运行。解释器不会一次把整个程序转译出来,只像一位“中间人”,每次运行程序时都要先转成另一种语言再作运行,因此解释器的程序运行速度比较缓慢。它每转译一行程序叙述就立刻运行,然后再转译下一行,再运行,如此不停地进行下去。

解释器的好处是它消除了编译整个程序的负担,但也会让运行时的效率打了折扣。相对地,编译器并不运行程序或原代码,而是一次将其翻译成另一种语言,如机器码,以供多次运行而无需再经编译。其制成品无需依赖编译器而运行,程序运行速度比较快。

解释器运行程序的方法有:

1.       直接运行高级编程语言 (如 Shell 自带的解释器)

2.       转换高级编程语言码到一些有效率的字节码 (Bytecode),并运行这些字节码

3.       以解释器包含的编译器对高级语言编译,并指示处理器运行编译后的程序 (例如: JIT)

PerlPythonMATLAB,与Ruby 是属于第二种方法,而 UCSD Pascal则是属于第三种方式。在转译的过程中,这组高级语言所写成的程序仍然维持在源代码的格式(或某种中继语言的格式),而程序本身所指涉的动作或行为则由解释器来表现。

使用解释器来运行程序会比直接运行编译过的机器码来得慢,但是相对的这个直译的行为会比编译再运行来得快。这在程序开发的雏型化阶段和只是撰写试验性的代码时尤其来得重要,因为这个“编辑-直译-除错”的循环通常比“编辑-编译-运行-除错”的循环来得省时许多。

在解释器上运行程序比直接运行编译过的代码来得慢,是因为解释器每次都必须去分析并转译它所运行到的程序行,而编译过的程序就只是直接运行。这个在运行时的分析被称为"直译式的成本"。在解释器中,变量的访问也是比较慢的,因为每次要访问变量的时候它都必须找出该变量实际存储的位置,而不像编译过的程序在编译的时候就决定好了变量的位置了。

在使用解释器来达到较快的开发速度和使用编译器来达到较快的运行进度之间是有许多妥协的。有些系统(例如有一些LISP)允许直译和编译的代码互相调用并共享变量。这意味着一旦一个子程序在解释器中被测试并除错过之后,它就可以被编译以获得较快的运行进度。许多解释器并不像其名称所说的那样运行原始代码,反而是把原始代码转换成更压缩的内部格式。举例来说,有些BASIC的解释器会把keywords取代成可以用来在jump table中找出相对应指令的单一byte符号。解释器也可以使用如同编译器一般的文字分析器lexical analyzer)和语法分析器parser)然后再转译产生出来的抽象语法树abstract syntax tree)。

可携性佳,直译式程序相较于编译式程序有较佳的可携性,可以容易的在不同软硬件平台上运行。而编译式程序经过编译后的程序则只限定于运行在开发环境平台。

字节码解释器

考量程序运行之前所需要分析的时间,存在了一个介于直译与编译之间的可能性。例如,用Emacs Lisp所撰写的源代码会被编译成一种高度压缩且优化的另一种 Lisp 源代码格式,这就是一种字节码(bytecode),而它并不是机器码(因此不会被绑死在特定的硬件上)。这个"编译过的"码之后会被字节码直译器(使用C写成的)转译。在这种情况下,这个"编译过的"码可以被说成是虚拟机(不是真的硬件,而是一种字节码解释器)的机器码。这个方式被用在 Open Firmware 系统所使用的 Forth 代码中: 原始程序将会被编译成 "F code" (一种字节码),然后被一个特定平台的虚拟机直译和运行。

实时编译(Just-in-time compilation)

即时编译,又名JIT,是指一种在运行时期把字节码编译成原生机器码的技术;这项技术是被用来改善虚拟机的性能的。该技术在近几年来才开始获得重视,而它后来模糊了直译、字节码直译及编译的差异性。在.NETJava的平台上都有用到JIT的技术。大约在1980年代Smalltalk语言出现的时候JIT的技术就存在了。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值