Skip to content

Commit 3ab5db7

Browse files
committed
feat(instrumentor): capture per-edge source locations during probe insertion
Each JaCoCo probe now records its source file, method name, and line number. The sign bit of each packed line number carries the isFunctionEntry flag. Data is surfaced via EdgeLocationData. ClassInstrumentor wraps edge count and location data in CoverageResult. RuntimeInstrumentor forwards locations to the native SourceLocationRegistry for PC symbolization.
1 parent 2fe4c75 commit 3ab5db7

3 files changed

Lines changed: 113 additions & 10 deletions

File tree

src/main/java/com/code_intelligence/jazzer/agent/RuntimeInstrumentor.kt

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.code_intelligence.jazzer.agent
1818

1919
import com.code_intelligence.jazzer.instrumentor.ClassInstrumentor
2020
import com.code_intelligence.jazzer.instrumentor.CoverageRecorder
21+
import com.code_intelligence.jazzer.instrumentor.EdgeLocationData
2122
import com.code_intelligence.jazzer.instrumentor.Hook
2223
import com.code_intelligence.jazzer.instrumentor.InstrumentationType
2324
import com.code_intelligence.jazzer.utils.ClassNameGlobber
@@ -246,14 +247,15 @@ class RuntimeInstrumentor(
246247
// or there will be additional coverage points injected if any calls are inserted
247248
// and JaCoCo will produce a broken coverage report.
248249
coverageIdSynchronizer.withIdForClass(internalClassName) { firstId ->
249-
coverage(firstId).also { actualNumEdgeIds ->
250-
CoverageRecorder.recordInstrumentedClass(
251-
internalClassName,
252-
bytecode,
253-
firstId,
254-
actualNumEdgeIds,
255-
)
256-
}
250+
val result = coverage(firstId)
251+
CoverageRecorder.recordInstrumentedClass(
252+
internalClassName,
253+
bytecode,
254+
firstId,
255+
result.numEdges,
256+
)
257+
registerSourceLocations(result.locations, firstId)
258+
result.numEdges
257259
}
258260
// Hook instrumentation must be performed after data flow tracing as the injected
259261
// bytecode would trigger the GEP callbacks for byte[].
@@ -265,4 +267,21 @@ class RuntimeInstrumentor(
265267
instrumentedBytecode
266268
}
267269
}
270+
271+
private fun registerSourceLocations(
272+
locations: EdgeLocationData?,
273+
firstEdgeId: Int,
274+
) {
275+
if (locations == null) return
276+
try {
277+
com.code_intelligence.jazzer.runtime.SourceLocationRegistry.registerLocations(
278+
locations.sourceFile,
279+
locations.methodNames,
280+
firstEdgeId,
281+
locations.edgeData,
282+
)
283+
} catch (_: UnsatisfiedLinkError) {
284+
// Native library not loaded (e.g. standalone instrumentation).
285+
}
286+
}
268287
}

src/main/java/com/code_intelligence/jazzer/instrumentor/ClassInstrumentor.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,27 @@ import com.code_intelligence.jazzer.runtime.CoverageMap
2121
fun extractClassFileMajorVersion(classfileBuffer: ByteArray): Int =
2222
((classfileBuffer[6].toInt() and 0xff) shl 8) or (classfileBuffer[7].toInt() and 0xff)
2323

