metracer – Java Code Instrumentation and Stack Map Frames

Stack Map Frames in Java is a contradictory addition to the language: they speed up class loading but the same time make big troubles for a code instrumentation tools. During adoption of an ASM framework to a metracer I have stumbled with following main two issues:

  1. necessity to resolve a “common super class” during instrumentation which causes many ClassNotFoundException
  2. VerifyError exceptions with message like ‘Stack map does not match the one at exception handler’

 

Regarding problem #1.

The issue manifestation is following:  code instrumented with ASM fails with ClassNotFoundException in runtime. This issue hurts badly for JavaEE applications because of class loaders isolation.

What is happening is that ASM in some circumstances must resolve a “common super class” for two different classes (details could be found here) during a computation of stack map frames. The default implementation of ASM (see here) tries to traverse an hierarchy of two classes by loading them (via Class.forName call).  However due to class loaders isolation this traversal may easily fail. In other words instrumentation of a single Java class turned from an isolated action into an opened one – in a common case you just can’t take a bytecode of a class and modify it as you wish because you now need an information about other classes as well (which may be unavailable yet)

Alas there is no ideal solution for this case. Me personally stick to an upgraded solution found in one of them ASM tests ClassWriterComputeFramesTest.java. The core of the solution is a manual  digging within a current class loader: finding a class file, loading it with a ClassReader, examining its content and so on.  The upgrade comprises of an additional traversing of class loaders hierarchy (not just sticking to a current one) – this makes it possible to solve “common super class” issue for in most of the cases.

The code itself of an upgrade solution sits in MetracerClassWriter.java.

 

Regarding problem #2.

The errors like ‘Stack map does not match the one at exception handler’ (or similar which refer to stack maps in some way) arise from the incorrectly computed stack map frames for an instrumented code. This errors typically happens when you try to insert code at the end of the method, e.g. introduce something like a try  / finally  enclosing. I’ve noted that Javassist is far less lucky in generating of stack map frames in this case than ASM – the latter generally makes them correct (see ClassWriter.COMPUTE_FRAMES).

However even with ASM one must aware of a pitfall with constructors. Attempt to wrap a constructor’s body with a  try  / finally (following a best ASM practices of doing so via AdviceAdpater) would result in exception VerifyError  with message ‘Stack map does not match the one at exception handler’, e.g.:

stack_map_frames_issue

VerifyError for an instrumented class in constructor

The point here is that a class constructor could have an initial stack map frame with flag flagThisUninit. According to Java platform documentation this flag must be always there for all constructors but I’ve personally only observed such behavior for copy constructors only, e.g.:

This flag spoils all the things because stack map frames recorded in a class file doesn’t have a flags field and consequently this means that they all will have an empty flag set implicitly (see picture above). After struggling a lot I’ve finally found a solution – one must start enclosing a constructors body with  try  / finally only AFTER invokespecial is complete. After invokespecial is complete flag flagThisUninit goes away and instrumented code is honored by JVM verificator.

correct_insertion_point

Correct insertion point for a try / finally block within a constructor

Code of doing so is in PatternMatchedMethodMutator.java.

 

 

 

Leave a Reply

Your email address will not be published.