BISM: Bytecode-Level Instrumentation for Software Monitoring

07/08/2020
by   Chukri Soueidi, et al.
Inria
0

BISM (Bytecode-Level Instrumentation for Software Monitoring) is a lightweight bytecode instrumentation tool that features an expressive high-level control-flow-aware instrumentation language. The language follows the aspect-oriented programming paradigm by adopting the joinpoint model, advice inlining, and separate instrumentation mechanisms. BISM provides joinpoints ranging from bytecode instruction to method execution, access to comprehensive static and dynamic context information, and instrumentation methods. BISM runs in two instrumentation modes: build-time and load-time. We demonstrate BISM effectiveness using two experiments: a security scenario and a general runtime verification case. The results show that BISM instrumentation incurs low runtime and memory overheads.

READ FULL TEXT VIEW PDF
POST COMMENT

Comments

There are no comments yet.

Authors

page 1

page 2

page 3

page 4

06/02/2021

Efficient and Expressive Bytecode-Level Instrumentation for Java Programs

We present an efficient and expressive tool for the instrumentation of J...
06/07/2018

Specification of State and Time Constraints for Runtime Verification of Functions

Techniques for runtime verification often utilise specification language...
07/24/2018

Racets: Faceted Execution in Racket

Faceted Execution is a linguistic paradigm for dynamic information-flow ...
03/29/2021

Tigris: a DSL and Framework for Monitoring Software Systems at Runtime

The understanding of the behavioral aspects of a software system is an e...
08/27/2019

Who is to Blame? Runtime Verification of Distributed Objects with Active Monitors

Since distributed software systems are ubiquitous, their correct functio...
08/25/2019

Proceedings of the Second Workshop on Verification of Objects at RunTime EXecution

This volume contains the post-proceedings of the second Workshop on Veri...
09/27/2019

Comparing Static and Dynamic Weighted Software Coupling Metrics

Coupling metrics are an established way to measure software architecture...
This week in AI

Get the week's most popular data science and artificial intelligence research sent straight to your inbox every Saturday.

1 Introduction

Instrumentation is essential to the software monitoring workflow [3]. Instrumentation allows extracting information from a running software to abstract the execution into a trace fed to a monitor. Depending on the information needed by the monitor, the granularity level of the extracted information may range from coarse (e.g., a function call) to fine (e.g., an assignment to a local variable, a jump in the control flow).

For software instrumentation, aspect-oriented programming (AOP) [11] is a popular and convenient paradigm where instrumentation is a cross-cutting concern. For Java programs, runtime verification tools [9, 2] have for long relied on AspectJ [10], which is one of the reference AOP implementations for Java. AspectJ provides a high-level pointcut/advice model for convenient instrumentation. However, AspectJ does not offer enough flexibility to perform some instrumentation tasks that require to reach low-level code regions, such as bytecode instructions, local variables of a method, and basic blocks in the control-flow graph (CFG).

Yet, there are several low-level bytecode manipulation frameworks such as ASM [7] and BCEL [1]. However, writing instrumentation in such frameworks is tedious and requires expertise on the bytecode. Other bytecode instrumentation frameworks, from which DiSL [12] is the most remarkable, enable flexible low-level instrumentation and, at the same time, provide a high-level language. However, DiSL does not allow inserting bytecode instructions directly but provides custom transformers where a developer needs to revert to low-level bytecode manipulation frameworks. This makes various scenarios tedious to implement in DiSL or incur a considerable bytecode overhead.

Contributions.

In this paper, we introduce BISM (Bytecode-Level Instrumentation for Software Monitoring), a lightweight bytecode instrumentation tool that features an expressive high-level instrumentation language. The language inspires from the AOP paradigm by adopting the joinpoint model, advice inlining, and separate instrumentation mechanisms. In particular, BISM provides a separate class to specify instrumentation code, and offers a variety of joinpoints ranging from bytecode instruction to basic block and method execution. BISM also provides access to a set of comprehensive joinpoints-related static and dynamic contexts to retrieve some relevant information, and a set of instrumentation methods to be called at joinpoints to insert code, invoke methods, and print information. BISM is control-flow aware. That is, it generates CFGs for all methods and offers this information at joinpoints and context objects. Moreover, BISM provides a variety of control-flow properties, such as capturing conditional jump branches and retrieving successor and the predecessor basic blocks. Such features help instrumenting tools using a control-flow analysis, for instance, in the security domain, to detect control-flow attacks, such as test inversions and arbitrary jumps.

