精华内容
下载资源
问答
  • JVM-invokedynamic指令分析

    万次阅读 2018-11-29 14:59:22
    invokedynamic指令是java7引入的。这是自 1.0 以来第一次引入新的虚拟机指令。到了 java 8 这条指令才第一次在 java 应用,用在 lambda 表达式里。在这之前的方法调用指令为 invokestatic、invokespecial、...

           invokedynamic指令是java7引入的。这是自 1.0 以来第一次引入新的虚拟机指令。到了 java 8 这条指令才第一次在 java 应用,用在 lambda 表达式里。在这之前的方法调用指令为 invokestatic、invokespecial、invokevirtual、invokeinterface。这四种指令和包含目标方法类名,方法名以及方法描述符的符号引用绑定。为了实现invokedynamic,java7引入了更底层,更灵活的方法抽象:方法句柄。

    方法句柄的概念
            方法句柄是一个强类型,可以直接执行的引用。方法句柄的类型是由所指向方法的参数类型和返回类型组成。它是用来确认方法句柄是否适配的唯一关键。方法句柄的功能与反射想类似。下面从不同的角度对比一下反射-代理-方法句柄

            反射需要调用setAccesible(),可能会受到禁止警告,代理类如果是内部类创建,就只能访问受限的函数和方法,而方法句柄在访问的上下文中对所有方法都拥有完整的权限。
            反射的执行速度回受可变长的参数,基本类型的自动装拆箱,以及最重要的方法内联的影响,代理调用的方法和调用函数的方式是一样的,速度最快,方法句柄自然不可能有代理的执行速度,它的速度受jvm的各类配置所影响。
    方法句柄的调用
            方法句柄的调用有两种,一种是需要严格匹配参数类型的invokExact:假设一个方法句柄需要接受一个Object类型的参数,如果你传入一个String类型的参数,那么在运行时就会抛出异常。但方法句柄API有个注解@PolymorphicSignature,如果被调用的方法有这个注解的话,编译器会根据所所传入的参数来生成方法描述符,而不是采用目标方法的方法描述符。

            另一种是invoke,可以自动适配参数类型。invoke方法会调用MethodHandle.asType方法,生成一个适配器句柄。在调用目标方法时先进行适配,然后再返回给调用者。

    方法句柄的操作
            jvm在调用invokExact方法的时候,会调用一个共享的、与方法句柄类型相关的特殊适配器LambdaForm当中。当然,要想看这个类的话,可以添加启动参数  -Djava.lang.invoke.MethodHandle.DUMP_CLASS_FIFLE=true 来导出这个类的class文件。

           在LambdaForm中会调用Invokers.checkExactType去检查参数类型,然后调用Invokes.CheckCustomized方法。最后,调用方法句柄的invokeBasic方法。invokeBasic同样会被jvm特殊对待,执行时,会调用到方法句柄本身所持有的适配器里,也是一个LambdaForm。这个LambdaForm会获取方法句柄里的MemberName字段,作为参数调用linkToStatic方法。当然,linkToStatic也会做特殊处理,会根据传入的参数 储存的索引  直接跳转到目标方法。  在这些方法里 Invokes.CheckCustomized在超过 一个阈值时就会进行优化  阈值可用参数 -Djava.lang.invoke.MethodHandle.CUSTOMIZE_THRESHOLD进行设置,默认是127.当调用次数大于这个阈值的时候,Invokes.CheckCustomized会为该方法句柄生成一个特有的适配器,这个适配器就会将方法句柄作为常量 直接获取MemberName类型的字段 供linkToStatic调用。

            方法句柄的调用和反射一样,都是间接调用,因此  ,会被无法进行方法内联而影响性能。不过,方法句柄的内联瓶颈在于JIT能否将方法句柄作为常量。下面,就来具体的讲讲

    invokedynamic指令
            invokedynamic是java7引入的新指令,主要是用来支持动态语言的方法调用。将调用点 抽象成一个java类,并将原本由jvm控制的方法调用以及方法链接暴露给了应用程序。在运行过程中,每条invokedynamic指令都会绑定一个调用点,且会调用该调用点链接的方法句柄。在第一次执行invokedynamic时,jvm会调用该指令对应的启动方法,来生成调用点,并绑定到invokedynamic指令里。之后再次运行的话,就直接调用绑定的调用点链接的方法句柄。
     

    展开全文
  • JVM字节码指令 及 反编译分析

    千次阅读 2016-12-26 22:46:51
    下面详细了解JVM字节码指令:先字节码指令组成结构有个大体了解,并通过前面的"getMap"方法的字节码数据来分析JVM指令及操作码助记符,而后了解字节码指令与数据类型的关系,最后分类说明JVM指令的功能及注意事项...

    JVM字节码指令 及 反编译分析

          在文章《Java前端编译:Java源代码编译成Class文件的过程》了解到javac编译的大体过程,在《Java Class文件结构解析 及 实例分析验证》中了解到了Class文件结构,我们可以知道Class文件中的各方法表后面的"code"属性存储了各方法对应的JVM字节码指令。

           下面我们详细了解JVM字节码指令:先对字节码指令组成结构有个大体了解,并通过前面的"getMap"方法的字节码数据来分析JVM指令及操作码助记符,而后了解字节码指令与数据类型的关系,最后分类说明JVM指令的功能及注意事项。

    1、字节码指令集概述

    1-1、字节码指令的组成结构

            一条JVM指令由一个字节长度、代表某种特定操作含义的数字(称为操作码,Opcode),以及跟随其后的零到多个代表此操作所需参数(称为操作数,Operands)构成。

            1、对于JVM指令,由于JVM采用面向操作数栈而不是寄存器的架构,所以大多指令只有操作码

            2、对于操作码,由于操作码长度为一个字节,所以操作码(指令)最多不超过256条

            3、对于操作数长度超过了一个字节的情况(取决于操作码):由于Class文件格式放弃了编译后代码的操作数长度对齐,所以,当JVM处理超过一个字节长度的数据时,需要在运行时从字节中重建出具体数据的结构,如16位数据需要(byte1 << 8)|byte2操作(Big-Endian 顺序存储——即高位在前的字节序)

    这种操作会使得在解释执行时损失一些性能,但这也可以省略很多填充和间隔符号,尽可能获得短小精干的编译代码,数据量小,传输效率高;

    1-2、操作码助记符及指令解释

           在《Java Class文件结构解析 及 实例分析验证》"3-8节、属性表集合"的Code属性分析中,可以看到"getMap"方法程序如下:

           使用javac编译为Class文件后,"getMap"方法表对应的Code属性中有22个字节JVM字节码指令数据,如下:

           使用javap反编译后的JVM指令如下:

          JVM指令的解释:"new #4"、"dup"、"aload_1"、"invokeinterface #9, 3"等;

          操作码助记符:"new"、"dup"、aload_1"、"invokeinterface等;

          操作数:"new "一个操作数"#4"、"invokeinterface"两个"#9, 3";其中的"#"号表示索引常量池中的第几项常量数据。

          从字节码到指令解释的"手动"翻译过程如下:

        (A)、"BB0004":先是一个操作码"BB",查询《JVM操作码助记符表》可以看到,而后再《JVM指令集》的介绍中找到"new"指令的详细说明,知道后面接一个操作数,并且是两个字节长度,所以操作数是"0004",即"BB0004"就表示指令"new #4";

        (B)、"59":查表得知表示"dup"指令,后面没有操作数;

        (C)、"B70005":其中"B7"查指令集表得知为"invokeinterface",后面接一个两字节的操作数,即表示指令"invokespecial #5";

          整理如下:

    字节码指令

    操作码

    操作数1

    操作数2

    操作数3

    助记符及解释

    备注

    BB0004

    BB

    (00<<8)|04

       

    new #4

    "#"号表示索引常量池中的第几项常量数据;

    59

    59

         

    dup

    该指令无操作数

    B70005

    B7

    (00<<8)|05

       

    invokespecial #5

     

    4C

    4C

         

    astore_1

     

    2B

    2B

         

    aload_1

     

    B20006

    B2

    (00<<8)|06

       

    getstatic #6

     

    1208

    12

    08

       

    ldc #8

    操作数为一个字节

    B900090300

    12

    (00<<8)|09

    03

    00

    invokeinterface #9, 3

    第3个操作数是为了给 Oracle 实现的虚拟机的额外操作数而预留的空间

    57

    57

         

    pop

     

    2B

    2B

         

    aload_1

     

    B0

    B0

         

    areturn

     

    JVM规范中《操作码助记符表》:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-7.html

    JVM规范中《JVM指令集》介绍:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5

    1-3、JVM指令与数据类型

    1、指令助记符与数据类型

    (A)、大多数指令包含了其操作所对应的数据类型信息,如:

          iload指令用于从局部变量表中加载int类型数据到操作数栈中;

          fload指令加载的则是float类型数据;

    (B)、对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务,如:

          i代表int类型,f代表float类型,d代表double,a代表reference;

    (C)、数组类型的一般带有array字符,如:

           arraylength指令;

    (D)、还有一些指令与数据类型无关,如:

          无条件跳转指令goto;

    2、"Not Orthogonal"特性与数据类型转换

          由于操作码最多不超过256条,不可能每种对数据的操作都为每种类型单独一条指令,即并非第种数据类型和每一种操作都有对应的指令,这称为"Not Orthogonal"特性;

          大部分指令都没有支持boolean(没有任何支持)、byte、char和short数据类型;JVM、编译器会在编译期或运行期将byte和short类型的数据带符号扩展(Sign-Extend)为相应的Int类型数据;将boolean和char类型数据零位扩展(Zero-Extend)为相应的int类型数据;

          处理这些类型的数组时,会转换为int类型的字节码指令来处理;

    2、JVM指令分类说明

          可以将JVM指令操作按用途分为9类,下面分别介绍。

    2-1、加载和存储指令

          用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,包括如下内容指令:

          1、将一个局部变量加载到操作栈:iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>;

          2、将一个数值从操作数栈存储到局部变量表:istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>;

          3、将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_ml、iconst_<i>、lconst_<i>、fconst_<i>、dconst_<i>;

          4、扩充局部变量表的访问索引的指令:wide;

          <>结尾的代表了一组指令,如iload_<n>,代表了iload_1、iload_2、iload_3、iload_4;

          它们省略了显式的操作数,不需要进行操作数的动作,实际上操作数据隐含在指令中,如iload_1与iload操作数为0时完全一致。

    2-2、运算指令

          用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶;

          可以分为两种:1、对整型数据进行运算的指令;2、对浮点型数据进行运算的指令;整数与浮点数算术指令在溢出和被零除时也有各自不同的行为表现。

          所有算术指令如下:

          1、加法指令:iadd、ladd、fadd、dadd;

          2、减法指令:isub、lsub、fsub、dsub;

          3、乘法指令:imul、lmul、fmul、dmul;

          4、除法指令:idiv、ldiv、fdiv、ddiv;

          5、求余指令:irem、lrem、frem、drem;

          6、取反指令:ineg、lneg、fneg、dneg;

          7、位移指令:ishl、ishr、iushr、lshl、lshr、lushr、;

          8、按位或指令:ior、lor;

          9、按位与指令:iand、land;

          10、按位异或指令:ixor、lxor;

          11、局部变量自增指令:iinc;

          12、比较指令:dempg、dempl、fempg、fempl、lemp;

    1、整型数据的运算

          JVM规范没有定义整型数据溢出的具体运算结果。

          只定义除法指令(idiv和ldiv)以及求余指令(irem和lrem)中,当出现除数为零时会导致JVM抛出ArithmethicException异常;除此外,其他任何整型数据运算都不应该抛出异常。

          另外,前面说过,没有直接支持boolean、byte、char和short数据类型的算术指令,使int类型的指令代替。

    2、浮点型数据的运算

          VM规范要求JVM处理浮点数时,必须严格遵守IEEE 754规范中规定的行为和限制,包括非正规浮点数值(Denormalized Floating-point Number)和逐级下溢(Gradual Underflow)的运算规则。

    A)、浮点型数据的舍入模式

          浮点数运算时,非精确的结果必须舍入为可被表示的最接近的精确值,采用IEEE 754默认的舍入模式,优先选择最低有效位为零的,称为向最接近数舍入模式

          而把浮点数转换为整数时,采用IEEE 754标准的向零舍入模式,即把小数部分的有效字节丢弃,如:

                    float f1 = (float) 1.0;

                    float f2 = (float) 0.8;

                    double d1 = 1.00000005;

                    double d2 = 1.00000015;

                    double d3 = 1.00000025;

                    //测试向最接近数舍入模式

                    float fd1 = (float) (f1+d1);

                    float fd2 = (float) (f1+d2);

                    float fd3 = (float) (f1+d3);

                    System.out.println("fd1 = " + fd1);

                    System.out.println("fd3 = " + fd2);    

                    System.out.println("fd3 = " + fd3);

                    //测试向零舍入模式

                    int i = (int) (f1+f2);

                    System.out.println("i = " + i);

                输出:    

                    fd1 = 2.0

                    fd3 = 2.0000002

                    fd3 = 2.0000002

                    i = 1

    (B)、异常、溢出、NaN值

          JVM处理浮点数运算时,不会抛出任何运行时异常;

          当一个操作产生溢出时,使用有符号的无穷大来表示;

          没有数学定义的值,使用NaN值来表示;    

    (C)、比较

          JVM在long类型数值比较时,采用有符号的比较方式;

          而在浮点数值比较(dempg、dempl、fempg、fempl)时,采用IEEE 754定义的无符号比较(Nosignaling Comparisons)方式;

    2-3、类型转换指令

          用于将两种不同的数值类型进行相互转换;

    1、宽化类型转换

          JVM直接支持(无需显式的转换指令)宽化类型转换(Widening Numeric Conversions,即小范围类型向大范围类型的安全转换),如下:

          (A)、int类型到long、float、double类型;

          (B)、long类型到float、double类型;

          (C)、float类型到double类型;

    2、窄化类型转换

          而处理窄化类型转换(Narrowing Numeric Conversions)时,必须显式地使用转换指令来完成,包括:

          i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2f;

          这可能导致转换结果产生不同的正负号、不同的数量级的情况,转换过程中还可能导致精确度丢失,但JVM不会抛出任何运行时异常,如:

    (A)、int或long类型窄化转换为整数类型T(T字节长度为N)时,转换过程仅仅是简单地丢弃除最低位N个字节以外的内容;

    (B)、浮点值窄化转换为整数类型T(T限于int或long类型)时,遵循以下规则:

          1)、如果浮点值是NaN,那转换结果是T为0;

          2)、如果浮点值不是无穷大,采用向零舍入模式取整,获得其整数值v;

          如果v在T的表示范围内,T就等于v;

          否则,根据v的符号,转换为T所能表示的最大或最小正数;

    (C)、double类到float类型的窄化转换采用向最接近数舍入模式,舍入得到一个可以用float表示的数字;

          1)、如果该数字绝对值太小无法用float来表示,将返回float类型的正负零;

          2)、如果该数字绝对值太大无法用float来表示,将返回float类型的正负无穷大;

          3)、而NaN转换还是NaN;

    2-4、对象创建与访问指令

          JVM对类实例和数组创建和操作使用了不同的字节码指令,包括:

            1、创建类实例的指令:new;

            2、创建数组的指令:newarray、anewarray、multianewarray;

            3、访问类字段(static字段或类变量)和实例字段(非static字段或实例变量)的指令:getfield、putfield、getstatic、putstatic;        

            4、把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload;

            5、把一个操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore;

            6、取数组长度的指令:arraylength;

            7、检查类实例类型的指令:instanceof、checkcast;

    2-5、操作数栈管理指令

          JVM直接操作操作数栈的指令

            1、将操作数栈的栈顶一个或两个元素出栈:pop、pop2;

            2、复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2;

            3、将栈最顶端的两个数值互换:swap;

    2-6、控制转移指令

          用于让JVM有条件或无条件地从指定的位置指令(而不是控制转移指令的下一条指令)继续执行程序,即有条件或无条件地修改PC寄存器的值。

          控制转移指令如下:

            1、条件分支:ifeq、iflt、ifle、ifgt、ifge、ifnull、ifnonnull、empeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne;

            2、复合条件分支:tableswitch、lookupswitch;

            3、无条件分支:goto、goto_w、jsr、jsr_w、ret;

          JVM有专门处理int、reference类型和检测null值的指令;

          对boolean、byte、char和short类型的条件分支比较操作,都是使用int类型的比较操作指令;

          对long、float和double类型的条件分支比较操作,则先会执行相应类型的比较运算指令(dempg、dempl、fempg、fempl、lemp),然后返回一个整型值到操作数栈中,再执行int类型的条件分支比较操作完成跳转;

          所以int类型的条件分支指令是最为丰富和强大的。    

    2-7、操作数栈管理指令

          方法调用指令主要是的以下5条

            1、invokevirtual指令:用于调用对象的实例方法,根据实际类型进行分派(虚方法分派),最常见的分派方式;

            2、invokeinterface指令:用于调用对象接口方法,运行时会搜索一个实现了该接口方法的对象,找出适合的方法进行调用;

            3、invokespecial指令:用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法;

            4、invokestatic指令:用于调用类方法(static方法)

            5、invokedynamic指令:用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法;

          前面4条指令的分派逻辑都固化在JVM内,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的

          方法调用指令与数据类型无关,而方法返回指令是根据返回值类型区分的,包括:

          1、ireturn、lreturn、freturn、dreturn、areturn;

          2、return:void方法、实例初始化方法以及类和接口的类初始化方法使用。

    2-8、操作数栈管理指令

          Java程序中显式抛出异常的操作(throw语句)都是由athrow指令来实现的;

          还有许多运行时异常,会在JVM指令检测到异常时自动抛出,如idiv或ldiv指令除数为零时,自动抛出ArithmeticException异常;

          另外,处理异常的catch语句,不是由字节码指令实现的(JDK1.4.2前是由jsr和ret指令实现),而是采用异常表来完成(方法表中Code属性有异常表)。

    2-9、同步指令

          JVM支持方法级的同步和方法内部一段指令序列的同步,两种同步结构都使用管程(Monitor)来支持,如下:

    1、方法级的同步

           是隐式的,无须通过字节码指令来控制,实现在方法调用和返回操作之中;

          方法调用时,先检查方法表(method_info)的ACC_SYNCHRONIZED访问标志,设置了表示该方法为同步方法;

          执行线程需要先成功持有管程,才能执行该方法,方法完成返回时释放管程;

          如果方法执行期间抛出异常,且方法内部无法处理,管程在异常抛出到方法外时自动释放;

    2、同步一段指令序列

          通常在java程序中由synchronized语句来表示,有monitorenter和monitorexit两条指令来支持;

          需要javac编译器和JVM共同协作支持;

          一条monitorenter指令需要一条monitorexit指令对应,所以,编译器可能会自动产生一个可以处理所有异常的异常处理器,来执行monitorexit指令;

    2-10、其他

    1、三个保留操作码

          有三个是保留操作码,它们是被Java虚拟机内部使用的,不能真的出现在一个有效的Class文件之中:

          两个操作码值分别为 254(0xfe)和 255(0xff),助记符分别为impdep1和impdep2的两个操作码是作为"后门"和"陷阱"出现,目的是在某些硬件和软件中提供一些与实现相关的功能;

          第三个操作码值分别为 202(0xca)、助记符为 breakpoint 的操作码是用于调试器实现断点功能;

    2、虚拟机错误

          当Java虚拟机出现了内部错误,或者由于资源限制导致虚拟机无法实现Java语言中的语义时,Java虚拟机将会抛出一个属于VirtualMachineError的子类的异常对象实例;

          可能会出现在Java虚拟机运作过程中的任意时刻,主要错误如下:

    (A)、InternalError

          Java虚拟机实现的软件或硬件错误都会导致InternalError异常的出现,InternalError是一个典型的异步异常,它可能出现在程序中的任何位置。

    (B)、OutOfMemoryError

          当Java虚拟机实现耗尽了所有虚拟和物理内存,并且内存自动管理子系统无法回收到足够共新对象分配所需的内存空间时,虚拟机将抛出OutOfMemoryError异常。

    (C)、StackOverflowError

          当Java虚拟机实现耗尽了线程全部的栈空间,这种情况经常是由于程序执行时无限制的递归调用而导致的,虚拟机将会抛出StackOverflowError异常。

    (D)、UnknownError

          当某种异常或错误出现,但虚拟机实现无法确定具体实际是哪种异常或错误的时候,将会抛出UnknownError异常。

     

          到这里,我们大体了解JVM字节码指令是什么,有些什么功能了,但是一个方法的JVM指令执行过程是怎么样的呢?这个需要先来了解JVM如何加载Class文件,JVM运行时的数据区是怎么样的,这样才能解释得清楚指令操作的是什么,如前篇文章分析的"getMap"方法,其中"ldc #8"直接将第8项常量"java"字符串加载到操作数栈顶,这个常量"java"字符串存储在哪里,操作数栈又是什么。

         后面我们将分别去了解:JVM运行时数据区、JIT编译--在运行时把Class文件字节码编译成本地机器码的过程、以及JVM垃圾收集相关内容……

     

    【参考资料】

    1、《The Java Virtual Machine Specification》Java SE 8 Edition:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html

    2、《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版 第6章

    3、《The Java Language Specification》Java SE 8 Edition:https://docs.oracle.com/javase/specs/jls/se8/html/index.html

    4、Java前端编译:Java源代码编译成Class文件的过程

    展开全文
  • GCC后端指令生成分析(1)

    千次阅读 2016-10-28 13:56:03
    主要研究GCC指令生成阶段的各个步骤,重点在编译器代码与机器描述之间的接口函数和数据结构。以一小段代码片段为示例,逐步追溯直至汇编代码生成的整个过程。引言机器描述文件是形如*.md的文件,其核心部分是两类定义:...

    主要研究GCC指令生成阶段的各个步骤,重点在编译器代码与机器描述之间的接口函数和数据结构。以一小段代码片段为示例,逐步追溯直至汇编代码生成的整个过程。

    引言

    机器描述文件是形如*.md的文件,其核心部分是两类定义:“define_insn”和“define_expand”。

    md文件中define_insn模版示意图
    图1 md文件中define_insn模版示意图

    define_insn主要分为以下几个部分。

    1. 名字,对应图1中的“addqi3_cc”。
    2. 样式(pattern),对应于图1中的“[(set (reg:CC… … match_dup 2)))]”。规定了RTL指令体中的各种操作以及操作数的位置和操作数必须满足的条件。
    3. 条件,对应于图1中的“ix86_binary_operator_ok (PLUS, QImode, operands)”,指出此样板有效的前提条件。
    4. 输出模板(output template),对应于图1中的“add{b}\t{%2, %0 | %0, %2}”。确定与此样式相匹配的RTL指令的汇编输出形式。可分为三种:单汇编模板、多汇编模板和C代码。前两种给出的就是机器的汇编指令,第三种不能直接给出指令,C代码被编译器吸收并且动态产生汇编模板。
    5. 指令属性,对应于图1中的“[(set attr … … )])”。给出与该样式所匹配的指令的属性。在编译器的编译过程中,编译器主体与机器描述文件相交互,最终生成汇编代码,完成编译器的使命。

    下文中将逐步介绍x86架构下(i386.md文件)后端执行的流程,主要分为三个部分:RTL指令的生成及汇编代码的生成;md文件的分析;编译器后端辅助工具的分析。

    RTL及汇编代码的生成

    相关文件

    GCC编译器从源代码转换到汇编代码的过程,主要经历了三个阶段的中间表示:GIMPLE→RTL→ASM。GIMPLE是比较高层次的语法树;RTL代表指令的rtx表达式;ASM是汇编码。
    前端调用后端的函数接口主要是expand_*系列函数集,这一系列函数的作用是完成从tree到rtl的转换。这些函数按照功能分类在以下一些文件中被实现:stmt.c, calls.c, expr.c, explow.c, expmed.c, function.c, optabs.c, integrate.c。

    1. stmt.c中的expand_*函数的作用是将前端的语句级别上的语法树转换为等义的rtl。因此stmt.c中的这些expand_*函数是被前端parser最先调用的。换句话说,这些函数是“GCC后端”的第1功能层(顶层)。它们会调用exp*.c的一些expand_*函数来真正完成对“表达式”的求值(也即表达式的tree→rtx,并返回rtx),以及调用emit-rtl.c中gen_rtx和gen_reg_rtx等功能函数。
    2. calls.c也是语句级别上的,它将函数调用语句的tree转换为rtl。
    3. function.c将函数一级的tree转换为rtl。
    4. expr.c, explow.c, expmed.c完成对表达式的tree到rtx的转换。这些文件中的expand_*函数基本可以归纳为第2功能层。它这里会将一步引用insn_emit.c文件中的gen_*等功能函数以及optabs.c文件中的expand_*函数。
    5. optabs.c文件中的expand_*函数,作用是将基本的一元操作和二元操作转换为rtx。它属于 expand_*函数集中的第3功能层。它这里接着往下就会引用与平台相关的一些函数模块(从 md 自动生成的一些程序文件,如 insn-output.c、insn-emit.c以及其它 insn-*系列文件)。

    常量的生成

    rtx包含不同的类型,主要有常量、指令等。指令rtx的生成,需要从md文件中读取信息来实现;而常量的生成不需要。常量的生成步骤的追溯过程如下所描述。

    1. 在 GCC 源码中,通过调用 gen_rtx_raw_CONST_INT 来产生一个 rtx 的常量。
    2. 在编译时生成的文件genrtl.h(编译器辅助工具gengenrtl从rtl.def中生成)中,使用了图2中的宏定义。
    3. gen_rtx_fmt_w_stat又通过调用rtx_alloc_stat来分配一个rtx,截止目前为止与机器无关。而gengenrtl工具如何从rtl.def生成genrtl.h的问题,将在下一节讨论。

    这里写图片描述
    图2 生成常量的相关宏定义

    指令的生成

    接下来,研究一个简单的加法指令如何生成。从函数expand_ binop开始追溯分析。md文件中关于加法指令的一个描述入口如图3所示。
    这里写图片描述
    图3 一个加法指令模板

    GCC辅助工具genemit将从这条expand指令生成五个不同模式(mode)的gen_*形式的函数到insn-emit.c文件中,包括gen_addqi3, gen_addhi3, gen_addsi3, gen_adddi3, gen_addti3。genemit逐一读取md文件中的每条记录,根据记录的类型属于insn、expand或者split,分别调用对应的gen_insn、gen_expand或者gen_split来生成insn-emit.c文件中的gen_xxx函数(例如gen_addsi3)。
    考虑生成单精度整数加法的运算指令。按照md文件中对应条目的描述,产生这一RTL指令的函数是gen_addsi3,这是一个在生成GCC时所产生的中间文件中定义的一个函数。使用gdb调试编译器,把断点设置在该函数上,此时函数的调用栈如图4所示。
    这里写图片描述
    图4 执行到gen_addsi3时的调用栈

    这里写图片描述
    图5 expand_binop_directly函数体

    因为我们重点关注表示insn的rtx的生成,所以从调用栈中#3条目,也就是expand_binop_directly函数开始分析。该函数开头的一段代码如图5所示。以函数体中的第2条语句为重点分析对象,该语句通过调用函数find_widening_optab_handler,来获得一个insn_code类型的变量icode的值。这个icode将作为一个重点的索引值,引导最终rtx的生成,它的作用如下。

    1. 作为一个索引值,从数组insn_data中,来获取当前二元操作的两个操作数的模式。
    2. 图3调用栈中#2号函数maybe_gen_insn以icode和操作数的个数、保存操作数的数组为参数,生成代表当前操作的insn作为返回值,赋给变量pat。
    3. 通过emit_insn(pat)把pat发射出去。

    因此,将以icode为线索,追溯insn的具体产生过程。
    首先需要解决的是栈中#4号函数expand_binop中optab类型的参数binoptab是怎么产生的,因为这个binoptab,在expand_binop_directly函数中调用find_widening_optab_handler函数时,用作参数,以生成返回的icode。
    在insn-opinit.h文件中,optab被声明为enum optab_tag类型。这个类型依据optabs.def文件中各种宏的定义,取其名字,作为enum optab_tag中的元素。insn-opinit.h是生成编译器时动态产生的文件,源代码中的genopinit.c文件被编译为genopinit程序,该程序读取optabs.def文件,生成insn-opinit.h。binoptab的值是利用语法树的code作为参数,调用函数optab_for_tree_code (code, type, optab_default)生成的。而语法树的code,则是在文件tree.def中定义的,同样在tree-core.h文件中,与optab类型的定义相似,以宏定义的形式声明tree_code,并用作struct GTY(()) tree_base 中的一个域。整个过程的示意图如图6所示。

    这里写图片描述
    图6 binoptab生成过程示意图

    综上所述,binoptab是在optabs-tree.c文件中,综合了从tree.def和c-common.def中定义的语法树上的操作码和optab.def中定义的操作,建立两种操作之间的关系。以树的操作码为输入,生成optabs.def中定义的对应的操作码。
    接下来研究如何从optabs.def中定义的操作码,查找icode,也就是find_widening_optab_handler的简要执行流程。icode所对应的数据类型insn_code定义在文件insn-codes.h中,该文件是编译GCC的时候生成的,位于$build(生成GCC时的编译目录)之下。在编译GCC时,GCC源代码中的gencodes.c文件被编译成可执行文件gencodes,被放置于$build/gcc/build目录下。gencodes再读取架构特定的机器描述文件(在这里为i386.md),生成insn-codes.h,放置在$build/gcc目录下。
    gencodes产生中间文件的步骤如下。

    1. 通过调用read_md_rtx函数来读取md文件中的条目。
    2. 对于满足下列两个条件的条目,取其名字name组成CODE_FOR_name,作为enum insn_code枚举类型中的一个项。(1) 条目为define_insn或者define_expand型的;(2) 名字不为空,并且名字不以”*”开头的。

    接下来的一个问题是,如何根据optab(本文中对应的是binoptab)的类型以及操作数的机器模式,查找对应的icode。这一步是机器相关部分和机器无关部分的结合点。经过一系列的追溯,更深一层的查找函数为定义在optabs-query.c中的widening_optab_handler (optab op, enum machine_mode to_mode, enum machine_mode from_mode)。三个参数分别为在optabs.def中定义的binop,以及两个操作符的模式。该函数把这三个参数组合一个unsigned类型(4个字节)的数scode,scode的4个字节的组成为:前两个字节用于保存binop,第三个字节保存from_mode,第四个字节保存to_mode。继续追溯,最终是insn-opinit.c文件中的lookup_handler函数实现了根据scode查找icode的功能。该函数以其输入参数scode为索引,在数组pat上进行二分法查找。数组pat的数据类型是一个名为optab_pat的结构,该结构有两个域,一个是类型为数值的scode,另一个就是类型为insn_code的icode。
    struct optab_pat {
    unsigned scode;
    enum insn_code icode;
    };
    static const struct optab_pat pats[NUM_OPTAB_PATTERNS] = {
    { 0x010e0f, CODE_FOR_extendqihi2 },
    { 0x010e10, CODE_FOR_extendqisi2 },
    { 0x010e11, CODE_FOR_extendqidi2 },
    { 0x010f10, CODE_FOR_extendhisi2 },
    { 0x010f11, CODE_FOR_extendhidi2 },
    { 0x011011, CODE_FOR_extendsidi2 },
    ……
    }
    从上面的描述中,可以知道,有两项重要的数据值得被研究。第一项是pats(如上述代码所示),也就是由元素scode和icode所组成的数组。另一项是insn_data,可以用icode为索引在其中查找操作数、操作数的模式等(图5中所示)。pats被定义在生成GCC时编译目录下的文件insn-opinit.c,是由程序genopinit读取md文件生成;而insn_data被定义在同样目录下的insn-output.c文件中,是由genoutput程序读取md文件生成。
    在获得icode之后,用它和其他数据作为参数,调用maybe_gen_insn函数,可以生成insn指令。在函数maybe_gen_insn中,通过以下调用,来生成insn:
    GEN_FCN(icode)(ops[0].value, ops[1].value);
    而宏GEN_FUN在文件insn-opinit.h中,被定义为:
    #define GEN_FUN(CODE)
    (insn_data[code].genfun);
    也就是说,最终是通过使用数组insn_data中code所对应的元素的genfun成员函数来生成insn。在本文的加法例子下,对应的insn_data[code].genfun为gen_addsi3。而gen_addsi3则定义在insn-emit.c中,如前文所述,该文件由程序genemit生成。
    至此,insn的生成流程基本清楚了,如图6所示,但是一些gen***类型的程序需要进一步分析。
    这里写图片描述
    图6 生成insn的概要流程图

    汇编代码的生成

    汇编代码的生成比较简单,主要在final.c文件中处理。文件中的final_scan_insn负责为一个insn生成汇编代码,最主要的操作分为以下两步。

    1. 调用recog_memoized函数,识别insn,并获取其代码编号insn_code_number。获取insn_code_number的方式有两种:一是从insn所保存的信息中获取;另一种是调用recog函数来生成,INSN_CODE (insn) = recog (PATTERN (insn), insn, 0)。recog函数定义在insn-recog.c文件中,与之前提到的insn-*.c形式的文件类似,此文件也是genrecog程序读取机器描述文件(md)生成的,具体内容在下一节中分析。
      2.根据insn_code_number和insn的值,调用get_insn_template,生成包含汇编指令的字符串templ。templ = get_insn_template(insn_code_number, insn),这里所要用到的相关信息主要从insn_data中获得。如前所述,insn_data数据定义在文件insn-output.c中的生成,将在genoutput工具分析一节中详细分析。
    展开全文
  • Nginx源码分析之 upstream指令

    千次阅读 2018-06-16 09:34:23
    Nginx 源码分析 upstream指令 想要的解决问题: 1:upstream存储结构 2:动态 upstream 流程(proxy_pass跟随变量或者域名) 最简单的配置文件 http { upstrem abc { server 1.1.1.1; server 2.2.2.2; } ...

    #Nginx 源码分析 upstream指令

    想要的解决问题:

    1:upstream存储结构
    2:动态 upstream 流程(proxy_pass跟随变量或者域名)

    最简单的配置文件

    http {
        upstrem abc {
            server 1.1.1.1;
            server 2.2.2.2;
        }
        
        upstrem efg {
            server 3.3.3.3;
            server 4.4.4.4;
        }
        
        server {
            listen 80;
    
            location / {
                proxy_pass http://abc;
            }
        }
    }
    

    存储结构

    首先,upstream是复杂指令,里面还需要解析 server 指令;所以其存储形式如下:
    每个upstream指令用ngx_http_upstream_srv_conf_t表示,每个server指令,用ngx_http_upstream_server_t描述。

    ngx_http_upstream_server_t
    ----ngx_http_upstream_srv_conf_t    
    --------ngx_http_upstream_server_t
    ------------1.1.1.1
    ------------2.2.2.2
    
    ngx_http_upstream_server_t
    ----ngx_http_upstream_srv_conf_t    
    --------ngx_http_upstream_server_t
    ------------3.3.3.3
    ------------4.4.4.4
    

    我们来看一下Nginx如何将upstream组织成如上存储结构的:

    首先,每解析一个upstream块时,就往 ngx_http_upstream_main_conf_t 中插入upstreams:每个upstream指令都会往 ngx_http_upstream_main_conf_t->upstreams数组中插入一个对象。每个对象是 ngx_http_upstream_srv_conf_t类型的数据结构。ngx_http_upstream_srv_conf_t对象的名字就是 upstream指令后面跟的名字abc或者efg,方便proxy_pass指令根据名字查找到;

    static char *
    ngx_http_upstream(ngx_conf_t *cf, ngx_command_t *cmd, void *dummy)
    {
        ........
        ngx_http_upstream_srv_conf_t  *uscf;
        /*每解析一个upstream,就生成一个ngx_http_upstream_srv_conf_t对象,当然也会查重*/
        uscf = ngx_http_upstream_add(cf, &u, NGX_HTTP_UPSTREAM_CREATE
                                             |NGX_HTTP_UPSTREAM_WEIGHT
                                             |NGX_HTTP_UPSTREAM_MAX_FAILS
                                             |NGX_HTTP_UPSTREAM_FAIL_TIMEOUT
                                             |NGX_HTTP_UPSTREAM_DOWN
                                             |NGX_HTTP_UPSTREAM_BACKUP);
    
    }
    

    换句话说,每个 ngx_http_upstream_srv_conf_t 挂在 ngx_http_upstream_main_conf_t中的upstreams成员下面。ngx_http_upstream_srv_conf_t用来描述一个upstream块,ngx_http_upstream_main_conf_t->upstreams 用来管理所有 upstream块。

    每个 ngx_http_upstream_srv_conf_t中,有个 servers成员,其类型是ngx_http_upstream_server_t,用来描述一个 server,换句话说,每解析到一个server指令,则会新建一个ngx_http_upstream_server_t对象挂在其所属的upstream的servers字段中。

    至此,解析完upstream以及其server指令后,其存储在内存的结构描述完成;http_proxy 模块是如何找到对应的upsream呢?

    proxy_pass 引用 upstream

    proxy_pass 为非变量

        location / {
            proxy_pass http://abc;
        }
    

    在解析proxy_pass指令时通过ngx_http_upstream_add函数,找到对应的upstream(由ngx_http_upstream_srv_conf_t描述)

        plcf->upstream.upstream = ngx_http_upstream_add(cf, &u, 0);
        if (plcf->upstream.upstream == NULL) {
            return NGX_CONF_ERROR;
        }
    

    由于Nginx可能会先解析到proxy_pass指令,也有可能先解析到upstream指令,所以统一调用ngx_http_upstream_add来创建/查找 upstream,通过flag来控制流程,核心就是谁先解析,就由谁来创建。
    由此可见,Nginx在配置解析阶段,就就确定好了upstream,在滴啊用ngx_http_upstream_init_request时,通过uscf = u->conf->upstream;直接获得这个ngx_http_upstream_srv_conf_tu->conf是在函数ngx_http_proxy_handler时,指向了配置结构的内存)。

    proxy_pass 为 变量

    通常,我们需要根据不同条件来选择不同的upstream,使用变量就方便的多。

        set $ups abc;
        if ( $host = "www.efg.com" ) {
            set $ups efg;
        }
        location / {
            proxy_pass http://$ups;
        }
    

    在解析proxy_pass指令时,发现后面跟随了变量

        url = &value[1];
    
        n = ngx_http_script_variables_count(url);
    
        if (n) {
    
            ngx_memzero(&sc, sizeof(ngx_http_script_compile_t));
    
            sc.cf = cf;
            sc.source = url;
            sc.lengths = &plcf->proxy_lengths;
            sc.values = &plcf->proxy_values;
            sc.variables = n;
            sc.complete_lengths = 1;
            sc.complete_values = 1;
    
            if (ngx_http_script_compile(&sc) != NGX_OK) {
                return NGX_CONF_ERROR;
            }
    
    #if (NGX_HTTP_SSL)
            plcf->ssl = 1;
    #endif
    
            return NGX_CONF_OK;
        }
    

    plcf->proxy_lengthsplcf->proxy_values 被赋上了值。即plcf->upstream.upstream中的upstream字段未被赋值。

    具体被赋值的时间点,是在业务处理流程函数ngx_http_proxy_handlerngx_http_upstream_init_request被处理的。

        if (plcf->proxy_lengths == NULL) {
            //不是变量
            ctx->vars = plcf->vars;
            u->schema = plcf->vars.schema;
    #if (NGX_HTTP_SSL)
            u->ssl = (plcf->upstream.ssl != NULL);
    #endif
    
        } else {
            //变量的情况下走这里
            if (ngx_http_proxy_eval(r, ctx, plcf) != NGX_OK) {
                return NGX_HTTP_INTERNAL_SERVER_ERROR;
            }
        }
    

    所以核心函数是ngx_http_proxy_evalproxy_lengthsproxy_values保存着待处理的变量,该函数就是将其变量变成str。

    static ngx_int_t
    ngx_http_proxy_eval(ngx_http_request_t *r, ngx_http_proxy_ctx_t *ctx,
        ngx_http_proxy_loc_conf_t *plcf)
    {
        ngx_str_t             proxy;
    
        if (ngx_http_script_run(r, &proxy, plcf->proxy_lengths->elts, 0,
                                plcf->proxy_values->elts)
            == NULL)
        {
            return NGX_ERROR;
        }
    
        //proxy.data 就是变量被替换后的字符串,例如此时proxy.data就是字符串"http://efg"
    
        ......
        u->resolved = ngx_pcalloc(r->pool, sizeof(ngx_http_upstream_resolved_t));
        if (u->resolved == NULL) {
            return NGX_ERROR;
        }
        
        if (url.addrs && url.addrs[0].sockaddr) {
            u->resolved->sockaddr = url.addrs[0].sockaddr;
            u->resolved->socklen = url.addrs[0].socklen;
            u->resolved->naddrs = 1;
            u->resolved->host = url.addrs[0].name;
    
        } else {
            //将"efg"赋值给 u->resolved->host
            u->resolved->host = url.host;
        }
    
    
    }
    
    

    此时,还没找真正的upstream,只是将变量翻译成了对应的值。

    static void
    ngx_http_upstream_init_request(ngx_http_request_t *r)
    {
        ......
        if (u->resolved == NULL) {
    
            uscf = u->conf->upstream;
    
        } else {
    
    #if (NGX_HTTP_SSL)
            u->ssl_name = u->resolved->host;
    #endif
    
            host = &u->resolved->host;
    
            //如果proxy_pass 配置为变量,且变量翻译后为一个ip时,走这里
            if (u->resolved->sockaddr) {
    
                if (u->resolved->port == 0
                    && u->resolved->sockaddr->sa_family != AF_UNIX)
                {
                    ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                                  "no port in upstream \"%V\"", host);
                    ngx_http_upstream_finalize_request(r, u,
                                                   NGX_HTTP_INTERNAL_SERVER_ERROR);
                    return;
                }
    
                if (ngx_http_upstream_create_round_robin_peer(r, u->resolved)
                    != NGX_OK)
                {
                    ngx_http_upstream_finalize_request(r, u,
                                                   NGX_HTTP_INTERNAL_SERVER_ERROR);
                    return;
                }
    
                ngx_http_upstream_connect(r, u);
    
                return;
            }
    
            umcf = ngx_http_get_module_main_conf(r, ngx_http_upstream_module);
    
            uscfp = umcf->upstreams.elts;
    
            //如果proxy_pass 配置为变量,且变量翻译后为非IP时走这里
            //根据 u->resolved->host 从 存储结构中找到对应的upstream,找到后,后续的流程就和未配置变量一样。
            for (i = 0; i < umcf->upstreams.nelts; i++) {
    
                uscf = uscfp[i];
    
                if (uscf->host.len == host->len
                    && ((uscf->port == 0 && u->resolved->no_port)
                         || uscf->port == u->resolved->port)
                    && ngx_strncasecmp(uscf->host.data, host->data, host->len) == 0)
                {
                    goto found;
                }
            }
    
    }
    

    proxy_pass 为 域名

    例如

        location / {
            proxy_pass http://www.example.com;
        }
    

    此时,在 proxy_pass 时创建了 upstream对象 ngx_http_upstream_srv_conf_t,但是里面的 servers 字段为空,则会在ngx_http_upstream_init_main_conf时,查询域名对应的dns。这也就是为什么如果proxy_pass 写死域名时,不会动态更新DNS的原因,比较获取DNS的流程是在进程启动阶段,后续就不更新了。

    调用栈如下:

    ngx_inet_resolve_host
    ngx_http_upstream_init_round_robin
    ngx_http_upstream_init_main_conf
    

    proxy_pass 为 ip

    例如

        location / {
            proxy_pass http://127.0.0.1;
        }
    

    在解析 proxy_pass 命令时,调用 ngx_http_upstream_add 创建了 ngx_http_upstream_srv_conf_t 对象,然后在执行ngx_parse_url函数时,解析到了发现参数是ip地址,此时他会创建一个 ngx_http_upstream_server_t 对象,而不是等待 server 指令时创建 ngx_http_upstream_server_t 对象。

    server指令参数为域名

        upstrem abc {
            server www.example.com;
        }
        
        server {
            listen 80;
    
            location / {
                proxy_pass http://abc;
            }
        }
    

    在处理 upsream 的 server 指令时,会调用 ngx_parse_url 处理 server参数,其中会获取域名对应的ip,解析到ip后,其余的处理流程,就和server指令参数是ip的一样了。

    proxy_pass 如何区分参数 是 域名 还是 upstream块的名字?

    例如

        upstrem www.exapmle.com {
            server 127.0.0.1;
        }
        
        server {
            listen 80;
    
            location / {
                proxy_pass http://www.exapmle.com;
            }
        }
    

    Nginx是去 解析www.example.com 的ip然后去访问,还是去127.0.0.1去访问?

    Nginx 逻辑是这样的:
    1:proxy_pass 参数 除了是ip地址以为,其余的全部默认为 upstream 块名字。
    2:proxy_pass 参数 如果是变量,变量解析完成之后,按照1处理。
    3:当 配置解析完成之后,发现 upstream 里面的 server 不存在时,则在ngx_http_upstream_init_round_robin中,进行解析。

    由此可见,域名解析都在配置阶段进行了处理,若是想要通过变量形式动态获取到某个域名,默认情况下,解析域名是行不通的。因为变量处理时在请求处理阶段早就过了配置解析阶段。

    例如下面这个配置,想要访问 "www.efg.com"是不成功的,因为Nginx会去找名为"www.efg.com"的upstream块,但是未找到。

        server {
            listen 80;
    
            set $ups abc;
            if ( $host = "www.efg.com" ) {
                set $ups "www.efg.com";
            }
            location / {
                proxy_pass http://$ups;
            }
        }
    

    想要动态解析域名的方式是 配置 resolver指令。

        resolver 114.114.114.114;
        server {
            listen 80;
    
            set $ups abc;
            if ( $host = "www.efg.com" ) {
                set $ups "www.efg.com";
            }
            location / {
                proxy_pass http://$ups;
            }
        }
    

    由此可见,Nginx代码真烂

    展开全文
  • 本篇为《JVM指令分析实例》的第四篇,相关实例均使用Oracle JDK 1.8编译,并使用javap生成字节码指令清单。 前几篇传送门: JVM指令分析实例一(常量、局部变量、for循环) JVM指令分析实例二(算术运算、常量池、...
  • JVM字节码分析--invoke指令一、方法调用的5个字节码指令二、方法的静态绑定与动态绑定三、invokestatic、invokevirtual 、invokespecial 、invokeinterface 和 invokedynamic 指令使用场景 一、方法调用的5个字节码...
  • RISC-V处理器:1.取指令 RTL 代码分析

    千次阅读 2019-03-08 21:51:25
    指令 RTL 代码分析 1. 取指特点 指令在存储空间中所处的地址,称为它的指令PC(Program Counter) 取指是指处理器将指令,按照其指令PC,从存储器中读取出来的过程 处理器从存储器中取出指令的目标是:快速和连续...
  • 指令格式基本定义基本格式零地址指令一地址指令二地址指令三地址指令四地址指令操作码分类定长操作码扩展操作码(不定长操作码)操作类型寻址方式指令寻址数据寻址立即寻址间接寻址寄存器寻址寄存器间接寻址寄存器...
  • 指令系统

    千次阅读 2015-04-24 10:33:51
    一种是控制流,由它来控制数据信息的处理。 一、指令系统的发展   计算机的程序是由一系列的指令组成的,指令就是要计算机执行某种操作的命令。  从计算机组成的层次结构来说,计算机的指令有微指令、机器...
  • 在程序运行时,为了提高执行性能,编译器和处理器会对指令进行重排序,JMM为了保证在不同的编译器和CPU上有相同的结果,通过插入特定类型的内存屏障来禁止特定类型的编译器重排序和处理器重排序,插入一条内存屏障会...
  • 指令系统,寻址方式

    千次阅读 多人点赞 2018-12-23 19:01:52
    如果要在计算机上执行,必须要利用编译程序或者是汇编程序把高级语言编写的程序,指令,或者是汇编指令变成由0,1代码组成的机器指令,才能够在计算机中由计算机的硬件按序进行执行。 机器指令 机器指令;计算机系统...
  • Lua源码分析 -- 虚拟机以及指令解释

    千次阅读 2018-05-28 16:55:37
    对于每一个函数,Lua的编译器将创建一个原型(prototype),它由一组指令及其使用到的常量组成[1].最初的Lua虚拟机是基于栈的.到1993年,Lua5.0版本,采用了基于寄存器的虚拟机,使得Lua的解释效率得到提升, 1、指令系统与...
  • 一、指令系统的发展 1、引 (1)、冯氏结构 ①、存储程序 ②、程序控制 (2)、计算机指令是由一系列机器指令组成的 ①、指令是计算机执行某种操作的命令 ②、每个指令的执行过程靠硬件实现 (3)、指令是软件和硬件的分...
  • 汇编指令翻译器

    千次阅读 2017-04-07 17:22:51
    同时,需要检测汇编程序中的错误,并进行统计。  整个翻译器包括获取存储单词,指令检测,翻译和统计三个部分。如下图  1,单词获取及存储  首先,需要从给定的程序文本文件中,获取单词,包括宏定义、全局变量、...
  • GCC源码分析(五)——指令生成

    万次阅读 多人点赞 2012-12-02 00:07:04
    一、前言  又有好久没写了,的确很忙。前篇介绍了GCC的pass格局,它是GCC中间语言部分的核心架构,也是贯穿整个编译流程的核心。在完成优化处理之后,GCC...其中的RTL就是和指令紧密相关的一种结构,它是指令生成的起
  • Angular深入理解之指令

    千次阅读 2018-02-24 17:31:02
    Angular深入理解之指令 指令有什么功能 Attribute directives 属性指令 Structural directives 结构指令 自定义属性指令 自定义结构指令 Angular深入理解之指令 对于初学Angular的同学来说,指令无疑是最...
  • 计组—指令及寻址

    千次阅读 2018-11-27 16:40:34
    指令 程序是由一系列指令组成的,指令是要计算机执行某种操作的命令。包括:微指令、宏指令、机器指令。 微指令:微程序级指令,属于硬件 宏指令:由若干条机器指令组成的软件指令,属于软件 机器指令:介于微...
  • 目录控制转移类指令无条件转移指令JMP段内转移、直接寻址段内转移、间接寻址条件转移指令判断单个标志位状态比较无符号数高低比较有符号数大小循环指令(loop)子程序指令中断指令系统功能调用字符输出的功能调用字符...
  • MIPS cache指令

    千次阅读 2016-03-25 16:33:43
    MIPS cache指令指令编码: 31...26 25...21 20...16 15......0 CACHE(101111) base op offset 格式: cache op,offset(base) 执行op制定的cace操作,16位的offset经符号位扩展,添加到base寄存器以...
  • 苹果iOS捷径(快捷指令)自动填写表单

    万次阅读 多人点赞 2020-10-06 11:27:33
    文章主要介绍了如何通过捷径(快捷指令)调用 JavaScript 在网页运行的方式快速填写表单。
  • 计算机组成原理(4.3)—— MIPS指令系统(RSIC)

    千次阅读 多人点赞 2020-08-16 22:03:24
    前一篇文章分析指令系统(ISA)的设计方法,这里以MIPS指令系统为例进行分析 前文链接:计算机组成原理(4.1)—— 指令系统设计 文章目录一、MIPS架构基础1. 寄存器数据指定(1)MIPS架构中的寄存器安排(2)...
  • 语音指令

    千次阅读 2020-11-16 22:07:23
    前言:该技术会用到特定于移动设备的API,Microsoft Phone 已经被微软...语音指令是通过一种名为VoiceCommandDefinition(VCD)文件来定义,当应用程序运行后通过相关API进行安装注册。VCD文件安装成功后,开发者为应.
  • 下文开始具体分析BACNET协议中几种基本的指令结构,本篇是分析最基本的WHO IS指令。WHO IS指令是BACNET主机向BACNET设备询问基本信息的指令。 由于BACNET/IP协议是封装在UDP协议当中,因此整个数据包的从低层到...
  • 8086汇编指令全集

    万次阅读 多人点赞 2016-04-07 15:08:39
    8086汇编指令全集 学习汇编语言,最关键的就在于汇编指令集的掌握以及计算机工作方式的理解,以下是80X86汇编过程中经常用到的一些汇编指令。 从功能分类上来说,一共可分为 一、 数据传送指令:MOV、XCHG、LEA、...
  • mips指令

    千次阅读 2015-06-24 13:00:40
    MIPS指令特点: 1、所有指令都是32位编码; 2、有些指令有26位供目标地址编码;有些则只有16位。因此要想加载任何一个32位值,就得用两...4、有32个通用寄存器,每个寄存器32位(32位机)或64位(64位机); 5、
  • ARM指令集详解

    万次阅读 多人点赞 2018-07-18 23:48:27
    ARM指令集详解 1. 汇编 1.1. 通用寄存器   通用寄存器 37个寄存器,31个通用寄存器,6个状态寄存器,R13堆栈指针sp,R14返回指针,R15为PC指针, cpsr_c代表的是这32位中的低8位,也就是控制位 CPSR有4个...
  • 分析指令对指令的操作码部分进行阶码,分析这条指令要完成什么功能,是指令集中哪一条指令 执行指令,CPU的控制器发出各种操作命令,由这些操作命令控制相应的部件去完成指令要求的操作,这些操作命令具有...
  • Linux指令大全

    千次阅读 2020-06-27 17:26:57
    基础指令,网络应用指令与系统管理指令

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 129,582
精华内容 51,832
关键字:

对指令进行分析确定指令类型