网站建设过程中要怎么打开速度,晋江网站建设公司,wordpress怎么清空所有内容,网站开发人员岗位要求在上一篇Java核心: 注解处理器中我们提到#xff0c;通过实现AbstractProcessor#xff0c;并调用javac -processor能够生成代码来实现特殊逻辑。不过它存在两个明显的问题: 只能新增源文件来扩展逻辑#xff0c;无法修改现有的类或方法 必须有一个单独的编译过程Java核心: 注解处理器中我们提到通过实现AbstractProcessor并调用javac -processor能够生成代码来实现特殊逻辑。不过它存在两个明显的问题: 只能新增源文件来扩展逻辑无法修改现有的类或方法 必须有一个单独的编译过程调用javac -processor或者用Maven的annotationProcessor不适用于已经编译好的jar
这一篇我们讲解ASM的目的就是解决问题1它不但能建新类还能修改已有类比如为POJO类生成toString方法为Service类的业务方法提供类似AOP的增强。本文的讲解思路如下
asm的能力在于分析、生成和修改字节码class文件的结构和字节码对使用和理解asm会有帮助所以我们从讲解Class文件结构开始 理解asm对字节码操作建立的抽象模型核心组件和工作流程 对类字节码和asm抽象都有概念后从实战出发解决3个实际问题: 生成类、生成toString方法、打印方法入参和执行耗时
1. Class文件结构
通过javap -v Account.class能够查看Class文件的详细信息为了方便查看我们做了删减它看起是这样的 整个Class文件的内容包含很多内容这里我们只列出其中的核心部分 类信息包括类的访问标志(public、abstract)、名称、父类、接口、版本(编译.java文件的JDK版本) 常量池包括类/方法/字段的引用和名称以及代码中使用的字面常量等 类属性通过类属性提供如SourceFile表示源文件名称RuntimeInvisibleAnnotations表示运行时注解、外部引用等 内部类通过属性InnerClasses提供 字段常量池保存字段引用Fieldref记录了字段所属的类、字段类型和名称 方法常量池保存方法引用Methodref记录了方法所属的类、名称、参数和描述符等等 字节码通过方法引用关联能找到这个方法内的字节码本地变量表(LocalVariableTable)、异常表(ExceptionTable)、注解信息(RuntimeVisibleAnnotations)等等 2. ASM工作模式
ASM提供了字节码的分析、生成和修改能力它的能力当然是基于它对字节码的理解之上构建的。ASM支持两类API一类是基于事件的(类型XML解析的SAX)一类是基于语法树的(类似于XML的DOM)。事件模型的性能会更好一些这里我们主要讲解和使用事件模型。事件模型将ASM抽象成3个核心组件ClassReader、Visitor(ClassVisitor、MethodVisitor等)、ClassWriter整个字节码的处理过程可以想象成这样一张处理的数据流图整个处理过程分为3步: 生成事件流图中CR节点表示ClassReader用于读取类定义解析并触发事件 过滤和转换图中空白节点被抽象为Visitor常见的有ClassVisitor、MethodVisitor、FieldVisitor等接收事件触发过滤/修改事件传递给CW同时它还支持生成新事件 终结操作符图中CW节点表示ClassWriter起始ClassWriter也是Visitor接口的实现不同的是它的visit方法会生成类的字节码比如调用ClassWriter.visitMethod会在类中新增方法 1. ClassVisitor
要使用asm的事件模型API我们的核心任务就是定义和提供这个处理流程中的核心组件。我们来看看ClassVisitor的核心接口它实际上是和Class文件结构对应的它的抽象是基于Class文件的
1. 类信息 - visit(int version, int access, String name, String signature, String superName, String[] interfaces)
这个ClassReader开始访问某个类的起点我们来看看每个参数的定义 参数 说明 举例 version 类的版本号对应Java版本 V1_8指Java 8的代码 access 访问标志方法是否public、static、synchronized等等见Opcodes.ACC_*定义 Opcodes.ACC_PUBLIC name 类名包名中的.换成/ com/keyniu/shop/Product signature 泛型标签名 superName 父类名没有的话默认父类是Object java/lang/Object interfaces 接口名数组 new String[]{java/io/Serializable}
下面是我们举例的一组参数各个参数值大概是长这样的
visit(V1_8, ACC_PUBLIC, com/keyniu/shop/Product, null, java/lang/Object, new String[]{java/io/Serializable});
2. 源文件 - visitSource(String source, String debug)
用于获取javap -v输出里的SourceFile和SourceDebugExtension前一个字段是.java文件的名称后一个是额外的调试信息比如JSP编译为字节码 参数 说明 举例 source 编译当前class的.java文件名称 Account.java debug 额外的DEBUG信息 null
下面是我们举例的一组参数
visitSource(Account.java, null);
3. 访问外部类 - visitOuterClass(String owner, String name, String descriptor) 参数 说明 举例 owner 外部类类名格式com/keyniu/asm/Outer com/keyniu/asm/Outer name 外部类简单名Outer Outer descriptor 外部类描述符 Lcom/keyniu/asm/Outer;
下面是我们举例的一组参数
visitOuterClass(com/keyniu/asm/Outer, Outer, Lcom/keyniu/asm/Outer;);
4. 访问类上的注解 - visitAnnotation(String descriptor, boolean visible) 参数 说明 举例 descriptor 注解描述符 Lcom/keyniu/asm/ToString; visible 是否运行时可见Retention元注解 true
下面是我们举例的一组参数
visitAnnotation(Lcom/keyniu/asm/ToString;, true);
5. 访问字段 - visitField(int access, String name, String desc, String signature, Object value) 参数 说明 举例 access 访问标志用于表示是否public、static、synchronized等 Opcodes.ACC_PUBLIC name 字段名 remain desc 类型描述符对象类型(如String返回的是Ljava/lang/String;) 基本类型按预定义 I大写i表示int类型 signature 泛型签名 null value 静态常量字段的初始值
下面是我们举例的一组参数
visitField(ACC_PUBLIC ACC_FINAL, remain, I, null, null);
6. 访问方法 - visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) 参数 说明 举例 access 访问标志用于表示是否public、static、synchronized等 Opcodes.ACC_PUBLIC name 方法名 transfer transfer 格式是(参数类型)返回值类型具体类型遵循Java里的通用类型描述符 (Ljava/lang/String;I)V
2. MethodVisitor
ClassVisitor有点像设计模式里的抽象工厂除了处理由ClassReader触发的类读取上的事件它还需要在访问注解、字段、方法是调用工厂方法创建其他的Visitor实例涉及Visitor如下: ModuleVisitor支持模块/包的读取写入操作 AnnotationVisitor支持处理类/方法/字段(visitAnnotation)和参数/返回值/异常/泛型类型(visitTypeAnnotation)的注解 MethodVisitor支持方法的处理 FieldVisitor支持字段的处理 RecordComponentVisitor支持record类型的字段处理 3. 实战: 准备工作
学习一大堆理论并不能搞明白如何游泳有了基本的概念后现在是时候下水实践一下了。实践之前我们先准备好实践的材料 定义业务类Account后续我们会对Account进行编辑生成一个toString方法返回所有字段值的拼接修改transfer方法打印入参和执行耗时 注解ToString标注了ToString的类生成toString方法 注解Diagnostic标注了Diagnositc的方法打印入参和执行耗时 自定义类加载器SingleClassClassLoader将保存在byte[]的字节码加载为Class对象
1. Account
package com.keyniu.asm;
ToString
public class Account {private int remain 99;Diagnosticpublic void transfer(String sb, int amount) {System.out.println(transfer to sb amount: amount);try {Thread.sleep((int) (Math.random() * 1000));} catch (InterruptedException e) {}}
}
2. ToString
package com.keyniu.asm;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;Target(ElementType.TYPE)
Retention(RetentionPolicy.CLASS)
public interface ToString {
}
3. Diagnostic
package com.keyniu.asm.diagnostic;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;Target(ElementType.METHOD)
Retention(RetentionPolicy.RUNTIME)
public interface Diagnostic {
}
4. SingleClassClassLoader
public class SingleClassClassLoader extends ClassLoader {private String name;private byte[] codes;public SingleClassClassLoader(String name, byte[] code) {this.name name;this.codes code;}public Class? findClass(String name) throws ClassNotFoundException {if (this.name.equals(name)) {return defineClass(name, codes, 0, codes.length);}throw new ClassNotFoundException(name);}
} 4. 实战: 新建类
万事俱备我们开始第一个实战从0到1的生成一个全新的类提供默认构造函数。这个过程可以拆解为5步对应代码里的步骤x阅读 创建ClassWriter这里必要重要的是参数里的ClassWriter.COMPUTE_FRAMES表示让asm自动计算局部变量表、操作数栈的大小 创建SimpleClass类这一步执行完相当于class SimpleClass已经定义 使用MethodVisitor创建init方法(构造函数)创建字节码调用父类(java.lang.Object)的默认构造函数。这一点和Java代码里不同ClassWriter不会自动生成默认构造函数 方法和类创建完成后需要调用MethodVisitor.visitMaxs、MethodVisitor.visitEnd以及ClassWriter(ClassVisitor)的visitEnd方法结束写入 使用自定义类加载器加载字节码创建并使用对象当然我们也可以将它写入到.class文件
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {ClassWriter cw new ClassWriter(ClassWriter.COMPUTE_FRAMES); // 步骤1// 创建Classcw.visit(Opcodes.V17, Opcodes.ACC_PUBLIC, com/keyniu/asm/SimpleClass, null, java/lang/Object, null); // 步骤2// 创建方法MethodVisitor mv cw.visitMethod(Opcodes.ACC_PUBLIC, init, ()V, null, null); // 步骤3mv.visitVarInsn(Opcodes.ALOAD, 0); // this入栈mv.visitMethodInsn(Opcodes.INVOKESPECIAL,java/lang/Object,init,()V,false); // 调用父类initmv.visitInsn(Opcodes.RETURN); // 退出方法mv.visitMaxs(0,0); // 触发计算局部变量表、操作数栈大小mv.visitEnd(); // 结束方法写入 // 步骤4cw.visitEnd(); // 结束类写入byte[] bs cw.toByteArray();SingleClassClassLoader cl new SingleClassClassLoader(com.keyniu.asm.SimpleClass, bs); // 步骤5Class? clazz cl.findClass(com.keyniu.asm.SimpleClass);Object instance clazz.newInstance();System.out.println(clazz.getName() : instance);
}
5. 实战: 生成toString方法
通过asm能够实现类似于lombok的操作对于注解了ToString的类自动生成toString方法这个方法读取每个字段值拼接后返回。实现过程粗略的讲是这样的: 使用ClassReader读取并解析一个现有的类触发事件这里我们用的Account类 自定义ClassVisitor处理visitAnnotation事件确认Account类上是否有ToString注解 自定义ClassVisitor处理visitField事件记录Account上所有的字段 自定义ClassVisitor处理visitEnd事件使用MethodVisitor创建toString方法编辑字节码拼接字段值后返回
我们需要定义自己的ClassVisitor判断类是否ToString标注收集类中的字段创建toString方法核心操作步骤如下对应代码的”步骤x来阅读 回调visit记录当前的类名这里的值是: com/keyniu/asm/Account 回调visitAnnotation记录当前类十分有标注ToString注解 回调visitField记录类中的所有字段和描述符 回调visitEnd在类遍历结束时根据之前收集的信息是否注解ToString、类名、字段信息生成toString方法的定义和字节码 创建StringBuilder用于拼接toString的结果 拼类名和左括号Account( 拼字段值多个字段直接用,分隔 拼右括号StringBuilder最终值的格式是: Account(字段值1,字段值2 调用StringBuilder.toString将这个结果作为方法返回值返回
package com.keyniu.asm.lombok;import org.objectweb.asm.*;import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;public class ToStringClassVisitor extends ClassVisitor {private boolean isAnnotated false;private MapString, String fields new LinkedHashMap();private String className;protected ToStringClassVisitor(ClassVisitor classVisitor) { // classVisitor接收一个ClassWriter用于生成字节码super(Opcodes.ASM9, classVisitor);}Overridepublic void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {this.className name; // 步骤1记录类名格式com/keyniu/asm/Accountsuper.visit(version, access, name, signature, superName, interfaces);}Overridepublic AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {if (Lcom/keyniu/asm/lombok/ToString;.equals(descriptor)) {isAnnotated true; // 步骤2记录是否有标记ToString注解}return super.visitAnnotation(descriptor, visible);}Overridepublic FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {fields.put(name, descriptor); // 步骤3记录所有字段信息return super.visitField(access, name, descriptor, signature, value);}Overridepublic void visitEnd() {if (isAnnotated fields.size() 0) { // 步骤4创建toString方法MethodVisitor mv cv.visitMethod(Opcodes.ACC_PUBLIC, toString, ()Ljava/lang/String;, null, null);mv.visitCode();// 步骤4.a 创建StringBuildermv.visitTypeInsn(Opcodes.NEW, java/lang/StringBuilder);mv.visitInsn(Opcodes.DUP);mv.visitMethodInsn(Opcodes.INVOKESPECIAL, java/lang/StringBuilder, init, ()V, false);// 步骤4.b 拼接类名执行完后StringBuilderAccount(mv.visitLdcInsn(className.substring(className.lastIndexOf(/) 1) ();mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, java/lang/StringBuilder, append, (Ljava/lang/String;)Ljava/lang/StringBuilder;, false);IteratorMap.EntryString, String it fields.entrySet().iterator();while (it.hasNext()) {Map.EntryString, String kv it.next();String fieldName kv.getKey();String fieldDesc kv.getValue();mv.visitVarInsn(Opcodes.ALOAD, 0); // 载入thismv.visitFieldInsn(Opcodes.GETFIELD, className, fieldName, fieldDesc); // 步骤4.c 加载字段到操作数栈// 步骤4.c 拼接字段值到StringBuilderAccount(字段值mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, java/lang/StringBuilder, append, ( fieldDesc )Ljava/lang/StringBuilder;, false);if (it.hasNext()) { // 步骤4.c 如果不是最后一个字段拼接, StringBuilderAccount(字段值,mv.visitLdcInsn(,);mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, java/lang/StringBuilder, append, (Ljava/lang/String;)Ljava/lang/StringBuilder;, false);}}// 步骤4.d 拼接右括号 StringBuilderAccount(字段值1,字段值2)mv.visitLdcInsn());mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, java/lang/StringBuilder, append, (Ljava/lang/String;)Ljava/lang/StringBuilder;, false);// 步骤5将StringBuilder转为String使用ARETURN返回 mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, java/lang/StringBuilder, toString, ()Ljava/lang/String;, false);mv.visitInsn(Opcodes.ARETURN);// 步骤6计算本地变量表、操作数栈的大小mv.visitMaxs(0, 0);mv.visitEnd();}super.visitEnd();}
}
ToStringClassVisitor也准备好了之后剩下要做的就是读取Account类触发事件调用ToStringClassVisitor生成字节码并通过类加载器加载测试toString方法了
public static void main(String[] args) throws Exception {System.out.println(Path.of());String className com.keyniu.asm.Account;// 读字节码ClassReader cr new ClassReader(className);// 写字节码ClassWriter cw new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);ClassVisitor cv new ToStringClassVisitor(cw);// 跑事件流cr.accept(cv, ClassWriter.COMPUTE_FRAMES); // 0表示不计算最大堆栈大小和最大局部变量表大小// 获取修改后的字节码并写入到文件byte[] transformedBytes cw.toByteArray();SingleClassClassLoader cl new SingleClassClassLoader(className, transformedBytes);Class? clazz cl.findClass(className);Object instance clazz.newInstance();Method toString clazz.getDeclaredMethod(toString);System.out.println(toString.invoke(instance));
}
执行main方法查看输出可以确定我们的实现已经生效如果在Account里新增一个String字段值等于randy那么输出会自动变成Account(99,randy) 6. 实战: 打印参数和执行耗时
打印参数和执行耗时对于我们排查问题很有帮助。我们定义一个Diagnostic注解使用asm对注解了Diagnostic的方法进行修改打印入参记录调用耗时。处理过程可以的分为4步: 准备测试用类Account和注解Diagnostic 实现ClassVisitor覆盖visitMethod方法目的是注入自己的MethodVisitor实现 实现MethodVisitor在visitAnnotation中判断当前方法是否有Diagnostic注解在visitCode时打印参数记录开始执行时间 实现MethodVisitor在visitInsn中判断是否是方法执行的最后一条指令(返回或抛异常)是的话计算耗时并打印
第1步是准备测试类Account和Diagnostic的定义在之前已经给出。第2步是实现自己的ClassVisitor内部逻辑也相当简单只需要覆写visitMethod方法返回我们的DiagnosticMethodVisitor即可。
package com.keyniu.asm.diagnostic;import org.objectweb.asm.*;public class DiagnosticClassVisitor extends ClassVisitor {protected DiagnosticClassVisitor(ClassVisitor classVisitor) {super(Opcodes.ASM9, classVisitor);}Overridepublic MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {MethodVisitor mv super.visitMethod(access, name, descriptor, signature, exceptions);return new DiagnosticMethodVisitor(name, mv);}
}
第3步是实现DiagnosticMethodVisitor在visitAnnotation时判断方法是否标注Diagnostic在visitCode中读取并打印入参记录方法开始执行的时间(System.currentTimeMillis)到本地变量中
package com.keyniu.asm.diagnostic;import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;import java.util.UUID;public class DiagnosticMethodVisitor extends MethodVisitor {private boolean isDiagnostic false;private String methodName;private String traceId UUID.randomUUID().toString();protected DiagnosticMethodVisitor(String methodName, MethodVisitor methodVisitor) {super(Opcodes.ASM9, methodVisitor);this.methodName methodName;}Overridepublic AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {if (Lcom/keyniu/asm/diagnostic/Diagnostic;.equals(descriptor)) { // 判断注解是否为我们感兴趣的DiagnosticisDiagnostic true;}return super.visitAnnotation(descriptor, visible);}Overridepublic void visitCode() {if (isDiagnostic) {mv.visitFieldInsn(Opcodes.GETSTATIC, java/lang/System, out, Ljava/io/PrintStream;); // 将System.out放入操作数栈后续会调用out.printlnmv.visitTypeInsn(Opcodes.NEW, java/lang/StringBuilder); // 创建StringBuilder并调用构造函数initmv.visitInsn(Opcodes.DUP);mv.visitMethodInsn(Opcodes.INVOKESPECIAL, java/lang/StringBuilder, init, ()V, false);mv.visitLdcInsn(traceId: traceId call this.methodName with params: ); // 使用ldc指令放入字符常量打印参数的前置// 弹出栈顶的两个元素(StringBuilder的引用、要拼接的参数)调用append方法将返回值(StringBuilder自己)压入栈顶后续类似命令不再解释参照这里mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, java/lang/StringBuilder, append, (Ljava/lang/String;)Ljava/lang/StringBuilder;, false);mv.visitVarInsn(Opcodes.ALOAD, 1); // 加载index1的值(第1个参数), index0是this引用mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, java/lang/StringBuilder, append, (Ljava/lang/String;)Ljava/lang/StringBuilder;, false);mv.visitLdcInsn( , ); // 插入分隔符mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, java/lang/StringBuilder, append, (Ljava/lang/String;)Ljava/lang/StringBuilder;, false);mv.visitVarInsn(Opcodes.ILOAD, 2); // 加载index2的值(第2个参数)mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, java/lang/StringBuilder, append, (I)Ljava/lang/StringBuilder;, false);// 将StringBuilder转为String压入栈顶为输出做好准备 mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, java/lang/StringBuilder, toString, ()Ljava/lang/String;, false);// 输出内容mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, java/io/PrintStream, println, (Ljava/lang/String;)V, false);// 获取当前时间记录到本地变量mv.visitMethodInsn(Opcodes.INVOKESTATIC, java/lang/System, currentTimeMillis, ()J);mv.visitVarInsn(Opcodes.LSTORE, 4);mv.visitMaxs(0, 0);}super.visitCode();}}第4步是在方法结束前重新取一个当前时间减去开始时间就是方法的执行时间并打印。在visitInst回调我们能取得方法中的每个指令在方法返回(RETURN)或抛异常(ATHROW)前正是插入这段逻辑的合适位置。
public class DiagnosticMethodVisitor extends MethodVisitor {...Overridepublic void visitInsn(int opcode) {if (isDiagnostic) {if ((Opcodes.IRETURN opcode opcode Opcodes.RETURN) || Opcodes.ATHROW opcode) { // 方法返回之前mv.visitFieldInsn(Opcodes.GETSTATIC, java/lang/System, out, Ljava/io/PrintStream;);mv.visitMethodInsn(Opcodes.INVOKESTATIC, java/lang/System, currentTimeMillis, ()J);mv.visitVarInsn(Opcodes.LLOAD, 4);mv.visitInsn(Opcodes.LSUB);mv.visitFieldInsn(Opcodes.GETSTATIC, java/lang/System, out, Ljava/io/PrintStream;);mv.visitLdcInsn(traceId: traceId call this.methodName timeCost: );mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, java/io/PrintStream, print, (Ljava/lang/String;)V, false);mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, java/io/PrintStream, println, (J)V, false);}}super.visitInsn(opcode);}}
到这里整个Diagnostic工具已经开发完成了下面我们创建一段测试代码来看看怎么用是否能达成预期的效果
package com.keyniu.asm.diagnostic;import com.keyniu.asm.utils.SingleClassClassLoader;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.util.TraceClassVisitor;import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.nio.file.Path;public class DiagnosticMain {public static void main(String[] args) throws Exception {System.out.println(Path.of());String className com.keyniu.asm.Account;// 读字节码ClassReader cr new ClassReader(className);// 写字节码ClassWriter cw new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);ClassVisitor cv new TraceClassVisitor(new DiagnosticClassVisitor(cw), new PrintWriter(System.out));// 跑事件流cr.accept(cv, ClassWriter.COMPUTE_FRAMES); // 0表示不计算最大堆栈大小和最大局部变量表大小// 获取修改后的字节码并写入到文件byte[] transformedBytes cw.toByteArray();SingleClassClassLoader cl new SingleClassClassLoader(className, transformedBytes);Class? clazz cl.findClass(className);Object instance clazz.newInstance();Method toString clazz.getDeclaredMethod(transfer, String.class, int.class);System.out.println(toString.invoke(instance, randy, 9));}}
看输出我们确定想要的目标已经实现了。额外要提一下的是这里我们用TraceClassVisitor包装了DiagnosticClassVisitor目的是打印最终的字节码(如下图)并不影响实际的执行。 A. 参考资料 The Class File FormatChapter 4. The class File Format JMV Instruction SetChapter 6. The Java Virtual Machine Instruction Set ASM Guidehttps://asm.ow2.io/asm4-guide.pdf