We demonstrate BISM effectiveness using two complementary experiments. The first experiment shows how BISM can be used to instrument for a security scenario, more particularly, to detect test inversions in the control-flow of AES (Advanced Encryption Standard). The second experiment demonstrates a general runtime verification case, where we used BISM to instrument seven applications from DaCapo benchmark [6] to verify the classical HasNext, UnsafeIterator and SafeSyncMap properties. We also compare BISM’s performance to DiSL using three metrics: size, memory footprint, and runtime of the instrumented code. The results show that BISM instrumentation incurs a smaller size and memory footprint. Regarding the runtime of the instrumented code, in load-time instrumentation, BISM always performs better, and in build-time instrumentation, BISM performs better except for two out of seven benchmarks in the second experiment. We observe, in load-time, that the two tools perform similarly when many classes are in the scope of instrumentation but not affected. This stems from (1) DiSL’s faster generation of static objects and (2) the static analysis performed by BISM on all classes in the scope, even if not used.

Paper organization.

The rest of the paper is organized as follows. Section 2 overviews the design goals and features of BISM. Section 3 presents the language featured by BISM. Section 4 presents the implementation of BISM. Section 5 presents case studies and a comparison between BISM and DiSL. Section 6 discusses related work. Finally, Section 7 concludes.

2 BISM Design and Features

BISM is implemented on top of ASM [7] with the following goals and features.

Instrumentation mechanism.

BISM language follows the AOP paradigm. It provides a mechanism to write separate instrumentation classes. An instrumentation class specifies the instrumentation code to be inserted in the target program at chosen joinpoints. BISM offers joinpoints that range from bytecode instruction to basic block and method execution. It also offers several instrumentation methods and, additionally, accepts instrumentation code written in the ASM syntax. The instrumentation code is eventually compiled by BISM into bytecode instructions and inlined in the target program at the exact joinpoint locations.

Access to program context.

BISM offers access to complete static information about instructions, basic blocks, methods, and classes. It also offers dynamic context objects that provide access to values that will only be available at runtime such as values of local variables, stack values, method arguments, and results. Moreover, BISM allows accessing instance and static fields of these objects. Furthermore, new local variables can be created within the scope of a method to pass values between joinpoints.

Control flow context.

BISM generates the CFGs of target methods out-of-the-box and offers this information within joinpoints and context objects. In addition to basic block entry and exit joinpoints, BISM provides specific control-flow related joinpoints such as OnTrueBranchEnter and OnFalseBranchEnter which capture conditional jump branches. Moreover, it provides a variety of control-flow properties within the static context objects. For example, it is possible to traverse the CFG of a method to retrieve the successors and the predecessors of basic blocks, moreover, edges are labeled denoting if it is the True or False branch of a conditional jump. Furthermore, BISM provides an optional feature to display the CFGs of methods before and after instrumentation, which gives developers visual assistance for analysis and insight on how to instrument the code and optimize it.

Compatibility with ASM.

BISM uses ASM extensively and relays all its generated class representations within the static context objects. Furthermore, it allows for inserting raw bytecode instructions by using the ASM data types. In this case, it is the responsibility of the user to write instrumentation code free from compilation and runtime errors. If the user unintentionally inserts faulty instructions, the code might break. The ability to insert ASM instructions provides highly expressive instrumentation capabilities, especially when it comes to inlining the monitor code into the target program.

Bytecode coverage.

BISM can run in two modes: build-time (as a standalone application) with static instrumentation, and load-time with an agent (utilizing java.lang .instrument) that intercepts all classes loaded by the JVM and instruments before the linking phase. In build-time, BISM is capable of instrumenting all the compiled classes and methods111Excluding the native and abstract methods, as they do not have bytecode representation.. In load-time, BISM is capable of instrumenting additional classes, including classes from the Java class library that are flagged as modifiable. The modifiable flag keeps certain core classes outside the scope of BISM. Note, modifying such classes is rather needed in dynamic program analysis (e.g., profiling, debugging).

3 BISM Language

We demonstrate the language in BISM, which allows developers to write transformers (i.e., instrumentation classes). The language provides joinpoints (Section 3.1) which capture exact points of program executions, static and dynamic contexts (Sections 3.2 and 3.3) which retrieve relevant information at joinpoints, and instrumentation methods (Section 3.4) used to instrument a target program.

3.1 Joinpoints

Joinpoints identify different execution points of a program; they mark bytecode regions where instrumentation can be inlined in the target program. BISM offers a closed set of joinpoints capable of capturing different points of a program execution, classified into three categories: Instruction, Basic Block, and Method joinpoints. BISM does not implement the notion of a pointcut, where a developer may select multiple joinpoints and instruments at once. We list below the set of all joinpoints available and specify where the instrumented code is executed with respect to the bytecode regions.

