WALA/com.ibm.wala.shrike/src/com/ibm/wala/shrikeBT/shrikeCT/ClassInstrumenter.java

493 lines
16 KiB
Java

/*******************************************************************************
* Copyright (c) 2002,2006 IBM Corporation.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package com.ibm.wala.shrikeBT.shrikeCT;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import com.ibm.wala.shrikeBT.Compiler;
import com.ibm.wala.shrikeBT.ConstantPoolReader;
import com.ibm.wala.shrikeBT.Constants;
import com.ibm.wala.shrikeBT.Decoder.InvalidBytecodeException;
import com.ibm.wala.shrikeBT.ExceptionHandler;
import com.ibm.wala.shrikeBT.Instruction;
import com.ibm.wala.shrikeBT.MethodData;
import com.ibm.wala.shrikeBT.ReturnInstruction;
import com.ibm.wala.shrikeBT.Util;
import com.ibm.wala.shrikeBT.analysis.Analyzer.FailureException;
import com.ibm.wala.shrikeBT.analysis.ClassHierarchyProvider;
import com.ibm.wala.shrikeCT.ClassConstants;
import com.ibm.wala.shrikeCT.ClassReader;
import com.ibm.wala.shrikeCT.ClassWriter;
import com.ibm.wala.shrikeCT.CodeReader;
import com.ibm.wala.shrikeCT.CodeWriter;
import com.ibm.wala.shrikeCT.InvalidClassFileException;
import com.ibm.wala.shrikeCT.LineNumberTableReader;
import com.ibm.wala.shrikeCT.LineNumberTableWriter;
import com.ibm.wala.shrikeCT.LocalVariableTableReader;
import com.ibm.wala.shrikeCT.LocalVariableTableWriter;
import com.ibm.wala.shrikeCT.StackMapConstants.StackMapFrame;
import com.ibm.wala.shrikeCT.StackMapTableReader;
import com.ibm.wala.shrikeCT.StackMapTableWriter;
/**
* This class provides a convenient way to instrument every method in a class. It assumes you are using ShrikeCT to read and write
* classes. It's stateful; initially every method is set to the original code read from the class, but you can then go in and modify
* the methods.
*/
final public class ClassInstrumenter {
final private boolean[] deletedMethods;
final private MethodData[] methods;
final private CodeReader[] oldCode;
final private ClassReader cr;
final private ConstantPoolReader cpr;
private boolean createFakeLineNumbers = false;
private int fakeLineOffset;
private final String inputName;
private final ClassHierarchyProvider cha;
/**
* Create a class instrumenter from raw bytes.
*/
public ClassInstrumenter(String inputName, byte[] bytes, ClassHierarchyProvider cha) throws InvalidClassFileException {
this(inputName, new ClassReader(bytes), cha);
}
/**
* @return name of resource from which this class was read
*/
public String getInputName() {
return inputName;
}
/**
* Calling this means that methods without line numbers get fake line numbers added: each bytecode instruction is treated as at
* line 'offset' + the offset of the instruction.
*/
public void enableFakeLineNumbers(int offset) {
createFakeLineNumbers = true;
fakeLineOffset = offset;
}
/**
* Create a class instrumenter from a preinitialized class reader.
*
* @throws IllegalArgumentException if cr is null
*/
public ClassInstrumenter(String inputName, ClassReader cr, ClassHierarchyProvider cha) {
if (cr == null) {
throw new IllegalArgumentException("cr is null");
}
this.cr = cr;
this.cha = cha;
methods = new MethodData[cr.getMethodCount()];
oldCode = new CodeReader[methods.length];
cpr = CTDecoder.makeConstantPoolReader(cr);
deletedMethods = new boolean[methods.length];
this.inputName = inputName;
}
/**
* @return the reader for the class
*/
public ClassReader getReader() {
return cr;
}
/**
* Implement this interface to instrument every method of a class using visitMethods() below.
*/
public static interface MethodExaminer {
/**
* Do something to the method.
*/
public void examineCode(MethodData data);
}
private void prepareMethod(int i) throws InvalidClassFileException {
if (deletedMethods[i]) {
methods[i] = null;
} else if (methods[i] == null) {
ClassReader.AttrIterator iter = new ClassReader.AttrIterator();
cr.initMethodAttributeIterator(i, iter);
for (; iter.isValid(); iter.advance()) {
if (iter.getName().equals("Code")) {
CodeReader code = new CodeReader(iter);
CTDecoder d = new CTDecoder(code, cpr);
try {
d.decode();
} catch (InvalidBytecodeException e) {
throw new InvalidClassFileException(code.getRawOffset(), e.getMessage());
}
MethodData md = new MethodData(d, cr.getMethodAccessFlags(i), CTDecoder.convertClassToType(cr.getName()), cr
.getMethodName(i), cr.getMethodType(i));
methods[i] = md;
oldCode[i] = code;
return;
}
}
}
}
/**
* Indicate that the method should be deleted from the class.
*
* @param i the index of the method to delete
*/
public void deleteMethod(int i) {
deletedMethods[i] = true;
}
private final static ExceptionHandler[] noHandlers = new ExceptionHandler[0];
// Xiangyu
// create a empty method body and then user can apply patches later on
public MethodData createEmptyMethodData(String name, String sig, int access) {
// Instruction[] instructions=new Instruction[0];
Instruction[] instructions = new Instruction[1];
instructions[0] = ReturnInstruction.make(Constants.TYPE_void);
ExceptionHandler[][] handlers = new ExceptionHandler[instructions.length][];
Arrays.fill(handlers, noHandlers);
int[] i2b = new int[instructions.length];
for (int i = 0; i < i2b.length; i++) {
i2b[i] = i;
}
MethodData md = null;
try {
md = new MethodData(access, Util.makeType(cr.getName()), name, sig, instructions, handlers, i2b);
} catch (InvalidClassFileException ex) {
ex.printStackTrace();
}
return md;
}
/**
* Do something to every method in the class. This will visit all methods, including those already marked for deletion.
*
* @param me the visitor to apply to each method
*/
public void visitMethods(MethodExaminer me) throws InvalidClassFileException {
for (int i = 0; i < methods.length; i++) {
prepareMethod(i);
if (methods[i] != null) {
me.examineCode(methods[i]);
}
}
}
/**
* Get the current state of method i. This can be edited using a MethodEditor.
*
* @param i the index of the method to inspect
*/
public MethodData visitMethod(int i) throws InvalidClassFileException {
prepareMethod(i);
return methods[i];
}
/**
* Get the original code resource for the method.
*
* @param i the index of the method to inspect
*/
public CodeReader getMethodCode(int i) throws InvalidClassFileException {
prepareMethod(i);
return oldCode[i];
}
/**
* Reset method i back to the code from the original class, and "undelete" it if it was marked for deletion.
*
* @param i the index of the method to reset
*/
public void resetMethod(int i) {
deletedMethods[i] = false;
methods[i] = null;
}
/**
* Replace the code for method i with new code. This also "undeletes" the method if it was marked for deletion.
*
* @param i the index of the method to replace
* @throws IllegalArgumentException if md is null
*/
public void replaceMethod(int i, MethodData md) {
if (md == null) {
throw new IllegalArgumentException("md is null");
}
deletedMethods[i] = false;
methods[i] = md;
oldCode[i] = null;
md.setHasChanged();
}
/**
* Check whether any methods in the class have actually been changed.
*/
public boolean isChanged() {
for (int i = 0; i < methods.length; i++) {
if (deletedMethods[i] || (methods[i] != null && methods[i].getHasChanged())) {
return true;
}
}
return false;
}
/**
* Create a class which is a copy of the original class but with the new method code. We return the ClassWriter used, so more
* methods and fields (and other changes) can still be added.
*
* We fix up any debug information to be consistent with the changes to the code.
*/
public ClassWriter emitClass() throws InvalidClassFileException {
return emitClass(new ClassWriter());
}
public ClassWriter emitClass(ClassWriter w) throws InvalidClassFileException {
emitClassInto(w);
return w;
}
/**
* Copy the contents of the old class, plus any method modifications, into a new ClassWriter. The ClassWriter must be empty!
*
* @param w the classwriter to copy into.
*/
private void emitClassInto(ClassWriter w) throws InvalidClassFileException {
w.setMajorVersion(cr.getMajorVersion());
w.setMinorVersion(cr.getMinorVersion());
w.setRawCP(cr.getCP(), false);
w.setAccessFlags(cr.getAccessFlags());
w.setNameIndex(cr.getNameIndex());
w.setSuperNameIndex(cr.getSuperNameIndex());
w.setInterfaceNameIndices(cr.getInterfaceNameIndices());
int fieldCount = cr.getFieldCount();
for (int i = 0; i < fieldCount; i++) {
w.addRawField(new ClassWriter.RawElement(cr.getBytes(), cr.getFieldRawOffset(i), cr.getFieldRawSize(i)));
}
for (int i = 0; i < methods.length; i++) {
MethodData md = methods[i];
if (!deletedMethods[i]) {
if (md == null || !md.getHasChanged()) {
w.addRawMethod(new ClassWriter.RawElement(cr.getBytes(), cr.getMethodRawOffset(i), cr.getMethodRawSize(i)));
} else {
CTCompiler comp = CTCompiler.make(w, md);
comp.setPresetConstants(cpr);
try {
comp.compile();
} catch (Error ex) {
ex.printStackTrace();
throw new Error("Error compiling method " + md + ": " + ex.getMessage());
} catch (Exception ex) {
ex.printStackTrace();
throw new Error("Error compiling method " + md + ": " + ex.getMessage());
}
CodeReader oc = oldCode[i];
int flags = cr.getMethodAccessFlags(i);
// we're not installing a native method here
flags &= ~ClassConstants.ACC_NATIVE;
w.addMethod(flags, cr.getMethodNameIndex(i), cr.getMethodTypeIndex(i), makeMethodAttributes(i, w, oc, comp.getOutput(), md));
Compiler.Output[] aux = comp.getAuxiliaryMethods();
if (aux != null) {
for (int j = 0; j < aux.length; j++) {
Compiler.Output a = aux[j];
w.addMethod(a.getAccessFlags(), a.getMethodName(), a.getMethodSignature(), makeMethodAttributes(i, w, oc, a, md));
}
}
}
}
}
ClassReader.AttrIterator iter = new ClassReader.AttrIterator();
cr.initClassAttributeIterator(iter);
for (; iter.isValid(); iter.advance()) {
w.addClassAttribute(new ClassWriter.RawElement(cr.getBytes(), iter.getRawOffset(), iter.getRawSize()));
}
}
private static CodeWriter makeNewCode(ClassWriter w, Compiler.Output output) {
CodeWriter code = new CodeWriter(w);
code.setMaxStack(output.getMaxStack());
code.setMaxLocals(output.getMaxLocals());
code.setCode(output.getCode());
code.setRawHandlers(output.getRawHandlers());
return code;
}
private LineNumberTableWriter makeNewLines(ClassWriter w, CodeReader oldCode, Compiler.Output output)
throws InvalidClassFileException {
int[] newLineMap = null;
int[] oldLineMap = LineNumberTableReader.makeBytecodeToSourceMap(oldCode);
if (oldLineMap != null) {
// Map the old line number map onto the new bytecodes
int[] newToOldMap = output.getNewBytecodesToOldBytecodes();
newLineMap = new int[newToOldMap.length];
for (int i = 0; i < newToOldMap.length; i++) {
int old = newToOldMap[i];
if (old >= 0) {
newLineMap[i] = oldLineMap[old];
}
}
} else if (createFakeLineNumbers) {
newLineMap = new int[output.getCode().length];
for (int i = 0; i < newLineMap.length; i++) {
newLineMap[i] = i + fakeLineOffset;
}
} else {
return null;
}
// Now compress it into the JVM form
int[] rawTable = LineNumberTableWriter.makeRawTable(newLineMap);
if (rawTable == null || rawTable.length == 0) {
return null;
} else {
LineNumberTableWriter lines = new LineNumberTableWriter(w);
lines.setRawTable(rawTable);
return lines;
}
}
private static LocalVariableTableWriter makeNewLocals(ClassWriter w, CodeReader oldCode, Compiler.Output output)
throws InvalidClassFileException {
int[][] oldMap = LocalVariableTableReader.makeVarMap(oldCode);
if (oldMap != null) {
// Map the old map onto the new bytecodes
int[] newToOldMap = output.getNewBytecodesToOldBytecodes();
int[][] newMap = new int[newToOldMap.length][];
int[] lastLocals = null;
for (int i = 0; i < newToOldMap.length; i++) {
int old = newToOldMap[i];
if (old >= 0) {
newMap[i] = oldMap[old];
lastLocals = newMap[i];
} else {
newMap[i] = lastLocals;
}
}
int[] rawTable = LocalVariableTableWriter.makeRawTable(newMap, output);
if (rawTable == null || rawTable.length == 0) {
return null;
} else {
LocalVariableTableWriter locals = new LocalVariableTableWriter(w);
locals.setRawTable(rawTable);
return locals;
}
} else {
return null;
}
}
private ClassWriter.Element[] makeMethodAttributes(int m, ClassWriter w, CodeReader oldCode, Compiler.Output output, MethodData md)
throws InvalidClassFileException {
CodeWriter code = makeNewCode(w, output);
int codeAttrCount = 0;
LineNumberTableWriter lines = null;
LocalVariableTableWriter locals = null;
StackMapTableWriter stacks = null;
if (oldCode != null) {
lines = makeNewLines(w, oldCode, output);
if (lines != null) {
codeAttrCount++;
}
locals = makeNewLocals(w, oldCode, output);
if (locals != null) {
codeAttrCount++;
}
if (oldCode.getClassReader().getMajorVersion() > 50) {
try {
List<StackMapFrame> sm = StackMapTableReader.readStackMap(oldCode);
String[][] varTypes = null;
int[] newToOld = output.getNewBytecodesToOldBytecodes();
int[][] vars = LocalVariableTableReader.makeVarMap(oldCode);
if (vars != null) {
varTypes = new String[newToOld.length][];
for(int i = 0; i < newToOld.length; i++) {
int idx = newToOld[i];
if (idx != -1 && vars[idx] != null) {
varTypes[i] = new String[vars[idx].length / 2];
for(int j = 1; j < vars[idx].length; j += 2) {
int type = vars[idx][j];
varTypes[i][j/2] = type==0? null: oldCode.getClassReader().getCP().getCPUtf8(type);
}
}
}
}
stacks = new StackMapTableWriter(w, md, output, cha, varTypes , sm);
codeAttrCount++;
} catch (IOException | FailureException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
ClassWriter.Element[] codeAttributes = new ClassWriter.Element[codeAttrCount];
int codeAttrIndex = 0;
if (lines != null) {
codeAttributes[codeAttrIndex++] = lines;
}
if (locals != null) {
codeAttributes[codeAttrIndex++] = locals;
}
if (stacks != null) {
codeAttributes[codeAttrIndex++] = stacks;
}
code.setAttributes(codeAttributes);
ClassReader.AttrIterator iter = new ClassReader.AttrIterator();
cr.initMethodAttributeIterator(m, iter);
int methodAttrCount = iter.getRemainingAttributesCount();
if (oldCode == null) {
methodAttrCount++;
}
ClassWriter.Element[] methodAttributes = new ClassWriter.Element[methodAttrCount];
for (int i = 0; iter.isValid(); iter.advance()) {
if (iter.getName().equals("Code")) {
methodAttributes[i] = code;
code = null;
if (oldCode == null) {
throw new Error("No old code provided, but Code attribute found");
}
} else {
methodAttributes[i] = new ClassWriter.RawElement(cr.getBytes(), iter.getRawOffset(), iter.getRawSize());
}
i++;
}
if (oldCode == null) {
if (code == null) {
throw new Error("Old code not provided but existing code was found and replaced");
}
methodAttributes[methodAttrCount - 1] = code;
}
return methodAttributes;
}
}