/* Copyright (c) 2006, Sriram Srinivasan * * You may distribute this software under the terms of the license * specified in the file "License" */ package kilim.tools; import static kilim.Constants.*; import java.io.File; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.io.LineNumberReader; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Type; /** * This is a replacement for the jasmin bytecode assembler and uses the same * syntax. The main reason for writing it is that jasmin (v 2.1 at the time of * writing) didn't correcly support annotations. That is, the annotations * inserted by jasmin don't show up in java.lang.reflect.Method, even though the * annotations are in the class file. It was easier to write this tool than to * release a separate fix for jasmin. * <pre> * Usage: java kilim.tools.Asm [options] <.j file(s)> * Options: * -d <dir> : output directory (default: '.') * -q : quiet (default: verbose) * -nf : no stack frames (default: compute stack frames) * </pre> * If stack frames are requested (default), the version of the class file is V1_6, otherwise * it is V1_5. * * @author sriram srinivasan (sriram@malhar.net) */ public class Asm { static boolean quiet = false; static String outputDir = "."; static Pattern wsPattern = Pattern.compile("\\s+"); static Pattern commentPattern = Pattern.compile("^;.*$| ;[^\"]*"); private boolean eofOK = false; private ClassWriter cv; private MethodVisitor mv; private int maxLocals = 1; private int maxStack = 1; private HashSet<String> declaredLabels = new HashSet<String>(); private HashMap<String, Label> labels = new HashMap<String, Label>(); private String className; private String methodName; private String fileName; private Line line, bufferedLine; private Matcher lastMatch= null; // for error context private Pattern lastPattern = null; private LineNumberReader reader; static HashMap<String, Integer> modifiers = new HashMap<String, Integer>(); static { modifiers.put("public", ACC_PUBLIC); modifiers.put("private", ACC_PRIVATE); modifiers.put("protected", ACC_PROTECTED); modifiers.put("static", ACC_STATIC); modifiers.put("final", ACC_FINAL); modifiers.put("super", ACC_SUPER); modifiers.put("synchronized", ACC_SYNCHRONIZED); modifiers.put("volatile", ACC_VOLATILE); modifiers.put("transient", ACC_TRANSIENT); modifiers.put("native", ACC_NATIVE); modifiers.put("interface", ACC_INTERFACE); modifiers.put("abstract", ACC_ABSTRACT); modifiers.put("strict", ACC_STRICT); modifiers.put("enum", ACC_ENUM); } public static void main(String[] args) throws IOException { List<String> files = parseArgs(args); for (String arg : files) { if (!quiet) {System.out.println("Asm: " + arg);} new Asm(arg).write(); } } public Asm(String afileName) throws IOException { fileName = afileName; reader = new LineNumberReader(new FileReader(fileName)); cv = new ClassWriter(computeFrames? ClassWriter.COMPUTE_FRAMES : 0); try { parseClass(); } catch (EOF eof) { if (!eofOK) { System.err.println("Premature end of file: " + fileName); System.exit(1); } } catch (AsmException e) { System.err.println(e.getMessage()); System.exit(1); } catch (RuntimeException e) { System.out.println("File: " + fileName); if (methodName != null) { System.out.println("Method: " + methodName); } System.out.println(""); System.out.println("Line " + line); System.out.println("Last pattern match: " + lastPattern); throw e; } } // .class public final Foo/Bar/Baz private static String classNamePatternStr = "[\\w/$]+"; private static String modifierPatternStr = "public|private|protected|static|final|synchronized|volatile|transient|native|abstract|strict| "; private static Pattern classPattern = Pattern.compile("\\.(class|interface) ((" + modifierPatternStr + ")*)(" + classNamePatternStr + ")$"); private void parseClass() { readLine(); // match class declaration int acc = 0; if (!lineMatch(classPattern)) { err("Expected .class or .interface declaration"); } if (line.startsWith(".interface")) { acc = ACC_INTERFACE; } acc |= parseModifiers(group(2)); className = group(4); String superClassName = parseSuper(); String[] interfaces = parseInterfaces(); cv.visit((computeFrames ? V1_6 : V1_5), acc, className, null, superClassName, interfaces); parseClassBody(); eofOK = true; } private int parseModifiers(String s) { if (s == null) return 0; s = s.trim(); if (s.equals("")) return 0; int acc = 0; for (String modifier : split(wsPattern, s)) { if (!modifiers.containsKey(modifier)) { err("Modifier " + modifier + " not recognized"); } acc |= modifiers.get(modifier); } return acc; } private static Pattern superPattern = Pattern.compile("\\.super (" + classNamePatternStr + ")$"); private String parseSuper() { readLine(); if (!lineMatch(superPattern)) { err("Expected .super <superclass>"); } return group(1); } private static Pattern implementsPattern = Pattern.compile("\\.implements +(" + classNamePatternStr + ")$"); private String[] parseInterfaces() { StringList interfaces = new StringList(); while (true) { readLine(); if (!lineMatch(implementsPattern)) { putBackLine(); return interfaces.toArray(); } interfaces.add(group(1)); } } private void parseClassBody() { while (true) { readLine(); // this breaks out of the loop upon EOF if (lineMatch(fieldPattern)) { parseField(); } else if (lineMatch(methodPattern)) { parseMethod(); } else if (lineMatch(annotationPattern)){ readLine(); if (!line.startsWith(".end annotation")) { err(".end annotation not present"); } } else { err("Expected field, method or annotation in class body"); } } } // The field declaration "public final String fileName = "foobar" is declared as // .field public final fieldName [[Ljava/lang/String; = "foobar" // .field (modifier)* name type (= constval)? private static String namePatternStr = "[$\\w]+"; private static String descPatternStr = "[$\\[\\w/;]+"; private static Pattern fieldPattern = Pattern.compile(".field +((" + modifierPatternStr + ")*) +(" + namePatternStr + ") +(" + descPatternStr + ") *(= *(.*))?"); private void parseField() { String name = group(3); String desc = group(4); String valueStr = group(6); Object value = valueStr == null ? null : parseValue(valueStr, (desc.equals(D_DOUBLE) || desc.equals(D_LONG))); cv.visitField( parseModifiers(group(1)), name, // field name desc, null, // no signature value); } // A method declaration for // private static final Object[][]foobar(int, long, boolean) // is specified as // .method private final static foobar(IJZ)[[Ljava/lang/Object; // .method <init>(IJZ) private static String methodNamePatternStr = "[<>\\w]+"; // private static Pattern methodPattern = Pattern.compile(".method +(("+ modifierPatternStr + ")*) ("+ methodNamePatternStr + ") *([(][^\\s]+)"); private void parseMethod() { eofOK = false; methodName = group(3); int acc = parseModifiers(group(1)); String desc = group(4); String[] exceptions = parseMethodExceptions(); mv = cv.visitMethod( acc, methodName, desc, // method desc null, // signature exceptions); parseMethodBody(); eofOK = true; } private static Pattern throwsPattern = Pattern.compile("^ *\\.throws +(" + classNamePatternStr + ")"); private String[] parseMethodExceptions() { StringList l = new StringList(); while (true) { readLine(); if (!lineMatch(throwsPattern)) { putBackLine(); return l.toArray(); } l.add(group(1)); } } private void parseMethodBody() { labels.clear(); declaredLabels.clear(); mv.visitCode(); while (true) { readLine(); if (line.startsWith(".end method")) { break; } else if (line.startsWith(".")) { parseMethodDirective(); } else if (lineMatch(labelPattern)) { parseLabel(); } else { parseInstructions(); } } checkLabelDeclarations(); mv.visitMaxs(maxStack, maxLocals); mv.visitEnd(); } private static Pattern labelPattern = Pattern.compile("^(\\w+) *: *$"); private void parseLabel() { String str = group(1); if (declaredLabels.contains(str)) { err("Duplicate label " + str); } else { declaredLabels.add(str); Label l = getLabel(str); mv.visitLabel(l); } } private void checkLabelDeclarations() { for (String key: labels.keySet()) { if (!declaredLabels.contains(key)) { throw new AsmException("Label " + key + " not declared in " + methodName); } } } static Pattern localsPattern = Pattern.compile(".limit +locals +([0-9]+)"); static Pattern stackPattern = Pattern.compile(".limit +stack +([0-9]+)"); static Pattern catchPattern = Pattern.compile(".catch +(" + classNamePatternStr + ") +from +([\\w]+) +to +([\\w]+) +using +([\\w]+)"); static Pattern annotationPattern = Pattern.compile(".annotation +((visible) )?([\\w/;]+)"); private void parseMethodDirective() { if (lineMatch(localsPattern)) { maxLocals = parseInt(group(1)); } else if (lineMatch(stackPattern)) { maxStack = parseInt(group(1)); } else if (lineMatch(catchPattern)) { String exceptionType = group(1); if (exceptionType.equals("all")) { exceptionType = null; } Label fromLabel = getLabel(group(2)); Label toLabel = getLabel(group(3)); Label usingLabel = getLabel(group(4)); mv.visitTryCatchBlock(fromLabel, toLabel, usingLabel, exceptionType); } else if (lineMatch(annotationPattern)) { parseAnnotation(); } else if (!quiet) { System.err.println("Directive ignored: " + line); } } private void parseAnnotation() { String s = group(2); boolean visible = s == null ? false : s.equals("visible"); String desc = group(3); mv.visitAnnotation(desc, visible); readLine(); if (!line.startsWith(".end annotation")) { err(".end annotation not present"); } } static String opcodeStrs[] = { "nop", "aconst_null", "iconst_m1", "iconst_0", "iconst_1", "iconst_2", "iconst_3", "iconst_4", "iconst_5", "lconst_0", "lconst_1", "fconst_0", "fconst_1", "fconst_2", "dconst_0", "dconst_1", "bipush", "sipush", "ldc", "ldc_w", "ldc2_w", "iload", "lload", "fload", "dload", "aload", "iload_0", "iload_1", "iload_2", "iload_3", "lload_0", "lload_1", "lload_2", "lload_3", "fload_0", "fload_1", "fload_2", "fload_3", "dload_0", "dload_1", "dload_2", "dload_3", "aload_0", "aload_1", "aload_2", "aload_3", "iaload", "laload", "faload", "daload", "aaload", "baload", "caload", "saload", "istore", "lstore", "fstore", "dstore", "astore", "istore_0", "istore_1", "istore_2", "istore_3", "lstore_0", "lstore_1", "lstore_2", "lstore_3", "fstore_0", "fstore_1", "fstore_2", "fstore_3", "dstore_0", "dstore_1", "dstore_2", "dstore_3", "astore_0", "astore_1", "astore_2", "astore_3", "iastore", "lastore", "fastore", "dastore", "aastore", "bastore", "castore", "sastore", "pop", "pop2", "dup", "dup_x1", "dup_x2", "dup2", "dup2_x1", "dup2_x2", "swap", "iadd", "ladd", "fadd", "dadd", "isub", "lsub", "fsub", "dsub", "imul", "lmul", "fmul", "dmul", "idiv", "ldiv", "fdiv", "ddiv", "irem", "lrem", "frem", "drem", "ineg", "lneg", "fneg", "dneg", "ishl", "lshl", "ishr", "lshr", "iushr", "lushr", "iand", "land", "ior", "lor", "ixor", "lxor", "iinc", "i2l", "i2f", "i2d", "l2i", "l2f", "l2d", "f2i", "f2l", "f2d", "d2i", "d2l", "d2f", "i2b", "i2c", "i2s", "lcmp", "fcmpl", "fcmpg", "dcmpl", "dcmpg", "ifeq", "ifne", "iflt", "ifge", "ifgt", "ifle", "if_icmpeq", "if_icmpne", "if_icmplt", "if_icmpge", "if_icmpgt", "if_icmple", "if_acmpeq", "if_acmpne", "goto", "jsr", "ret", "tableswitch", "lookupswitch", "ireturn", "lreturn", "freturn", "dreturn", "areturn", "return", "getstatic", "putstatic", "getfield", "putfield", "invokevirtual", "invokespecial", "invokestatic", "invokeinterface", "unused", "new", "newarray", "anewarray", "arraylength", "athrow", "checkcast", "instanceof", "monitorenter", "monitorexit", "wide", "multianewarray", "ifnull", "ifnonnull", "goto_w", "jsr_w" }; private static boolean computeFrames = true; private final static HashMap<String, Integer> opcodeMap = new HashMap<String, Integer>(); private final static byte[] visitTypes; private final static int INSN = 0; private final static int VAR = 1; private final static int LDC = 2; private final static int JUMP = 3; private final static int TABLESWITCH = 4; private final static int LOOKUPSWITCH = 5; private final static int FIELD = 6; private final static int METHOD = 7; private final static int TYPE = 8; private final static int MULTIANEWARRAY = 9; private final static int INT = 10; private final static int IINC = 11; static { for (int i = 0; i < opcodeStrs.length; i++) { opcodeMap.put(opcodeStrs[i], i); } opcodeMap.put("invokenonvirtual", opcodeMap.get("invokespecial")); // Generated the table from asm.Opcode visitTypes = new byte[] { INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INT, INT, LDC, LDC, LDC, VAR, VAR, VAR, VAR, VAR, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, VAR, VAR, VAR, VAR, VAR, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, IINC, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, INSN, JUMP, JUMP, JUMP, JUMP, JUMP, JUMP, JUMP, JUMP, JUMP, JUMP, JUMP, JUMP, JUMP, JUMP, JUMP, JUMP, VAR, TABLESWITCH, LOOKUPSWITCH, INSN, INSN, INSN, INSN, INSN, INSN, FIELD, FIELD, FIELD, FIELD, METHOD, METHOD, METHOD, METHOD, INSN, TYPE, INT, TYPE, INSN, INSN, TYPE, TYPE, INSN, INSN, INSN, MULTIANEWARRAY, JUMP, JUMP, JUMP, JUMP }; } static final Pattern insnPattern = Pattern.compile("(\\w+)( +(.*))?"); static final Pattern quotedPattern = Pattern.compile("(.*)"); static final Pattern casePattern = Pattern.compile("(\\w+) *: *(\\w+)"); static final Pattern methodInvokePattern = Pattern.compile("("+ classNamePatternStr + ")[/.](" + methodNamePatternStr + ") *([(].*?[)]" + descPatternStr + ") *(, *\\d+)?"); static final Pattern fieldSpecPattern = Pattern.compile("([\\w/.$]+)[/.]([\\w$]+) +([^\\s]+)"); private void parseInstructions() { // line read in parseMethodBody() if (!lineMatch(insnPattern)) { err("Instruction is not well-formed"); } String insn = group(1); String operand = null; if (groupCount() == 3) { operand = group(3); if (operand != null) { operand = operand.trim(); } } if (!opcodeMap.containsKey(insn)) { err("Instruction " + insn + " not recognized"); } int opcode = opcodeMap.get(insn); switch (visitTypes[opcode]) { case INSN: mv.visitInsn(opcode); break; case VAR: mv.visitVarInsn(opcode, parseInt(operand)); break; case LDC: mv.visitLdcInsn(parseValue(operand, (opcode == LDC2_W))); break; case JUMP: Label l = getLabel(operand); mv.visitJumpInsn(opcode, l); break; case TABLESWITCH: { int min = parseInt(operand); ArrayList<Label> labelList = new ArrayList<Label>(10); Label defLabel = null; while (true) { readLine(); if (line.startsWith("default")) { lineMatch(casePattern); defLabel = getLabel(group(2)); break; } else { labelList.add(getLabel(line.s)); } } Label[] labels = labelList.toArray(new Label[labelList.size()]); int max = min + labels.length - 1; mv.visitTableSwitchInsn(min, max, defLabel, labels); break; } case LOOKUPSWITCH: { ArrayList<Integer> keyList = new ArrayList<Integer>(10); ArrayList<Label> labelList = new ArrayList<Label>(10); Label defLabel = null; while (true) { readLine(); if (lineMatch(casePattern)) { Label lab = getLabel(group(2)); String keystr = group(1); if (keystr.equals("default")) { defLabel = lab; break; } else { int key = parseInt(keystr); keyList.add(key); labelList.add(lab); } } else { err("Ill-formed switch instruction"); } } Label[] labels = labelList.toArray(new Label[labelList.size()]); int[] keys = new int[keyList.size()]; for (int i = 0; i < keys.length; i++) { keys[i] = keyList.get(i); } mv.visitLookupSwitchInsn(defLabel, keys, labels); break; } case FIELD: { // getstatic foo/bar/Baz/fieldName I if (operand == null || !match(operand, fieldSpecPattern)) { err("Expected field access of the form foo/Bar/fieldName I"); } String owner = group(1); String name = group(2); String desc = group(3); mv.visitFieldInsn(opcode, owner, name, desc); break; } case METHOD: { // invokevirtual foo/bar/Baz/methodName(IJZ)V if (operand == null || !match(operand, methodInvokePattern)) { err("Expected method invocation of the form /foo/Bar/methodName(IJ)V"); } String owner = group(1); String name = group(2); String desc = group(3); mv.visitMethodInsn(opcode, owner, name, desc); break; } case TYPE: opcheck("expected type", operand); mv.visitTypeInsn(opcode, operand); break; case MULTIANEWARRAY: { opcheck("expected array type and dimensions", operand); String words[] = split(wsPattern, operand); mv.visitMultiANewArrayInsn(words[0], parseInt(words[1])); break; } case INT: { int op = -1; if (opcode == NEWARRAY) { if (operand.equals("boolean")) { op = T_BOOLEAN; } else if (operand.equals("char")) { op = T_CHAR; } else if (operand.equals("float")) { op = T_FLOAT; } else if (operand.equals("double")) { op = T_DOUBLE; } else if (operand.equals("byte")) { op = T_BYTE; } else if (operand.equals("short")) { op = T_SHORT; } else if (operand.equals("int")) { op = T_INT; } else if (operand.equals("long")) { op = T_LONG; } else { err("Unknown type for newarray: " + operand); } } else { op = parseInt(operand); } mv.visitIntInsn(opcode, op); break; } case IINC: { opcheck("Expected iinc <var> <inc amount>", operand); String words[] = split(wsPattern, operand); int var = parseInt(words[0]); int increment = parseInt(words[1]); mv.visitIincInsn(var, increment); break; } default: err("INTERNAL ERROR: UNKNOWN TYPE OF INSTRUCTION"); } } private void opcheck(String errMessage, String operand) { if (operand == null) { err(errMessage); } } private Object parseValue(String s, boolean isDoubleWord) { Object ret = null; if (s == null) { err("Expected constant value "); } if (s.startsWith("\"")) { if (isDoubleWord) { err("long or double value expected instead of string"); } if (s.charAt(s.length() - 1) != '"') { err("Ill-formed string"); } ret = s.substring(1, s.length() - 1); } else if (s.startsWith("L")) { // class name ret = Type.getType(s); } else { if (s.indexOf('.') == -1) { if (isDoubleWord) { ret = (Long)parseLong(s); } else { ret = (Integer) parseInt(s); } } else { if (isDoubleWord) { ret = (Double)parseDouble(s); } else { ret = (Float)parseFloat(s); } } } return ret; } int parseInt(String s) { if (s == null) { err("Expected integer"); } try { return Integer.parseInt(s.trim()); } catch (NumberFormatException nfe) { err("Expected integer value, got " + s); } return 0; } long parseLong(String s) { if (s == null) { err("Expected long"); } try { return Long.parseLong(s.trim()); } catch (NumberFormatException nfe) { err("Expected long value, got " + s); } return 0L; } float parseFloat(String s) { if (s == null) { err("Expected float"); } try { return Float.parseFloat(s); } catch (NumberFormatException nfe) { err("Expected float, got " + s); } return 0.0f; } double parseDouble(String s) { if (s == null) { err("Expected float"); } try { return Double.parseDouble(s); } catch (NumberFormatException nfe) { err("Expected double, got " + s); } return 0.0; } Label getLabel(String s) { if (s == null) { err("Expected label string"); } Label ret = labels.get(s); if (ret == null) { ret = new Label(); labels.put(s, ret); } return ret; } private void err(String s) { String msg = String.format("%s: %d: %s\n", fileName, line.n, s); msg += line.s; throw new AsmException(msg); } // return the next non-empty line after stripping comments private Line readLine() { if (bufferedLine != null) { line = bufferedLine; bufferedLine = null; return line; } while (true) { Line l = getLine(); String s = l.s.trim(); s = commentPattern.matcher(s).replaceAll(""); if (s.length() > 0) { l.s = s; line = l; return l; } } } private void putBackLine() { bufferedLine = line; } boolean eofSeen = false; private Line getLine() { if (eofSeen) { throw new EOF(); } try { String s = reader.readLine(); if (s == null) { eofSeen = true; s = ""; } return new Line(reader.getLineNumber(), s); } catch (IOException ioe) { ioe.printStackTrace(); throw new EOF(); } } boolean match(String s, Pattern p) { lastMatch = p.matcher(s); lastPattern = p; return lastMatch.find(); } boolean lineMatch(Pattern p) { lastMatch = p.matcher(line.s); lastPattern = p; return lastMatch.find(); } String group(int i) { String ret = lastMatch.group(i); return ret; } int groupCount() { return lastMatch.groupCount(); } static String[] split(Pattern p, String s) { return p.split(s); } private void write() throws IOException { String dir = outputDir + "/" + getDirName(className); mkdir(dir); String fileName = outputDir + '/' + className + ".class"; FileOutputStream fos = new FileOutputStream(fileName); fos.write(cv.toByteArray()); fos.close(); System.out.println("Wrote: " + fileName); } private static void mkdir(String dir) throws IOException { File f = new File(dir); if (!f.exists()) { if (!f.mkdirs()) { throw new IOException("Unable to create directory: " + dir); } } } private static String getDirName(String className) { int end = className.lastIndexOf('/'); return (end == -1) ? "" : className.substring(0, end); } private static List<String> parseArgs(String[] args) { ArrayList<String> ret = new ArrayList<String>(args.length); for (int i = 0; i < args.length; i++) { String arg = args[i]; if (arg.equals("-d")) { outputDir = args[++i]; } else if (arg.equals("-q")) { quiet = true; } else if (arg.equals("-nf")) { computeFrames = false; } else { ret.add(arg); } } return ret; } } @SuppressWarnings("serial") class EOF extends RuntimeException { } class Line { int n; String s; Line(int num, String str) { n = num; s = str; } public String toString() { return String.format("%4d: %s\n", n, s); } public boolean startsWith(String str) { return s.startsWith(str); } } @SuppressWarnings("serial") class StringList extends ArrayList<String> { public String[] toArray() { String[] ret = new String[size()]; return this.toArray(ret); } } @SuppressWarnings("serial") class AsmException extends RuntimeException { public AsmException(String s) {super(s);} }