Instruction joinpoints.

BISM provides the following instruction-related joinpoints:

  • BeforeInstruction: captures execution before a bytecode instruction. If the instruction is the entry point of a basic block, the code executes after the instruction.

  • AfterInstruction: captures execution after a bytecode instruction. If the instruction is the exit point of a basic block, the code executes before thee instruction.

  • BeforeMethodCall: captures execution before a method call instruction and after loading all needed values on the stack.

  • AfterMethodCall: captures execution immediately after a method call instruction and before storing the return value from the stack, if any.

Method joinpoints.

BSIM also provides two method-related joinpoints:

  • OnMethodEnter: captures execution on method entry block, same execution rules as OnBasicBlockEnter.

  • OnMethodExit: captures execution on all exit blocks of a method before the return or throw instruction.

Basic block joinpoints.

In addition to the previous joinpoints, BISM provides basic block-related joinpoints which facilities instrumenting for control-flow analysis:

  • OnBasicBlockEnter: captures execution at the entry of a basic block, at the first real instruction222Real instructions are instructions that actually get executed, as opposite to some special Java bytecode instructions such as Label and Line number instructions..

  • OnBasicBlockExit: captures execution after the last instruction of a basic block; except when last instruction is a JUMP/RETURN/THROW instruction, then it executes before the last instruction.

  • OnTrueBranchEnter: captures execution on the entry of a successor block after a conditional jump on True evaluation.

  • OnFalseBranchEnter: captures execution on the entry of a successor block after a conditional jump on False evaluation.

The order of which joinpoints are visited first when entering a method is as follows: OnMethodEnter, OnBasicBlockEnter, OnTrueBranchEnter, OnFalseBranchEnter, BeforeInstruction, BeforeMethodCall, AfterMethodCall, AfterInstruction, OnBasicBlockExit, OnMethodExit.

3.2 Static Context

Static context objects provide relevant static information at joinpoints. These objects can be used to retrieve information about a bytecode instruction, a method call, a basic block, a method, and a class. BISM performs static analysis on target programs and provides additional control-flow-related static information such as basic block successors and predecessors. We list all the static context objects available and their properties.

Instruction context.

The Instruction context provides all relevant information about a single instruction being visited, and it contains the following fields:

  • index: a unique instruction index.

  • node: the ASM org.objectweb.asm.tree.AbstractInsNode that can be casted into a more specific AbstractInsNode sub type.

  • opcode: the bytecode instruction opcode.

  • next: the next instruction in the current basic block. Null if at the end of a basic block.

  • previous: the previous instruction in the basic block. Null if at the beginning of a basic block.

  • isConditionalJump(): true if instruction is a conditional jump instruction.

  • isBranchingInstruction(): true if instruction is an instance of JumpInsnNode, LookupSwitchInsnNode, TableSwitchInsnNode, or opcode is ATHROW, RET, IRETURN or RETURN.

  • stackOperandsCountIfConditionalJump(): the number of stack operands a conditional jump consumes. Equal to -1 in the case of non-conditional jumps.

  • getBasicValueFrame(): contains a list of all local variables, stack items, and their types at the stack frame before executing the current instruction.

  • getSourceValueFrame(): contains a list of all local variables and stack items and their source i.e. what instruction created/manipulated them.

  • methodName: the method name; the owner of the current instruction.

  • basicBlock: the BasicBlock context of the current instruction.

  • className: the name of the class; the owner of the current instruction.

MethodCall context.

A special type of Instruction context (only available before and after method calls). In addition to its Instruction context, it provides the following fields:

  • methodOwner: the name of the called class (callee).

  • methodName: the name of the method called.

  • currentClassName: the name of the calling class.

  • node: the ASM MethodInsnNode instruction.

  • ins: references the instruction.

BasicBlock context.

The BasicBlock context provides information about the current basic block being visited, and it contains the following fields:

  • id: a unique String that identifies the basic block.

  • index: a unique index that identifies the basic block inside a class.

  • blockType: the block type, which can be Normal, ConditionalJump, Goto, Switch, or Return.

  • size: the number of instructions in the basic block.

  • getSuccessorBlocks(): all successors of the basic block as per the CFG.

  • getPredecessorBlocks(): all predecessors of the basic block as per the CFG.

  • getTrueBranch(): the target block after a conditional jump evaluates to true, is null if the block does not end with a conditional jump.

  • getFalseBranch(): the target block after a conditional jump evaluates to false, is null if the block does not end with a conditional jump.

  • getFirstInstruction(): the first AbstractInsNode in the basic block.

  • getFirstRealInstruction(): the first real instruction in the basic block.

  • getLastRealInstruction(): the last real instruction in the basic block.

  • method: the Method context of the basic block.

