程序是如何运行的,这三步骤带你入门(编译、链接、装入)

在多道程序环境下,要使程序运行,必须先为之创建进程。而创建进程的第一件事,便是将程序和数据装入内存。如何将一个用户源程序变为一个可在内存中执行的程序,通常都要经过以下几个步骤:

首先是要编译:

由编译程序(Compiler)将用户源代码编译成cpu可执行的目标代码,产生了若干个目标模块(Object Module)(即若干程序段)。形成的目标代码,每个目标代码都是以0为基址顺序进行编址,原来用符号名访问的单元用具体的数据——单元号取代。这样生成的目标程序占据一定的地址空间,称为作业的逻辑地址空间,简称逻辑空间。

在逻辑空间中每条指令的地址和指令中要访问的操作数地址统称为逻辑地址 。很简单,逻辑地址就是你源程序里使用的地址,或者源代码经过编译以后编译器将一些标号,变量转换成的地址。

其次是链接

由链接程序(Linker)将编译后形成的一组目标模块(程序段),以及它们所需要的库函数链接在一起,形成一个完整的装入模块(Load Module);

最后是装入(地址重定位)

由装入程序(Loader)将装入模块装入物理内存。物理内存是真实存在的插在主板内存槽上的内存条的容量的大小。

物理内存内存是由若干个存储单元组成的,每个存储单元有一个编号,这种编号可唯一标识一个存储单元,称为内存地址(或物理地址)。我们可以把内存看成一个从0字节一直到内存最大容量逐字节编号的存储单元数组,即每个存储单元与内存地址的编号相对应。

装入模块虽然具有统一的地址空间,但它仍是以“0”作为参考地址,即是浮动的。要把它装入内存执行,就要确定装入内存的实际物理地址,并修改程序中与 地址有关的代码,这一过程叫做地址重定位。地址重定位主要是把逻辑地址转换成物理内存绝对地址,这个工作又称为地址映射。

图4-2 对用户程序的处理步骤

源程序经过编译后,可得到一组目标模块,再利用链接程序将这组目标模块链接,形成装入模块。根据链接时间的不同,可把链接分成如下三种:

(1) 、静态链接。在程序运行之前,先将各目标模块及它们所需的库函数,链接成一个完整的装配模块,以后不再拆开。我们把这种事先进行链接的方式称为静态链接方式。

(2)、 装入时动态链接。这是指将用户源程序编译后所得到的一组目标模块,在装入内存时,采用边装入边链接的链接方式。

(3)、 运行时动态链接。这是指对某些目标模块的链接,是在程序执行中需要该(目标)模块时,才对它进行的链接。

1.静态链接方式(Static Linking)

我们通过一个例子来说明在实现静态链接时应解决的一些问题。在图 4-4(a)中示出了经过编译后所得到的三个目标模块A、B、C,它们的长度分别为 L、M和N。在模块A中有一条语句CALL B,用于调用模块B。在模块B中有一条语句CALL C,用于调用模块C。B和C都属于外部调用符号,在将这几个目标模块装配成一个装入模块时,须解决以下两个问题:

(1) 对相对地址进行修改。在由编译程序所产生的所有目标模块中,使用的都是相对地址,其起始地址都为 0,每个模块中的地址都是相对于起始地址计算的。在链接成一个装入模块后,原模块B和 C在装入模块的起始地址不再是 0,而分别是 L和 L+M,所以此时须修改模块B和C中的相对地址,即把原B中的所有相对地址都加上 L,把原 C中的所有相对地址都加上L+M。

(2) 变换外部调用符号。将每个模块中所用的外部调用符号也都变换为相对地址,如把B 的起始地址变换为 L,把 C 的起始地址变换为 L+M,如图 4-4(b)所示。这种先进行链接所形成的一个完整的装入模块,又称为可执行文件。通常都不再拆开它,要运行时可直接将它装入内存。这种事先进行链接,以后不再拆开的链接方式,称为静态链接方式。

图 4-4 程序链接示意图

2.装入时动态链接(Load-time Dynamic Linking)

用户源程序经编译后所得的目标模块,是在装入内存时边装入边链接的,即在装入一个目标模块时,若发生一个外部模块调用事件,将引起装入程序去找出相应的外部目标模块,并将它装入内存,还要按照图4-4所示的方式来修改目标模块中的相对地址。装入时动态链接方式有以下优点:

