精华内容
下载资源
问答
  • Class文件格式实战:使用ASM动态生成class文件

    万次阅读 多人点赞 2014-04-05 13:24:09
    本专栏前面的文章,主要详细讲解了Class文件格式,并且在上一篇文章中做了总结。 众所周知, JVM在运行时, 加载并执行class文件, 这个class文件基本上都是由我们所写的java源文件通过javac编译而得到的。 但是,...


    概述


    本专栏前面的文章,主要详细讲解了Class文件的格式,并且在上一篇文章中做了总结。 众所周知, JVM在运行时, 加载并执行class文件, 这个class文件基本上都是由我们所写的java源文件通过javac编译而得到的。 但是, 我们有时候会遇到这种情况:在前期(编写程序时)不知道要写什么类, 只有到运行时, 才能根据当时的程序执行状态知道要使用什么类。 举一个常见的例子就是JDK中的动态代理。这个代理能够使用一套API代理所有的符合要求的类, 那么这个代理就不可能在JDK编写的时候写出来, 因为当时还不知道用户要代理什么类。 


    当遇到上述情况时, 就要考虑这种机制:在运行时动态生成class文件。 也就是说, 这个class文件已经不是由你的Java源码编译而来,而是由程序动态生成。 能够做这件事的,有JDK中的动态代理API, 还有一个叫做cglib的开源库。 这两个库都是偏重于动态代理的, 也就是以动态生成class的方式来支持代理的动态创建。 除此之外, 还有一个叫做ASM的库, 能够直接生成class文件,它的api对于动态代理的API来说更加原生, 每个api都和class文件格式中的特定部分相吻合, 也就是说, 如果对class文件的格式比较熟练, 使用这套API就会相对简单。 下面我们通过一个实例来讲解ASM的使用, 并且在使用的过程中, 会对应class文件中的各个部分来说明。


    ASM示例:HelloWorld


    ASM的实现基于一套Java API, 所以我们首先得到ASM库, 在这个我使用的是ASM 4.0的jar包 。 


    首先以ASM中的HelloWorld实例来讲解, 比如我们要生成以下代码对应的class文件:

    public class Example {
    
    	public static void main (String[] args) {
    		System.out.println("Hello world!");
    }

    但是这个class文件不能在开发时通过上面的源码来编译成, 而是要动态生成。 下面我们介绍如何使用ASM动态生成上述源码对应的字节码。


    下面是代码示例(该实例来自于ASM官方的sample):

    import java.io.FileOutputStream;
    
    import org.objectweb.asm.ClassWriter;
    import org.objectweb.asm.MethodVisitor;
    import org.objectweb.asm.Opcodes;
    
    public class Helloworld extends ClassLoader implements Opcodes {
    
        public static void main(final String args[]) throws Exception {
    
    
            //定义一个叫做Example的类
            ClassWriter cw = new ClassWriter(0);
            cw.visit(V1_1, ACC_PUBLIC, "Example", null, "java/lang/Object", null);
    
            //生成默认的构造方法
            MethodVisitor mw = cw.visitMethod(ACC_PUBLIC,
                    "<init>",
                    "()V",
                    null,
                    null);
    
            //生成构造方法的字节码指令
            mw.visitVarInsn(ALOAD, 0);
            mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
            mw.visitInsn(RETURN);
            mw.visitMaxs(1, 1);
            mw.visitEnd();
    
            //生成main方法
            mw = cw.visitMethod(ACC_PUBLIC + ACC_STATIC,
                    "main",
                    "([Ljava/lang/String;)V",
                    null,
                    null);
    
            //生成main方法中的字节码指令
            mw.visitFieldInsn(GETSTATIC,
                    "java/lang/System",
                    "out",
                    "Ljava/io/PrintStream;");
    
            mw.visitLdcInsn("Hello world!");
            mw.visitMethodInsn(INVOKEVIRTUAL,
                    "java/io/PrintStream",
                    "println",
                    "(Ljava/lang/String;)V");
            mw.visitInsn(RETURN);
            mw.visitMaxs(2, 2);
    
            //字节码生成完成
            mw.visitEnd();
    
            // 获取生成的class文件对应的二进制流
            byte[] code = cw.toByteArray();
    
    
            //将二进制流写到本地磁盘上
            FileOutputStream fos = new FileOutputStream("Example.class");
            fos.write(code);
            fos.close();
    
            //直接将二进制流加载到内存中
            Helloworld loader = new Helloworld();
            Class<?> exampleClass = loader.defineClass("Example", code, 0, code.length);
    
            //通过反射调用main方法
            exampleClass.getMethods()[0].invoke(null, new Object[] { null });
    
            
        }
    }
    

    下面详细介绍生成class的过程:


    1 首先定义一个类


    相关代码片段如下:

            //定义一个叫做Example的类
            ClassWriter cw = new ClassWriter(0);
            cw.visit(V1_1, ACC_PUBLIC, "Example", null, "java/lang/Object", null);

    ClassWriter类是ASM中的核心API , 用于生成一个类的字节码。 ClassWriter的visit方法定义一个类。 

    第一个参数V1_1是生成的class的版本号, 对应class文件中的主版本号和次版本号, 即minor_version和major_version 。 

    第二个参数ACC_PUBLIC表示该类的访问标识。这是一个public的类。 对应class文件中的access_flags 。

    第三个参数是生成的类的类名。 需要注意,这里是类的全限定名。 如果生成的class带有包名, 如com.jg.zhang.Example, 那么这里传入的参数必须是com/jg/zhang/Example  。对应class文件中的this_class  。

    第四个参数是和泛型相关的, 这里我们不关新, 传入null表示这不是一个泛型类。这个参数对应class文件中的Signature属性(attribute) 。
     
    第五个参数是当前类的父类的全限定名。 该类直接继承Object。 这个参数对应class文件中的super_class 。 

    第六个参数是String[]类型的, 传入当前要生成的类的直接实现的接口。 这里这个类没实现任何接口, 所以传入null 。 这个参数对应class文件中的interfaces 。 


    2 定义默认构造方法, 并生成默认构造方法的字节码指令 


    相关代码片段如下:
            //生成默认的构造方法
            MethodVisitor mw = cw.visitMethod(ACC_PUBLIC,
                    "<init>",
                    "()V",
                    null,
                    null);
    
            //生成构造方法的字节码指令
            mw.visitVarInsn(ALOAD, 0);
            mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
            mw.visitInsn(RETURN);
            mw.visitMaxs(1, 1);
            mw.visitEnd();


    使用上面创建的ClassWriter对象, 调用该对象的visitMethod方法, 得到一个MethodVisitor对象, 这个对象定义一个方法。 对应class文件中的一个method_info 。 


    第一个参数是 ACC_PUBLIC , 指定要生成的方法的访问标志。 这个参数对应method_info 中的access_flags 。 

    第二个参数是方法的方法名。 对于构造方法来说, 方法名为<init> 。 这个参数对应method_info 中的name_index , name_index引用常量池中的方法名字符串。 

    第三个参数是方法描述符, 在这里要生成的构造方法无参数, 无返回值, 所以方法描述符为 ()V  。 这个参数对应method_info 中的descriptor_index 。 

    第四个参数是和泛型相关的, 这里传入null表示该方法不是泛型方法。这个参数对应method_info 中的Signature属性。

    第五个参数指定方法声明可能抛出的异常。 这里无异常声明抛出, 传入null 。 这个参数对应method_info 中的Exceptions属性。

    接下来调用MethodVisitor中的多个方法, 生成当前构造方法的字节码。 对应method_info 中的Code属性。

    1 调用visitVarInsn方法,生成aload指令, 将第0个本地变量(也就是this)压入操作数栈。

    2 调用visitMethodInsn方法, 生成invokespecial指令, 调用父类(也就是Object)的构造方法。

    3 调用visitInsn方法,生成return指令, 方法返回。 

    4 调用visitMaxs方法, 指定当前要生成的方法的最大局部变量和最大操作数栈。 对应Code属性中的max_stack和max_locals 。 

    5 最后调用visitEnd方法, 表示当前要生成的构造方法已经创建完成。 


    3 定义main方法, 并生成main方法中的字节码指令


    对应的代码片段如下:
            mw = cw.visitMethod(ACC_PUBLIC + ACC_STATIC,
                    "main",
                    "([Ljava/lang/String;)V",
                    null,
                    null);
    
            //生成main方法中的字节码指令
            mw.visitFieldInsn(GETSTATIC,
                    "java/lang/System",
                    "out",
                    "Ljava/io/PrintStream;");
    
            mw.visitLdcInsn("Hello world!");
            mw.visitMethodInsn(INVOKEVIRTUAL,
                    "java/io/PrintStream",
                    "println",
                    "(Ljava/lang/String;)V");
            mw.visitInsn(RETURN);
            mw.visitMaxs(2, 2);
            mw.visitEnd();

    这个过程和上面的生成默认构造方法的过程是一致的。 读者可对比上一步执行分析。


    4 生成class数据, 保存到磁盘中, 加载class数据


    对应代码片段如下:
            // 获取生成的class文件对应的二进制流
            byte[] code = cw.toByteArray();
    
    
            //将二进制流写到本地磁盘上
            FileOutputStream fos = new FileOutputStream("Example.class");
            fos.write(code);
            fos.close();
    
            //直接将二进制流加载到内存中
            Helloworld loader = new Helloworld();
            Class<?> exampleClass = loader.defineClass("Example", code, 0, code.length);
    
            //通过反射调用main方法
            exampleClass.getMethods()[0].invoke(null, new Object[] { null });

    这段代码首先获取生成的class文件的字节流, 把它写在本地磁盘的Example.class文件中。 然后加载class字节流, 并通过反射调用main方法。

    这段代码执行完, 可以看到控制台有以下输出:
    Hello world!

    然后在当前测试工程的根目录下, 生成一个Example.class文件文件。



    下面我们使用javap反编译这个class文件:
    javap -c -v -classpath . -private Example

    输出的完整信息如下:
    Classfile /C:/Users/纪刚/Desktop/生成字节码/AsmJavaTest/Example.class
      Last modified 2014-4-5; size 338 bytes
      MD5 checksum 281abde0e2012db8ad462279a1fbb6a4
    public class Example
      minor version: 3
      major version: 45
      flags: ACC_PUBLIC
    Constant pool:
       #1 = Utf8               Example
       #2 = Class              #1             //  Example
       #3 = Utf8               java/lang/Object
       #4 = Class              #3             //  java/lang/Object
       #5 = Utf8               <init>
       #6 = Utf8               ()V
       #7 = NameAndType        #5:#6          //  "<init>":()V
       #8 = Methodref          #4.#7          //  java/lang/Object."<init>":()V
       #9 = Utf8               main
      #10 = Utf8               ([Ljava/lang/String;)V
      #11 = Utf8               java/lang/System
      #12 = Class              #11            //  java/lang/System
      #13 = Utf8               out
      #14 = Utf8               Ljava/io/PrintStream;
      #15 = NameAndType        #13:#14        //  out:Ljava/io/PrintStream;
      #16 = Fieldref           #12.#15        //  java/lang/System.out:Ljava/io/PrintStream;
      #17 = Utf8               Hello world!
      #18 = String             #17            //  Hello world!
      #19 = Utf8               java/io/PrintStream
      #20 = Class              #19            //  java/io/PrintStream
      #21 = Utf8               println
      #22 = Utf8               (Ljava/lang/String;)V
      #23 = NameAndType        #21:#22        //  println:(Ljava/lang/String;)V
      #24 = Methodref          #20.#23        //  java/io/PrintStream.println:(Ljava/lang/String;)V
      #25 = Utf8               Code
    {
      public Example();
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #8                  // Method java/lang/Object."<init>":()V
             4: return
    
      public static void main(java.lang.String[]);
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=2, args_size=1
             0: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
             3: ldc           #18                 // String Hello world!
             5: invokevirtual #24                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
             8: return
    }

    正是一个标准的class格式的文件, 它和以下源码是对应的:

    public class Example {
    
    	public static void main (String[] args) {
    		System.out.println("Hello world!");
    }

    只是, 上面的class文件不是由这段源代码生成的, 而是使用ASM动态创建的。 



    ASM示例二: 生成字段, 并给字段加注解


    上面的HelloWorld示例演示了如何生成类和方法, 该示例演示如何生成字段, 并给字段加注解。 

    public class BeanTest extends ClassLoader implements Opcodes {
    
    	/*
    	 * 生成以下类的字节码
    	 * 
    	 * public class Person {
    	 * 
    	 * 		@NotNull
    	 * 		public String name;
    	 * 
    	 * }
    	 */
    
    	public static void main(String[] args) throws Exception {
    		
    		/********************************class***********************************************/
    
    		// 创建一个ClassWriter, 以生成一个新的类
    
    		ClassWriter cw = new ClassWriter(0);
    		cw.visit(V1_6, ACC_PUBLIC, "com/pansoft/espdb/bean/Person", null, "java/lang/Object", null);
    		
    		
    		
    		/*********************************constructor**********************************************/
    		
    		MethodVisitor mw = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null,
    				null);
    		mw.visitVarInsn(ALOAD, 0);
    		mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
    		mw.visitInsn(RETURN);
    		mw.visitMaxs(1, 1);
    		mw.visitEnd();
    		
    		
    		/*************************************field******************************************/
    	
    		//生成String name字段
    		FieldVisitor  fv = cw.visitField(ACC_PUBLIC, "name", "Ljava/lang/String;", null, null);
    		AnnotationVisitor  av = fv.visitAnnotation("LNotNull;", true);
    		av.visit("value", "abc");
    		av.visitEnd();
    		fv.visitEnd();
    
    		
    		
    		/***********************************generate and load********************************************/
    		
    		byte[] code = cw.toByteArray();
    		
    		BeanTest loader = new BeanTest();
    		Class<?> clazz = loader.defineClass(null, code, 0, code.length);
    		
    		
    		/***********************************test********************************************/
    		
    		Object beanObj = clazz.getConstructor().newInstance();
    		
    		clazz.getField("name").set(beanObj, "zhangjg");
    		
    		String nameString = (String) clazz.getField("name").get(beanObj);
    		System.out.println("filed value : " + nameString);
    		
    		String annoVal = clazz.getField("name").getAnnotation(NotNull.class).value();
    		System.out.println("annotation value: " + annoVal);
    		
    	}
    }

    上面代码是完整的代码, 用于生成一个和以下代码相对应的class:
        public class Person {
        
             @NotNull
             public String name;
        
        }

    生成类和构造方法的部分就略过了, 和上面的示例是一样的。 下面看看字段和字段的注解是如何生成的。 相关逻辑如下:

    		FieldVisitor  fv = cw.visitField(ACC_PUBLIC, "name", "Ljava/lang/String;", null, null);
    		AnnotationVisitor  av = fv.visitAnnotation("LNotNull;", true);
    		av.visit("value", "abc");
    		av.visitEnd();
    		fv.visitEnd();

    ClassWriter的visitField方法, 用于定义一个字段。 对应class文件中的一个filed_info 。 

    第一个参数是字段的访问修饰符, 这里传入ACC_PUBLIC表示是一个public的属性。 这个参数和filed_info 中的access_flags相对应。

    第二个参数是字段的字段名。 这个参数和filed_info 中的name_index相对应。

    第三个参数是字段的描述符, 这个字段是String类型的,它的字段描述符为 "Ljava/lang/String;" 。 这个参数和filed_info 中的descriptor_index相对应。

    第四个参数和泛型相关的, 这里传入null, 表示该字段不是泛型的。 这个参数和filed_info 中的Signature属性相对应。

    第五个参数是字段的值, 只适用于静态字段,当前要生成的字段不是静态的, 所以传入null 。 这个参数和filed_info 中的ConstantValue属性相对应。

    使用visitField方法定义完当前字段, 返回一个FieldVisitor对象。 下面调用这个对象的visitAnnotation方法, 为该字段生成注解信息。 visitAnnotation的两个参数如下:

    第一个参数是要生成的注解的描述符, 传入"LNotNull;" 。

    第二个参数表示该注解是否运行时可见。 如果传入true, 表示运行时可见, 这个注解信息就会生成filed_info 中的一个RuntimeVisibleAnnotation属性。 传入false, 表示运行时不可见,个注解信息就会生成filed_info 中的一个RuntimeInvisibleAnnotation属性 。 

    接下来调用上一步返回的AnnotationVisitor对象的visit方法, 来生成注解的值信息。 



    ClassWriter的其他重要方法


    ClassWriter中还有其他一些重要方法, 这些方法能够生成class文件中的所有相关信息。 这些方法, 以及对象生成class文件中的什么信息, 都列在下面:

        //定义一个类
        public void visit(
            int version,
            int access,
            String name,
            String signature,
            String superName,
            String[] interfaces)
    
    
        //定义源文件相关的信息,对应class文件中的Source属性
        public void visitSource(String source, String debug)
    
        //以下两个方法定义内部类和外部类相关的信息, 对应class文件中的InnerClasses属性
        public void visitOuterClass(String owner, String name, String desc) 
    
        public void visitInnerClass(
            String name,
            String outerName,
            String innerName,
            int access)
    
    
        //定义class文件中的注解信息, 对应class文件中的RuntimeVisibleAnnotations属性或者RuntimeInvisibleAnnotations属性
        public AnnotationVisitor visitAnnotation(String desc, boolean visible)
    
        //定义其他非标准属性
        public void visitAttribute(Attribute attr)
    
    
    
        //定义一个字段, 返回的FieldVisitor用于生成字段相关的信息
        public FieldVisitor visitField(
            int access,
            String name,
            String desc,
            String signature,
            Object value)
    
    
        //定义一个方法, 返回的MethodVisitor用于生成方法相关的信息
        public MethodVisitor visitMethod(
            int access,
            String name,
            String desc,
            String signature,
            String[] exceptions)

    每个方法都是和class文件中的某部分数据相对应的, 如果对class文件的格式比较熟悉的话, 使用ASM生成一个简单的类, 还是很容易的。


    总结


    在本文中, 通过使用开源的ASM库, 动态生成了两个类。 通过讲解这两个类的生成过程, 可以加深对class文件格式的理解。 因为ASM库中的每个API都是对应class文件中的某部分信息的。 如果对class文件格式不熟悉, 可以参考本专栏之前的讲解class文件格式的一系列博客。 

    本文使用的两个示例都放在了一个单独的, 可直接运行的工程中, 该工程已经上传到我的百度网盘, 这个工程的lib目录中, 有ASM 4.0的jar包。 和该工程一起打包的, 还有ASM 4.0的源码和示例程序。 

    上述资源下载地址: http://pan.baidu.com/s/1dDmq84T 




    更多关于深入理解Java的文章, 请关注我的专栏 : http://blog.csdn.net/column/details/zhangjg-java-blog.html

    更多关于Java和Android等其他技术的文章, 请关注我的博客: http://blog.csdn.net/zhangjg_blog



    展开全文
  •   在Linux下,可执行文件/动态库文件/目标文件(可重定向文件)都是同一种文件格式,我们把它称之为ELF文件格式。   虽然它们三个都是ELF文件格式但都各有不同:   可执行文件没有section header table 。 ...

    ELF文件格式

      在Linux下,可执行文件/动态库文件/目标文件(可重定向文件)都是同一种文件格式,我们把它称之为ELF文件格式。
      虽然它们三个都是ELF文件格式但都各有不同:
      可执行文件没有section header table 。
      目标文件没有program header table。
      动态库文件俩个 header table 都有,因为链接器在链接的时候需要section header table 来查看目标文件各个 section 的信息然后对各个目标文件进行链接,而加载器在加载可执行程序的时候需要program header table ,它需要根据这个表把相应的段加载到相应的虚拟内存(虚拟地址空间)中。
    这里写图片描述
      这个图中俩个header表的位置在实际中不一定这么放,只是为了好看这么画的。

    目标文件的ELF格式组成

    max.s 文件下述讨论的列子

    .section .data
    data_items: #These are the data items
    .long 3,67,34,222,45,75,54,34,44,33,22,11,66,0
    .section .text
    .globl _start
    _start:
    movl $0, %edi # move 0 into the index register
    movl data_items(,%edi,4), %eax # load the first byte of
    data
    movl %eax, %ebx # since this is the first item,
    %eax is
    # the biggest
    start_loop: # start loop
    cmpl $0, %eax # check to see if we've hit the
    end
    je loop_exit
    incl %edi # load next value
    movl data_items(,%edi,4), %eax
    cmpl %ebx, %eax # compare values
    jle start_loop 
    movl %eax, %ebx # move the value as the largest
    jmp start_loop # jump to loop beginning
    loop_exit:
    movl $1, %eax #1 is the _exit() syscall
    int $0x80
    找一个最大值,把它当中main函数的返回值

    1.ELF header

    这里写图片描述
    从上面我们可以看到该ELF文件的大概属性,是什么操作系统,还有是什么处理器架构。从 start of section headers 可以看出section 头表在目标文件的起始地址,从 size of section headers 可以看出来目标文件中每一个section的大小,接着这个字段的下面的字段说了该目标文件共有多少 section。再接着的字段说了,section header table 在目标文件的相对位置。
    program headers 地址为0,说明没有那么表明这个ELF文件是个 .o文件。

    2.section header table

    这里写图片描述

      这个是 section header table ,里面详细说明了各个节的信息。我们可以看到 data , text这俩个节都是在汇编中已经有的,其他的6个节都是汇编器加的。addr那一列表明了这些节在虚拟地址空间的位置全为0,没问题因为现在还没被链接,off/size 表明了相应节在ELF文件(.o) 中的位置和长度(大小)。data段 大小 0x38, 没问题的在汇编文件中定义了14个全局变量,刚好56字节对应0x38.
    bss 段大小为0,也没问题的,bss段记录未初始化的全局变量,我们没有定义。

    这里写图片描述
    根据ELF Header 的信息可以画出各个节在ELF文件布局。上面显示了节头表中共有8个 section Header , 每个section Header 记录了相应 section 对应的信息 , 与每个节之后被加载到内存中的所对应的权限。

    3.strtab/shstrtab

    接下来用hexdump把目标文件的内容打印出来。
    这里写图片描述
    这里写图片描述
    对照着上面的图,可以看到0x98 shstrtab(section header str table) 对应的内容都是每个section 名字的ASCII码形式。
    strtab 对应文件地址的0x288中,这个表中保存的是我们 .c 文件中全局变量/函数/源文件的名字。

    4.rel.text / symtab

    这里写图片描述

    rel.text

    ret.text 节记录了该目标文件中,有那些位置需要链接器进行重定向。

    symbol table

    符号表中记录了,每一个全局符号(即源文件中的函数名/全局变量名)所处于那个section。并且显示出了每一个符号的bind 绑定属性,这个属性在链接器链接的时候有用,local属性的符号立马进行解析绑定,而GLOBAL的却不会。直到遇到一个强状态的GLOBAL属性的符号才会进行绑定,具体是否是强状态,看该全局变量是否是已初始化过的。

    可执行文件的ELF格式

    这里写图片描述
    这里写图片描述
    把上面的 max.o 文件进行链接生成一个可执行程序后,我们再用readelf 读取它的信息如上图所示。
    多了俩个 program headers ,少了俩个 section header。
    少了俩个节头,那俩个一个是bss一个是rel.text , rel.text 在链接的时候被使用完就丢弃了, bss 没使用也被丢弃了。
    多了俩个 program headers ,如上每一个program Header记录一个数据段,共俩个在这个列子中。一个数据段从 0x08048000 开始 , 属性只读/可执行/对齐数4k。另一个大家自己看。

    根据上面的信息我画出来了 可执行文件的格式图

    这里写图片描述
    这里写图片描述
    最后加载器根据, program Header 中的信息,把相应段加载到了虚拟地址空间对应的物理内存中。在目标文件和可执行文件中,可以看出来目标文件中所有的符号地址在链接的过程中都被替换成了确切的虚拟地址。

    总结下各个Header 的意义

    Section Header table :保存了各个 Section Header的描述信息,在目标文件/可执行文件的所述位置与权限信息。
    text :记录了目标文件/可执行文件的代码(二进制指令)
    data :记录了目标文件/可执行文件的全局变量的数据
    shstrtable : 记录了所有Section的名字(以文本方式记录)
    strtable : 记录了全局变量/函数的名字/源文件的名字(以文本形式)
    symbol table : 记录了各个全局符号(变量名/函数名)所属那个Section.
    rel.text.table :记录了链接时需要的重定向信息(即把相应的代码/数据链接到何处.
    bss : 记录了所属bss段的大小,并没有记录真实的数据。(bss段为未初始化全局变量的数据段)
    ELF Header : 记录了 ELF文件的一些属性信息。
    program Header table : 记录了 program Header 的信息。
    program Header :记录了把那些Section合并成了同一个 Segment,它被用于加载器,加载到内存时使用。
    所有的 Section 的Header都被记录到了 Section Header table 中。(Section 与SectionHeader 俩个东西)

    段中的虚拟地址

    加载首先分为三种方式,第一种绝对加载,第二种可重定位加载,第三种动态运行时加载。
    第一种方式,提供给加载器的可执行文件的所有地址都是物理地址,这种方式很多缺点,首先程序员得知道分配到那个物理地址处,其次一个地方修改,所有地方都要修改。
    第二种方式,可执行文件中的地址都是从0开始的,当被加载到物理内存中,对起每个地址 加上起始物理内存的地址即可。缺点不方便换入换出,无法使用虚拟内存。
    第三种方式,动态运行时加载,即所有内存都写成虚拟地址,当确切的要访问那个虚拟地址的时候,由MMU和页表对其解释成真正的物理地址,方便换入换出,可见在我的Linux下使用的是,动态运行时加载。(换入/换出指的是 页面置换,这个是虚存机制,为了能使同一时间执行更多的进程,加入的一个概念,把阻塞态的进程的内存页中的信息写到硬盘上,然后等需要加载时,再加载到内存中,在linux中 swap区就是用来页面置换的)

    静态链接

    静态链接就是,在链接阶段,让链接编辑器把目标文件所用到的函数/符号在静态库文件中找到相应的 .o 文件进行链接。(链接器一般都是先找是否有 相应名的动态库文件,没有才链接 静态库文件的这点需要注意)
    静态库文件其实就是对一堆 . o 文件的一种打包。

    生成静态库指令

    ar -rc libxxx.a xxx.o xxx.o

    使用静态库文件

    gcc -l xxx -L ./ -I xxx.h main.c -o a.out -static
    -l(小写L) 指定库名字 -L 指定库路径 -I 指定头文件

    动态链接

    动态链接,就是在运行的时候,当使用到动态库中的函数了,由运行时链接器把相应的动态库加载到地址空间的共享区中。在链接阶段,可执行文件并不链接动态库的内容。

    gcc   -shared -fPIC  *.c  -o  XXXlib.so  生成动态库文件(使用gcc 直接使源文件编译生成elf格式的动态库文件)
    gcc  -lxxx -L ./  -I XXX.h  -o a.out 使用动态库文件

    我们用ldd 指令去模拟运行可执行程序,会发现动态库 not found,那是因为动态库链接器找不到相应的动态库。动态链接器的搜索路径:
    1. 首先去 环境 变量LD_LIBRARY_PATH 中查找
    2. 然后没有去,/etc/ld.so.cache 动态链接器的缓存中文件查找,这个文件是由 /etc/ld.so.conf生成。如果我们要给动态链接器的配置文件加路径,直接把路径写到文件后, 再 ldconfig -v 即可。
    3. 如果还没有,去/usr/lib 或 /lib 目录下查找,在64位平台下 还有 /usr/lib64 /lib64 这俩个目录。我们可以直接把动态库添加到这个目录

    动态链接的过程(动态链接器)

    首先第一次加载动态库的时候,jmp 指令会跳到该相应的符号段,由于动态库相应的文件没被加载到内存中,所以就执行了一个动态链接器的函数,最终由动态链接器把相应的动态库加载进来。以后再对动态库的函数调用时,就不再次加载了,jmp 指令后的地址处中所保存的函数地址就是刚被加载到内存中的函数地址。。(上面说的jmp 指令后的地址处所包含的地址意思是,jmp * 0xXXXXXXXX 这个是间接跳转,跳转的目标地址是后面这个地址空间中所保存的函数地址,0xXXXXXXXX就可以看出一个函数指针变量的地址,而它存储的内容那个函数地址正是我们想要执行的函数。其实在这里动态连接器调用了 dlopen 函数来加载的动态库)

    in _dl_runtime_resolve () from /lib/ld-linux.so.2
    就是进入了这个函数,大家可以自己测试下,我写的这个博客图片太多了,不想再放图片了

    dlopen 动态链接

    用dlopen 函数就不用在使用前面所讲的,什么 LD_LIBRARY_PATH 啊什么的了。

      1 #include<stdio.h>
      1 #include "add.h"
      2 #include<dlfcn.h>
      3 int main()
      4 {
      5     void * dl =NULL;
      6     int(*add)(int a, int b);
      7     dl = dlopen("./libadd.so",RTLD_LAZY);
      8     if(dl==NULL)
      9     {
     10         return 1;
     11     }
     12     add = (int(*)(int,int))dlsym(dl,"ADD");
     13    // int result = ADD(10,20);
     14     int result = add(10,20);
     15     printf("hehe %d",result);
     16     return 0;
     17 }   
    

    使用 dlopen 的时候,编译的时候需要加 -ldl 选项。

    动态链接/静态链接优缺点

    优点

    动态链接优点:
    1.不占可执行文件大小,在内存中只有一份实例,多个进程可共享一个动态库,节省内存。
    2.便于升级,如果需要升级动态库模块时,可执行文件不需改变,直接升级动态库文件即可。
    静态链接优点
    1.可移植性好,如果把可执行文件放到其他电脑上,不用再需要动态库文件了。
    2.运行的时候不用再把相应的库文件链接到内存,效率高。

    缺点

    动态库
    1.移植性不好,如果其他电脑上运行可执行程序,没有动态库的话,会发生 not found ,导致程序无法正常运行。并且它是运行时加载,效率可能会慢些。
    静态库
    1.由于在链接的时候,被链接编辑器和目标文件一起链接成可执行程序,结果就是造成可执行程序过大,并且如果在内存中有多个相同的可执行程序,那么静态库文件在内存中也存在多个实例,浪费空间。(因为多个相同的静态库文件的代码段被加载到了内存)
    2. 不便于升级,因为需要重新链接可执行程序。

    链接期

     链接期,主要做了符号解析与符号重定向。
     我们使用gcc链接程序的时候是有依赖关系的,链接器对于未解析的符号会保留先来,然后把当前 .o 文件丢弃,继续往后遍历去解析其他 .o 文件,如果找到了响应的符号,会对之前未解析的符号进行符号解析与绑定地址。
     基于这个原理,所以链接的时候文件与文件之间也是有依赖关系的。

    test.cc
    void test()
     {
     }
    main.cc
    int main()
    {
       test();
    }
    gcc  test.o main.o -o a.out 
    会包 未定义 test 函数

     就像上面的列子,所以基础的 .o 文件在链接的时候应该往前面放。

    动态链接/静态链接图

    链接加载图 可参考 操作系统 精神与设计原理 第七章的图
    上面的例子,取自于 Linux c 一站式编程中的ELF文件和动态/静态链接。

    参考

    深入理解操作系统 第三版
    操作系统精髓与设计原理 第八版
    Linux C 一站式编程

    展开全文
  • 有时候我们需要保存动态数组到ini文件中。QSetting提供了很方便的接口给...首先给个例子简单说明动态数组的保存格式: struct User { string userName; string password; }; User users[3]; 如果我们希望在ini文

    有时候我们需要保存动态数组到ini文件中。QSetting提供了很方便的接口给用户进行ini文件的读写,这一点我们稍后说,首先看看动态数组在ini文件中的结构。


    首先给个例子简单说明动态数组的保存格式:

    struct User {
           string userName;
           string password;
    };
    User users[3];

    如果我们希望在ini文件中保存users数组,文件内容如下:

    [users]

    1\userName=***

    1\password=***

    2\userName=***

    2\password=***

    3\userName=***

    3\password=***

    size=3

    其中size是自动侦测到的长度。

    ini文件以键值对的形式保存数据,简单数据的保存格式很简单,即key=value。key是键,value是值。在上面的例子中user表示一个组,在这个组下有3个user数组的元素,每个元素的键都以其索引(从1开始)开头,通过分隔符“\”连接子键,因为User结构体有userName和password两个成员变量,所以这里的子键就取这两个成员变量名。


    如果数组内部还有数组,比如:

    struct User {
           string userName;
           string password;
           int attr[4];
    };

    则ini文件的内容如下:

    [users]

    1\userName=***

    1\password=***

    1\attr\1\name=***

    1\attr\2\name=***

    1\attr\3\name=***

    1\attr\4\name=***

    1\attr\size=4

    1\size=1

    为简单起见,这里的users的数组长度为1,在这唯一的一个元素中含有一个attr数组,数组长度为4。对于这个数组,我们需要一个名字,即attr,后面通过分割符接上索引号,不像user,数组中的元素就是一个值,我这里依然还是给它定义了一个子键的名字,即name。


    总结一下,对于动态数组,其键的构成如下:

    上级键\数组索引\元素名

    • 上级键就是这个数组所在的位置,可以是一个组,就像user一样,数组索引之前没有其他字符,但是最上方存在[User]表示这个数组是在User组中,且是User组中的唯一成员。像attr这样,上级键其实就是user数组中的某个元素的键,由于user位于顶层,也就是user中的索引号。
    • 数组索引是对应元素在数组中的位置,从1开始
    • 元素名是数组结构体中各成员的名称


     QSetting读写动态数组

    1. 将User数组写入ini文件

        struct User {
    <pre name="code" class="cpp">       QString userName;
           QString password;
           int attr[4];
    }; User users[1]; QSettings setting(QObject::tr("***.ini"), QSettings::IniFormat); setting.beginReadArray(QObject::tr("User")); users[0].userName = setting.value(QObject::tr("userName")).toString(); users[0].password = setting.value(QObject::tr("password")).toString(); setting.setArrayIndex(0); int n = setting.beginReadArray(QObject::tr("attr")); for (int i = 0; i < 4; ++i) { setting.setArrayIndex(i); users[0].attr[i] = setting.value(QObject::tr("name")).toInt(); } setting.endArray(); setting.endArray();
    
    


    2. 从ini文件读出数据写入User数组

        QSettings setting(QObject::tr("***.ini"), QSettings::IniFormat);
        setting.beginWriteArray(QObject::tr("User"));
        setting.setValue(users[0].userName);
        setting.setValue(users[0].password);
        setting.setArrayIndex(0);
        setting.beginWriteArray(QObject::tr("attr"));
        for (int i = 0; i < 4; ++i) {
            setting.setArrayIndex(i);
            setting.setValue(users[0].attr[i]);
        }
        setting.endArray();
        setting.endArray();





    展开全文
  • pcap文件格式及写pcap文件

    千次阅读 2016-04-01 11:30:21
    主题:pcap文件格式及写pcap文件   Pcap文件格式,这个网络上资料比较多,参考即可。   一、pcap文件格式(该部分引用网络资料) 原文网址:http://www.cnblogs.com/kernel0815/p/3803304.html 第一部分:PCAP包文件...

    主题:pcap文件格式及写pcap文件

     

    Pcap文件格式,这个网络上资料比较多,参考即可。

     

    一、pcap文件格式(该部分引用网络资料)

    原文网址:http://www.cnblogs.com/kernel0815/p/3803304.html

    第一部分:PCAP包文件格式

    (一)、基本格式:

       文件头 数据包头数据报数据包头数据报......

    (二)、文件头:

    文件头结构体,libpcap源码中定义如下
     struct pcap_file_header {
            bpf_u_int32 magic;
            u_short version_major;
            u_short version_minor;
            bpf_int32 thiszone;     /* gmt to local correction */
            bpf_u_int32 sigfigs;    /* accuracy of timestamps */
            bpf_u_int32 snaplen;    /* max length saved portion of each pkt */
            bpf_u_int32 linktype;   /* data link type (LINKTYPE_*) */
    };
    说明:
     1、标识位:32位的,这个标识位的值是16进制的 0xa1b2c3d4
    2、主版本号:16位, 默认值为0x2
    3、副版本号:16位,默认值为0x04
    4、区域时间:32位,实际上该值并未使用,因此可以将该位设置为0
    5、精确时间戳:32位,实际上该值并未使用,因此可以将该值设置为0
    6、数据包最大长度:32位,该值设置所抓获的数据包的最大长度,如果所有数据包都要抓获,将该值设置为65535;例如:想获取数据包的前64字节,可将该值设置为64
    7、链路层类型:32位, 数据包的链路层包头决定了链路层的类型。
     
    以下是数据值与链路层类型的对应表
    0            BSD       loopback devices, except for later OpenBSD
    1            Ethernet, and Linux loopback devices   以太网类型,大多数的数据包为这种类型。
    6            802.5 Token Ring
    7            ARCnet
    8            SLIP
    9            PPP
    10          FDDI
    100        LLC/SNAP-encapsulated ATM
    101        raw IP, with no link
    102        BSD/OS SLIP
    103        BSD/OS PPP
    104        Cisco HDLC
    105        802.11
    108        later OpenBSD loopback devices (with the AF_value in network byte order)
    113               special Linux cooked capture
    114               LocalTalk

     

    (三)、 packet数据包头:

    struct pcap_sf_pkthdr {

        struct pcap_timeval ts; /* time stamp */

        bpf_u_int32 caplen; /* length of portion present */

        bpf_u_int32 len; /* length this packet (off wire) */

    };

    struct pcap_timeval {

        bpf_int32 tv_sec; /* seconds */

        bpf_int32 tv_usec; /* microseconds */

    };


    说明:
     1、时间戳,包括:
    秒计时:32位,一个UNIX格式的精确到秒时间值,用来记录数据包抓获的时间,记录方式是记录从格林尼治时间的197011 00:00:00 到抓包时经过的秒数;
    微秒计时:32位, 抓取数据包时的微秒值。

    2、数据包长度:32 ,标识所抓获的数据包保存在pcap文件中的实际长度,以字节为单位。
     
    3、数据包实际长度: 所抓获的数据包的真实长度,如果文件中保存不是完整的数据包,那么这个值可能要比前面的数据包长度的值大。

    (四)、packet数据:

      Packet(通常就是链路层的数据帧)具体内容,长度就是Caplen,这个长度的后面,就是当前PCAP文件中存放的下一个Packet数据包,也就是说:PCAP文件里面并没有规定捕获的Packet数据包之间有什么间隔字符串,下一组数据在文件中的起始位置。我们需要靠第一个Packet包确定。最后,Packet数据部分的格式其实就是标准的网路协议格式了可以任何网络教材上找得到。

     

     

    二、写pcap文件

    官网上关于libpcap的介绍挺全面,可以参考,以下为写pcap文件的代码,这里只列出需要调用的函数,函数参数原型参考官方文档。

    说明:/* ... ...*/表示省略的代码。头文件为#include <pcap.h>,编译时链接-lpcap

      如果没有安装libpcap动态库的话,centos下,镜像源和网络正常的情况下,安装命令为:

    yum install libpcap

    yum install libpcap-devel

     

    int test{

    /* ... ...*/

    pcap_dumper_t *pdumper;

    pcap_t *handler;

    handler = pcap_open_dead(1, 65535); /* 不限制包的长度 */

    pdumper = pcap_dump_open(handler, pcap_path); /* handler是函数内部malloc的,查看了下源代码没有释放,所以还是需要调用者释放的 */

    if(handler){

    free(handler);

    handler  = NULL;

    }

     

    struct pcap_pkthdr hdr;

    hdr.ts.tv_sec = 0;

    hdr.ts.tv_usec = 0;

     

    DataUnit *p = ptr_head;

    int len = 0;

    int loop_count = 0;

    char sessid[SESSLEN+1] = {0};

     

    /* 获取文件中的第一个会话id */

    memcpy(sessid, p->sessid, strlen(p->sessid));

    sessid[strlen(p->sessid)] = '\0';

    int count_len = 0;

    char *buf = NULL;

    /* 构造数据包结束标识 */

    char end_flag[SESSLEN + 1];

    loop_count = 0;

    while(loop_count < SESSLEN){

    end_flag[loop_count] = 'F';

    loop_count++;

    }

    end_flag[SESSLEN] = '\0';

    loop_count = 500;

    // while((strcmp(sessid ,p->sessid) == 0) && (count_len++ < FILEPACKETS)){

    while((count_len++ < FILEPACKETS)&&(loop_count--)){

    if(strcmp(p->sessid, end_flag) == 0)

    break;

    len = (p+1)->offset - p->offset;

    printf("packet len = %d\n", len);

    buf = (char*)malloc(len+1);

    if(NULL == buf)

    goto err_exit_free;

    memcpy(buf, ptr_read + p->offset, len);

    /* 构造数据包头 */

    hdr.caplen = len;

    hdr.len = len;

     

    /* 数据包写入 */

    pcap_dump((u_char*)pdumper, &hdr, buf);

    free(buf);

    buf = NULL;

    p++;

    }

    /* 清空缓冲区 */

    pcap_dump_flush(pdumper);

    pcap_dump_close(pdumper);

    /* ... ...*/

     

    }

    展开全文
  • 首先,如果没有特殊需求,不推荐把基本磁盘转换为动态磁盘。动态磁盘的好处在于磁盘间相互扩容方便,然而现在的电脑硬盘越来越大,一般用不到这个功能。...最简单的方法就是把所有动态磁盘格式化重新分区...
  • 在通常的Server/Client方式MIS开发中,总是有... PB(PowerBuilder)有一种以PSR结尾的特殊的保存报表的文件格式(本文简称作PSR文件)。根据数据窗口可以直接读取PSR文件生成报表的原理,程 序
  • ELF文件格式解析

    万次阅读 多人点赞 2016-05-21 11:28:55
    ELF文件格式说明。
  • ELF文件格式

    千次阅读 2013-02-02 14:46:37
    ELF文件格式 在Blackfin的Linux世界中,有两种基本的文件格式: FLAT:二进制的Flat文件通常被称为BFLT,它是基于原始的a.out格式的一种相对简单的轻量级可执行格式。BFLT文件是嵌入式Linux的默认文件格式。...
  • ELF文件格式详解

    万次阅读 2016-07-21 00:36:58
    ARM的可执行文件格式是ELF格式文件,下文对ELF格式做个详细的介绍。 序言 1. OBJECT文件  导言  ELF头(ELF Header)  Sections  String表(String Table)  Symbol表(Symbol Table)  重定位(Relocation) 2. ...
  • android中使用InputStreamReader读取文件数据时,需要传入参数编码格式(charset),而当目标文件的编码格式不固定时,就需要动态获取编码格式。 InputStreamReader inputReader = new InputStreamReader...
  • MP3文件格式全解

    万次阅读 2016-08-11 17:27:52
    WAV 格式文件头(除了文件头就是音频数据了),很简单,不用多说 struct WAVFmtHeader { char strRIFF[4]; /* 'RIFF' 资源文件标志,固定字符 */ unsigned long dwTotalByte; /* 从下一个成员开始到文件结尾的总...
  • so文件格式详解

    万次阅读 2016-09-28 15:28:21
    UNIX 系统实验室 ( UNIXSystem Laboratories, USL)开发并发布, 作为应用程序二进制接口 ( Application BinaryInterface, ABI)的一部分,它是一种常用的目标文件格式,主要包含以下三种类型 1、可重定位文件:...
  • 文件格式名称汇总

    千次阅读 2019-04-20 16:45:33
    文件格式大全 不同的文件,有不同的文件格式,区别这些格式常常是文件名的后缀名不同,现统计常用文件后缀名如下,供大家参考和查阅。 A 对象代码库文件 AAM Authorware shocked文件 AAS Authorware shocked包 ABF ...
  • 文件格式大全

    千次阅读 2012-08-07 09:10:00
    文件格式大全不同的文件,有不同的文件格式,区别这些格式常常是文件名的后缀名不同,现统计常用文件后缀名如下,供大家参考和查阅。 UJL2IF-x A 对象代码库文件 {V6&((E8 AAM Authorware shocked文件 V`y^m@U! ...
  • 文件格式、后缀名、图片格式详解

    千次阅读 2019-03-12 08:51:19
    .acm:音频压缩管理驱动程序,为Windows系统提供各种声音格式的编码和解码功能 .aif:声音文件,支持压缩,可以使用WindowsMediaPlayer和QuickTimePlayer播放 .AIF:音频文件,使用WindowsMediaPlayer播放 .AIFC:...
  • dll文件格式

    千次阅读 2017-04-26 16:41:19
    DLL文件(Dynamic Linkable Library 即动态链接库文件),是一种不能单独运行的文件,它允许程序共享执行特殊任务所必需的代码和其他资源比较大的应用程序都由很多模块组成,这些模块分别完成相对独立的功能,它们彼此...
  • 最近研究了一下elf文件格式,发现好多资料写的都比较繁琐,可能会严重打击学习者的热情,我把自己研究的结果和大家分享,希望我的描述能够简洁一些。 一、基础知识  elf是一种文件格式,用于存储Linux程序. 它...
  • NES文件格式

    万次阅读 2011-08-14 09:59:51
    NES文件格式 http://www.bjsgm.com/a/a.asp?B=101&ID=12 9、NES文件格式 .NES文件为模拟用来储存NES卡带的映像。下面是一个.NES文件的结构。 偏移 字节数 内容 0-3 4 字符串“N
  • ini配置文件格式

    万次阅读 多人点赞 2019-05-29 11:32:34
    为什么要用INI文件?如果我们的程序没有任何配置文件时,这样的程序对外是全封闭的,一旦程序需要修改一些参数必须要修改程序代码本身并重新编译,这样很不好,所以要用配置文件,让程序出厂后还能根据需要进行必要...
  • MIDI 文件格式分析

    千次阅读 2017-12-08 18:33:39
    MIDI 文件属于二进制文件,这种文件一般都有如下基本结构: 文件头+数据描述 文件头一般包括文件的类型,因为 Midi 文件仅以.mid 为扩展名的就有 0 类和 1 类两种,而大家熟悉的位 图文件格式就更多了,所以才会出现...
  • RTF文件格式分析

    千次阅读 2016-07-15 15:28:06
    RTF是文本格式的一种,是RichTextFormat的缩写,意即丰富的文本格式,主要用于各种...摘 要 RTF是一种在不同操作系统下不同应用软件之间进行文本和图象信息交换的文件格式。以RTF格式作为多媒体系统中文本媒体的一
  • 在VS中新建一个 MFC单文档或多文档的应用程序,可以通过“文件(File)/打开(open)”的打开文件选择的对话框,那如何选择文件过滤器呢,比如要实现选择打开文件格式为*.bmp或*.jpg的图像文件。一般可以通过编程的...
  •  ①、确定各个模板的.xls文件格式  ②、定义模板的存在的参数,如第一行的参数,第二行的参数等  ③、excel文件中针对行 列定位方式,如 (0, 0, 0,0)  ④、处理从数据库获取的数据格式key-value 如 name 小.....
  • 多媒体计算机图像文件格式

    千次阅读 2009-11-04 13:11:00
    图形和图像以文件形式存储。图形和图像文件格式分两大类:一类是静态图像文件格式,一类是动态图像文件格式。静态图像文件格式有:GIF,TIF,BMP,PCX,JPG,PCD等;动态图像文件格式有AVI,MPG等。

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 641,783
精华内容 256,713
关键字:

属于动态文件格式的是