24+
data class CoverageResult(
25+
val numEdges: Int,
26+
val locations: EdgeLocationData?,
27+
)
28+
2429
class ClassInstrumentor(
2530
private val internalClassName: String,
2631
bytecode: ByteArray,
2732
) {
2833
var instrumentedBytecode = bytecode
2934
private set
3035

31-
fun coverage(initialEdgeId: Int): Int {
36+
fun coverage(initialEdgeId: Int): CoverageResult {
3237
val edgeCoverageInstrumentor =
3338
EdgeCoverageInstrumentor(
3439
defaultEdgeCoverageStrategy,
3540
defaultCoverageMap,
3641
initialEdgeId,
3742
)
3843
instrumentedBytecode = edgeCoverageInstrumentor.instrument(internalClassName, instrumentedBytecode)
39-
return edgeCoverageInstrumentor.numEdges
44+
return CoverageResult(edgeCoverageInstrumentor.numEdges, edgeCoverageInstrumentor.buildEdgeLocations())
4045
}
4146

4247
fun traceDataFlow(instrumentations: Set<InstrumentationType>) {

src/main/java/com/code_intelligence/jazzer/instrumentor/EdgeCoverageInstrumentor.kt

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import com.code_intelligence.jazzer.third_party.org.jacoco.core.internal.instr.P
3030
import org.objectweb.asm.ClassReader
3131
import org.objectweb.asm.ClassVisitor
3232
import org.objectweb.asm.ClassWriter
33+
import org.objectweb.asm.Label
3334
import org.objectweb.asm.MethodVisitor
3435
import java.lang.invoke.MethodHandle
3536
import java.lang.invoke.MethodHandles.publicLookup
@@ -82,6 +83,22 @@ interface EdgeCoverageStrategy {
8283
val loadLocalVariableStackSize: Int
8384
}
8485

86+
/**
87+
* Per-edge source location data collected during instrumentation.
88+
*
89+
* @param sourceFile Qualified source path, e.g. "com/example/Foo.java"
90+
* @param methodNames Deduplicated method name table (SimpleClassName.method)
91+
* @param edgeData Flat array: [packedLine0, methodIdx0, packedLine1, methodIdx1, ...]
92+
* The sign bit of each packedLine encodes isFunctionEntry.
93+
*/
94+
class EdgeLocationData(
95+
val sourceFile: String,
96+
val methodNames: Array<String>,
97+
val edgeData: IntArray,
98+
) {
99+
override fun toString(): String = "EdgeLocationData(sourceFile=$sourceFile, methods=${methodNames.size}, edges=${edgeData.size / 2})"
100+
}
101+
85102
// An instance of EdgeCoverageInstrumentor should only be used to instrument a single class as it
86103
// internally tracks the edge IDs, which have to be globally unique.
87104
class EdgeCoverageInstrumentor(
@@ -106,10 +123,38 @@ class EdgeCoverageInstrumentor(
106123
),
107124
)
108125

126+
// ── Source location tracking ──────────────────────────────────
127+
private var sourceFile: String = ""
128+
private var simpleClassName: String = ""
129+
private val methodNameList = mutableListOf<String>()
130+
private val methodNameIndex = mutableMapOf<String, Int>()
131+
private val locationData = mutableListOf<Int>()
132+
133+
private fun internMethodName(name: String): Int =
134+
methodNameIndex.getOrPut(name) {
135+
methodNameList.add(name)
136+
methodNameList.size - 1
137+
}
138+
139+
/** Returns the collected edge locations, or null if no edges were instrumented. */
140+
fun buildEdgeLocations(): EdgeLocationData? {
141+
if (numEdges == 0) return null
142+
return EdgeLocationData(
143+
sourceFile,
144+
methodNameList.toTypedArray(),
145+
locationData.toIntArray(),
146+
)
147+
}
148+
109149
override fun instrument(
110150
internalClassName: String,
111151
bytecode: ByteArray,
112152
): ByteArray {
153+
val lastSlash = internalClassName.lastIndexOf('/')
154+
simpleClassName = if (lastSlash >= 0) internalClassName.substring(lastSlash + 1) else internalClassName
155+
// Fallback source file if the class has no SourceFile attribute.
156+
sourceFile = internalClassName
157+
113158
val reader = InstrSupport.classReaderFor(bytecode)
114159
val writer = ClassWriter(reader, 0)
115160
val version = InstrSupport.getMajorVersion(reader)
@@ -144,6 +189,8 @@ class EdgeCoverageInstrumentor(
144189
/**
145190
* A [ProbeInserter] that injects bytecode instrumentation at every control flow edge and
146191
* modifies the stack size and number of local variables accordingly.
192+
*
193+
* Also records the source location of each probe for PC symbolization.
147194
*/
148195
private inner class EdgeCoverageProbeInserter(
149196
access: Int,
@@ -152,7 +199,25 @@ class EdgeCoverageInstrumentor(
152199
mv: MethodVisitor,
153200
arrayStrategy: IProbeArrayStrategy,
154201
) : ProbeInserter(access, name, desc, mv, arrayStrategy) {
202+
private var currentLine = 0
203+
private var isFirstProbe = true
204+
private val methodIdx = internMethodName("$simpleClassName.$name")
205+
206+
override fun visitLineNumber(
207+
line: Int,
208+
start: Label,
209+
) {
210+
currentLine = line
211+
super.visitLineNumber(line, start)
212+
}
213+
155214
override fun insertProbe(id: Int) {
215+
// Pack isFuncEntry into the sign bit of the line number.
216+
val packedLine = if (isFirstProbe) (currentLine or (1 shl 31)) else currentLine
217+
locationData.add(packedLine)
218+
locationData.add(methodIdx)
219+
isFirstProbe = false
220+
156221
strategy.instrumentControlFlowEdge(mv, id, variable, coverageMapInternalClassName)
157222
}
158223

@@ -182,6 +247,20 @@ class EdgeCoverageInstrumentor(
182247
) : ClassProbesAdapter(cpv, trackFrames) {
183248
override fun nextId(): Int = nextEdgeId()
184249

250+
override fun visitSource(
251+
source: String?,
252+
debug: String?,
253+
) {
254+
if (source != null) {
255+
// sourceFile was initialized to the internal class name (e.g. "com/example/Outer$Inner")
256+
// in instrument(). Extract the package prefix and prepend it to the declared source
257+
// file name, giving e.g. "com/example/Outer.java".
258+
val packageEnd = sourceFile.lastIndexOf('/')
259+
sourceFile = if (packageEnd >= 0) sourceFile.substring(0, packageEnd + 1) + source else source
260+
}
261+
super.visitSource(source, debug)
262+
}
263+
185264
override fun visitEnd() {
186265
cpv.visitTotalProbeCount(numEdges)
187266
// Avoid calling super.visitEnd() as that invokes cpv.visitTotalProbeCount with an

0 commit comments

Comments
 (0)