Instrumentation + BCEL | ASM

Discussion in 'Java' started by Boris Gorjan, Jun 6, 2006.

  1. Boris Gorjan

    Boris Gorjan Guest

    I'm trying to utilize instrumentation to modify bytecode of the loaded classes.
    Having added MyTransformer to Instrumentation in MyAgent's premain(String,
    Instrumentation) and using VM comand line argument -javaagent:myjar.jar=myargs I
    can intercept the bytecode before it is loaded, through the

    public byte[] transform(
    ClassLoader loader,
    String className,
    Class redefiningClass,
    ProtectionDomain domain,
    byte[] bytes
    )
    throws IllegalClassFormatException
    {
    System.out.println("Transforming: " + className);
    return bytes;
    }

    OK so far.

    Now, MyTransformer is doing nothing at the moment, except returning unmodified
    bytecode. What I'd like to do is this: for a specified method I'd like to wrap
    the whole body of the method in a try - finally, like this:

    try {
    MyTiming.start();
    /*
    Old method body / code.
    */
    }
    finally {
    MyTiming.end();
    }

    What's the easyest way to do this?

    I looked into BCEL; using DecendingVisitor (and MyVisitor which implements
    Visitor: all the visit*() methods) I can go through the bytecode in a similar
    way as using a SAX parser for XML. But, I don't know how to modify the code in
    an above mentioned way letting most of it through unmodified.

    Can anyone give me some ideas / guidance, please?

    How about using ASM? I'd also be happy using that instead of BCEL. I read it's
    somewhat faster.

    Thanks.
    Boris Gorjan, Jun 6, 2006
    #1
    1. Advertising

  2. bcel can do it. Dont exacly remember how try - catch works in bytecode,
    but i remember api is quite simple. if u'r capable of obtaining method
    object, u should be able to get instruction list, and put some
    instructions at the beginning, and at the end (look for them in the api
    docs), but u have to remember to go through all instructions in that
    method, and correct all jumps (if, catch, while and so on). If u want some
    sample code i can send it to u.

    --
    SaSol
    Marcin Wielgus, Jun 6, 2006
    #2
    1. Advertising

  3. Boris Gorjan

    Boris Gorjan Guest

    Marcin Wielgus wrote:
    > bcel can do it. Dont exacly remember how try - catch works in bytecode,
    > but i remember api is quite simple. if u'r capable of obtaining method
    > object,


    As things stand now I can obtain everything in org.apache.bcel.classfile.Visitor
    (now I see that there's also an org.apache.bcel.generic.Visitor).

    What I don't know how to do (yet ;-) ) is how to pack all the data I obtain
    through that Visitor, into a new bytecode (byte[]). A good exercise would be to
    pack it unmodified, I know, but that'd take me a long time.

    Intuitively I'd go about it this way: make a new JavaClass(), use its set*()
    methods to fill it with data obtained through a Visitor (an implementation of
    the classfile one) and finally use some dump(*) or getBytes() method to produce
    the bytecode. Am I on the right track?

    > u should be able to get instruction list,


    I can get org.apache.bcel.classfile.Method and org.apache.bcel.classfile.Code
    for that method, but neither of them has a "handle" on InstructionList. I don't
    see it, to be precise.

    I can't seem to "connect" a Visitor, that is instances of classes its visit*()
    methods take as arguments, and those with their respective InstructionLists.

    > and put some
    > instructions at the beginning, and at the end (look for them in the api
    > docs), but u have to remember to go through all instructions in that
    > method, and correct all jumps (if, catch, while and so on). If u want
    > some sample code i can send it to u.


    I'd be most grateful. Thanks. Use the boris.gorjan address, please.
    Boris Gorjan, Jun 6, 2006
    #3
  4. Boris Gorjan

    Boris Gorjan Guest

    Boris Gorjan wrote:

    > I can get org.apache.bcel.classfile.Method and
    > org.apache.bcel.classfile.Code for that method, but neither of them has
    > a "handle" on InstructionList. I don't see it, to be precise.
    >
    > I can't seem to "connect" a Visitor, that is instances of classes its
    > visit*() methods take as arguments, and those with their respective
    > InstructionLists.


    Found it! In Visitors visit*() methods, just use .getCode() from an argument
    object and create a new InstructionList. Like this:

    public void visitCode(Code code) {
    InstructionList instructionList = new InstructionList(code.getCode());
    // ...
    }
    Boris Gorjan, Jun 6, 2006
    #4
  5. Boris Gorjan

    Boris Gorjan Guest

    Boris Gorjan wrote:

    Nailed it! With a little help from my firends. ;-)
    [imagine a lengthy oscars-style-thank-you litany here]

    Just one more thing. As said in my initial post, MyTransformer (implementing
    ClassFileTransformer) implements a method

    public byte[] transform(ClassLoader classLoader, String className, Class
    redefiningClass, ProtectionDomain domain, byte[] bytes),

    where byte[] bytes are supposed to be "the input byte buffer in class file
    format - must not be modified" (quoting J2SE 1.5 API docs).

    I only know how to obtain an instance of org.apache.bcel.classfile.JavaClass
    (which I later transform) from org.apache.bcel.Repository like this:

    JavaClass clazz = Repository.lookupClass(className.replace('/', '.'));

    Is there a (simple) way to obtain that instance using byte[] bytes directly?
    Boris Gorjan, Jun 6, 2006
    #5
  6. Boris Gorjan

    Piotr Kobzda Guest

    Boris Gorjan wrote:

    > How about using ASM? I'd also be happy using that instead of BCEL. I
    > read it's somewhat faster.


    That is quite easy task for ASM, if you know what is exactly the
    bytecode you want to achieve.

    See code provided below for one of the possible solutions.
    It could be interesting to see, how your already achieved BCEL solution
    looks like (I have no experience with BCEL), so if it's not a problem
    for you, please send it here.


    Also consider using some APO framework, for example AspectJ, which
    allows doing similar things (and much more) easier.


    Regards,
    piotr

    --

    import org.objectweb.asm.*;
    import static org.objectweb.asm.Opcodes.*;

    class AddTimingCallsClassAdapter extends ClassAdapter {
    AddTimingCallsClassAdapter(ClassVisitor cv) {
    super(cv);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String
    desc, String signature, String[] exceptions) {

    return new MethodAdapter(super.visitMethod(access, name, desc,
    signature, exceptions)) {

    Label startLabel = new Label();
    Label finallyLabel = new Label();
    int retOpcode = RETURN;


    private void genStartCode() {
    super.visitMethodInsn(INVOKESTATIC, "MyTiming", "start", "()V");
    }

    private void genEndCode() {
    super.visitMethodInsn(INVOKESTATIC, "MyTiming", "end", "()V");
    }

    @Override
    public void visitCode() {
    super.visitCode();

    super.visitLabel(startLabel);

    genStartCode();

    // ...
    }

    @Override
    public void visitInsn(int opcode) {
    if (isRetInsn(opcode)) {
    retOpcode = opcode;
    super.visitJumpInsn(GOTO, finallyLabel);
    } else {
    super.visitInsn(opcode);
    }
    }

    @Override
    public void visitEnd() {
    // ...

    Label endLabel = new Label();
    super.visitLabel(endLabel);
    super.visitVarInsn(ASTORE, 1);

    genEndCode();

    super.visitVarInsn(ALOAD, 1);
    super.visitInsn(ATHROW);

    super.visitLabel(finallyLabel);

    genEndCode();

    super.visitInsn(retOpcode);

    super.visitTryCatchBlock(startLabel, endLabel, endLabel, null);

    super.visitEnd();
    }

    };
    }

    static boolean isRetInsn(int opcode) {
    switch(opcode) {
    case IRETURN:
    case LRETURN:
    case FRETURN:
    case DRETURN:
    case ARETURN:
    case RETURN:
    return true;
    default:
    return false;
    }
    }

    }
    Piotr Kobzda, Jun 6, 2006
    #6
  7. Boris Gorjan

    Piotr Kobzda Guest

    Boris Gorjan napisał(a):

    > Is there a (simple) way to obtain that instance using byte[] bytes
    > directly?


    Based on BCEL Manual and Javadoc, it should work this way:

    JavaClass clazz = new ClassParser(new ByteArrayInputStream(bytes),
    className).parse();

    (I've never tried it out)


    Regards,
    piotr
    Piotr Kobzda, Jun 6, 2006
    #7
  8. Boris Gorjan

    Boris Gorjan Guest

    Piotr Kobzda wrote:
    > Boris Gorjan napisał(a):
    >
    >> Is there a (simple) way to obtain that instance using byte[] bytes
    >> directly?

    >
    > Based on BCEL Manual and Javadoc, it should work this way:
    >
    > JavaClass clazz = new ClassParser(new ByteArrayInputStream(bytes),
    > className).parse();
    >
    > (I've never tried it out)


    It works. Thanks.
    Boris Gorjan, Jun 6, 2006
    #8
  9. Boris Gorjan

    Piotr Kobzda Guest

    Boris Gorjan wrote:

    > Thanks for your code. Mine is also attached.


    Thanks for yours. Comparing it with my ASM code I start to see more
    differences between this two bytecode manipulation frameworks. Now I
    think these differences, in general, are like a differences between DOM
    and SAX, both gives us possibilities for processing bytecode/XML, but
    each in a different way.
    As the result ASM is still my favorite Java bytecode processing
    framework. :)

    Just one comment on your code. I feel a bytecode generated using your
    code is not exactly what you'd like to achieve.

    If I understand your code well, your transformation results are similar
    to the following sample code:

    void someMethod() {
    | try {
    | MyTiming.start();

    ....

    RuntimeException e ...
    | MyTiming.end();
    throw e;

    ....

    | MyTiming.end();
    return;

    | } catch(Throwable t) {
    | MyTiming.end();
    | throw t;
    | }
    }

    (where | indicates the method changes performed by transformation)

    First problem with that is in a places where the original method's code
    throws an exception. In such a cases your end() method is called twice
    (first time from call added into try clause body, second time from a
    catch body).

    Second similar problem is with end() calls added into a try body, an
    exception thrown from your end() method will cause another call of end()
    from a catch body.


    To achieve the result mentioned in your first post, you should generate
    (which my ASM code strive to do) a bytecode similar to the following
    sketch code:

    void someMethod() {
    | try {
    | MyTiming.start();

    ....

    RuntimeException e ...
    throw e;

    ....

    | goto end;
    | //return; <- replaced with goto

    | } catch(Throwable t) {
    | MyTiming.end();
    | throw t;
    | }
    | end:
    | MyTiming.end();
    }


    If you'd like try my ASM code to see differences, simply add the
    following code to your transform method:

    ClassReader cr = new ClassReader(bytes);
    ClassWriter cw = new ClassWriter(true);
    ClassVisitor cv = new AddTimingCallsClassAdapter(cw);
    cr.accept(cv, false);
    return cw.toByteArray();


    Regards,
    piotr
    Piotr Kobzda, Jun 7, 2006
    #9
  10. Boris Gorjan

    Boris Gorjan Guest

    Boris Gorjan wrote:
    > When I execute the call to so modified class.method
    > (MyTiming.example1())


    Make that TimingTest.example1().
    Boris Gorjan, Jun 7, 2006
    #10
  11. Boris Gorjan

    Piotr Kobzda Guest

    Boris Gorjan wrote:

    > public class TimingTransformerAsm implements ClassFileTransformer {

    [...]
    > //dump(bytes);
    > try {
    > ClassWriter classWriter = new ClassWriter(0);


    I do not recognize this constructor of ClassWriter, which version of ASM
    are you using?


    > public class AddTimingCallsClassAdapter extends ClassAdapter {
    >
    > private ClassVisitor parent;


    You don't need that field. ClassAdapter has a protected instance
    variable called 'cv' for you.

    > private void genStartCode() {
    > super.visitMethodInsn(
    > Opcodes.INVOKESTATIC,
    > MyTiming.class.getName(),


    Here is the problem. You are using Java language class name, not the JVM
    expected one. You can use ASM helper Type class to get that name, this way:

    Type.getType(MyTiming.class).getClassName()

    > MyTiming.class.getName(),


    The same here.

    >
    > public void visitEnd() {


    This is another problem (my bug, just discovered by me). visitEnd() will
    be called for abstract and native methods too.
    To fix that replace with:

    public void visitMaxs(int maxStack, int maxLocals) {

    > super.visitEnd();


    and this, with:

    super.visitMaxs(maxStack, maxLocals);


    > After transformation:
    >
    > // access flags 1
    > public example1(Ljava/lang/String;)V
    > TRYCATCHBLOCK L0 L1 L1 null
    > L0
    > INVOKESTATIC path.MyTiming.start ()V

    ^
    should be '/' after correction.


    Regards,
    piotr
    Piotr Kobzda, Jun 7, 2006
    #11
  12. Boris Gorjan

    Boris Gorjan Guest

    Piotr Kobzda wrote:
    > Boris Gorjan wrote:
    >
    >> public class TimingTransformerAsm implements ClassFileTransformer {

    > [...]
    >> //dump(bytes);
    >> try {
    >> ClassWriter classWriter = new ClassWriter(0);

    >
    > I do not recognize this constructor of ClassWriter, which version of ASM
    > are you using?


    I downloaded 3.0 beta 2. Perhaps I should play it safe and stay with 2.2.1. If
    3.0 isn't released before I release my code, I'll have to go with 2.2.1 anyway.

    The API docs for 3.0 beta 2 say:

    ---------------------

    ClassWriter

    public ClassWriter(int flags)

    Constructs a new ClassWriter object.

    Parameters:
    flags - option flags that can be used to modify the default behavior of
    this class. See COMPUTE_MAXS, COMPUTE_FRAMES.

    ---

    COMPUTE_MAXS

    public static final int COMPUTE_MAXS

    Flag to automatically compute the maximum stack size and the maximum number
    of local variables of methods. If this flag is set, then the arguments of the
    visitMaxs method of the MethodVisitor returned by the visitMethod method will be
    ignored, and computed automatically from the signature and the bytecode of each
    method.

    ---

    COMPUTE_FRAMES

    public static final int COMPUTE_FRAMES

    Flag to automatically compute the stack map frames of methods from scratch.
    If this flag is set, then the calls to the MethodVisitor.visitFrame(int, int,
    java.lang.Object[], int, java.lang.Object[]) method are ignored, and the stack
    map frames are recomputed from the methods bytecode. The arguments of the
    visitMaxs method are also ignored and recomputed from the bytecode. In other
    words, computeFrames implies computeMaxs.

    ---------------------

    I thought I'd use 0, hoping for some reasonable default behaviour. Should I use
    something else?

    >> public class AddTimingCallsClassAdapter extends ClassAdapter {
    >>
    >> private ClassVisitor parent;

    >
    > You don't need that field. ClassAdapter has a protected instance
    > variable called 'cv' for you.


    OK.

    >> private void genStartCode() {
    >> super.visitMethodInsn(
    >> Opcodes.INVOKESTATIC,
    >> MyTiming.class.getName(),

    >
    > Here is the problem. You are using Java language class name, not the JVM
    > expected one. You can use ASM helper Type class to get that name, this way:
    >
    > Type.getType(MyTiming.class).getClassName()


    Tried this. Surprisingly, to no avail. But then I used
    MyTiming.class.getName().replace('.', '/') and it worked like a charm. Thanks a
    million.

    >> MyTiming.class.getName(),

    >
    > The same here.
    >
    >>
    >> public void visitEnd() {

    >
    > This is another problem (my bug, just discovered by me). visitEnd() will
    > be called for abstract and native methods too.
    > To fix that replace with:
    >
    > public void visitMaxs(int maxStack, int maxLocals) {
    >
    >> super.visitEnd();

    >
    > and this, with:
    >
    > super.visitMaxs(maxStack, maxLocals);


    Which reminds me...

    Not only should abstract and native methods stay unmodified, but, to be on the
    safe side, already modified methods (or the ones with hardcoded calls of
    MyTiming.start() and MyTiming.end()) should not be modified again (starting the
    timer twice in a row could even be regarded as an error). I'll have to include
    that in my code. If I run into trouble, and I most probably will, I'll have to
    scream for help again. So, stay tuned or I'll be
    back^H^H^H^H^H^H^H^H^H^H^H^H^H^H^H, please. ;-)

    >> After transformation:
    >>
    >> // access flags 1
    >> public example1(Ljava/lang/String;)V
    >> TRYCATCHBLOCK L0 L1 L1 null
    >> L0
    >> INVOKESTATIC path.MyTiming.start ()V

    > ^
    > should be '/' after correction.
    >
    >
    > Regards,
    > piotr


    Again, thanks a lot.
    Boris Gorjan, Jun 7, 2006
    #12
  13. Boris Gorjan

    Piotr Kobzda Guest

    Boris Gorjan wrote:

    > I downloaded 3.0 beta 2. Perhaps I should play it safe and stay with
    > 2.2.1. If 3.0 isn't released before I release my code, I'll have to go
    > with 2.2.1 anyway.


    OK, it explains my confusion, I'm still using ASM 2.1.


    > I thought I'd use 0, hoping for some reasonable default behaviour.
    > Should I use something else?


    As I've shown in my code usage example, I'm using COMPUTE_MAXS
    equivalent (true for computeMaxs parameter of ClassWriter's
    constructor). This is needed because generated additional bytecode may
    require an increasing of minimum local variables stack size (of course
    you can compute it in visitMaxs, if you wish). COMPUTE_FRAMES option is
    new for me, and i think you can freely skip it in your case.

    >>
    >> Type.getType(MyTiming.class).getClassName()

    >
    > Tried this. Surprisingly, to no avail. But then I used
    > MyTiming.class.getName().replace('.', '/') and it worked like a charm.


    Heh, I'm to hurry... I thought about Type.getInternalName(), which in
    fact, gives the same results as your workaround.


    >
    > Which reminds me...
    >
    > Not only should abstract and native methods stay unmodified, but, to be
    > on the safe side, already modified methods (or the ones with hardcoded
    > calls of MyTiming.start() and MyTiming.end()) should not be modified
    > again (starting the timer twice in a row could even be regarded as an
    > error). I'll have to include that in my code. If I run into trouble, and
    > I most probably will, I'll have to scream for help again. So, stay tuned
    > or I'll be back^H^H^H^H^H^H^H^H^H^H^H^H^H^H^H, please. ;-)


    OK. :)

    But I think you can simply skip this problem, simply don't do use your
    start() and end() methods directly. :)
    If you are really care about it, make these methods private and change
    (transform) to public at runtime.


    Regards,
    piotr
    Piotr Kobzda, Jun 7, 2006
    #13
  14. Boris Gorjan

    Boris Gorjan Guest

    Piotr Kobzda wrote:

    >> Not only should abstract and native methods stay unmodified, but, to
    >> be on the safe side, already modified methods (or the ones with
    >> hardcoded calls of MyTiming.start() and MyTiming.end()) should not be
    >> modified again (starting the timer twice in a row could even be
    >> regarded as an error). I'll have to include that in my code. If I run
    >> into trouble, and I most probably will, I'll have to scream for help
    >> again. So, stay tuned or I'll be back^H^H^H^H^H^H^H^H^H^H^H^H^H^H^H,
    >> please. ;-)

    >
    > OK. :)
    >
    > But I think you can simply skip this problem, simply don't do use your
    > start() and end() methods directly. :)


    Ideally, yes. However, MyTiming has an additional functionality not covered by
    automatical transformation (yet :) ) : it can time an arbitrary block of code.
    Provided one uses unique ids (unique within a method), one can call:

    void myMethod() {

    try {
    MyTiming.start("myid");
    //do something here
    }
    finally {
    MyTiming.end("myid");
    }

    //do something here

    try {
    MyTiming.start("anotherid");
    //do something here
    }
    finally {
    MyTiming.end("anotherid");
    }

    }

    As I don't know how to identify particular blocks to do this through
    instrumentation / transformation, I thought I'd leave this functionality for
    "manual" use.

    By "identify" I mean:

    1. how to specify those blocks in an init (properties) file used to initialize
    the adapter and

    2. how to transform those blocks from within the adapter.

    That complicates things a bit. Adapter and MyTiming have to be compatible
    (compatibly intialized, to be exact): adapter has to transform only those
    methods that MyTiming will later take into account. And those are generally
    quite few because MyTiming has to store quite a bit of data: at least a stack
    trace equivalent (+ blockid, if there is one) and a list of durations for every
    timed block / method.

    > If you are really care about it, make these methods private and change
    > (transform) to public at runtime.


    A sneaky idea. :) A good one, though. But easyer said than done. Perhaps someday.
    Boris Gorjan, Jun 8, 2006
    #14
  15. Boris Gorjan

    Boris Gorjan Guest

    Piotr Kobzda wrote:

    [snip]

    > This is another problem


    I have another one, too. Namely, using your code I can't transform methods

    1. which don't have any arguments
    (verifier says java.lang.VerifyError: (class: path/to/TimingTest, method: test
    signature: ()V) Illegal local variable number) )

    and

    2. methods with $ in their names (like access$0) -> inner classes?
    (verifier says java.lang.VerifyError: (class: path/to/TimingTest, method:
    access$0 signature: (Lpath/to/TimingTest;)V) Illegal local variable number)

    Could you look into that, please?

    Another thing: my (first ;-) ) effort of detecting already modified mehods
    failed miserably. I added another visit method into MethodAdapter:

    public void visitMethodInsn(
    final int opcode,
    final String owner,
    final String aName,
    final String aDesc
    ) {
    super.visitMethodInsn(opcode, owner, aName, desc);
    if(
    owner.equals("path/to/MyTiming)
    && (
    aName.equals("start")
    || aName.equals("end)
    )
    ) {
    throw new RuntimeException(name + desc
    + " already contains timing calls. Aborting.");
    }
    }

    Strangely enough, with this method added, I can only transform methods with
    exactly one argument of type String). For all others a transformed class fails
    verification.

    Any suggestions?

    [snip]
    Boris Gorjan, Jun 8, 2006
    #15
  16. Boris Gorjan

    Piotr Kobzda Guest

    Boris Gorjan wrote:
    > Piotr Kobzda wrote:
    >
    > [snip]
    >
    >> This is another problem

    >
    > I have another one, too. Namely, using your code I can't transform methods
    >
    > 1. which don't have any arguments (verifier says
    > java.lang.VerifyError: (class: path/to/TimingTest, method: test
    > signature: ()V) Illegal local variable number) )
    >
    > and
    >
    > 2. methods with $ in their names (like access$0) -> inner classes?
    > (verifier says java.lang.VerifyError: (class: path/to/TimingTest,
    > method: access$0 signature: (Lpath/to/TimingTest;)V) Illegal local
    > variable number)
    >
    > Could you look into that, please?


    It seems to me you have incorrectly computed max locals value for your
    methods. Have you set computeMaxs option in your ClassWriter?

    My code (with ASM 2.1) transforms perfect all kind of methods in my
    tests (synthetic, parameterized etc.).

    There is thousand possible reasons of your code malfunction, but first
    of all ensure you are not fighting with some bugs of ASM beta 3, so if
    you haven't done it yet, switch to the stable ASM version.


    > Another thing: my (first ;-) ) effort of detecting already modified
    > mehods failed miserably. I added another visit method into MethodAdapter:
    >
    > public void visitMethodInsn(
    > final int opcode,
    > final String owner,
    > final String aName,
    > final String aDesc
    > ) {
    > super.visitMethodInsn(opcode, owner, aName, desc);
    > if(
    > owner.equals("path/to/MyTiming)
    > && (
    > aName.equals("start")
    > || aName.equals("end)
    > )
    > ) {
    > throw new RuntimeException(name + desc
    > + " already contains timing calls. Aborting.");
    > }
    > }
    >
    > Strangely enough, with this method added, I can only transform methods
    > with exactly one argument of type String). For all others a transformed
    > class fails verification.


    Very strange... And I have no concept what's the reason of that, show a
    little more code.


    > Any suggestions?


    After reading this and your previous post I think, that you don't have
    to care about existing start() and end() calls within your code.
    Similar problem exist for example in a case of recursive calls, your
    start() method will be called many times before end(). And it should be
    YourTiming start() and end() methods implementation responsibility to
    treat such a scenarios well.

    You said before that there is some kind of stack-trace computed in your
    implementation, so having it in a calls to end() you can find out which
    start() this call is related to (simply ignoring all unrecognized end()
    calls). This needs some kind of method calls stack reflected in your
    implementation but will give your users a freedom of choices.
    Moreover, you will no longer need end() taking a String parameter in
    this scenario, because you can assume that each end() call is always
    related to the last start() call (parameterized or not) performed in the
    same level of method calls stack.

    Hope that's clear. :)


    Regards,
    piotr
    Piotr Kobzda, Jun 8, 2006
    #16
  17. Boris Gorjan

    Boris Gorjan Guest

    Piotr Kobzda wrote:
    > Boris Gorjan wrote:
    >> Piotr Kobzda wrote:
    >>
    >> [snip]
    >>
    >>> This is another problem

    >>
    >> I have another one, too. Namely, using your code I can't transform
    >> methods
    >>
    >> 1. which don't have any arguments (verifier says
    >> java.lang.VerifyError: (class: path/to/TimingTest, method: test
    >> signature: ()V) Illegal local variable number) )
    >>
    >> and
    >>
    >> 2. methods with $ in their names (like access$0) -> inner classes?
    >> (verifier says java.lang.VerifyError: (class: path/to/TimingTest,
    >> method: access$0 signature: (Lpath/to/TimingTest;)V) Illegal local
    >> variable number)
    >>
    >> Could you look into that, please?

    >
    > It seems to me you have incorrectly computed max locals value for your
    > methods. Have you set computeMaxs option in your ClassWriter?


    Erm... I did. Just now. ;-)

    According to tests so far, it works fine. Thanks.

    > My code (with ASM 2.1) transforms perfect all kind of methods in my
    > tests (synthetic, parameterized etc.).
    >
    > There is thousand possible reasons of your code malfunction, but first
    > of all ensure you are not fighting with some bugs of ASM beta 3, so if
    > you haven't done it yet, switch to the stable ASM version.


    Tried that, too. Tests up to now show no visible differences. I haven't compared
    bytecode, yet.

    >> Another thing: my (first ;-) ) effort of detecting already modified
    >> mehods failed miserably. I added another visit method into MethodAdapter:
    >>
    >> public void visitMethodInsn(
    >> final int opcode,
    >> final String owner,
    >> final String aName,
    >> final String aDesc
    >> ) { super.visitMethodInsn(opcode, owner, aName,
    >> desc);
    >> if(
    >> owner.equals("path/to/MyTiming")
    >> && (
    >> aName.equals("start")
    >> || aName.equals("end")
    >> )
    >> ) {
    >> throw new RuntimeException(name + desc
    >> + " already contains timing calls. Aborting.");
    >> }
    >> }
    >>
    >> Strangely enough, with this method added, I can only transform methods
    >> with exactly one argument of type String). For all others a
    >> transformed class fails verification.

    >
    > Very strange... And I have no concept what's the reason of that, show a
    > little more code.


    My bad. As I renamed arguments of visitMethodInsn(...) I left "desc" of
    super.super.visitMethodInsn() unchanged. Should have been _aDesc_. Plus a couple
    of missing double-quotes which disappeared when I prepared the code for posting
    (I refactored quite a bit since the beginning and I try to keep some level of
    consistency for the sake of this conversation).

    But now I have another problem. Throwing a RuntimeException effectively aborts
    transformation: Transformer catches the exception and returns unmodified bytes.

    I'd like to be able to do this on a mehod level: skip the methods already
    containing timing calls (leaving them unmodified), and be able to transform
    others. I do something similar first thing in visitMethod(...) (see attached
    code) but, unlike detecting calls within the body of a method, I know which
    methods are registered in advance.

    (I know I keep repeating myself, but...) Any suggestions?

    >> Any suggestions?

    >
    > After reading this and your previous post I think, that you don't have
    > to care about existing start() and end() calls within your code.
    > Similar problem exist for example in a case of recursive calls, your
    > start() method will be called many times before end(). And it should be
    > YourTiming start() and end() methods implementation responsibility to
    > treat such a scenarios well.


    Currently it only detects them. There are some possibilities:
    1. keep only the first call and (silently) ignore other consecutive ones
    2. keep the last one and (silently) dispose of the ones before
    3. throw an error
    4. ?

    > You said before that there is some kind of stack-trace computed in your
    > implementation, so having it in a calls to end() you can find out which
    > start() this call is related to (simply ignoring all unrecognized end()
    > calls).


    That's what I'm doing. But I don't seem to know how to get hold of method
    signature, so there can be problems if consecutive threads call timed methods
    with the same name, but different signatures. I can't rely on line numbers, because:

    1. MyTiming.start and MyTiming.end are called in different lines (so I can't
    pair them up)
    2. There may even not be line number info if the bytecode is doctored that way
    (am I right?).

    > This needs some kind of method calls stack reflected in your
    > implementation but will give your users a freedom of choices.
    > Moreover, you will no longer need end() taking a String parameter in
    > this scenario, because you can assume that each end() call is always
    > related to the last start() call (parameterized or not) performed in the
    > same level of method calls stack.
    >
    > Hope that's clear. :)


    Well, not quite, but I think I'll keep those parametrized calls to be able to
    time blocks of code within the method.

    package path.to.instrumentation.asm;

    import java.util.Map;

    import org.objectweb.asm.ClassAdapter;
    import org.objectweb.asm.ClassVisitor;
    import org.objectweb.asm.Label;
    import org.objectweb.asm.MethodAdapter;
    import org.objectweb.asm.MethodVisitor;
    import org.objectweb.asm.Opcodes;

    import path.to.MyTiming;

    public class TimingTransformerAsmClassAdapter extends ClassAdapter {

    private static final boolean DEBUG = false;

    private static final boolean VERBOSE = true || DEBUG;

    private Map methodNames;

    public TimingTransformerAsmClassAdapter(
    final ClassVisitor classVisitor,
    final Map methodNames
    ) {
    super(classVisitor);
    this.methodNames = methodNames;
    }

    public MethodVisitor visitMethod(
    final int access,
    final String name,
    final String desc,
    final String signature,
    final String[] exceptions
    ) {
    final String nameDesc = name + desc;
    if(!methodNames.isEmpty()) {
    if(
    !methodNames.containsKey(name)
    && !methodNames.containsKey(nameDesc)
    ) {
    verboseln("skipping unregistered method " + nameDesc);
    return cv.visitMethod(access, name, desc, signature, exceptions);
    }
    }

    //If ClassWriter in TimingTransformerAsm.transform(...) is not
    //initialized properly (meaning: if it does not compute the maximum
    //stack size and the maximum number of local variables automatically:
    //-> new ClassWriter(ClassWriter.COMPUTE_MAXS); for 3.0 beta and
    //-> new ClassWriter(true); for 2.2.1),
    //the following block must be uncommented. Otherwise transfomed classes
    //won't necessarily pass verification!
    // if(desc.startsWith("()")) // Can't transform methods with no args!?!
    // {
    // verboseln("skipping special method (no args!?!) " + nameDesc);
    // return cv.visitMethod(access, name, desc, signature, exceptions);
    // }
    //
    // if(name.indexOf("$") >= 0) //Don't transform inner class methods!?!
    // {
    // verboseln("skipping special method (inner class?!?) " + nameDesc);
    // return cv.visitMethod(access, name, desc, signature, exceptions);
    // }

    verboseln("transforming method " + nameDesc);

    return
    new MethodAdapter(
    super.visitMethod(access, name, desc, signature, exceptions)
    ) {
    final Label startLabel = new Label();
    final Label finallyLabel = new Label();
    int retOpcode = Opcodes.RETURN;

    private void genStartCode() {
    super.visitMethodInsn(
    Opcodes.INVOKESTATIC,
    MyTiming.INTERNAL_CLASS_NAME,
    MyTiming.MARK_START_METHOD_NAME,
    "()V"
    );
    }

    private void genEndCode() {
    super.visitMethodInsn(
    Opcodes.INVOKESTATIC,
    MyTiming.INTERNAL_CLASS_NAME,
    MyTiming.MARK_STOP_METHOD_NAME,
    "()V"
    );
    }

    public void visitCode() {
    super.visitCode();
    super.visitLabel(startLabel);
    genStartCode();
    }

    public void visitInsn(final int opcode) {
    if(isRetInsn(opcode)) {
    retOpcode = opcode;
    super.visitJumpInsn(Opcodes.GOTO, finallyLabel);
    }
    else {
    super.visitInsn(opcode);
    }
    }

    public void visitMethodInsn(
    final int opcode,
    final String owner,
    final String aName,
    final String aDesc
    ) {
    super.visitMethodInsn(opcode, owner, aName, aDesc);
    if(
    owner.equals("path/to/MyTiming)
    && (
    aName.equals("start")
    || aName.equals("end")
    )
    ) {
    throw new RuntimeException(nameDesc
    + " already contains timing calls. Aborting.");
    }
    }

    public void visitMaxs(final int maxStack, final int maxLocals) { // public void visitEnd() {
    final Label endLabel = new Label();
    super.visitLabel(endLabel);
    super.visitVarInsn(Opcodes.ASTORE, 1);

    genEndCode();

    super.visitVarInsn(Opcodes.ALOAD, 1);
    super.visitInsn(Opcodes.ATHROW);

    super.visitLabel(finallyLabel);

    genEndCode();

    super.visitInsn(retOpcode);

    super.visitTryCatchBlock(
    startLabel,
    endLabel,
    endLabel,
    null
    );

    super.visitMaxs(maxStack, maxLocals); //super.visitEnd();
    }
    };
    }

    private static boolean isRetInsn(final int opcode) {
    switch(opcode) {
    case Opcodes.IRETURN:
    case Opcodes.LRETURN:
    case Opcodes.FRETURN:
    case Opcodes.DRETURN:
    case Opcodes.ARETURN:
    case Opcodes.RETURN:
    return true;
    default:
    return false;
    }
    }


    private static final void out(String s) {
    System.out.print(
    TimingTransformerAsmClassAdapter.class.getSimpleName()
    + "> " + s
    );
    }

    private static final void outln(String s) {
    System.out.println(
    TimingTransformerAsmClassAdapter.class.getSimpleName()
    + "> " + s
    );
    }

    private static final void verbose(String s) {
    if(VERBOSE)
    out(s);
    }

    private static final void verboseln(String s) {
    if(VERBOSE)
    outln(s);
    }

    private static final void debug(String s) {
    if(DEBUG)
    out(s);
    }

    private static final void debugln(String s) {
    if(DEBUG)
    outln(s);
    }
    }
    Boris Gorjan, Jun 9, 2006
    #17
  18. Boris Gorjan

    Boris Gorjan Guest

    Boris Gorjan wrote:

    [snip]

    If during transformation, instead of

    public static void MyTiming.start() and
    public static void MyTiming.end()

    I wanted to incorporate

    public static void MyTiming.start(String) and
    public static void MyTiming.end(String),

    where String would be a signature of the method being modified (argument desc in
    this case), what do I have to do?

    In my BCEL code (see BCEL code in my post from June 6thin this thread) I'd

    1. replace "V()" with "(Ljava/lang/String;)V" in both calls of
    constantPoolGen.addMethodref(...),

    2. PUSH a "signature" onto InstructionList before calling INVOKESTATIC. Like this:
    InstructionList additional = new InstructionList();
    additional.append(new PUSH(constantPoolGen, method.getSignature()));
    additional.append(new INVOKESTATIC(startRef));

    (same for endRef)

    3. increase i by 1


    OK, I know that (in TimingTransformerAsmClassAdapter below) I have to replace
    the signature "()V" with "(Ljava/lang/String;)V" in private void genStartCode()
    and private void genEndCode(). After that, I... don't know. Any hints?

    > package path.to.instrumentation.asm;
    >
    > import java.util.Map;
    >
    > import org.objectweb.asm.ClassAdapter;
    > import org.objectweb.asm.ClassVisitor;
    > import org.objectweb.asm.Label;
    > import org.objectweb.asm.MethodAdapter;
    > import org.objectweb.asm.MethodVisitor;
    > import org.objectweb.asm.Opcodes;
    >
    > import path.to.MyTiming;
    >
    > public class TimingTransformerAsmClassAdapter extends ClassAdapter {
    >
    > private static final boolean DEBUG = false;
    >
    > private static final boolean VERBOSE = true || DEBUG;
    >
    > private Map methodNames;
    >
    > public TimingTransformerAsmClassAdapter(
    > final ClassVisitor classVisitor,
    > final Map methodNames
    > ) {
    > super(classVisitor);
    > this.methodNames = methodNames;
    > }
    >
    > public MethodVisitor visitMethod(
    > final int access,
    > final String name,
    > final String desc,
    > final String signature,
    > final String[] exceptions
    > ) {
    > final String nameDesc = name + desc;
    > if(!methodNames.isEmpty()) {
    > if(
    > !methodNames.containsKey(name)
    > && !methodNames.containsKey(nameDesc)
    > ) {
    > verboseln("skipping unregistered method " + nameDesc);
    > return cv.visitMethod(access, name, desc, signature, exceptions);
    > }
    > }
    >
    > //If ClassWriter in TimingTransformerAsm.transform(...) is not
    > //initialized properly (meaning: if it does not compute the maximum
    > //stack size and the maximum number of local variables automatically:
    > //-> new ClassWriter(ClassWriter.COMPUTE_MAXS); for 3.0 beta and
    > //-> new ClassWriter(true); for 2.2.1),
    > //the following block must be uncommented. Otherwise transfomed classes
    > //won't necessarily pass verification!
    > // if(desc.startsWith("()")) // Can't transform methods with no args!?!
    > // {
    > // verboseln("skipping special method (no args!?!) " + nameDesc);
    > // return cv.visitMethod(access, name, desc, signature, exceptions);
    > // }
    > //
    > // if(name.indexOf("$") >= 0) //Don't transform inner class methods!?!
    > // {
    > // verboseln("skipping special method (inner class?!?) " + nameDesc);
    > // return cv.visitMethod(access, name, desc, signature, exceptions);
    > // }
    >
    > verboseln("transforming method " + nameDesc);
    >
    > return
    > new MethodAdapter(
    > super.visitMethod(access, name, desc, signature, exceptions)
    > ) {
    > final Label startLabel = new Label();
    > final Label finallyLabel = new Label();
    > int retOpcode = Opcodes.RETURN;
    >
    > private void genStartCode() {
    > super.visitMethodInsn(
    > Opcodes.INVOKESTATIC,
    > MyTiming.INTERNAL_CLASS_NAME,
    > MyTiming.MARK_START_METHOD_NAME,
    > "()V"
    > );
    > }
    >
    > private void genEndCode() {
    > super.visitMethodInsn(
    > Opcodes.INVOKESTATIC,
    > MyTiming.INTERNAL_CLASS_NAME,
    > MyTiming.MARK_STOP_METHOD_NAME,
    > "()V"
    > );
    > }
    >
    > public void visitCode() {
    > super.visitCode();
    > super.visitLabel(startLabel);
    > genStartCode();
    > }
    >
    > public void visitInsn(final int opcode) {
    > if(isRetInsn(opcode)) {
    > retOpcode = opcode;
    > super.visitJumpInsn(Opcodes.GOTO, finallyLabel);
    > }
    > else {
    > super.visitInsn(opcode);
    > }
    > }
    >
    > public void visitMethodInsn(
    > final int opcode,
    > final String owner,
    > final String aName,
    > final String aDesc
    > ) {
    > super.visitMethodInsn(opcode, owner, aName, aDesc);
    > if(
    > owner.equals("path/to/MyTiming)
    > && (
    > aName.equals("start")
    > || aName.equals("end")
    > )
    > ) {
    > throw new RuntimeException(nameDesc
    > + " already contains timing calls. Aborting.");
    > }
    > }
    >
    > public void visitMaxs(final int maxStack, final int maxLocals) { // public void visitEnd() {
    > final Label endLabel = new Label();
    > super.visitLabel(endLabel);
    > super.visitVarInsn(Opcodes.ASTORE, 1);
    >
    > genEndCode();
    >
    > super.visitVarInsn(Opcodes.ALOAD, 1);
    > super.visitInsn(Opcodes.ATHROW);
    >
    > super.visitLabel(finallyLabel);
    >
    > genEndCode();
    >
    > super.visitInsn(retOpcode);
    >
    > super.visitTryCatchBlock(
    > startLabel,
    > endLabel,
    > endLabel,
    > null
    > );
    >
    > super.visitMaxs(maxStack, maxLocals); //super.visitEnd();
    > }
    > };
    > }
    >
    > private static boolean isRetInsn(final int opcode) {
    > switch(opcode) {
    > case Opcodes.IRETURN:
    > case Opcodes.LRETURN:
    > case Opcodes.FRETURN:
    > case Opcodes.DRETURN:
    > case Opcodes.ARETURN:
    > case Opcodes.RETURN:
    > return true;
    > default:
    > return false;
    > }
    > }
    >
    >
    > private static final void out(String s) {
    > System.out.print(
    > TimingTransformerAsmClassAdapter.class.getSimpleName()
    > + "> " + s
    > );
    > }
    >
    > private static final void outln(String s) {
    > System.out.println(
    > TimingTransformerAsmClassAdapter.class.getSimpleName()
    > + "> " + s
    > );
    > }
    >
    > private static final void verbose(String s) {
    > if(VERBOSE)
    > out(s);
    > }
    >
    > private static final void verboseln(String s) {
    > if(VERBOSE)
    > outln(s);
    > }
    >
    > private static final void debug(String s) {
    > if(DEBUG)
    > out(s);
    > }
    >
    > private static final void debugln(String s) {
    > if(DEBUG)
    > outln(s);
    > }
    > }
    Boris Gorjan, Jun 9, 2006
    #18
  19. Boris Gorjan

    Boris Gorjan Guest

    Boris Gorjan wrote:

    [snip]

    > OK, I know that (in TimingTransformerAsmClassAdapter below) I have to
    > replace the signature "()V" with "(Ljava/lang/String;)V" in private void
    > genStartCode() and private void genEndCode(). After that, I... don't
    > know.


    Now I know: insert super.visitLdcInsn(desc); into genStartCode() and genEndCode();

    [snip]
    Boris Gorjan, Jun 12, 2006
    #19
    1. Advertising

Want to reply to this thread or ask your own question?

It takes just 2 minutes to sign up (and it's free!). Just click the sign up button to choose a username and then you can ask your own questions on the forum.
Similar Threads
  1. Thomas Zangl

    BCEL and IfThenElse construct

    Thomas Zangl, May 4, 2005, in forum: Java
    Replies:
    1
    Views:
    432
    =?ISO-8859-1?Q?Daniel_Sj=F6blom?=
    May 4, 2005
  2. Thomas Zangl

    [BCEL] If then else construct - again

    Thomas Zangl, May 13, 2005, in forum: Java
    Replies:
    4
    Views:
    820
    =?ISO-8859-1?Q?Daniel_Sj=F6blom?=
    May 13, 2005
  3. Replies:
    1
    Views:
    788
    Thomas Hawtin
    Sep 7, 2005
  4. Francesco Devittori

    ASM (vs. BCEL) - can I do this?

    Francesco Devittori, Dec 20, 2005, in forum: Java
    Replies:
    2
    Views:
    1,306
    Francesco Devittori
    Dec 21, 2005
  5. oulan bator

    Creating a Constructor in BCEL

    oulan bator, Jan 25, 2006, in forum: Java
    Replies:
    5
    Views:
    888
    oulan bator
    Jan 26, 2006
Loading...

Share This Page