(1) 、便于修改和更新。对于经静态链接装配在一起的装入模块,如果要修改或更新其中的某个目标模块,则要求重新打开装入模块。这不仅是低效的,而且有时是不可能的。若采用动态链接方式,由于各目标模块是分开存放的,所以要修改或更新各目标模块是件非常容易的事。

(2)、便于实现对目标模块的共享。在采用静态链接方式时,每个应用模块都必须含有其目标模块的拷贝,无法实现对目标模块的共享。但采用装入时动态链接方式,OS则很容易将一个目标模块链接到几个应用模块上,实现多个应用程序对该模块的共享。

3.运行时动态链接(Run-time Dynamic Linking)

在许多情况下,应用程序在运行时,每次要运行的模块可能是不相同的。但由于事先无法知道本次要运行哪些模块,故只能是将所有可能要运行到的模块都全部装入内存,并在装入时全部链接在一起。显然这是低效的,因为往往会有些目标模块根本就不运行。比较典型的例子是作为错误处理用的目标模块,如果程序在整个运行过程中都不出现错误,则显然就不会用到该模块。 近几年流行起来的运行时动态链接方式,是对上述在装入时链接方式的一种改进。这种链接方式是将对某些模块的链接推迟到程序执行时才进行链接,亦即,在执行过程中,当发现一个被调用模块尚未装入内存时,立即由OS去找到该模块并将之装入内存,把它链接到调用者模块上。凡在执行过程中未被用到的目标模块,都不会被调入内存和被链接到装入模块上,这样不仅可加快程序的装入过程,而且可节省大量的内存空间。

为了阐述上的方便,我们先介绍一个无需进行链接的单个目标模块的装入过程。该目标模块也就是装入模块。在将一个装入模块装入内存时,可以有绝对装入方式、可重定位装入方式和动态运行时装入方式,下面分别简述之。

1.绝对装入方式(Absolute Loading Mode)

在编译时,如果知道程序将驻留在内存的什么位置,那么,编译程序将产生绝对地址的目标代码。即按照物理内存的位置赋予实际的物理地址。例如,事先已知用户程序(进程)驻留在从R处开始的位置,则编译程序所产生的目标模块(即装入模块)便从R处开始向上扩展。绝对装入程序按照装入模块中的地址,将程序和数据装入内存。装入模块被装入内存后,由于程序中的逻辑地址与实际内存地址完全相同,故不须对程序和数据的地址进行修改。程序中所使用的绝对地址,既可在编译或汇编时给出,也可由程序员直接赋予。

这个方式的优点:是CPU执行目标代码快。

缺点:1)是由于内存大小限制,能装入内存并发执行的进程数大大减少

2)编译程序必须知道内存的当前空闲地址部分和其地址,并且把进程的不同程序段连续地存放起来,编译非常复杂。由于程序

因此,通常是宁可在程序中采用符号地址,然后在编译或汇编时,再将这些符号地址转换为绝对地址。

如何把虚拟内存地址空间变换到内存唯一的一维物理线性空间?涉及到两个问题:

  • 一是虚拟空间的划分问题。
  • 二是把虚拟空间中已经链接和划分好的内容装入内存,并将虚拟空间地址映射内存地址的问题。即地址映射。

地址映射就是建立虚拟地址与内存地址的关系。

2.静态地址重定位(可重定位装入方式 Relocation Loading Mode)

绝对装入方式只能将目标模块装入到内存中事先指定的位置。在多道程序环境下,编译程序不可能预知所编译的目标模块应放在内存的何处,因此,绝对装入方式只适用于单道程序环境。在多道程序环境下,所得到的目标模块的起始地址通常是从 0 开始的,程序中的其它地址也都是相对于起始地址计算的。此时应采用可重定位装入方式,根据内存的当前情况,将装入模块装入到内存的适当位置。

静态地址重定位:即在程序装入对目标代码装入内存的过程中完成,是指在程序开始运行前,程序中指令和数据的各个地址均已完成重定位,即完成虚拟地址到内存地址映射。地址变换通常是在装入时一次完成的,以后不再改变。

值得注意的是, 在采用可重定位装入程序将装入模块装入内存后, 会使装入模块中的所有逻辑地址与实际装入内存的物理地址不同,图4-3示出了这一情况。

