Using Instrumentation to record unhandled exception
I think you should use ASM to manipulate bytecode directly. Here is algoritms:
- Visit all try/catch blocks (see visitTryCatchBlock) and save all
handler
labels - Visit instructions until one of the
handler
labels met. -
After
handler
label insert logging codeGETSTATIC java/lang/System out LDC "exception X occured" INVOKEVIRTUAL java/io/PrintStream println (java/lang/String)V
And ensure that your javaagent works fine. Checkt that your MANIFEST.MF file contains proper premain
declaration and enables class transformation.
About your current code. Here
if (className.startsWith("com/alu/")) {
return insertLog(className, classBeingRedefined, classfileBuffer);
}
you transforming classes inside particular package. That classes contain code that, in particular, throw exceptions.
And here
if(className.endsWith("Exception")){
System.out.println("============= exception occured "+className);
}
you log of class being retransfomed when it is first loaded by JVM, when its name ends with "Exception"
. Not when exception occured. But transforming exception is useless itself. So I guess you should proceed like this:
if (className.startsWith("com/alu/")) {
System.out.println("============= class transformed "+ className);
return insertLog(className, classBeingRedefined, classfileBuffer);
}
So you could know that your agent at least works.
You have to deal with code like this
try {
if(search.equals("Category")){
//do operation
}
} catch (Exception e) {
}
where exceptions are swallowed. You transform methods that they will be like this:
try {
try {
if(search.equals("Category")){
//do operation
}
} catch (Exception e) {
}
} catch (Exception e) {
e.printStackTrace();
}
Of course, when exception was swallowed by the first catch
, the second one never cathes it. Instead, you should transform existing catch
blocks themself, to get the following code:
try {
if(search.equals("Category")){
//do operation
}
} catch (Exception e) {
e.printStackTrace();
}
Above I shown you how to achieve this with ASM.
After more research, I found a starting point(thanks to @jarnbjo), which I believe take me to complete solution
There is an option to retransform System
classes,
SimpleClassTransformer transformer = new SimpleClassTransformer();
instrumentation.addTransformer(transformer,true);
try {
instrumentation.retransformClasses(java.lang.NullPointerException.class);
} catch (UnmodifiableClassException e) {
e.printStackTrace();
}
I applied this code in my premain
class and able to reload the NullPointerException class. I still would need to work on Transformer
implementation to enable instant recording of exception.
[update 2] Finally I got a break through!!
After re transforming the required Exception class, I injected my code right inside the constructor of the Exception
class.
Here is my transformer class,
public class SimpleClassTransformer implements ClassFileTransformer{
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException {
System.out.println("----------------- "+className);
if (className.startsWith("com/alu/")) {
return insertLog(className, classBeingRedefined, classfileBuffer);
}
if(className.endsWith("Exception")){
System.out.println("============= exception occured");
return recordError(className, classBeingRedefined, classfileBuffer);
}
return classfileBuffer;
}
private byte[] recordError(String name, Class clazz, byte[] b){
ClassPool pool = ClassPool.getDefault();
CtClass cl = null;
try {
cl = pool.makeClass(new java.io.ByteArrayInputStream(b));
if (cl.isInterface() == false) {
CtConstructor[] constr=cl.getDeclaredConstructors();
for(CtConstructor con:constr){
con.insertAfter("System.out.println(\"Hey hurrray I got you mannnnnnn -------\"); ");
}
b = cl.toBytecode();
}
} catch (Exception e) {
System.out.println(e);
} finally {
if (cl != null) {
cl.detach();
}
}
return b;
}
I solved this challenge this way. I used javaassist method addCatch
.
Short demo
CtClass exceptionCtClass = ClassPool.getDefault().makeClass("java.lang.Exception");
method.addCatch("{ System.out.println(\"Caught exception\");\n$_e.printStackTrace();\nthrow $_e; }", exceptionCtClass, "$_e");
Full example
public class ExceptionReporterAgent {
public static void premain(final String agentArgs, final Instrumentation inst) {
inst.addTransformer(new ExceptionReporterTransformer());
}
}
public class ExceptionReporterTransformer implements ClassFileTransformer {
private CtClass exceptionCtClass;
public byte[] transform(ClassLoader loader, String className, Class redefiningClass, ProtectionDomain domain, byte[] bytes) {
return transformClass(redefiningClass, bytes);
}
private byte[] transformClass(Class classToTransform, byte[] b) {
ClassPool pool = ClassPool.getDefault();
CtClass cl = null;
try {
cl = pool.makeClass(new java.io.ByteArrayInputStream(b));
if (cl.getName().endsWith("ClassCException")) {
exceptionCtClass = cl; //Or any exception, you can move this logic into constructor.
} else {
modifyClass(cl);
}
b = cl.toBytecode();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (cl != null) {
cl.detach();
}
}
return b;
}
private byte[] modifyClass(CtClass cl) throws Exception {
CtBehavior[] methods = cl.getDeclaredBehaviors();
for (CtBehavior method : methods) {
changeMethod(method);
}
return cl.toBytecode();
}
private void changeMethod(CtBehavior method) throws CannotCompileException {
method.addLocalVariable("$_start", CtClass.longType);
method.addLocalVariable("$_end", CtClass.longType);
method.addLocalVariable("$_total", CtClass.longType);
method.insertBefore("{ $_start = System.currentTimeMillis(); }");
//For methods returning String
// method.insertAfter("{ $_end = System.currentTimeMillis();\n$_total = $_end - $_start;\nSystem.out.println(\"Total: \" + $_total);return \"AAA\"; }");
method.addCatch("{ System.out.println(\"Caught exception\");\n$_e.printStackTrace();\nthrow $_e; }", exceptionCtClass, "$_e");
//For methods returning String
// method.insertAfter("{ System.out.println(\"Finally\");\nreturn \"ABC\"; }", true);
method.insertBefore("{ System.out.println(\"Before\"); }");
System.out.println("Modifying method: " + method.getName());
}
}