精华内容
下载资源
问答
  • 在执行过程中最重要的是
    万次阅读
    2021-01-30 16:22:46

    gradle 插件以及执行过程解析


    在 Android Studio 中,项目都是使用 Gradle 来构建的,那么我们通常使用的 Gradle 插件是如何执行的呢?

    Gradle 的执行步骤

    简单说 gradle 是一种构建工具,用来控制代码的编译、构建、打包等过程,有点像 C/C++ 项目中的 Make 工具。gradle 执行一次 build 总共可以分为三个步骤:

    1. 初始化阶段

    读取根工程中 setting.gradle 中的 include 信息,确定当前项目中哪几个工程加入构建,创建 project 实例。

    例如:

    include ':app', ':moudle1', ':moudle2'
    

    Gradle 构建时会去读取根工程中 setting.gradle 中的 include 信息,决定有哪几个工程加入构建,创建 project 实例,例如示例中有3个工程参与了构建。

    2. 配置阶段

    执行项目中的 build.gradle 脚本文件,配置 project 对象,一个对象由多个任务组成,此阶段也会去创建、配置 task 及相关信息。

    3. 执行阶段

    根据 gradle 命令传递过来的 task 名称,执行相关依赖任务。

    Gradle 插件的使用方法

    Gradle 的插件一般有以下使用方式:

    • 直接在项目中的 gradle 文件里编写,这种方式的缺点是无法复用插件代码,在其他项目中还得复制一遍代码。
    • 在独立的项目里编写插件,然后发布到中央仓库,之后直接引用就可以了,优点就是可复用。

    project 和 tasks

    我们再来看两个重要的概念:project 和 tasks。

    在 grade 中的两大重要的概念,分别是 project 和 tasks。每一次构建都是有至少一个 project 来完成,注意,这里的 project 和 Android studio 中的 project 不是一个概念。每个 project 有至少一个 tasks。每一个 build.grade 文件代表着一个 project。tasks 在 build.gradle 中定义。当初始化构建进程,gradle 会基于 build 文件,集合所有的 project 和 tasks,一个 tasks 包含了一系列动作,然后它们将会按照顺序执行,一个动作就是一段被执行的代码,这有点类似与方法的概念。

    Task

    gradle 的工作都由一个个 Task 来执行。Task 可以指定它所依赖的 Task,或者它要在另一个 Task 之前或者之后运行。

    将处理逻辑分在不同的 Task 中有这么两个好处:

    1. 业务解耦,有利于维护和提高代码健壮性。
    2. 增量编译,当 Task 的输入/输出没有变化时,不用再次运行,直接复用。

    创建 Task 常见的方式有两种:

    1. 在 build.gradle 中直接创建。
    2. 通过插件创建。

    build.gradle 中一般创建功能简单的 Task,逻辑复杂的 Task 通常由插件创建,否则会使得 build.gradle 文件臃肿不堪。可见插件也是 gradle 一个重要组成部分,我们再来看看插件是做什么的。

    插件

    gradle 核心的逻辑比较简单,丰富的构建功能都是通过插件的方式扩展的。比如 Android 的构建逻辑肯定不是 gradle 官方代码自带的,而是 Android 写了对应的 gradle 插件来实现。这个特性保证了 gradle 功能的灵活性。

    在 Android 中,最常见的两个插件分别是:

    1. com.android.application
    2. com.android.library

    那么插件如何使用呢?它通常在 build.gradle 文件中来使用的:

    apply plugin: 'com.android.application'
    

    这表示所在的 module 是一个 app module,而使用 com.android.library 插件表示所在 module 是一个 library module,两种插件分别会对该 module 做不同的配置。

    好了,gradle 以及 gradle 插件主要的理论知识我们已经了解了,下一章我们来分析它在 Android studio 中的使用。


    **PS:更多精彩内容,请查看 --> 《Android 性能优化》
    **PS:更多精彩内容,请查看 --> 《Android 性能优化》
    **PS:更多精彩内容,请查看 --> 《Android 性能优化》

    更多相关内容
  • 符号执行入门

    万次阅读 多人点赞 2018-05-09 08:12:08
    0x00 前言此前阅读了AEG相关的一些文章,发现符号执行可以说是基石,如果不能充分理解符号执行就很难真正深入AEG的研究。于是我找了一些符号执行领域的经典论文,预计会做一系列的总结,主要包括以下几个内容:(1)...

    0x00 前言

    此前阅读了AEG相关的一些文章,发现符号执行可以说是基石,如果不能充分理解符号执行就很难真正深入AEG的研究。于是我找了一些符号执行领域的经典论文,预计会做一系列的总结,主要包括以下几个内容:(1)符号执行的基本概念;(2)符号执行的分类与发展;(3)符号执行面临的挑战和解决方案。

    本文主要介绍关于符号执行的基本概念。对于符号执行入门,有两篇文章可以参考。其一是2010年David Brumley团队在S&P会议上发表的《All You Ever Wanted to Knowabout Dynamic Taint Analysis and Forward Symbolic Execution (but Might HaveBeen Afraid to Ask)》[1]。这篇文章同时介绍了动态污点分析和前向符号执行的基本概念,作者构造了一种简化的中间语言,用来形式化地描述这两种程序分析技术的根本原理。其二是2011年CristianCadar发表在ACM通讯上的一篇短文《Symbolic execution forsoftware testing: three decades later》[2],以较为通俗的语言和简单的例子阐述了符号执行的基本原理,并介绍了符号执行技术的发展历程和面临挑战。

    其实这两篇文章的作者都是二进制分析领域大名鼎鼎的人物,卡内基梅隆的David Brumley是AEG的提出者,其带领团队是DARPACGC比赛的第一名;英国帝国理工的CristianCadar则是符号执行引擎KLEE[3]的作者——KLEE在符号执行领域的地位不言而喻。这两篇文章各有千秋:前者更加学术化一些,用中间语言进行的形式化描述有些晦涩难懂,但对于进一步研究符号执行引擎的源码很有帮助;后者则更通俗一些,有助于初学者的理解,且对于符号执行的发展脉络有更多的介绍。

    本文主要依据Cristian Cadar的文章,以较通俗的语言解释符号执行的基本原理。同时还会简单介绍符号执行面临的挑战和解决方案,这部分也会借鉴David Brumley文章中的内容。最后,将梳理一下符号执行领域的主要工作分类以及后续文章的脉络。

    0x01 基本原理

    符号执行的关键思想就是,把输入变为符号值,那么程序计算的输出值就是一个符号输入值的函数。这个符号化的过程在上一篇AEG文章中已有简要阐述,简而言之,就是一个程序执行的路径通常是true和false条件的序列,这些条件是在分支语句处产生的。在序列的i^{th}位置如果值是true,那么意味着i^{th}条件语句走的是then这个分支;反之如果是false就意味着程序执行走的是else分支。

    那么,如何形式化地表示符号执行的过程呢?程序的所有执行路径可以表示为树,叫做执行树。接下来我们就以一个例子来阐述通过符号执行遍历程序执行树的过程。

    左边的代码中,testme()函数有3条执行路径,组成了右图中的执行树。直观上来看,我们只要给出三个输入就可以遍历这三个路径,即图中绿色的x和y取值。符号执行的目标就是能够生成这样的输入集合,在给定的时间内探索所有的路径。

    为了形式化地完成这个任务,符号执行会在全局维护两个变量。其一是符号状态\sigma,它表示的是一个从变量到符号表达式的映射。其二是符号化路径约束PC,这是一个无量词的一阶公式,用来表示路径条件。在符号执行的开始,符号状态\sigma会先初始化为一个空的映射,而符号化路径约束PC初始化为true。\sigma和PC在符号执行的过程中会不断更新。在符号执行结束时,PC就会用约束求解器进行求解,以生成实际的输入值。这个实际的输入值如果用程序执行,就会走符号执行过程中探索的那条路径,即此时PC的公式所表示的路径。

    我们以左图的例子来阐述这个过程。当符号执行开始时,符号状态 \sigma为空,符号路径约束PC为true。当我们遇到一个读语句,形式为var=sym_input(),即接收程序输入,符号执行就会在符号状态 \sigma中加入一个映射 var\rightarrow s,这里s就是一个新的未约束的符号值。左图中代码,main()函数的前两行会得到结果 \sigma =\left\{ x\rightarrow x_{0} , y\rightarrow y_{0}\right\},其中 x_{0}y_{0}是两个初始的未约束的符号化值。

    当我们遇到一个赋值语句,形式为v=e,符号执行就会将符号状态\sigma更新,加入一个v到\sigma \left( e \right)的映射,其中\sigma \left( e \right)就是在当前符号化状态计算e得到的表达式。例如,在左图中代码执行完第6行时,\sigma =\left\{  x \rightarrow x_{0},  y \rightarrow y_{0},  z \rightarrow 2y_{0}\right\}

    当我们遇到条件语句if(e) S1 else S2,PC会有两个不同更新。首先是PC更新为PC\wedge \sigma \left( e \right),这就表示then分支;然后是建立一个路径约束PC’,初始化为PC\wedge \neg \sigma \left( e \right),这就表示else分支。如果PC是可满足的,给一些实际值,那么程序执行就会走then分支,此时的状态为:符号状态\sigma和符号路径约束PC。反之如果PC’是可满足的,那么会建立另一个符号实例,其符号状态为\sigma,符号路径约束为PC’,走else分支。如果PC和PC’都不能满足,那么执行就会在对应路径终止。例如,第7行建立了两个不同的符号执行实例,路径约束分别是x_{0} =2y_{0}x_{0} \ne 2y_{0}。在第8行,又建立了两个符号执行实例,路径约束分别是\left( x_{0} =2y_{0} \right) \wedge \left( x_{0}>y_{0}+10 \right),以及\left( x_{0} =2y_{0} \right) \wedge \left( x_{0} \leq y _{0}+10 \right)

    如果符号执行遇到了exit语句或者错误(指的是程序崩溃、违反断言等),符号执行的当前实例会终止,利用约束求解器对当前符号路径约束赋一个可满足的值,而可满足的赋值就构成了测试输入:如果程序执行这些实际输入值,就会在同样的路径结束。例如,在左图例子中,经过符号执行的计算会得到三个测试输入:{x=0, y=1}, {x=2, y=1}, {x=30, y=15}。


    当我们遇到了循环和递归应该怎么办呢?如果循环或递归的终止条件是符号化的,包含循环和递归的符号执行会导致无限数量的路径。比如上图中的这个例子,这段代码就有无数条执行路径,每条路径的可能性有两种:要么是任意数量的true加上一个false结尾,要么是无穷多数量的false。我们形式化地表示包含n个true条件和1个false条件的路径,其符号化约束如下:


    (\wedge_ {i\in [1,n]}N_{i} >0) \wedge (N_{n+1}\leq 10)

    其中每个N_{i}都是一个新的符号化值,执行结尾的符号化状态是\left\{ N\rightarrow N_{n+1} ,sum\rightarrow \sum_{i\in [1,n]}^{}{N_{i}}  \right\}。其实这就是符号执行面临的问题之一,即如何处理循环中的无限多路径。在实际中,有一些方法可以应对,比如对搜索加入限制,要么是限制搜索时间的长短,要么是限制路径数量、循环迭代次数、探索深度等等。

    还需要考虑到的一个问题就是,如果符号路径约束包含不能由求解器高效求解的公式怎么办?比如说,如果原本的代码发生变化,把twice函数替换为下图中的语句,那么符号执行就会产生路径约束x_{0}\ne (y_{0} y_{0}) mod 50以及x_{0}= (y_{0} y_{0}) mod 50。我们做另外一个假设,如果twice是一个我们得不到源码的函数,也就是我们不知道这个函数有什么功能,那么符号执行会产生路径约束x_{0}\ne twice(y_{0})x_{0}= twice(y_{0}),其中twice是一个未解释的函数。这两种情况下,约束求解器都是不能求解这样的约束的,所以符号执行不能产生输入。

    其实我们上述介绍的内容,应该属于纯粹的静态符号执行的范畴。我们提出的两个问题,是导致静态符号执行不能够实用的原因之一。符号执行的概念早在1975年[4]就提出了,但是真正得到实用,却是在一种方式提出之后,即混合实际执行和符号执行,称为concolic execution,是真正意义上的动态符号执行。

    0x02 Concolic Execution

    最早将实际执行和符号执行结合起来的是2005年发表的DART[5],全称为“Directed Automated Random Testing”,以及2005年发表的CUTE[6],即“A concolic unit testing enginefor C”。

    Concolic执行维护一个实际状态和一个符号化状态:实际状态将所有变量映射到实际值,符号状态只映射那些有非实际值的变量。Concolic执行首先用一些给定的或者随机的输入来执行程序,收集执行过程中条件语句对输入的符号化约束,然后使用约束求解器去推理输入的变化,从而将下一次程序的执行导向另一条执行路径。简单地说来,就是在已有实际输入得到的路径上,对分支路径条件进行取反,就可以让执行走向另外一条路径。这个过程会不断地重复,加上系统化或启发式的路径选择算法,直到所有的路径都被探索,或者用户定义的覆盖目标达到,或者时间开销超过预计。

    我们依旧以上面那个程序的例子来说明。Concolic执行会先产生一些随机输入,例如{x=22, y=7},然后同时实际地和符号化地执行程序。这个实际执行会走到第7行的else分支,符号化执行会在实际执行路径生成路径约束x_{0} \ne 2 y_{0}。然后concolic执行会将路径约束的连接词取反,求解x_{0} = 2 y_{0}得到一个测试输入{x=2, y=1},这个新输入就会让执行走向一条不同的路径。之后,concolic执行会在这个新的测试输入上再同时进行实际的和符号化的执行,执行会取与此前路径不同的分支,即第7行的then分支和第8行的else分支,这时产生的约束就是(x_{0}=2y_{0})\wedge (x_{0}\leq y_{0}+10),生成新的测试输入让程序执行没有被执行过的路径。再探索新的路径,就需要将上述的条件取反,也就是(x_{0}=2y_{0})\wedge (x_{0}> y_{0}+10),通过求解约束得到测试输入{x=30, y=15},程序会在这个输入上遇到ERROR语句。如此一来,我们就完成了所有3条路径的探索。

    这个过程中,我们从一个实际输入{x=22, y=7}出发,得到第一个约束条件x_{0} \ne 2 y_{0},第一次取反得到x_{0} = 2 y_{0},从而得到测试输入{x=2, y=1}和新约束(x_{0}=2y_{0})\wedge (x_{0}\leq y_{0}+10);第二次取反得到(x_{0}=2y_{0})\wedge (x_{0}> y_{0}+10),从而求解出测试输入{x=30, y=15}。

    注意在这个搜索过程中,其实concolic执行使用了深度优先的搜索策略。

    本文作者Cristian Cadar在2006年发表EXE,以及2008年发表EXE的改进版本KLEE,对上述concolic执行的方法做了进一步优化。其创新点主要是在实际状态和符号状态之间进行区分,称之为执行生成的测试(Execution-Generated Testing),简称EGT。这个方法在每次运算前动态检查值是不是都是实际的,如果都是实际的值,那么运算就原样执行,否则,如果至少有一个值是符号化的,运算就会通过更新当前路径的条件符号化地进行。例如,对于我们的例子程序,第17行把y=sym_input()改变成y=10,那么第6行就会用实际参数20去调用函数twice,并实际执行。然后第7行变成if(20==x),符号执行会走then路径,加入约束x=20;对条件进行取反就可以走else路径,约束是x≠20。在then路径,第8行变成if(x>20),那么then路径就不能走了,因为此时有约束x=20。简言之,EGT本质上还是将实际执行与符号执行相结合,通过路径取反探索所有可能路径。

    正是因为concolic执行的出现,让传统静态符号执行遇到的很多问题能够得到解决——那些符号执行不好处理的部分、求解器无法求解的部分,用实际值替换就好了。使用实际值,可以让因外部代码交互和约束求解超时造成的不精确大大降低,但付出的代价就是,会有丢失路径的缺陷,牺牲了路径探索的完全性。我们举一个例子来说明这一点。假设我们原始例子程序做了改动,即把twice函数的定义改为返回(v*v)%50。假设执行从随机输入{x=22, y=7}开始,生成路径约束x_{0}\ne (y_{0} y_{0}) mod 50。因为约束求解器无法求解非线性约束,所以concolic执行的应对方法是,把符号值用实际值替换,此处会把y_{0}的值替换为7,这就将程序约束简化为x_{0}\ne49。通过求解这个约束,可以得到输入{x=49, y=7},走到一个此前没有走到的路径。传统静态符号执行是无法做到这一步的。但是,在这个例子中,我们无法生成路径true, false的输入,即约束x_{0}= (y_{0} y_{0}) mod 50\wedge (x_{0}\leq y_{0}+10),因为y_{0}的值已经实际化了,这就造成了丢失路径的问题,造成不完全性。

    然而总的来说,concolic执行的方法是非常实用的,有效解决了遇到不支持的运算以及应用与外界交互的问题。比如调用库函数和OS系统调用的情况下,因为库和系统调用无法插桩,所以这些函数相关的返回值会被实际化。


    0x03 面临挑战&解决方案

    符号执行曾经遇到过很多问题,使其难以应用在真实的程序分析中。经过研究者的不懈努力,这些问题多多少少得到了解决,由此也产生了一大批优秀的学术论文。这一部分将简单介绍其中的一些关键挑战以及对应的解决方案。

    1. 路径选择

    由于在每一个条件分支都会产生两个不同约束,符号执行要探索的执行路径依分支数指数增长。在时间和资源有限的情况下,应该对最相关的路径进行探索,这就涉及到了路径选择的问题。通过路径选择的方法缓解指数爆炸问题,主要有两种方法:1)使用启发式函数对路径进行搜索,目的是先探索最值得探索的路径;2)使用一些可靠的程序分析技术减少路径探索的复杂性。

    启发式搜索是一种路径搜索策略,比深度优先或者宽度优先要更先进一些。大多数启发式的主要目标在于获得较高的语句和分支的覆盖率,不过也有可能用于其他优化目的。最简单的启发式大概是随机探索的启发式,即在两边都可行的符号化分支随机选择走哪一边。还有一个方法是,使用静态控制流图(CFG)来指导路径选择,尽量选择与未覆盖指令最接近的路径。另一个方法是符号执行与进化搜索相结合,其fitness function用来指导输入空间的搜索,其关键就在于fitness function的定义,例如利用从动态或静态分析中得到的实际状态信息或者符号信息来提升fitness function。

    用程序分析和软件验证的思路去减少路径探索的复杂性,也是一种缓解路径爆炸问题的方式。一个简单的方法是,通过静态融合减少需要探索的路径,具体说来就是使用select表达式直接传递给约束求解器,但实际上是将路径选择的复杂性传递给了求解器,对求解器提出了更高的要求。还有一种思路是重用,即通过缓存等方式存储函数摘要,可以将底层函数的计算结果重用到高级函数中,不需要重复计算,减小分析的复杂性。还有一种方法是剪枝冗余路径,RWset技术的关键思路就是,如果程序路径与此前探索过的路径在同样符号约束的情况下到达相同的程序点,那么这条路径就会从该点继续执行,所以可以被丢弃。

    2. 约束求解

    符号执行在2005年之后的突然重新流行,一大部分原因是因为求解器能力的提升,能够求解复杂的路径约束。但是约束求解在某种程度上依然是符号执行的关键瓶颈之一,也就是说符号执行所需求的约束求解能力超出了当前约束求解器的能力。所以,实现约束求解优化就变得十分重要。这里主要介绍两种优化方法:不相关约束消除,增量求解。


    在符号执行的约束生成过程中,尤其是在concolic执行过程中,通常会通过条件取反的方式增加约束,一个已知路径约束的分支谓词会取反,然后结果的约束集会检查可满足性以识别另一条路径是否可行。一个很重要的现象是,一个程序分支通常只依赖一小部分程序变量,所以我们可以尝试从当前路径条件中移除与识别当前分支结果不相关的约束。例如,当前的路径条件是(x+y>10)\wedge (z>0)\wedge (y<12) \wedge (z-x=0)
    ,我们想对某个条件取反以探索新的路径,即求解(x+y>10)\wedge (z>0)\wedge \neg (y<12)产生新输入,其中\neg (y<12)是取反的条件分支,那么我们就可以去掉对z的约束,因为对\neg (y<12)的分支是不会有影响的。减小的约束集会给出x和y的新值,我们用此前执行的z值就可以生成新输入了。如果更形式化地说,算法会计算在取反条件所依赖的所有约束的传递闭包。

    另一种方法本质上也是利用重用的思想。符号执行中生成的约束集有一个重要特性,就是表示为程序源代码中的静态分支的固定集合。所以,很多路径有相似的约束集,可以有相似的解决方案。通过重用以前相似请求的结果,可以利用这种特性来提升约束求解的速度,这种方法在CUTE和KLEE中都有实现。举个例子来说明,在KLEE中,所有的请求结果都保存在缓存中,该缓存将约束集映射到实际变量赋值。例如,缓存中的一个映射可能是 (x+y<10)\wedge (x>5)\Rightarrow\left\{ x=6, y=3 \right\}
    。使用这些映射,KLEE可以迅速解答一些相似的请求类型,包括已经缓存的约束集的子集和超集。比如对于请求 (x+y<10)\wedge (x>5)\wedge(y\geq 0),KLEE可以迅速检查{x=6, y=3} 是一个可行的答案。这样就可以让求解过程加快很多。

    3. 内存建模

    程序语句如何翻译为符号化约束的精确性对符号执行得到的覆盖率有很大影响。内存建模就是一个很大的问题,在访问内存的时候,内存地址用来引用一个内存单元,当这个地址的引用来自于用户输入时,内存地址就成为了一个表达式。当符号化执行时,我们必须决定什么时候将这个内存的引用进行实际化。一个可靠的策略是,考虑为从任何可能满足赋值的加载,但这个可能值的空间很大,如果实际化不够精确,会造成代码分析的不精确。还有一个是别名问题,即地址别名导致两个内存运算引用同一个地址,比较好的方法是进行别名分析,事先推理两个引用是否指向相同的地址,但这个步骤要静态分析完成。KLEE使用了别名分析和让SMT考虑别名问题的混合方法。而DART和CUTE压根没解决这个问题,只处理线性约束的公式,不能处理一般的符号化引用。


    符号化跳转也是一个问题,主要是switch这样的语句,常用跳转表实现,跳转的目标是一个表达式而不是实际值。以往的工作用三种处理方法。1)使用concolic执行中的实际化策略,一旦跳转目标在实际执行中被执行,就可以将符号执行转向这个实际路径,但缺陷是实际化导致很难探索完全的状态空间,只能探索已知的跳转目标。2)使用SMT求解器。当我们到达符号跳转时,假设路径谓词为\Pi,跳转到e,我们可以让SMT求解器找到符合\Pi \wedge e的答案。但是这种方案相比其他方案效率会低很多。3)使用静态分析,推理整个程序,定位可能的跳转目标。实际中,源代码的间接跳转分析主要是指针分析。二进制的跳转静态分析推理在跳转目标表达式中哪些值可能被引用。例如,函数指针表通常实现为可能的跳转目标表。

    4. 处理并发

    大型程序通常是并发的。因为这种程序的内在特性,动态符号执行系统可以被用来高效地测试并发程序,包括复杂数据输入的应用、分布式系统以及GPGPU程序。

    0x04 发展脉络

    这一部分简单介绍一下符号执行的发展脉络,以时间的顺序,同时对值得关注的项目和论文做一个小小总结和推荐。其实我自己也并没有把这些论文读完,所以某种程度上也算是一个后续学习的规划。

    符号执行最初提出是在70年代中期,主要描述的是静态符合执行的原理,到了2005年左右突然开始重新流行,是因为引入了一些新的技术让符号执行更加实用。Concolic执行的提出让符号执行真正成为可实用的程序分析技术,并且大量用于软件测试、逆向工程等领域。在2005年作用涌现出很多工作,如DART[5]、CUTE[6]、EGT/EXE[7]、CREST[8]等等,但真正值得关注和细读的,应该是2008年Cristian Cadar开发的KLEE[3]。KLEE可以说是源代码符号执行的经典作品,又是开源的,后来的许多优秀的符号执行工具都是建立在KLEE的基础上,因此我认为研究符号执行,KLEE的文章是必读的。

    基于二进制的符号执行工具则是2009年EPFL的George Candea团队开发的S2E[9]最为著名,其开创了选择符号执行这种方式。2012年CMU的David Brumley团队提出的Mayhem[10]则采用了混合offline和online的执行方式。2008年UC Berkeldy的Dawn Song团队提出的BitBlaze[11]二进制分析平台中的Rudder模块使用了online的执行方式,也值得一看。总之,基于二进制的符号执行工作了解这三个就足够了。其中,S2E有开源的一个版本,非常值得仔细研究。最近比较火的angr[12],是一个基于Python实现的二进制分析平台,完全开源且还在不断更新,其中也实现了多种不同的符号执行策略。

    在优化技术上,近几年的两个工作比较值得一看其一是2014年David Brumley团队提出的路径融合方法,叫做Veritesting[13],是比较重要的工作之一,angr中也实现了这种符号执行方式。另一个是2015年Stanford的Dawson Engler(这可是Cristian Cadar的老师)团队提出的Under-Constrained SymbolicExecution[14]。

    另外,近年流行的符号执行与fuzzing技术相结合以提升挖掘漏洞效率,其实早在DART和2012年微软的SAGE[15]工作中就已经有用到这种思想,但这两年真正火起来是2016年UCSB的Shellphish团队发表的Driller[16]论文,称作符号辅助的fuzzing(symbolic-assisted fuzzing),也非常值得一看。

    0x05 小结

    本文来自于我在学习符号执行过程中的一点总结,我自己对于符号执行处于初步了解的阶段,文章中难免有疏漏和错误之处,请各位大牛能够不吝赐教,悉数指出。最近在学习过程中感觉到,要研究和实现漏洞自动利用的生成,符号执行是无论如何也绕不过去的,毕竟当前的AEG策略完全基于符号执行的思路。符号执行的原理理解起来还是很简单的,形式化的介绍也很清晰,但如何实现是另一个问题。在今后的学习中,计划对开源符号执行系统的源码进行研究,如果有可能的话,写一些心得体会,再向大家请教。

    参考文献

    [1] Schwartz E J, Avgerinos T, Brumley D.All You Ever Wanted to Know about Dynamic Taint Analysis and Forward SymbolicExecution (but Might Have Been Afraid to Ask) [C]// Security & Privacy.DBLP, 2010:317-331.

    [2] Cadar C, Sen K. Symbolic execution forsoftware testing: three decades later[M]. ACM, 2013.

    [3] C. Cadar, D. Dunbar, and D. Engler. KLEE:Unassisted and Automatic Generation of High-Coverage Tests for Complex SystemsPrograms. In Proceedings of the 8th USENIX Symposium on Operating SystemsDesign and Implementation (OSDI’08), volume 8, pages 209–224, 2008.

    [4] R. S. Boyer, B. Elspas, and K. N.Levitt. SELECT – a formal system for testing and debugging programs by symbolicexecution. SIGPLAN Not., 10:234–245, 1975.

    [5] P. Godefroid, N. Klarlund, and K. Sen.DART: Directed Automated Random Testing. In PLDI’05, June 2005.

    [6] K. Sen, D. Marinov, and G. Agha. CUTE:A concolic unit testing engine for C. In ESEC/FSE’05, Sep 2005.

    [7] C. Cadar, V. Ganesh, P. M. Pawlowski,D. L. Dill, and D. R. Engler. EXE: Automatically Generating Inputs of Death. InProceedings of the 13th ACM Conference on Computer and Communications Security,pages 322–335, 2006.

    [8] J. Burnim and K.Sen,“Heuristics forscalable dynamic test generation,” in Proc. 23rd IEEE/ACM Int. Conf. Autom.Software Engin., 2008, pp. 443–446.

    [9] V. Chipounov, V. Georgescu, C. Zamfir,and G. Candea. Selective Symbolic Execution. In Proceedings of the 5th Workshopon Hot Topics in System Dependability, 2009.

    [10] S. K. Cha, T. Avgerinos, A. Rebert,and D. Brumley. Unleashing Mayhem on Binary Code. In Proceedings of the IEEESymposium on Security and Privacy, pages 380–394, 2012.

    [11] Song D, Brumley D, Yin H, et al.BitBlaze: A New Approach to Computer Security via Binary Analysis[C]//Information Systems Security, International Conference, Iciss 2008, Hyderabad,India, December 16-20, 2008. Proceedings. DBLP, 2008:1-25.

    [12] Yan S, Wang R, Salls C, et al. SOK:(State of) The Art of War: Offensive Techniques in Binary Analysis[C]//Security and Privacy. IEEE, 2016:138-157.

    [13] T. Avgerinos, A. Rebert, S. K. Cha,and D. Brumley. Enhancing Symbolic Execution with Veritesting. pages 1083–1094,2014.

    [14] D. a. Ramos and D. Engler.Under-Constrained Symbolic Execution: Correctness Checking for Real Code. InProceedings of the 24th USENIX Security Symposium, pages 49–64, 2015.

    [15] P. Godefroid, M. Y. Levin, and D.Molnar. SAGE: Whitebox Fuzzing for Security Testing. ACM Queue, 10(1):20, 2012.

    [16] N. Stephens, J. Grosen, C. Salls, A.Dutcher, R. Wang, J. Corbetta, Y. Shoshitaishvili, C. Kruegel, and G. Vigna.Driller: Augmenting Fuzzing Through Selective Symbolic Execution. InProceedings of the Network and Distributed System Security Symposium, 2016.


    https://zhuanlan.zhihu.com/p/26690230

    展开全文
  • C++ 程序编译过程

    万次阅读 2021-06-23 22:31:29
    C语言的编译链接过程要把我们编写的一个c程序(源代码)转换成可以硬件上运行的程序(可执行代码),需要进行编译和链接。编译就是把文本形式源代码翻译为机器语言形式的目标文件的过程。链接是把目标文件、操作...

    前言

    C语言的编译链接过程要把我们编写的一个c程序(源代码)转换成可以在硬件上运行的程序(可执行代码),需要进行编译和链接。编译就是把文本形式源代码翻译为机器语言形式的目标文件的过程。链接是把目标文件、操作系统的启动代码和用到的库文件进行组织,形成最终生成可执行代码的过程。过程图解如下:

    在这里插入图片描述

    从图上可以看到,整个代码的编译过程分为编译和链接两个过程,编译对应图中的大括号括起的部分,其余则为链接过程。

    1. 编译过程

    编译过程又可以分成两个阶段:编译和汇编。

    编译
    编译是读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码,源文件的编译过程包含两个主要阶段:

    编译预处理
    读取c源程序,对其中的伪指令(以# 开头的指令)和特殊符号进行处理。
    伪指令主要包括以下四个方面:

    1. 宏定义指令,如# define Name TokenString,# undef等。
      对于前一个伪指令,预编译所要做的是将程序中的所有Name用TokenString替换,但作为字符串常量的 Name则不被替换。对于后者,则将取消对某个宏的定义,使以后该串的出现不再被替换。
    2. 条件编译指令,如# ifdef,# ifndef,# else,# elif,# endif等。
      这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。
    3. 头文件包含指令,如# include “FileName” 或者# include < FileName> 等。
      在头文件中一般用伪指令# define定义了大量的宏(最常见的是字符常量),同时包含有各种外部符号的声明。
      采用头文件的目的主要是为了使某些定义可以供多个不同的C源程序使用。因为在需要用到这些定义的C源程序中,只需加上一条# include语句即可,而不必再在此文件中将这些定义重复一遍。预编译程序将把头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。
      包含到c源程序中的头文件可以是系统提供的,这些头文件一般被放在/ usr/ include目录下。在程序中# include它们要使用尖括号(< >)。另外开发人员也可以定义自己的头文件,这些文件一般与c源程序放在同一目录下,此时在# include中要用双引号("")。
    4. 特殊符号,预编译程序可以识别一些特殊的符号。
      例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。

    预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。这个文件的含义同没有经过预处理的源文件是相同的,但内容有所不同。下一步,此输出文件将作为编译程序的输入而被翻译成为机器指令。

    编译、优化阶段
    经过预编译得到的输出文件中,只有常量;如数字、字符串、变量的定义,以及C语言的关键字,如main, if , else , for , while , { , } , + , - , * , \ 等等。

    编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。

    优化处理是编译系统中一项比较艰深的技术。它涉及到的问题不仅同编译技术本身有关,而且同机器的硬件环境也有很大的关系。优化一部分是对中间代码的优化。这种优化不依赖于具体的计算机。另一种优化则主要针对目标代码的生成而进行的。

    对于前一种优化,主要的工作是删除公共表达式、循环优化(代码外提、强度削弱、变换循环控制条件、已知量的合并等)、复写传播,以及无用赋值的删除,等等。

    后一种类型的优化同机器的硬件结构密切相关,最主要的是考虑是如何充分利用机器的各个硬件寄存器存放有关变量的值,以减少对于内存的访问次数。另外,如何根据机器硬件执行指令的特点(如流水线、RISC、CISC、VLIW等)而对指令进行一些调整使目标代码比较短,执行的效率比较高,也是一个重要的研究课题。

    经过优化得到的汇编代码必须经过汇编程序的汇编转换成相应的机器指令,方可能被机器执行。

    汇编
    汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。

    目标文件由段组成。通常一个目标文件中至少有两个段:

    1. 代码段:该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。
    2. 数据段:主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。

    UNIX环境下主要有三种类型的目标文件:

    1. 可重定位文件
      其中包含有适合于其它目标文件链接来创建一个可执行的或者共享的目标文件的代码和数据。
    2. 共享的目标文件
      这种文件存放了适合于在两种上下文里链接的代码和数据。
      第一种是链接程序可把它与其它可重定位文件及共享的目标文件一起处理来创建另一个目标文件;
      第二种是动态链接程序将它与另一个可执行文件及其它的共享目标文件结合到一起,创建一个进程映象。
    3. 可执行文件
      它包含了一个可以被操作系统创建一个进程来执行之的文件。

    汇编程序生成的实际上是第一种类型的目标文件。对于后两种还需要其他的一些处理方能得到,这个就是链接程序的工作了。

    2. 链接过程

    由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。

    例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。

    链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。

    根据开发人员指定的同库函数的链接方式的不同,链接处理可分为两种:

    1. 静态链接
      在这种链接方式下,函数的代码将从其所在的静态链接库中被拷贝到最终的可执行程序中。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码。
    2. 动态链接
      在此种方式下,函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所作的只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。

    对于可执行文件中的函数调用,可分别采用动态链接或静态链接的方法。使用动态链接能够使最终的可执行文件比较短小,并且当共享对象被多个进程使用时能节约一些内存,因为在内存中只需要保存一份此共享对象的代码。但并不是使用动态链接就一定比使用静态链接要优越。在某些情况下动态链接可能带来一些性能上损害。

    3. GCC的编译链接

    我们在linux使用的gcc编译器便是把以上的几个过程进行捆绑,使用户只使用一次命令就把编译工作完成,这的确方便了编译工作,但对于初学者了解编译过程就很不利了,下图便是gcc代理的编译过程:
    在这里插入图片描述
    从上图可以看到:

    1. 预编译
      将.c 文件转化成 .i文件
      使用的gcc命令是:gcc –E
      对应于预处理命令cpp
    2. 编译
      将.c/.h文件转换成.s文件
      使用的gcc命令是:gcc –S
      对应于编译命令 cc –S
    3. 汇编
      将.s 文件转化成 .o文件
      使用的gcc 命令是:gcc –c
      对应于汇编命令是 as
    4. 链接
      将.o文件转化成可执行程序
      使用的gcc 命令是: gcc
      对应于链接命令是 ld

    总结起来编译过程就上面的四个过程:预编译处理(.c) --> 编译、优化程序(.s、.asm)--> 汇编程序(.obj、.o、.a、.ko) --> 链接程序(.exe、.elf、.axf等)。

    4. 总结

    C语言编译的整个过程是非常复杂的,里面涉及到的编译器知识、硬件知识、工具链知识都是非常多的,深入了解整个编译过程对工程师理解应用程序的编写是有很大帮助的,希望大家可以多了解一些,在遇到问题时多思考、多实践。

    一般情况下,我们只需要知道分成编译和链接两个阶段,编译阶段将源程序(*.c) 转换成为目标代码(一般是obj文件,至于具体过程就是上面说的那些阶段),链接阶段是把源程序转换成的目标代码(obj文件)与你程序里面调用的库函数对应的代码连接起来形成对应的可执行文件(exe文件)就可以了,其他的都需要在实践中多多体会才能有更深的理解。




    C/C++编译过程
    C/C++编译过程主要分为4个过程
    1) 编译预处理
    2) 编译、优化阶段
    3) 汇编过程
    4) 链接程序

    一、编译预处理
    (1)宏定义指令,如#define Name TokenString,#undef等。 对于前一个伪指令,预编译所要做的是将程序中的所有Name用TokenString替换,
    但作为字符串常量的 Name则不被替换。对于后者,则将取消对某个宏的定义,使以后该串的出现不再被替换。
    (2)条件编译指令,如#ifdef,#ifndef,#else,#elif,#endif等。 这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。
    预编译程序将根据有关的文件,将那些不必要的代码过滤掉
    (3) 头文件包含指令,如#include “FileName"或者#include 等。 在头文件中一般用伪指令#define定义了大量的宏(最常见的是字符常量),
    同时包含有各种外部符号的声明。 包含到c源程序中的头文件可以是系统提供的,这些头文件一般被放在/usr/include目录下。
    在程序中#include它们要使用尖括号(< >)。
    另外开发人员也可以定义自己的头文件,这些文件一般与c源程序放在同一目录下,此时在#include中要用双引号(”")。
    (4)特殊符号,预编译程序可以识别一些特殊的符号。 例如在源程序中出现的#line标识将被解释为当前行号(十进制数),
    上面程序实现了对宏line的运用
    (5)预处理模块 预处理工作由#pragma命令完成,#Pragma命令将设定编译器的状态或者是指示编译器完成一些特定的动作。
    #pragma指令对每个编译器给出了一个方法,在保持与C和C++语言完全兼容的情况下,给出主机或操作系统专有的特征。
    依据定义,编译指示是机器或操作系统专有的,且对于每个编译器都是不同的。
    打开C标准库函数,如stdio.h,我们总能找到下面这一句指示编译器初始化堆栈

    #include "iostream"
    #line 100
    using namespace std;
    int main(int argc, char* argv[])
    {
    cout<<"__LINE__:"<<__LINE__<<endl;
    return 0;
    }
    

    /*--------------------

    • 输出结果为:
    • LINE:103
    • 本来输出的结果应该是 7,但是用#line指定行号之后,使下一行的行号变为,
    • 到输出语句恰为行103
      ---------------------*/
      C/C++编译过程
      或者程序指示编译器去链接系统动态链接库或用户自定义链接库

    二、编译、优化阶段
    经过预编译得到的输出文件中,只有常量;如数字、字符串、变量的定义,以及C语言的关键字,如main,if,else,for,while,{,}, +,-,*,\等等。
    《编译原理》 中我们可以了解到一个编译器对程序代码的编译主要分为下面几个过程:

    a) 词法分析
    b) 语法分析
    c) 语义分析
    d) 中间代码生成
    e) 代码优化
    f) 代码生成
    g) 符号表管理
    h) 将多个步骤组合成趟
    i) 编译器构造工具

    在这里我们主要强调对函数压栈方式(函数调用约定)的编译处理
    C与C++语言调用方式大体相同,下面是几种常用的调用方式:
    __cdecl 是C DECLaration的缩写(declaration,声明),表示C语言默认的函数调用方法:所有参数从右到左依次入栈,
    这些参数由调用者清除,称为手动清栈。被调用函数不需要求调用者传递多少参数,调用者传递过多或者过少的参数,
    甚至完全不同的参数都不会产生编译阶段的错误。
    _stdcall 是StandardCall的缩写,是C++的标准调用方式:所有参数从右到左依次入栈,如果是调用类成员的话,
    最后一个入栈的是this指针。这些堆栈中的参数由被调用的函数在返回后清除,使用的指令是 retnX,X表示参数占用的字节数,
    CPU在ret之后自动弹出X个字节的堆栈空间。称为自动清栈。函数在编译的时候就必须确定参数个数,
    并且调用者必须严格的控制参数的生成,不能多,不能少,否则返回后会出错。
    PASCAL 是Pascal语言的函数调用方式,在早期的c/c++语言中使用这种调用方式,
    参数压栈顺序与前两者相反,但现在我们在程序中见到的都是它的演化版本,其实

    #pragma comment(lib,_T("GDI32.lib"))
    #ifdef _MSC_VER
    /*
    * Currently, all MS C compilers for Win32 platforms default to 8 byte
    * alignment.
    */
    #pragma pack(push,_CRT_PACKING)
    #endif /* _MSC_VER */
    

    C/C++编译过程
    质是另一种调用方式
    _fastcall是编译器指定的快速调用方式。由于大多数的函数参数个数很少,使用堆栈传递比较费时。因此_fastcall通常规定将前两个(或若干个)参数由寄存器传递,其余参数还是通过堆栈传递。不同编译器编译的程序规定的寄存器不同。返回方式和_stdcall相当。
    _thiscall 是为了解决类成员调用中this指针传递而规定的。_thiscall要求把this指针放在特定寄存器中,该寄存器由编译器决定。VC使用ecx,Borland的C++编译器使用eax。返回方式和_stdcall相当。
    _fastcall 和 _thiscall涉及的寄存器由编译器决定,因此不能用作跨编译器的接口。所以Windows上的COM对象接口都定义为_stdcall调用方式。
    C中不加说明默认函数为_cdecl方式(C中也只能用这种方式),C++也一样,但是默认的调用方式可以在IDE环境中设置。简单的我们可以从printf函数看出
    printf使用从从左至右压栈,返回int型并由_CRTIMP指定封在动态链接库中。
    通过金典的hello world程序我们可以知道编译器对其argc和argv[]这两个参数进行了压栈,并且argc留在了栈顶
    优化处理是编译系统中一项比较艰深的技术。它涉及到的问题不仅同编译技术本身有关,而且同机器的硬件环境也有很大的关系。优化处理主要分为下面几个过程:

    1) 局部优化
    a) 基本块的划分
    b) 基本块的变换
    c) 基本块的DAG表示
    d) DAG的应用
    e) 构造算法讨论
    2) 控制流分析和循环优化
    a) 程序流图与循环

    /*经典的hello world*/
    #include <stdio.h>
    int main(int argc, char* argv[])
    {
    printf("hello world");
    return 0;
    }
    _Check_return_opt_ _CRTIMP int __cdecl printf(_In_z_ _Printf_format_string_ const char * _Format, ...);
    #define CALLBACK _stdcall /* Windows程序回调函数*/
    #define WINAPI _stdcall
    #define WINAPIV _cdecl
    #define PASCAL _stdcall /*在c++语言中使用了StandardCall调用方式*/
    #define PASCAL _cdecl/*在c语言中使用了C DECLaration调用方式*/
    

    C/C++编译过程

    b) 循环
    c) 循环的查找
    d) 可归约流图
    e) 循环优化
    3) 数据流的分析与全局优化
    a) 一些主要的概念
    b) 数据流方程的一般形式
    c) 到达一定值数据流方程
    d) 可用表达式及其数据流方程
    e) 活跃变量数据流方程
    f) 复写传播

    经过优化得到的汇编代码必须经过汇编程序的汇编转换成相应的机器指令,方可能被机器执行。

    三、汇编过程
    汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,
    都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。
    目标文件由段组成。通常一个目标文件中至少有两个段: 代码段:该段中所包含的主要是程序的指令。
    该段一般是可读和可执行的,但一般却不可写。 数据段:主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。

    四、链接程序
    由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。
    例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);
    在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。
    链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,
    使得所有的这些目标文件成为一个能够诶操作系统装入执行的统一整体。
    根据开发人员指定的同库函数的链接方式的不同,链接处理可分为两种:
    (1)静态链接 在这种链接方式下,函数的代码将从其所在地静态链接库中被拷贝到最终的可执行程序中。
    这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,
    其中的每个文件含有库中的一个或者一组相关函数的代码。
    (2) 动态链接
    在此种方式下,函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所作的只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。C/C++编译过程对于可执行文件中的函数调用,可分别采用动态链接或静态链接的方法。使用动态链接能够使最终的可执行文件比较短小,并且当共享对象被多个进程使用时能节约一些内存,因为在内存中只需要保存一份此共享对象的代码。但并不是使用动态链接就一定比使用静态链接要优越。在某些情况下动态链接可能带来一些性能上损害。
    ----------------------------------------------------作者 张彦升

    文章转载自:https://www.cnblogs.com/mickole/articles/3659112.html

    谢谢,创作不易,大侠请留步… 动起可爱的双手,来个赞再走呗 <( ̄︶ ̄)>

    展开全文
  • Java 代码 编译和执行过程

    千次阅读 2018-09-05 13:47:56
    流程图 Java代码编译是由Java源码编译器来完成...Java代码编译和执行的整个过程包含了以下三个重要的机制: Java源码编译机制 类加载机制 类执行机制 Java源码编译机制 Java 源码编译由以下三个过程组成: 分析和...

    流程图

    Java代码编译是由Java源码编译器来完成,流程图如下所示:

     


    这里写图片描述
    Java代码编译

     

    Java字节码(class文件)的执行是由JVM执行引擎来完成,流程图如下所示:


    这里写图片描述
    Java字节码的执行

     

    Java代码编译和执行的整个过程包含了以下三个重要的机制:
    Java源码编译机制
    类加载机制
    类执行机制

    Java源码编译机制

    Java 源码编译由以下三个过程组成:
    分析和输入到符号表
    注解处理
    语义分析和生成class文件
    流程图如下所示:

     


    这里写图片描述

     

    最后生成的class文件由以下部分组成:
    结构信息:包括class文件格式版本号及各部分的数量与大小的信息
    元数据:对应于Java源码中声明与常量的信息。包含类/继承的超类/实现的接口的声明信息、域与方法声明信息和常量池
    方法信息:对应Java源码中语句和表达式对应的信息。包含字节码、异常处理器表、求值栈与局部变量区大小、求值栈的类型记录、调试符号信息

    类加载机制

    JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:


    这里写图片描述

     

    1)Bootstrap ClassLoader
    负责加载 $JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类
    2)Extension ClassLoader
    负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包
    3)App ClassLoader
    负责记载classpath中指定的jar包及目录中class
    4)Custom ClassLoader
    属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader
    加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。

    类执行机制

    JVM是基于栈的体系结构来执行class字节码的。线程创建后,都会产生程序计数器(PC)和栈(Stack),程序计数器存放下一条要执行的指令在方法内的偏移量,栈中存放一个个栈帧,每个栈帧对应着每个方法的每次调用,而栈帧又是有局部变量区和操作数栈两部分组成,局部变量区用于存放方法中的局部变量和参数,操作数栈中用于存放方法执行过程中产生的中间结果。栈的结构如下图所示:


    这里写图片描述

     

    类被加载到虚拟机内存中开始,到卸载出内存为主,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个阶段称为连接(Linking)。


    这里写图片描述
    类的生命周期

     

    实例:

    如下图,Java程序从源文件创建到程序运行要经过两大步骤:1、源文件由编译器编译成字节码(ByteCode) 2、字节码由java虚拟机解释运行。因为java程序既要编译同时也要经过JVM的解释运行,所以说Java被称为半解释语言。


    java程序编译运行过程
    java程序编译运行过程

     

    //MainApp.java  
    public class MainApp {  
        public static void main(String[] args) {  
            Animal animal = new Animal("Puppy");  
            animal.printName();  
        }  
    }  
    //Animal.java  
    public class Animal {  
        public String name;  
        public Animal(String name) {  
            this.name = name;  
        }  
        public void printName() {  
            System.out.println("Animal ["+name+"]");  
        }  
    }  
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    第一步(编译): 创建完源文件之后,程序会先被编译为.class文件。Java编译一个类时,如果这个类所依赖的类还没有被编译,编译器就会先编译这个被依赖的类,然后引用,否则直接引用,这个有点象make。如果java编译器在指定目录下找不到该类所其依赖的类的.class文件或者.java源文件的话,编译器话报“cant find symbol”的错误。

    编译后的字节码文件格式主要分为两部分:常量池和方法字节码。常量池记录的是代码出现过的所有token(类名,成员变量名等等)以及符号引用(方法引用,成员变量引用等等);方法字节码放的是类中各个方法的字节码。下面是MainApp.class通过反汇编的结果,我们可以清楚看到.class文件的结构:


    这里写图片描述
    MainApp类常量池


    这里写图片描述
    MainApp类方法字节码

     

    第二步(运行):java类运行的过程大概可分为两个过程:1、类的加载 2、类的执行。需要说明的是:JVM主要在程序第一次主动使用类的时候,才会去加载该类。也就是说,JVM并不是在一开始就把一个程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次
    下面是程序运行的详细步骤:
    1. 在编译好java程序得到MainApp.class文件后,在命令行上敲java AppMain。系统就会启动一个jvm进程,jvm进程从classpath路径中找到一个名为AppMain.class的二进制文件,将MainApp的类信息加载到运行时数据区的方法区内,这个过程叫做MainApp类的加载。
    2. 然后JVM找到AppMain的主函数入口,开始执行main函数。
    3. main函数的第一条命令是Animal animal = new Animal(“Puppy”);就是让JVM创建一个Animal对象,但是这时候方法区中没有Animal类的信息,所以JVM马上加载Animal类,把Animal类的类型信息放到方法区中
    4. 加载完Animal类之后,Java虚拟机做的第一件事情就是在堆区中为一个新的Animal实例分配内存, 然后调用构造函数初始化Animal实例,这个Animal实例持有着指向方法区的Animal类的类型信息(其中包含有方法表,java动态绑定的底层实现)的引用。
    5. 当使用animal.printName()的时候,JVM根据animal引用找到Animal对象,然后根据Animal对象持有的引用定位到方法区中Animal类的类型信息的方法表,获得printName()函数的字节码的地址。
    6. 开始运行printName()函数。

     


    这里写图片描述
    java程序运行过程

     

    参考:
    http://blog.csdn.net/cutesource/article/details/5904542
    http://www.360doc.com/content/14/0218/23/9440338_353675002.shtml
    http://www.cnblogs.com/gw811/archive/2012/10/18/2730117.htmlx

    展开全文
  • pythonyield的用法详解——简单,清晰的解释

    万次阅读 多人点赞 2019-04-02 13:29:31
    首先我要吐槽一下,看程序的过程中遇见了yield这个关键字,然后百度的时候,发现没有一个能简单的让我懂的,讲起来真TM的都是头头是道,什么参数,什么传递的,还口口声声说自己的教程是简单的,浅显易懂的,我...
  • Java代码编译和执行的整个过程包含了以下三个重要的机制: Java源码编译机制类加载机制类执行机制 Java源码编译机制 Java源码编译由以下三个过程组成: 分析和输入到符号表注解处理语义分析和生成class文件 ...
  • mysql存储过程学习笔记

    万次阅读 多人点赞 2019-02-22 17:09:36
    一组为了完成特定功能的SQL 语句集,存储数据库,经过第一次编译后调用不需要再次编译,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。存储过程是数据库的一个重要对象。 二、...
  • java 编译和加载和执行类的全过程

    千次阅读 2017-01-13 15:53:28
    Java代码编译和执行的整个过程包含了以下三个重要的机制: Java源码编译机制类加载机制类执行机制 Java源码编译机制 Java 源码编译由以下三个过程组成: 分析和输入到符号表注解处理语义分析和生成cla
  • Java 代码编译和执行的整个过程

    千次阅读 2012-02-08 22:52:34
    Java代码编译和执行的整个过程包含了以下三个重要的机制: Java源码编译机制 类加载机制 类执行机制 Java源码编译机制 Java 源码编译由以下三个过程组成: 分析和输入到符号表 注解处理 语义分析和生成class...
  • 从面向过程到面向对象

    万次阅读 2015-05-27 14:12:02
    这期间,程序设计语言主要经历了从面向过程(如 C 和 Pascal 语言)到面向对象(如:C++、Java、Objective-C),再到面向组件编程(如 .NET 平台下的 C# 语言),以及正在快速发展的面向服务架构技术(如 SOA 和 ...
  • 论“事前-事中-事后”的重要

    万次阅读 2018-09-08 17:33:31
    最近,公司生产上出现了两个事故,一次是软件版本部署失误,一次是测试过程中硬件板卡异常。就这两个事情,我们谈一下日常非常熟悉...所谓事中,就是做事情的过程中对事前规划的事情进行执行,以及记录过程中出现...
  • Swift中文教程DOC版

    千次下载 热门讨论 2014-06-04 17:51:49
    Apple Swift编程语言入门教程 中文版本文档。主要通过实例讲解基础的语法
  • 12.3 序列化过程中使用XML 尽管使用专门的序列化技术比较便利和简洁,但不具有可移植性。XML是定义数据的标准,所以我们可以为上面的序列化例子创建一个XML模型。至少从理论上来说这种模型可以跨多平台和多语言。本...
  • MySQL数据库存储过程

    万次阅读 多人点赞 2017-03-22 19:34:29
    一组为了完成特定功能的SQL 语句集,存储数据库,经过第一次编译后再次调用不需要再次编译,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。存储过程是数据库的一个重要对象。——...
  • Java代码是怎么运行的?

    万次阅读 2018-11-06 00:52:56
    极客时间《深入拆解Java虚拟机》...Java 代码有很多种不同的运行方式,比如开发工具运行、双击执行 jar 文件运行、命令行运行,甚至可以网页运行。 Java 的运行离不开 JRE(Java 运行时环境), JRE ...
  • C/C++程序编译过程详解

    万次阅读 多人点赞 2017-11-13 14:51:06
    C语言的编译链接过程要把我们编写的一个c程序(源代码)转换成可以硬件上运行的程序(可执行代码),需要进行编译和链接。编译就是把文本形式源代码翻译为机器语言形式的目标文件的过程。链接是把目标文件、操作...
  • 数据库存储过程

    万次阅读 多人点赞 2017-12-02 19:07:18
    SQL语句需要先编译然后执行,而存储过程(Stored Procedure)是一组为了完成特定功能的SQL语句集,经编译后存储数据库,用户通过指定存储过程的名字并给定参数(如果该存储过程带有参数)来调用执行它。...
  • MySQL存储过程中的错误处理

    万次阅读 2016-10-14 10:30:15
    当MySQL执行存储过程遇到错误时,适当处理它,如继续执行或退出当前代码段,并返回有意义的错误提示是很重要的。一方面提高程序的容错能力,另一方便当程序出错时,开发人员也能准确定位错误的地方。 本章MySQL...
  • 如何可能行为数量较多的情况下有效地进行探索是增强学习中最重要的问题之一。其次,增强学习一个行为不仅可能影响到当前时刻的奖励,而且还可能影响到之后所有时刻的奖励。最坏的情况下,一个好行为不会...
  • C++编译链接过程详解

    万次阅读 多人点赞 2018-09-01 19:27:48
    C语言的编译链接过程要把我们编写的一个c程序(源代码)转换成可以硬件上运行的程序(可执行代码),需要进行编译和链接。编译就是把文本形式源代码翻译为机器语言形式的目标文件的过程。链接是把目标文件、操作...
  • 程序是怎么从代码到执行

    万次阅读 多人点赞 2014-06-03 20:35:16
    一个月多月没发文章了,虽然有几篇草稿箱里一直删改,但是太水.没好意思发出来.关于这个文章的问题是早就想问了的,但是以前一直基础不够,弄不明白.这学期刚好学了体系结构,也看CSAPP,总算可以说是算比较清楚的理解了...
  • Mybatis源码解析之四 mybatis执行流程

    万次阅读 2019-01-21 19:10:40
    目录 一、缘起 ...三、mybatis执行流程 1、mybatis的架构设计图 2、mybatis的执行流程图 3、mybatis执行流程时序图 4、Mybatis的核心组件 5、mybatis执行流程的分析 一、缘起 前三篇的文章...
  • 《手撸 Spring》 • 小傅哥.pdf

    千次下载 2021-08-11 16:32:43
    手写Spring 源码的过程中会摘取整体框架的核心逻辑,简化代码实现过程,保留核心功能,例如:IOC、AOP、Bean生命周期、上下文、作用域、资源处理等内容实现。 适合人群 1. 具备一定编程基础,工作1-3年的研发...
  • 附录B 生产环境运维过程中的最佳实践 附录B 生产环境运维过程中的最佳实践 作者:Ben Treynor Sloss 编辑:Betsy Beyer 可控的故障模式 配置文件,或者服务的任何输入都应该经过正确性和准确性检查再提供给服务。...
  • 什么是用户态?什么是内核态?如何区分?

    万次阅读 多人点赞 2018-04-13 20:33:38
    执行级别下,代码可以执行特权指令,访问任意的物理地址,这种CPU执行级别就对应着内核态。而相应的低级别执行状态下,代码的掌控范围会受到限制。只能对应级别允许的范围内活动。举例:intel x86 CPU有四种...
  • 存储过程(Stored Procedure)介绍

    千次阅读 2018-06-12 09:04:16
    一组为了完成特定功能的 SQL 语句集,这些 SQL 语句集存储数据库,经过第一次编译后,后续调用不需要再次编译,用户通过存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。 存储过程是数据库的一个...
  • Servlet的运行过程

    千次阅读 2017-02-22 12:23:45
    Servlet的运行过程 一、servlet的运行过程 ...如果是,则执行第4步,否则执行第2步 2、装载并创建给servlet的一个实例对象 3、调用servlet实例对象的init()方法。 4、创建一个用于封装HTTP请求的
  • 存储过程的优缺点

    万次阅读 2015-04-19 22:40:43
    为什么要用存储过程 几个去 IBM 面试的兄弟回来抱怨:去...存储过程真的那么重要吗,它到底有什么好处呢? 笔者认为,存储过程说白了就是一堆 SQL 的合并。中间加了点逻辑控制。 但是存储过程处理比较复杂的业务时比较
  • 度量模型的准确性 ·使用神经网络的有监督学习模型 ·使用神经网络的无监督学习模型 ·反向传播 本章的最后将介绍R学习过程的基本概念以及如何R环境实现。本章还涉及实现神经网络的不同算法,如何训练、测试和...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,331,341
精华内容 532,536
关键字:

在执行过程中最重要的是