前言

一个程序,从编写完代码,到被计算机运行,总共需要经历以下四步:

  1. 编译。编译器会将程序源代码编译成汇编代码。
  2. 汇编。汇编器会将汇编代码文件翻译成为二进制的机器码。
  3. 链接。链接器会将一个个目标文件和库文件链接在一起,成为一个完整的可执行程序。
  4. 载入。加载器会将可执行文件的代码和数据从硬盘加载到内存中,然后跳转到程序的第一条指令处开始运行。

链接器和加载器是由操作系统实现的程序。而编译器和汇编器则是由不同的编程语言自己实现的了。

这里需要展开来说一说,我们常用的高级语言,按照转化成机器码的方式不同可以分为编译型语言和解释型语言

  • 编译型语言要求由编译器提前将源代码一次性转换成二进制指令,即生成一个可执行程序,后续的执行无需重新编译。比如我们常见的 C、Golang 等,优点是执行效率高;缺点是可执行程序不能跨平台(不同的操作系统对不同的可执行文件的内部结构要求不同;另外,由于不同操作系统支持的函数等也可能不同,所以部分源代码也不能跨平台)。
  • 解释型语言不需要提前编译,程序只在运行时才由解释器翻译成机器码,每执行依次就要翻译一次。比如我们常见的 Python、PHP 等,优点是较方便(对编写用户而言,省去了编译的步骤),实时性高(每次修改代码后都可直接运行),能跨平台;缺点是效率低。
  • 半编译半解释型语言:还有一类比较特殊,混合了两种方式。源代码需要先编译成一种中间文件(字节码文件),然后再将中间文件拿到虚拟机中解释执行。比如我们常见的 Java、C# 等。

所以,要设计一门语言,还必须为其编写相应的编译器和解释器,将源代码转化为计算机可执行的机器码。由于不同的语言有不同的转化方式,接下来将以最常见的 C 语言为例,简单分析一下 编译→汇编→链接→载入 的过程。

总结:不同的语言会使用不同的方式将源代码转化为机器码,但是之后的链接和载入过程都是由操作系统完成的,都是相同的。

1 编译

编译是读取源程序,进行词法和语法分析,将高级语言代码转换为汇编代码。整个编译过程可以分为两个阶段。

1.1 预处理

  1. 对其中的伪指令(以 # 开头的指令)进行处理。

    • 将所有的 #define 删除,并且展开所有的宏定义;
    • 处理条件编译指令,如 #if、#elif、#else、endif 等;
    • 处理头文件包含指令,如 #include,将被包含的文件插入到该预编译指令的位置;
  2. 删除所有的注释。

  3. 添加行号和文件名标识。

1.2 编译

对预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后,产生相应的汇编代码文件。

2 汇编

将编译完的汇编代码文件翻译成机器指令,保存在后缀为 .o 的目标文件(Object File)中。

这个文件是一个 ELF 格式的文件(Executable and Linkable Format,可执行可链接文件格式),包括可以被执行的文件和可以被链接的文件(如目标文件 .o,可执行文件 .exe,共享目标文件 .so),有其固定的格式。

3 链接

由汇编程序生成的目标文件并不能被立即执行,还需要通过链接器(Linker),将有关的目标文件彼此相连接,使得所有的目标文件成为一个能够被操作系统载入执行的统一整体。

例如在某个源文件的函数中调用了另一个源文件中的函数;或者调用了库文件中的函数等等情况,都需要经过链接才能使用。

链接处理可以分为两种:

  • 静态链接:直接在编译阶段就把静态库加入到可执行文件当中去。优点:不用担心目标用户缺少库文件。缺点:最终的可执行文件会较大;且多个应用程序之间无法共享库文件,会造成内存浪费。
  • 动态链接:在链接阶段只加入一些描述信息,等到程序执行时再从系统中把相应的动态库加载到内存中去。优点:可执行文件小;多个应用程序之间可以共享库文件。缺点:需要保证目标用户有相应的库文件。

4 载入

加载器(Loader)会将可执行文件的代码和数据加载到内存(虚拟内存)中,然后跳转到程序的第一条指令开始执行程序。

说起来只是装载到内存中去那么简单的一句话,可是其实要是展开来说,会涉及到整个操作系统的内存管理。

虚拟内存

首先,为了避免进程所使用的内存地址相互影响,操作系统会为每个进程分配一套独立的虚拟内存地址,然后再提供一种机制,将虚拟内存地址和物理内存地址进行映射

  • 我们程序所使用的内存地址叫做虚拟内存地址(Virtual Memory Address)
  • 实际存在硬件里面的空间地址叫物理内存地址(Physical Memory Address)

用户空间

然后,操作系统将整个内存空间分为用户空间和内核空间,其中内核空间只有内核程序能够访问,且所有进程共用一个内核空间;而用户空间是专门给应用程序使用的,每当创建了一个新的进程,都要分配一个用户空间。

接下来以 32 位内存空间为例进行说明,32 位内存空间大小为 4GB,其中 1GB 为内核空间,3GB 为用户空间。用户空间中按照数据类型不同,划分为了不同的内存段,各类数据会被存放到各自的内存段中。

用户空间内存,从低到高分别是 6 种不同的内存段:

  • 程序文件段(.text),包括二进制可执行代码
  • 已初始化数据段(.data),包括静态常量
  • 未初始化数据段(.bss),包括未初始化的静态变量
  • 堆段,包括动态分配的内存,从低地址开始向上增长。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减);
  • 文件映射段,包括动态库、共享内存等,从低地址开始向上增长;
  • 栈段,包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是 8 MB。当然系统也提供了参数,以便我们自定义大小;栈段可以通过系统调用自动地扩充空间,但是不能回收空间,所以栈段设置得太大会导致内存泄露。