Method context.

The Method context provides info about the method being visited and has the following fields:

  • name: the name of the method.

  • methodNode: the ASM org.objectweb.asm.tree.MethodNode.

  • getNumberOfBasicBlocks(): the number of basic blocks in the method.

  • getEntryBlock(): the entry basic block.

  • getExitBlocks(): a list of all exiting basic blocks.

  • classContext: the Class context of the method.

Class context.

The Class context provides information about the class being instrumented and has the following fields:

  • name: the name of the method.

  • classNode: the ASM org.objectweb.asm.tree.ClassNode

Static contexts are composed in a hierarchical structure such that an Instruction context object contains a reference to its BasicBlock context, BasicBlock context to its Method context, and a Method context to its Class context.

public class BasicBlockTransformer extends Transformer {
    @Override
    public void onBasicBlockEnter(BasicBlock bb){
        String blockId = bb.method.className+"."+bb.method.name+"."+bb.id;
        print("Entered block:" + blockId)
    }
    @Override
    public void onBasicBlockExit(BasicBlock bb)
        String blockId = bb.method.className+"."+bb.method.name+"."+bb.id;
        print("Exited block:" + blockId)
    }
}
Listing 1: A transformer for intercepting basic block executions.

The transformer depicted in Listing 1 uses the joinpoints onBasicBlockEnter and onBasicBlockExit to intercept all basic block executions. The static context BasicBlock bb is used to get the block id, the method name, and the class name. Here, the instrumentation method print inserts a print invocation in the target program before and after every basic block execution.

3.3 Dynamic Context

In addition to static context, BISM provides dynamic context objects at all joinpoints. These objects are capable of accessing dynamic values that are possibly only known during the target program execution. Dynamic Context objects provide access to dynamic values that are possibly only known during execution. BISM gathers this information from local variables and operand stack, then weaves the necessary code to extract this information. In some cases (e.g., when accessing stack values), BISM might instrument additional local variables to store them for later use. We list the methods available in dynamic contexts; note all these calls return a DynamicValue object omitted for brevity:

  • getThis(): returns a reference to the class owner of the method being instrumented, and null if the class or method is static.

  • getLocalVariable(int): returns a reference to a local variable by index.

  • getStackValue(int): returns a reference to a stack value.

  • getInstanceField(String): returns a reference to an instance field in the class being instrumented, and null if static.

  • getInstanceField(DynamicValue, String, Class): returns a reference to an instance field in a DynamicValue, and null if field is static or the dynamic value is not an object.

  • getStaticField(String): returns a reference to a static field in the class being instrumented.

  • getStaticField(DynamicValue, String, Class): returns a reference to a static field in a DynamicValue, and null if the dynamic value is not an object.

Additionally, we list the values related to these methods:

  • getMethodArgs(int): returns a reference to a method argument by index starting at 1. Only available in MethodCall and Method joinpoints.

  • getMethodReceiver(): returns a reference to the object whose method is being called. Returns null for static methods. Only available only in MethodCall joinpoints.

  • getMethodResult(): returns a reference to a method result. Only available only in MethodCall joinpoints.

BISM also allows to add new local variables to a method explicitly; these are useful for different purposes like to pass data across joinpoints. Note that the scope of the values of these variables is the method where they are created.

  • addLocalVariable(Object value): creates a new local variable and sets it to a primitive value, then return its reference as a LocalVariable type. This is only available in Method joinpoints.

  • updateLocalVariable(LocalVariable, Object value): updates a LocalVariable and sets it to a primitive value. This is available in all joinpoints.

Listing 2 presents a transformer using afterMethodCall joinpoint to capture the return of an Iterator created from a List object, and retrieving dynamic data from the dynamic context object MethodCallDynamicContext dc. The example also shows how to limit the scope using an if-statement to a specific method. Note that BISM also provides a more general scope argument that can be specified at runtime to match packages, classes, and methods by names (using possibly wildcards).

public class IteratorTransformer extends Transformer {
 @Override
 public void afterMethodCall(MethodCall mc, MethodCallDynamicContext dc){
    if (mc.methodName.equals("iterator") && mc.methodOwner.endsWith("List")) {
        // Access to dynamic data
        DynamicValue callingClass = dc.getThis(mc);
        DynamicValue list = dc.getMethodTarget(mc);
        DynamicValue iterator = dc.getMethodResult(mc);
        // Invoking a monitor
        StaticInvocation sti =
            new StaticInvocation("IteratorMonitor", "iteratorCreation");
        sti.addParameter(callingClass);
        sti.addParameter(list);
        sti.addParameter(iterator);
        invoke(sti);
    }
 }
}
Listing 2: A transformer that intercepts the creation of an iterator from a List.

3.4 Instrumentation methods

A developer instruments the target program using specified instrumentation methods. BISM provides print methods with multiple options to invoke a print command. It also provides (i) invoke methods for static method invocation and (ii) insert methods for bytecode instruction insertion. These methods are compiled by BISM into bytecode instructions and inlined at the exact joinpoint locations. We list below the instrumentation methods available in BISM.

Printing on console.

Instrumenting print statements in the target program can be achieved via multiple print instrumentation methods available in BISM. These methods take either static values or dynamic values retrieved in joinpoints. Listing 1 shows an example of using one of the print helper methods to instrument the target program to print the basic block constructed id. We list all of these methods:

  • print(String): prints a message on the console.

  • println(String): prints a message on the console followed by a new line.

  • print(DynamicValue): prints the toString() of a dynamic value on the console.

  • printHash(DynamicValue): prints the unique identity hash code of a dynamic value.

  • print(String, boolean): similar to print(String) but if passed boolean true, the print stream will be err.

  • print(DynamicValue, boolean): similar to print(DynamicValue) but if passed boolean true, the print stream will be err.

  • printHash(DynamicValue, boolean): similar to printHash(DynamicValue) but if passed boolean true, the print stream will be err.

Invoking static methods.

Invoking external static methods can be achieved using the instrumentation method invoke. An object of type StaticInvocation should be constructed in a joinpoint and provided with the external class name,the method name, and parameters. Listing 2 depicts a transformer that instrument the target program to call an external monitor method iteratorCreation. The StaticInvocation constructor takes in the monitor class name and method name. addParameter() is then called to add parameters to the invocation, it supports either DynamicValue type, or any primitive type in Java including String type (any other type will be ignored). After that, invoke weaves the method call in the target program.

Raw bytecode instructions.

Inserting raw bytecode instructions can be achieved using two insert methods, one takes as an argument a single ASM AbstractInsnNode instruction, and the other takes a list of instructions. When using these methods, it is the responsibility of the developer to write correct instructions and avoid breaking the code. Errors can be introduced by ignoring the stack requirements and altering local variables. For Java 8 and above programs, using the insert methods to push new values on the stack or create local variables requires modifying the maxStack and maxLocals values. All static context objects, hold the needed ASM object MethodNode to increment the values maxLocals and maxStack from within the joinpoint. These methods are:

  • insert(AbstractInsnNode ins)

  • insert(List<AbstractInsnNode> ins)

4 BISM Implementation

BISM is an open-source tool

[5] implemented in Java using about 4,000 LOC and 40 classes distributed in separate modules. It uses ASM for bytecode parsing, analysis, and weaving. Fig. 1 shows BISM internal workflow.

Figure 1: Instrumentation process in BISM.

(1) User Input. In build-time mode, BISM takes a target program bytecode (.class or .jar) to be instrumented, and a transformer which specifies the instrumentation logic. In load-time mode, BISM only takes a transformer, which is used to instrument every class being loaded by the JVM. BISM provides several built-in transformers that can be directly used. Moreover, users can specify a scope to filter target packages, classes, or methods.
(2) Parse Bytecode. BISM uses ASM to parse the bytecode and to generate a tree object which contains all the class details, such as fields, methods, and instructions.
(3) Build CFG. BISM constructs the CFGs for all methods in the target class. If the transformer utilizes control-flow joinpoints (onTrueBranch and onFalseBranch), BISM eliminates all critical edges from the CFGs to avoid instrumentation errors. This is done by inserting empty basic blocks in the middle of critical edges. Note, BISM keeps copies of the original CFGs. Users can optionally enable the visualizer to store CFGs in HTML files on the disk.
(4) Generate Joinpoints and Context Objects. BISM iterates over the target class to generate all joinpoints utilizing the created CFGs. At each joinpoint, the relevant static and dynamic context objects are created.
(5) Transformer Weaving. BISM evaluates the used dynamic contexts based on the joinpoint static information and weaves the bytecode needed to extract concrete values from executions. It then weaves instrumentation methods by compiling them into bytecode instructions that are woven into the target program at the specified joinpoint.
(6) Output. The instrumented bytecode is then output back as a .class file in build-time mode, or passed as raw bytes to the JVM in load-time mode. In case of instrumentation errors, e.g., due to adding manual ASM instructions, BISM emits a weaving error. If the visualizer is enabled, instrumented CFGs are stored in HTML files on the disk.

5 Evaluation

We compare BISM with DiSL using two complementary experiments. To guarantee fairness, we switched off adding exception handlers around instrumented code in DiSL. In what follows, we illustrate how we carried out our experiments.

5.1 Inline Monitor to Detect Test Inversions

We instrument an external AES (Advanced Encryption Standard) implementation in build-time mode to detect test inversions. The instrumentation deploys inline monitors that duplicate all conditional jumps in their successor blocks to report test inversions. In BSIM, we use the beforeInstruction joinpoint to capture all conditional jumps. We extract the opcode from the static context object Instructions and use the instrumentation method insert to duplicate the jump-related stack values 333Note that extracting stack values can be also achieved using dynamic context method getStackValue and adding new local variables.. We then use the control-flow joinpoints OnTrueBranchEnter and onFalseBranchEnter to capture the blocks executing after the jump. We inline at the beginning of these blocks, utilizing insert, a duplicate test that reports any inconsistency. This test is written in bytecode instructions based on the last captured conditional jump, and reports a test inversion attack if it happens. In DiSL, we write multiple instrumentation snippets, using the BytecodeMarker to capture all conditional jumps before they occur. We implement a custom InstructionStaticContext object to retrieve additional static information from conditional jump instructions such as the index of a jump target. We also use the dynamic context object to retrieve stack values. We then store the extracted information in synthetic local variables, and we add a flag to specify that a jump has occurred. Using the BasicBlockMarker, we capture basic block entries and check if a jump occurred before entering each block. Accordingly, we re-evaluate the conditional jump in Java syntax using a switch statement on opcodes and the expected target. Hence, we report any inconsistency if it happened.

(a) Runtime (ms).
(b) Memory footprint (KB).
Figure 2: Runtime and memory footprint by AES on files of different sizes.

We use AES to encrypt then decrypt input files of different sizes, line by line. The bytecode size of the original AES class is 9 KB. After instrumentation, it is 10 KB (+11.11%) for BISM, and 128 KB (+1322%) for DiSL. The significant overhead in DiSL is due to the inability to inline the monitor in bytecode and having to instrument it in Java. Fig. 2

reports runtime and memory footprint with respect to file size (KB). For each input file, we performed 100 measurements and reported the mean and the standard deviation. The latter is very low. We used Java JDK 8u181 with 4 GB maximum heap size on a standard PC (Intel Core i7 2.2 GHz, 16 GB RAM) running macOS Catalina v10.15.5 64-bit. The results show that BISM incurs less overhead than DiSL for all file sizes. Table 

1 reports the number of events (i.e., checks duplicated).

Input File (KB)
Events (M) 0.92 1.82 3.65 7.34 14.94 29.53 58.50 117.24 233.10
Table 1: Number of events by AES class (in millions).

5.2 DaCapo Benchmarks

We compare BISM, DiSL and AspectJ in a general runtime verification scenario. We use HasNext, UnSafeIterator and SafeSyncMap properties. HasNext property specifies that a program should always call hasNext() before calling next() on an iterator. UnSafeIterator property specifies that a collection should not be updated when an iterator associated with it is being used. SafeSyncMap property specifies that a map should not be updated when an iterator associated with it is being used. We instrument, in build-time and load-time mode, the benchmarks in the DaCapo suite [6] (dacapo-9.12-bach), targeting only the packages specific to each benchmark. We implement an external monitor library to receive the events with methods that only count the number of invocations. Instrumentation in BISM is straightforward and written in one Transformer class. Method calls are captured using joinpoints beforeMethodCall and afterMethodCall, and are filtered by their names and owners using the static context object provided with the joinpoints. To limit the scope to the specific benchmark packages, we use the runtime argument scope. To access the receivers and results of the method calls (dynamic values), we utilize getMethodReceiver() and getMethodResult() methods. After that, we construct a StaticInvocation object and add the dynamic values to this object. Then to invoke the external monitor library, we utilize invoke instrumentation methods. Instrumentation in DiSL is written in several classes. For each different method call, we write a custom Marker that filters using ASM syntax for a method name and owner. For each marker, we implement an instrumentation snippet in the main instrumentation class. To limit the scope to certain packages, we use the scope annotation on each instrumentation snippet. To access the receivers of the method calls, we use argument processors, and to access method results we use their dynamic context method getStackValue(). Then to invoke the external monitors, we include the monitor library in the instrumentation package and call the methods directly. Instrumentation in AspectJ is written in one Aspect class. We capture method calls with call joinpoints. We defined respective pointcuts and advices to invoke the monitors. We limit the scope to specific packages in the configuration file.

(a) Build-time.
(b) Load-time.
Figure 3: DaCapo benchmarks execution time in ms.

We used Java JDK 8u251 with 2 GB maximum heap size on an Intel Core i9-9980HK (2.4 GHz, 8 GB RAM) running Ubuntu 20.04 LTS 64-bit. Fig. 3 reports the runtime for running BISM and DiSL in (a) build-time and (b) load-time. Our measurements correspond to the mean of 15 runs on each benchmark, also showing the standard deviation. For build-time mode, BISM instrumentation shows less overhead on average. For load-time mode, BISM shows less overhead in all benchmarks. Table 2 reports the number of emitted events in each benchmark444The number of emitted events matches between BISM and DiSL. Even with non-determinism in specific benchmarks, the variation in the number of events is negligible. the number of classes in the scope of instrumentation, instrumented classes (Ins.), and their bytecode size with overhead percentages (Ovh.). The results show that BISM incurs less overhead in all benchmarks.

Events Scope Ins. Original BISM DiSL
KB KB Ovh. % KB Ovh. %
avrora 2.5 M 1550 35 257 264 2.72 270 5.06
batik 0.52 M 2689 136 1544 1572 1.81 1588 2.85
fop 1.6 M 1336 172 1784 1808 1.35 1876 5.16
h2 28 M 472 61 694 704 1.44 720 3.75
pmd 6.6 M 721 90 756 774 2.38 794 5.03
sunflow 3.9 M 221 8 69 71 2.90 74 7.25
xalan 1.04 M 661 9 100 101 1.00 103 3.00
Table 2: Bytecode size of the instrumented benchmarks applications.

Our evaluation confirms that BISM is a lightweight tool that can be used efficiently in runtime verification. BISM incurs low overhead and produces fast and minimal bytecode. For the difference in bytecode size with DiSL, we observe that even with exception-handlers turned off, DiSL still wraps a targeted region with try-finally blocks when @After annotation is used. This is to guarantee that an event will be emitted after a method call, even if an exception is thrown. For load-time instrumentation, the overhead gap closes between BISM and DiSL in benchmarks that have a large number of classes in scope and a small number of instrumented classes. That is because BISM performs a full analysis of the classes in scope to generate its static context. While DiSL generates static context only after marking the needed regions, which is more efficient.

6 Related Work and Discussion

We compare BISM with general-purpose tools for instrumenting Java programs.

ASM [7] is a bytecode manipulation framework utilized by several tools, including BISM. ASM offers two APIs that can be used interchangeably to parse, load, and modify classes. However, to use ASM, a developer has to deal with the low-level details of bytecode instructions and the JVM. BISM offers extended ASM compatibility and provides abstraction with its aspect-oriented paradigm.

DiSL is a bytecode-level instrumentation framework designed for dynamic program analysis [12]. DiSL adopts an aspect-oriented paradigm. It provides an extensible joinpoint model and access to static and dynamic context information. Even though BISM provides a fixed set of joinpoints and static context objects, it performs static analysis on target programs to offer additional and needed out-of-the-box control-flow joinpoints with full static information. As for dynamic context objects, both BISM and DiSL provide equal access. However, DiSL provides typed dynamic objects. Also, both are capable of inserting synthetic local variables (restricted to primitive types in BISM). Both BISM and DiSL require basic knowledge about bytecode semantics from their users. In DiSL, writing custom markers and context objects also requires additional ASM syntax knowledge. However, DiSL does not allow the insertion of arbitrary bytecode instructions but provides a mechanism to write custom transformers in ASM that runs before instrumentation. Whereas, BISM allows to directly insert bytecode instructions, as such a mechanism is essential in many runtime monitoring scenarios, as seen in Section 5.1. All in all, DiSL provides more features (mostly targeted for writing dynamic analysis tools) and enables dynamic dispatch amongst multiple instrumentations and analysis without interference [4], while BISM is more lightweight as shown by our evaluation.

AspectJ [10] is the standard aspect-oriented programming [11] framework highly adopted for instrumenting Java applications. It provides a high-level language used in several domains like monitoring, debugging, and logging. AspectJ cannot capture bytecode instructions and basic blocks directly, forcing developers to insert additional code (like method calls) to the source program. With BISM, developers can target single bytecode instructions and basic block levels, and also have access to local variables and stack values. Furthermore, AspectJ introduces a significant instrumentation overhead and provides less control on where instrumentation snippets get inlined. In BISM, the instrumentation methods are weaved with minimal bytecode instructions and are always inlined next to the targeted regions.

7 Conclusion

This paper introduces BISM (Bytecode-Level Instrumentation for Software Monitoring), a lightweight bytecode instrumentation tool that features an expressive high-level instrumentation language inspired by the AOP paradigm. Overall, BISM is an effective tool for low-level and control-flow aware instrumentation, complementary to DiSL which is better suited for dynamic analysis (e.g. profiling). We believe that BISM can be used for lightweight and expressive runtime verification.

References

  • [1] Apache commons. Note: https://commons.apache.orgAccessed: 2020-06-18 Cited by: §1.
  • [2] E. Bartocci, Y. Falcone, B. Bonakdarpour, C. Colombo, N. Decker, K. Havelund, Y. Joshi, F. Klaedtke, R. Milewicz, G. Reger, G. Rosu, J. Signoles, D. Thoma, E. Zalinescu, and Y. Zhang (2019) First international competition on runtime verification: rules, benchmarks, tools, and final results of CRV 2014. Int. J. Softw. Tools Technol. Transf. 21 (1), pp. 31–70. External Links: Link Cited by: §1.
  • [3] E. Bartocci, Y. Falcone, A. Francalanza, and G. Reger (2018) Introduction to runtime verification. In Lectures on Runtime Verification - Introductory and Advanced Topics, Cited by: §1.
  • [4] W. Binder, P. Moret, É. Tanter, and D. Ansaloni (2016) Polymorphic bytecode instrumentation. Softw. Pract. Exp. 46 (10), pp. 1351–1380. Cited by: §6.
  • [5] BISM: Bytecode-Level Instrumentation for Software Monitoring. (en). External Links: Link Cited by: §4.
  • [6] S. M. Blackburn, R. Garner, C. Hoffmann, A. M. Khan, K. S. McKinley, R. Bentzur, A. Diwan, D. Feinberg, D. Frampton, S. Z. Guyer, M. Hirzel, A. L. Hosking, M. Jump, H. B. Lee, J. E. B. Moss, A. Phansalkar, D. Stefanovic, T. VanDrunen, D. von Dincklage, and B. Wiedermann (2006) The dacapo benchmarks: java benchmarking development and analysis. In Proceedings of the 21th Annual ACM SIGPLAN Conference on Object-Oriented Programming, Systems, Languages, and Applications, OOPSLA 2006, October 22-26, 2006, Portland, Oregon, USA, P. L. Tarr and W. R. Cook (Eds.), pp. 169–190. Cited by: §1, §5.2.
  • [7] E. Bruneton, R. Lenglet, and T. Coupaye (2002) ASM: a code manipulation tool to implement adaptable systems. In Adaptable and extensible component systems, External Links: Link Cited by: §1, §2, §6.
  • [8] C. Colombo and M. Leucker (Eds.) (2018) Runtime verification - 18th international conference, RV 2018, limassol, cyprus, november 10-13, 2018, proceedings. Lecture Notes in Computer Science, Vol. 11237, Springer. External Links: ISBN 978-3-030-03768-0 Cited by: 9.
  • [9] Y. Falcone, S. Krstic, G. Reger, and D. Traytel (2018) A taxonomy for classifying runtime verification tools. See Runtime verification - 18th international conference, RV 2018, limassol, cyprus, november 10-13, 2018, proceedings, Colombo and Leucker, pp. 241–262. Cited by: §1.
  • [10] G. Kiczales, E. Hilsdale, J. Hugunin, M. Kersten, J. Palm, and W. G. Griswold (2001) Getting started with AspectJ. Commun. ACM 44 (10), pp. 59–65. Cited by: §1, §6.
  • [11] G. Kiczales, J. Lamping, A. Mendhekar, C. Maeda, C. V. Lopes, J. Loingtier, and J. Irwin (1997) Aspect-oriented programming. In ECOOP’97, M. Aksit and S. Matsuoka (Eds.), LNCS, Vol. 1241, pp. 220–242. Cited by: §1, §6.
  • [12] L. Marek, A. Villazón, Y. Zheng, D. Ansaloni, W. Binder, and Z. Qi (2012) DiSL: a domain-specific language for bytecode instrumentation. In Proceedings of the 11th International Conference on Aspect-oriented Software Development, AOSD, Potsdam, Germany, R. Hirschfeld, É. Tanter, K. J. Sullivan, and R. P. Gabriel (Eds.), pp. 239–250. Cited by: §1, §6.