图4-3 作业装入内存时的情况

例如,在用户程序的 1000 号单元处有一条指令LOAD 1,2500,该指令的功能是将 2500 单元中的整数 365 取至寄存器 1。但若将该用户程序装入到内存的 10000~15000号单元而不进行地址变换, 则在执行11000号单元中的指令时,它将仍从 2500 号单元中把数据取至寄存器1而导致数据错误。由图4-3 可见,正确的方法应该是将取数指令中的地址 2500 修改成 12500,即把指令中的相对地址 2500 与本程序在内存中的起始地址 10000 相加,才得到正确的物理地址12500。除了数据地址应修改外,指令地址也须做同样的修改,即将指令的相对地址 1000 与起始地址 10000 相加,得到绝对地址 11000。

优点:无需硬件支持

缺点:1)程序重定位之后就不能在内存中搬动了;

2)要求程序的存储空间是连续的,不能把程序放在若干个不连续的区域中。

3.动态地址重地位(动态运行时装入方式 Dynamic Run-time Loading)

可重定位装入方式可将装入模块装入到内存中任何允许的位置,故可用于多道程序环境;但这种方式并不允许程序运行时在内存中移动位置。因为,程序在内存中的移动,意味着它的物理位置发生了变化, 这时必须对程序和数据的地址(是绝对地址)进行修改后方能运行。然而,实际情况是,在运行过程中它在内存中的位置可能经常要改变,此时就应采用动态运行时装入的方式。

动态地址重定位:不是在程序执行之前而是在程序执行过程中进行地址变换。更确切的说,是把这种地址转换推迟到程序真正要执行时才进行,即在每次访问内存单元前才将要访问的程序或数据地址变换成内存地址。动态重定位可使装配模块不加任何修改而装入内存。为使地址转换不影响指令的执行速度,这种方式需要一个重定位寄存器的支持,

优点:1)目标模块装入内存时无需任何修改,因而装入之后再搬迁也不会影响其正确执行,这对于存储器紧缩、解决碎片问题是极其有利的;

2)一个程序由若干个相对独立的目标模块组成时,每个目标模块各装入一个存储区域,这些存储区域可以不是顺序相邻的,只要各个模块有自己对应的定位寄存器就行。

缺点:需要硬件支持。

5.1. 构造动态链接库

DLL是包含函数和数据的模块,它的调用模块可为EXE或DLL,它由调用模块在运行时加载;加载时,它被映射到调用进程的地址空间。在VC中有一类工程用于创建DLL。

  • 库程序文件 .C:相当于给出一组函数定义的源代码;
  • 模块定义文件 .DEF:相当于定义链接选项,也可在源代码中定义;如:DLL中函数的引入和引出(dllimport和dllexport)。
  • 编译程序利用 .C文件生成目标模块 .OBJ
  • 库管理程序利用 .DEF文件生成DLL输入库 .LIB和输出文件 .EXP
  • 链接程序利用 .OBJ和 .EXP文件生成动态链接库 .DLL。

5.2. DLL的装入方法

1)装入时动态链接(load-time):

在编程时显式调用某个DLL函数,该DLL函数在可执行文件中称为引入(import)函数。

链接时需利用 .LIB文件。在可执行文件中为引入的每个DLL建立一个IMAGE_IMPORT_DESCRIPTOR结构。

在装入时由系统根据该DLL映射在进程中的地址改写Import Address Table中的各项函数指针。Hint是DLL函数在DLL文件中的序号,当DLL文件修改后,就未必指向原先的DLL函数。在装入时,系统会查找相应DLL,并把它映射到进程地址空间,获得DLL中各函数的入口地址,定位本进程中对这些函数的引用

装入时动态链接过程:

(注:Import Address Table是在装入时依据DLL模块的加载位置确定)。

DLL函数的调用过程:

2)运行时动态链接(run-time):

在编程时通过LoadLibrary(给出DLL名称,返回装入和链接之后该DLL的句柄), FreeLibrary, GetProcAddress(其参数包括函数的符号名称,返回该函数的入口指针)等API来使用DLL函数。这时不再需要引入库(import library)。

  • LoadLibrary或LoadLibraryEx把可执行模块映射到调用进程的地址空间,返回模块句柄;
  • GetProcAddress获得DLL中特定函数的指针,返回函数指针;
  • FreeLibrary把DLL模块的引用计数减1;当引用计数为0时,拆除DLL模块到进程地址空间的映射;

