Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Write an instrumentation agent to modify JCL code #3

Open
r30shah opened this issue Sep 23, 2021 · 6 comments · May be fixed by eclipse-openj9/openj9-utils#79
Open

Write an instrumentation agent to modify JCL code #3

r30shah opened this issue Sep 23, 2021 · 6 comments · May be fixed by eclipse-openj9/openj9-utils#79
Assignees

Comments

@r30shah
Copy link
Owner

r30shah commented Sep 23, 2021

Write an instrumentation agent in java that can modify JCL code in [1] and redirect it to [2]. You use either ASM[3] or javassist[4] to achieve that.
Aim of this work item is to get familiar with such Instrumentation API and prepare a foundation for our tool that will be attached to JVM and when sees the failure in the test, starts limiting down to the failing method and optimization.

For this issue, currently we can use the same demo code that we tried out in the code-sprint and produced a failure that throws Null Pointer Exception and debug the issue without need to recompile the JCL code.

[1]. https://github.com/ibmruntimes/openj9-openjdk-jdk11/blob/ddc29ca76069082bf6b57e52628e3c2ae05d7694/src/java.base/share/classes/java/lang/reflect/Method.java#L566
[2].

public static Object invoke(MethodAccessor ma, Object obj, Object[] args) throws InvocationTargetException {

[3]. https://asm.ow2.io
[4]. https://www.javassist.org/

@r30shah
Copy link
Owner Author

r30shah commented Sep 23, 2021

Attaching the simple experiment that uses javassist to modify calls in method for reference.
RedefineAgentExperiment.zip

Pasting the README from the zip here.

This readme contains a simple experiment that uses Java Instrumentation using javassist API to redefine a method in a class to call different method. There are two ways we can attach the agent to the JVM, static and dynamic. In static agent load (Agent provided by -javaagent: command line option), it runs the premain method implemented in the agent and will be called before java application starts. In case of dynamic load, we need to use separate JVM that can attach the Agent (Other JVM would need the PID of the main application to attach the agent). In this case a method named agentmain will be executed. In this example, I have provided both ways of using agent to redefine the method.

Step 1: Unzip the test.

$ unzip RedefineAgentExperiment.zip
$ ls
LoadAgent.java         RedefinitionAgent.java javassist.jar
Redefinition.mf        demo                   util
$ ls demo
Demo.java	Person.java
$ ls util
Constant.java

Demo.java contains our main application which will create new object of type Person every second and call a method getField1() off that object. getField1() method calls a method named returnConstant0 from the Person class which returns constant 1. Our demo will redefine this getField1() method in Person class to call the static methods declared in Constant.java (Static agent load will redefine it to call util.Constant.returnConstant1 and dynamic agent load will redefine it to call util.Constant.returnConstant2

Step 2: Compile the test code.

$ javac util/Constant.java demo/*.java
// Running without Redefining the method
$ java demo/Demo
Returning 1
Returning 1
...
Val =6000

Step 3: Compile the agent and prepare the RedefinitionAgent.jar (You can use any name)

$ javac -cp ./javassist.jar RedefinitionAgent.java
Now before creating a jar file, we need to create a MANIFEST file for the jar. Take a look at the file. For more details checkout the [reference](https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&cad=rja&uact=8&ved=2ahUKEwjNouPK9JXzAhVkGVkFHR4MCncQFnoECAsQAQ&url=https%3A%2F%2Fdocs.oracle.com%2Fjavase%2Ftutorial%2Fdeployment%2Fjar%2Fmanifestindex.html&usg=AOvVaw0cPP3x3SwDXP-qOpCBmYTb)
$ cat Redefinition.mf
Manifest-Version: 1.0
Premain-Class: RedefinitionAgent
Agent-Class: RedefinitionAgent
Class-path: javassist.jar
Can-Redefine-Classes: true
$ jar -cvfm RedefinitionAgent.jar Redefinition.mf javassist.jar RedefinitionAgent.*

Step 4: Now compile the LoadAgent.java which would be used to load the agent dynamically.

$ javac LoadAgent.java

Now as we have everything ready for the experiment, let's try both static and dynamic loading of agent.

Experiment 1 : Static Loading of agent

$ java  -javaagent:./RedefinitionAgent.jar demo/Demo
Premain called. will modify Person.getField1 to call util.Constant.returnConstant1
Person modified to callreturnConstant1 from util.Constant
Returning 100
Returning 100
...
Val =600000

Experiment 2 : Dynamic Loading of agent (Need two terminals)

Terminal 1

$ java demo/Demo
Returning 1
Returning 1
...

Terminal 2

$ java LoadAgent
Agent Attached successfully

Process in Terminal 2 will attach the agent to JVM in Terminal 1. If you observe the output in Terminal 1, you will see change

AgentMain called, will modify Person.getField1 to call util.Constant.returnConstant2
Person modified to callreturnConstant2 from util.Constant
Returning 200
...

Repository owner deleted a comment from ShaneKilloran Sep 23, 2021
@ShaneKilloran
Copy link
Collaborator

@qasimy123 @r30shah @fjeremic

Giving an update on where the agent is at.

The code can be found here
to run the agent you'll want to compile the java files adding a asm-9.2.jar and junit-4.12.jar to the classpath, and create a jar file including all the files in that repo as well as the asm-9.2.jar and junit-4.12.jar. You can then run the agent as a javaagent. the commands to do this are below:

$ /openj9-openjdk-jdk11/build/linux-x86_64-normal-server-release/jdk/bin/javac -cp ./asm-9.2.jar:./junit-4.12.jar DebugAgent.java DebugClassWriter.java InvokeDebugAdapter.java MethodDebugAdapter.java EvaluateDebugAdapter.java ExpectDebugAdapter.java

$ /openj9-openjdk-jdk11/build/linux-x86_64-normal-server-release/jdk/bin/jar -cvfm DebugAgent.jar DebugAgent.mf asm-9.2.jar junit-4.12.jar DebugAgent.java DebugClassWriter.java InvokeDebugAdapter.java MethodDebugAdapter.java EvaluateDebugAdapter.java ExpectDebugAdapter.java

Also I will add that the agent currently assumes that Qasim's changes to JITHelper already exist in that file. He highlights those changes in his most recent comments for #5. So to run the agent these need to be added as well.

Currently the java agent successfully rewrites method.invoke but fails to rewrite ExpectException.evaluate. To see the problem follow these steps:

  1. In DebugAgent.java, change the try block beginning at line 26 which currently looks like this:
try {
                        DebugClassWriter writer = new DebugClassWriter(c.getName());
                        return writer.applyDebugger(INVOKE);
}

and replace it with this:

try {
                        DebugClassWriter writer = new DebugClassWriter(c.getName());
                        writer.applyDebugger(INVOKE);
                        return b;
}

This will stop the method.invoke changes from being overwritten so that we can focus on the ExpectException failure.

  1. Now if you run the agent you should see the failure:
Exception in thread "main" java.lang.reflect.InvocationTargetException
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:567)
        at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndStartAgent(InstrumentationImpl.java:513)
        at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndCallPremain(InstrumentationImpl.java:525)
Caused by: java.lang.ClassFormatError
        at java.instrument/sun.instrument.InstrumentationImpl.retransformClasses0(Native Method)
        at java.instrument/sun.instrument.InstrumentationImpl.retransformClasses(InstrumentationImpl.java:167)
        at DebugAgent.premain(DebugAgent.java:49)

This failure doesn't include much information but I believe the cause to come from the difference in version between junit-4.12 (java 5) and our code.

To generate the asm code you see in EvaluateDebugAdapter.visitCode and InvokeDebugAdapter.visitCode I use a tool called the asmifier. I run the asmifier on the new expectExcetion code that @qasimy123 wrote but the problem is that since this code is version 11, ASM will generate different asm code then it would if it were java 5, and trying to insert this java 11 asm code into a java 5 class causes problems at some point.

To test this out, I fed the asmifier the original java 5 ExpectException class, and had our agent "rewrite" identical code using asm. This passed without error. I then took the exact same java 5 code and compiled it using java11 without any other changes. Using the asm code from this new class results in the same ClassFormatError above.

Its not that asm isn't backwords compatible, its just that the way it would write java 11 methods is different from java 5 so at some point this causes a conflict. You can see the difference between identical java 5 and java 11 junit ExpectException files here.

@fjeremic
Copy link
Collaborator

JVM bytecode is backwards compatible, so if it works on an earlier JVM version I see no reason it shouldn't with a later JVM version. Are you sure the bytecodes you're adding are valid? Perhaps you can try a trivial modification to evaluate and see if you still get ClassFormatError. I'm puzzled why this works on one JVM version but not another.

@ShaneKilloran
Copy link
Collaborator

@qasimy123 @r30shah @fjeremic

Given Qasim's latest changes that move most of the heavy lifting to JITHelper, I have updated the agent code and it now runs successfully.

I'm going to go through the code and clean it up where I can but if @r30shah or @fjeremic have time to take a look and review it before we move to the next steps that would be great.

I'm now looking in to using Maven to generate the jar and handle the dependencies so that we can automate this process as well.

@r30shah
Copy link
Owner Author

r30shah commented Nov 23, 2021

@ShaneKilloran I can check the current status of agent can change the calls we targeted in this issue and can easily compile the jar with Maven. Can you also add README.mdin your repo, to describe usage of this tool. It will help us getting tool committed to openj9-utils master repo.

@r30shah
Copy link
Owner Author

r30shah commented Nov 25, 2021

@ShaneKilloran One of the issues we discussed in past, was to set the Jit option forceUsePreexistence through agent. I tried using your agent to make a JNI call added in #9 to set the option before application is started.
This is work in progress change as in order to get the JNI call working from your agent I needed to export com.ibm.jit package from java.base as I couldn't get Maven to pick up --add-exports java.base/com.ibm.jit=ALL-UNNAMED compiler args to build the jar.
For your agent, you need to do two things,

  1. Add a JNI call in the premain of your agent to call com.ibm.jit.JITHelpers.setForceUsePreexistence();
  2. Update pom.xml to build a jar supplying --add-exports java.base/com.ibm.jit=ALL-UNNAMED options to javac compiler, so that we can remove exports com.ibm.jit from https://github.com/r30shah/openj9-jit-debug-agent/pull/9/files#diff-3c946a5c11ff7d7dd8329822f67935a8d839ae0b334be118e46fc7c4c5fd3aa2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants