Jacoco字节码植入原理(源码分析)
首先了解jacoco agent入口類(MANIFEST.M文件聲明):
入口類—PreMain:
代碼:
packageorg.jacoco.agent.rt.internal_6da5971;importjava.lang.instrument.Instrumentation;importorg.jacoco.agent.rt.internal_6da5971.core.runtime.AgentOptions;importorg.jacoco.agent.rt.internal_6da5971.core.runtime.IRuntime;importorg.jacoco.agent.rt.internal_6da5971.core.runtime.ModifiedSystemClassRuntime;publicfinal class PreMain{public static void premain(String options,Instrumentation inst)throws Exception{AgentOptions agentOptions = newAgentOptions(options);Agent agent = Agent.getInstance(agentOptions);IRuntime runtime = createRuntime(inst);runtime.startup(agent.getData());inst.addTransformer(newCoverageTransformer(runtime, agentOptions, IExceptionLogger.SYSTEM_ERR));}private static IRuntime createRuntime(Instrumentationinst)throws Exception{return ModifiedSystemClassRuntime.createFor(inst,"java/util/UUID");}}Jaococ使用asm實現字節碼植入,是對指令級別上的字節碼植入,從而可以定位到執行的代碼行,以達到覆蓋率的統計。在這個基礎上,jacoco有對類級別,方法級別,邏輯分支級別以及代碼行級別做了專門的處理封裝。具體的封裝類在internal.analysis.flow下面,涉及的類分別是ClassprobesAdapter.java(類級別),Instruction.java(指令級別),LabelFlowAnalysis.java(邏輯分支級別)和MethodProbesAdapter.java(方法級別)。
ClassprobesAdapter類核心代碼:
?
publicfinal MethodVisitor visitMethod(intaccess, String name, String desc, String signature, String[] exceptions){MethodProbesVisitor mv =this.cv.visitMethod(access, name, desc, signature, exceptions);MethodProbesVisitor methodProbes;final MethodProbesVisitor methodProbes;if (mv == null) {methodProbes =EMPTY_METHOD_PROBES_VISITOR;} else {methodProbes = mv;}new MethodSanitizer(null, access, name,desc, signature, exceptions){public void visitEnd(){super.visitEnd();LabelFlowAnalyzer.markLabels(this);MethodProbesAdapter probesAdapter = newMethodProbesAdapter(methodProbes, ClassProbesAdapter.this);if(ClassProbesAdapter.this.trackFrames){AnalyzerAdapter analyzer = new AnalyzerAdapter(ClassProbesAdapter.this.name,this.access, this.name, this.desc, probesAdapter); probesAdapter.setAnalyzer(analyzer);accept(analyzer);}else{accept(probesAdapter);}}};}可見類覆蓋率字節碼埋入實際上是對類中每一個方法和每一個邏輯分支做埋入,只要記錄調用類中方法的覆蓋代碼行,自然類的覆蓋就會被統計到。
接著看MethodProbesAdapter 中的代碼:
@Overridepublic void visitLabel(final Label label) {if (LabelInfo.needsProbe(label)) {if(tryCatchProbeLabels.containsKey(label)) {probesVisitor.visitLabel(tryCatchProbeLabels.get(label));}probesVisitor.visitProbe(idGenerator.nextId());}probesVisitor.visitLabel(label);}@Overridepublic void visitInsn(final int opcode) {switch (opcode) {case Opcodes.IRETURN:case Opcodes.LRETURN:case Opcodes.FRETURN:case Opcodes.DRETURN:case Opcodes.ARETURN:case Opcodes.RETURN:case Opcodes.ATHROW:probesVisitor.visitInsnWithProbe(opcode,idGenerator.nextId());break;default:probesVisitor.visitInsn(opcode);break;}}@Overridepublic void visitJumpInsn(final int opcode, final Label label) {if (LabelInfo.isMultiTarget(label)) {probesVisitor.visitJumpInsnWithProbe(opcode,label,idGenerator.nextId(), frame(jumpPopCount(opcode)));} else {probesVisitor.visitJumpInsn(opcode,label);}}private int jumpPopCount(final int opcode) {switch (opcode) {case Opcodes.GOTO:return 0;case Opcodes.IFEQ:case Opcodes.IFNE:case Opcodes.IFLT:case Opcodes.IFGE:case Opcodes.IFGT:case Opcodes.IFLE:case Opcodes.IFNULL:case Opcodes.IFNONNULL:return 1;default: // IF_CMPxx and IF_ACMPxxreturn 2;}}@Overridepublic void visitLookupSwitchInsn(final Label dflt, final int[]keys,final Label[] labels) {if (markLabels(dflt, labels)) {probesVisitor.visitLookupSwitchInsnWithProbes(dflt,keys, labels,frame(1));} else {probesVisitor.visitLookupSwitchInsn(dflt,keys, labels);}}@Overridepublic void visitTableSwitchInsn(final int min, final int max,final Label dflt, final Label...labels) {if (markLabels(dflt, labels)) {probesVisitor.visitTableSwitchInsnWithProbes(min,max, dflt,labels, frame(1));} else {probesVisitor.visitTableSwitchInsn(min,max, dflt, labels);}}在MethodProbesAdapter中明顯看到字節碼指令信息,對于一個方法的進入,jvm中是一個方法棧的創建,入口指令是入棧指令,退出是return:
privateint jumpPopCount(finalint opcode) {
??????? switch (opcode) {
??????? case Opcodes.GOTO:
??????????? return0;
??????? caseOpcodes.IFEQ:
??????? caseOpcodes.IFNE:
??????? caseOpcodes.IFLT:
??????? caseOpcodes.IFGE:
??????? caseOpcodes.IFGT:
??????? caseOpcodes.IFLE:
??????? caseOpcodes.IFNULL:
??????? caseOpcodes.IFNONNULL:
??????????? return1;
??????? default:// IF_CMPxx and IF_ACMPxx
??????????? return2;
??????? }
??? }
退出方法是return 指令:
publicvoid visitInsn(finalint opcode) {
??????? switch (opcode) {
??????? case Opcodes.IRETURN:
??????? caseOpcodes.LRETURN:
??????? caseOpcodes.FRETURN:
??????? caseOpcodes.DRETURN:
??????? caseOpcodes.ARETURN:
??????? caseOpcodes.RETURN:
??????? caseOpcodes.ATHROW:
??????????? probesVisitor.visitInsnWithProbe(opcode,idGenerator.nextId());
??????????? break;
??????? default:
??????? ??? probesVisitor.visitInsn(opcode);
??????????? break;
??????? }
??? }
邏輯跳轉的有switch,if
publicvoid visitTableSwitchInsn(finalint min, final int max,
??????????? final Label dflt, final Label...labels) {
??????? if (markLabels(dflt, labels)) {
??????????? probesVisitor.visitTableSwitchInsnWithProbes(min,max, dflt,
??????????????????? labels, frame(1));
??????? } else {
??????????? probesVisitor.visitTableSwitchInsn(min,max, dflt, labels);
??????? }
??? }
If分支:
case Opcodes.GOTO:
??????????? return0;
??????? caseOpcodes.IFEQ:
??????? caseOpcodes.IFNE:
??????? caseOpcodes.IFLT:
??????? caseOpcodes.IFGE:
??????? caseOpcodes.IFGT:
??????? caseOpcodes.IFLE:
??????? caseOpcodes.IFNULL:
??????? caseOpcodes.IFNONNULL:
??????????? return1;
??????? default:// IF_CMPxx and IF_ACMPxx
??????????? return2;
??????? }?
LabelFlowAnalysis主要實現代碼:
@Overridepublic void visitJumpInsn(final int opcode, final Label label) {LabelInfo.setTarget(label);if (opcode == Opcodes.JSR) {thrownew AssertionError("Subroutines not supported.");}successor = opcode != Opcodes.GOTO;first = false;}@Overridepublic void visitLabel(final Label label) {if (first) {LabelInfo.setTarget(label);}if (successor) {LabelInfo.setSuccessor(label);}}@Overridepublic void visitLineNumber(final int line, final Label start) {lineStart = start;}@Overridepublic void visitTableSwitchInsn(final int min, final int max,final Label dflt, final Label...labels) {visitSwitchInsn(dflt, labels);}@Overridepublic void visitLookupSwitchInsn(final Label dflt, final int[]keys,final Label[] labels) {visitSwitchInsn(dflt, labels);}@Overridepublic void visitInsn(final int opcode) {switch (opcode) {case Opcodes.RET:throw new AssertionError("Subroutinesnot supported.");case Opcodes.IRETURN:case Opcodes.LRETURN:case Opcodes.FRETURN:case Opcodes.DRETURN:case Opcodes.ARETURN:case Opcodes.RETURN:case Opcodes.ATHROW:successor = false;break;default:successor = true;break;}first = false;}首先要知道對于一串指令比如:
iLoad A;
iLoad B;
Add A,B;
iStore;
……
如果沒有跳轉指令 GOTO LABEL或者jump,那么指令值按順序執行的,所以我們只要在開始的時候添加一個探針就好,只要探針指令執行了,那么下面的指令一定會被執行的,除非有了跳轉邏輯。因此我們只要在每一個跳轉的開始和結束添加探針就好,就可以完全實現統計代碼塊的覆蓋,而沒有必要每一行都要植入探針。
接著在看Instruction代碼:
*/public void setPredecessor(final Instructionpredecessor,final int branch) {this.predecessor = predecessor;predecessor.addBranch();this.predecessorBranch = branch;}/***Marks one branch of this instruction as covered. Also recursively marks* allpredecessor instructions as covered if this is the first covered*branch.**@param branch* branch number to mark as covered*/public void setCovered(final int branch) {Instruction i = this;int b = branch;while (i != null) {if (!i.coveredBranches.isEmpty()) {i.coveredBranches.set(b);break;}i.coveredBranches.set(b);b = i.predecessorBranch;i = i.predecessor;}}Instruction的實現是為了記錄對應指令的代碼行,記錄在跳轉的label處對應的代碼行數,那么類推可以等到整個覆蓋和未覆蓋的代碼行。
上面已經了解我們對于類,方法,邏輯塊以及具體代碼的記錄和探針植入;接著我們需要了解具體植入的是什么指令。首先看下jacoco中探針植入類—ProbeInserter
ProbeInserter(final int access, final String name, finalString desc, final MethodVisitor mv,final IProbeArrayStrategyarrayStrategy) {super(InstrSupport.ASM_API_VERSION, mv);this.clinit =InstrSupport.CLINIT_NAME.equals(name);this.arrayStrategy = arrayStrategy;int pos = (Opcodes.ACC_STATIC &access) == 0 ? 1 : 0;for (final Type t :Type.getArgumentTypes(desc)) {pos += t.getSize();}variable = pos;}public void insertProbe(final int id) {// For a probe we set the correspondingposition in the boolean[] array// to true.mv.visitVarInsn(Opcodes.ALOAD, variable);// Stack[0]: [ZInstrSupport.push(mv, id);// Stack[1]: I// Stack[0]: [Zmv.visitInsn(Opcodes.ICONST_1);// Stack[2]: I// Stack[1]: I// Stack[0]: [Zmv.visitInsn(Opcodes.BASTORE);}private void visitInsn() {final Instruction insn = newInstruction(currentNode, currentLine);nodeToInstruction.put(currentNode,insn);instructions.add(insn);if (lastInsn != null) {insn.setPredecessor(lastInsn, 0);}final int labelCount =currentLabel.size();if (labelCount > 0) {for (int i = labelCount; --i >=0;) {LabelInfo.setInstruction(currentLabel.get(i),insn);}currentLabel.clear();}lastInsn = insn;}@Overridepublic final void visitIincInsn(final int var, final intincrement) {mv.visitIincInsn(map(var), increment);}@Overridepublic final void visitLocalVariable(final String name, final Stringdesc,final String signature, final Labelstart, final Label end,final int index) {mv.visitLocalVariable(name, desc,signature, start, end, map(index));}大致思路就是,在對應字節碼執行入口和跳轉入口處,放入probe,是一個數值(這個數值和probe id有關系),入棧之后加1,則記錄一次執行。所有放入的探針對應一個boolean [],探針入棧之后,那么boolean[] 對應的位置變成true,記錄執行了。
InstrSupport類中關鍵的兩個方法:
publicstatic void assertNotInstrumented(finalString member,final String owner) throwsIllegalStateException {if (member.equals(DATAFIELD_NAME) ||member.equals(INITMETHOD_NAME)) {throw new IllegalStateException(format("Class%s is already instrumented.", owner));}}/***Generates the instruction to push the given int value on the stack.*Implementation taken from*{@link org.objectweb.asm.commons.GeneratorAdapter#push(int)}.**@param mv* visitor to emit the instruction*@param value* the value to be pushed on the stack.*/public static void push(final MethodVisitor mv, final int value) {if (value >= -1 && value<= 5) {mv.visitInsn(Opcodes.ICONST_0 +value);} else if (value >= Byte.MIN_VALUE&& value <= Byte.MAX_VALUE) {mv.visitIntInsn(Opcodes.BIPUSH,value);} else if (value >= Short.MIN_VALUE&& value <= Short.MAX_VALUE) {mv.visitIntInsn(Opcodes.SIPUSH,value);} else {mv.visitLdcInsn(Integer.valueOf(value));}}Push是用來對于不同的變量值入棧的不同方式,當int取值-1~5時,JVM采用iconst指令將常量壓入棧中,當int取值-128~127時,JVM采用bipush指令將常量壓入棧中,當int取值-32768~32767時,JVM采用sipush指令將常量壓入棧中,當int取值-2147483648~2147483647時,JVM采用ldc指令將常量壓入棧中。
?
在jacoco對類和方法進行植入的時候,會對類的植入鎖定進行判斷,對應的類是instrumenter。
publicbyte[] instrument(finalbyte[] buffer, final String name)throws IOException {try {return instrument(newClassReader(buffer));} catch (final RuntimeException e) {throwinstrumentError(name, e);}}/***Creates a instrumented version of the given class if possible. The*provided {@link InputStream} is not closed by this method.**@param input* stream to read class definition from*@param name* a name used for exception messages*@return instrumented definition*@throws IOException* if reading data from the stream fails or the class can't be* instrumented*/public byte[] instrument(final InputStream input, final Stringname)throws IOException {final byte[] bytes;try {bytes =InputStreams.readFully(input);} catch (final IOException e) {throw instrumentError(name, e);}return instrument(bytes, name);}/***Creates a instrumented version of the given class file. The provided*{@link InputStream} and {@link OutputStream} instances are not closed by*this method.**@param input* stream to read class definition from*@param output* stream to write the instrumented version of the class to*@param name* a name used for exception messages*@throws IOException* if reading data from the stream fails or the class can't be* instrumented*/public void instrument(final InputStream input, finalOutputStream output,final String name) throwsIOException {output.write(instrument(input, name));}private IOException instrumentError(finalString name,finalException cause) {final IOException ex = new IOException(String.format("Errorwhile instrumenting %s.", name));ex.initCause(cause);return ex;}總結
以上是生活随笔為你收集整理的Jacoco字节码植入原理(源码分析)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JVMTM Tool Interface
- 下一篇: sonar 集群环境工作机制的深入理解