运行时动态链接的例子:

领取大礼包后台私信我【大礼包】,前100名额外赠送一份价值1699的内核资料包(含视频教程、电子书、实战项目及代码)

带你阅读linux内核源码:下载源码、编译内核并运行一个最小系统

要学习linux内核,先要能够编译内核,还需要了解内核如何启动根文件系统。这样你才能在修改linux内核代码之后,完成验证的过程。

本文教你完成下列过程:

1.下载linux并编译linux内核源码

2.编译busybox

3.制作一个最小的根文件系统

4.qemu启动你编译好的内核和根文件系统

linux源码下载地址:

https://mirrors.edge.kernel.org/pub/linux/kernel/

我选择的版本是:4.9.229

https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.9.229.tar.xz

下载到本地,解压,然后进入linux-4.9.229目录:

1.指定硬件体系架构。

为了减少安装编译器的过程,我在本例中使用的是x86,因为我的开发环境就是x86。如果你要编译arm的内核,则指定ARCH=arm且需要安装交叉编译器。

2.配置board config,此处配置为 x86_64_defconfig。好了,我们点好菜了,菜单就是x86_64_defconfig

3.配置内核

这一步其实是对第2步的菜单进行微调,我们需要内核支持ramdisk驱动,所以需要选中如下配置:

4.编译内核

编译成功后的内核位于:arch/x86_64/boot/bzImage

下载buysbox源码,下载地址:

https://busybox.net/downloads/

我使用的版本是busybox-1.30.0

1.解压buysbox源码

2.配置buysbox源码

在这里我们把busybox配置为静态编译,这样busybox在运行的时候就不需要额外的动态链接库了。

3.编译和安装

4.编译完成后的busybox就安装在源码根目录下的_install目录了,我们进入_install目录,补充一些必要的文件或目录,相关的shell命令如下:

这样一个最小的、完整的可以被内核启动的文件系统就齐活啦。

制作根文件系统镜像文件。

思路:

1.先制作一个空的镜像文件;

2.然后把此镜像文件格式化为ext3格式;

3.然后把此镜像文件挂载,并把根文件系统复制到挂载目录;

4.卸载该镜像文件。

5.打成gzip包。

最终生成的文件系统镜像名字为:rootfs.img.gz

准备好了内核和文件系统镜像,幸福的一刻马上来临了:

通过qemu模拟器启动我们自己编译的内核和文件系统:

这样一个完整的最小linux系统就起来啦:

好了,经过以上的步骤,你可以自己编译一个内核和文件系统。有了这样的基础,你在之后就可以自由的修改linux内核代码并run起来进行验证啦。

记得关注“技术简说”,持续分享linux内核知识,让你一步一步成为linux内核高手。

听说你还不知道Java代码是怎么运行的?

作者:Jay_huaxiao

作为一名Java程序员,我们需要知道Java代码是怎么运行的。最近复习了深入理解Java虚拟机这本书,做了一下笔记,希望对大家有帮助,如果有不正确的地方,欢迎提出,感激不尽。

java 代码运行主要流程

本文主要讲解流程如下:

  • java源文件编译为class字节码
  • 类加载器把字节码加载到虚拟机的方法区。
  • 运行时创建对象
  • 方法调用,执行引擎解释为机器码
  • CPU执行指令
  • 多线程切换上下文

编译

我们都知道,java代码是运行在Java虚拟机上的。但是java是一门面向对象的高级语言,它不仅语法非常复杂,抽象程度也非常高,并不能直接运行在计算机硬件机器上。

Java虚拟机(Java Virtual Machine 简称JVM)是运行所有Java程序的抽象计算机,是Java语言的运行环境。

因此,在运行Java程序之前,需要编译器把代码编译成java虚拟机所能识别的指令程序,这就是Java字节码,即class文件。

所以,Java代码运行的第一步是:把Java源代码编译成.class 字节码文件。

类加载

在Class文件中描述的各种信息,需要被加载到虚拟机之后才能运行和使用。因此,需要把class字节码文件加载到Java虚拟机来。

虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制。

加载

加载阶段,虚拟机需要完成以下3件事情:

  • 通过一个类的全限定名来获取定义此类的二进制字节流。
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