内存分页

之后,操作系统把整个虚拟和物理内存空间切成一段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间,叫做(Page)。在 Linux 下,每一页的大小为 4KB

虚拟地址与物理地址之间通过页表来映射,CPU 中的 MMU (Memory Management Unit,内存管理单元)就做将虚拟地址转换成物理地址的工作。

总结

至此,可执行文件载入内存的过程可以概括为以下几步:

  1. 给进程分配虚拟内存空间;
  2. 创建虚拟地址到物理地址的映射,创建页表;
  3. 加载代码段和数据段等数据,即将硬盘中的文件拷贝到物理内存页中,并在页表中写入映射关系;
  4. 把可执行文件的入口地址写入到 CPU 的 指令寄存器(PC)中,即可执行程序。

最后

本文简单的描述了一下一个程序,从编写完代码,到被计算机运行的过程,其实当中的每一步都十分复杂深奥,都值得深入学习,尤其是最后一步载入内存的过程,展开来说可以涉及到整个操作系统的内存管理,像分段、分页、多级页表、TLB、内存分配、内存泄露、内存回收、页面置换算法等等都没能详细说,如果有机会的话我会慢慢补上的。如果你感兴趣的话可以留言催更,让我更有学习的动力!!!

一个程序从编译到运行的全过程相关推荐

  1. Linux下开启openmp编译,OpenMP程序的编译和运行

    <OpenMP程序的编译和运行>由会员分享,可在线阅读,更多相关<OpenMP程序的编译和运行(13页珍藏版)>请在人人文库网上搜索. 1.SHANGHAI UNIVERSIT ...

  2. scala 编译插件_使用Scala插件在Griffon应用程序中编译和运行Scala代码

    scala 编译插件 用于Griffon的Scala插件 0.7.1版本现已发布. 这个插件可以在Griffon应用程序上编译和运行Scala代码. Scala插件使用LangBridge插件与其他J ...

  3. 什么是java源码文件,什么是字节码文件,初程序的编译和运行

    java源文件就是源代码文件,是指我们编写好的代码文件,即 当我们开始运行的时候系统会执行javac命令先把java源文件编译成字节码文件即: 这里科普一下java为什么会有字节码文件. 字节码产生的 ...

  4. java 保存后中文乱码_sublime编写java程序保存编译,运行之后中文显示乱码

    sublime是常见的java编辑工具之一,在初学者使用时可以更好的熟悉每条命令,以及逻辑关系.但在编写的java程序里包含中文字符的时候,在通过cmd命令提示符编译并运行之后会出现乱码,如下图: 很 ...

  5. java只能在运行和编译吗_如何在另一个Java程序中编译和运行Java程序?

    斯蒂芬大帝 我修改了代码以包括一些检查:public class Laj {  private static void printLines(String name, InputStream ins) ...

  6. 【计算机科学基础】程序的编译与运行

    文章目录 编程 搭建开发环境 CLion CentOS 编译 链接 加载 执行 编程 C语言源程序要依据语法规则编写. C语言源程序主要包括以下部分: 预处理器指令 函数 变量 语句和表达式 注释 C ...

  7. Linux下新建java程序,编译,运行(以Ubuntu为例)

    1.下载vim 命令行下输入以下命令:(笔者用的是ubuntu) sudo apt-get install vim-gtk 问是否继续的时候,输入"y"继续,等待直到完成. 至此完 ...

  8. go程序的编译和运行

  9. c语言程序不能运行,C语言编程,我写了一个程序,但不能运行.

    首先把a.txt和b.txt读入链表,然后判断是否有相同,如果没有相同的就输到C.txt中. #include #include #include typedef struct 首先把a.txt和b. ...

最新文章

  1. [svc]caffe安装笔记-显卡购买
  2. bzoj 1045 [HAOI2008] 糖果传递 —— 贪心
  3. jmeter响应断言使用_十二、Jmeter断言-响应断言、Json断言和Beanshell断言
  4. java executorser 停止_Java使用ExecutorService来停止线程服务
  5. 7.6~7.20集训总结(一)
  6. 大剑无锋之hadoop默认的数据类型都有哪些?
  7. 减少联调、高效集成,试试这个工具
  8. 关于sass(scss)、less、postcss、stylus等的用法与区别
  9. Struts2知识点总结大全
  10. 强化学习——From drew追寻Mitsuha的学习笔记
  11. 定制操作(传递函数或lambda表达式)
  12. 如何判断各种手机浏览器?
  13. mysql的limit经典用法及优化
  14. 完整阅读 何凯明最新一作:Masked Autoencoders Are Scalable Vision Learners
  15. 【学英语】老友记S1E1
  16. 如何设计测试用例?为什么要设计测试用例?
  17. js直接打开word文件
  18. 【luogu P3802】小魔女帕琪(概率期望)
  19. Java汇集接口、异常处理、常用使用类和集合等技术的实验项目
  20. 波音承认 737MAX 飞行模拟器存在缺陷;韩国政府计划从 Win7 迁移到 Linux

热门文章

  1. 中文句法分析及LTP使用
  2. WIN10 注册表winlogon乱删除后果及系统恢复
  3. 名策数据祝清:巴菲特不能复制,量化与程序化交易才是王道
  4. 感觉好极了----MacBookPro15苹果笔记本外接4K显示器
  5. #创新应用#摩瓦语音微博:语音时代的产物!
  6. 观察者模式( Observer Pattern)
  7. 打算自学编程,但是不知道该先学哪门语言入门?
  8. 【每日早报】2019/12/11
  9. 三菱FX3SA PLC通过FX3G-485BD模块及变频器通信指令和三菱D700变频器进行通讯记录
  10. win10键盘没坏打不出字怎么回事