加载阶段完成后,这些二进制字节流按照虚拟机所需的格式存储在方法区之中。

验证

为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,不会危害虚拟机的安全,Java虚拟机对输入的字节流走验证过程。

验证阶段包括四个阶段:文件格式验证、元数据验证、字节码验证、符号引用验证。

  • 文件格式验证: 验证字节流是否符合Class文件格式规范,如:是否以魔数0xCAFEBABE开头。
  • 元数据验证: 对字节码描述的信息进行语义分析,如:这个类的父类是否继承了不允许被继承的类(被final修饰的类);
  • 字节码验证: 主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。如:保证跳转指令不会跳转到方法体以外的字节码指令上。
  • 符号引用验证: 发生在虚拟机将符号引用转化为直接引用的时候,如:校验符号引用中通过字符串描述的全限定名是否能找到对应的类。

准备

准备阶段是正式为类变量分配内存并设置类变量初始值,这些变量所使用的内存都将在方法区中进行分配。如:

public static int value =123;

变量value在准备阶段过后的初始值是0而不是123。

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

比如:com.User类引用com.Tool类,在编译时,User类不知道Tool类的实际内存地址,因此只能使用符号com.Tool(假设)来表示。而在类加载加载User类的时候,可以通过虚拟机获取Tool类的实际内存地址,因此便可以将符号com.Tool替换为Tool类的实际内存地址,即直接引用地址。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行。

初始化

到了初始化阶段,才真正开始执行类中定义的Java字节码。在这个阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源。

创建对象

Java虚拟机是如何执行字节码的呢?我们先来看一下运行时创建对象。

Java是面向对象的编程语言,程序的运行是以对象为调用单位的。

  • 字节码文件加载到虚拟机的方法区后,在程序运行过程,通过 class字节码文件创建与其对应的对象信息 。
  • 创建对象的方式有:new关键字,反射等。
  • Java堆内存是线程共享的区域,创建后的对象信息就保存在Java堆内存中。

方法调用

JVM的调用单位是对象,但是真正执行功能性的代码还是对象上的方法。

在运行过程中,每当调用进入一个java方法,java虚拟机会在当前线程的java方法栈中生成一个栈帧,用以存放局部变量以及字节码的操作数。方法栈内存是线程私有的,每个线程都有自己的方法栈。如果对应的方法是本地方法,则对应的就是本地方法栈。

java运行时数据区域如下:

解释

当调用Java对象的某个方法时,JVM执行引擎会将该方法的字节码文件翻译成计算机所能识别的机器码,机器码信息保存在方法区中。翻译有解释执行和即时编译两种方式。

两种翻译方式的区别如下:

解释执行来一行代码,解释一行,大部分不常用的代码,都是采用这种方式。

即使编译

对于部分热点代码,将一个方法包含的所有字节码翻译成机器指令,以提高java虚拟机的运行效率。

即时编译是建立经典的二八定律上,即20%代码占据了80%的计算资源。

执行指令

  • Java程序被加载入内存后,指令也在内存中了。
  • 指令的指令寄存器IP,指向下一条待执行指令的地址。
  • CPU的控制单元根据IP寄存器的指向,将主存中的指令装载到指令寄存器,这些加载的指令就是一串二进制码,还需要译码器进行解码。
  • 解码后,如果需要获取操作数,则从内存中取数据,调用运算单元进行计算。

多线程上下文切换

CPU一通上电,就会周而复始从内存中获取指令、译码、执行。

  • 为了支持多任务,CPU 将执行时间这个资源划分成时间片,每个程序执行一段时间。
  • java虚拟机的多线程是通过线程轮流切换分配处理执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条程序中的指令。
  • 假设当前线程在运行中,CPU分配的时间执行完了,总得保存运行过的结果信息吧,要不然白白浪费之前的工作了,因此,程序计数器(PC寄存器)作用体现出来了,它是一块较小的内存空间,线程私有,可以看作当前线程执行的字节码的行号指示器。当CPU又给它分配时间跑的时候,可以把数据恢复,接着上一次执行到的位置继续执行就可以了。

原文:https://juejin.im/entry/5e6ccc05e51d4527110aa25f

本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com

点赞 0
收藏 0

文章为作者独立观点不代本网立场,未经允许不得转载。