Skip to content

Commit f57076b

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 f57076b

3 files changed

Lines changed: 105 additions & 10 deletions

File tree

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

Lines changed: 24 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,18 @@ class RuntimeInstrumentor(
265267
instrumentedBytecode
266268
}
267269
}
270+
271+
private fun registerSourceLocations(locations: EdgeLocationData?, firstEdgeId: Int) {
272+
if (locations == null) return
273+
try {
274+
com.code_intelligence.jazzer.runtime.SourceLocationRegistry.registerLocations(
275+
locations.sourceFile,
276+
locations.methodNames,
277+
firstEdgeId,
278+
locations.edgeData,
279+
)
280+
} catch (_: UnsatisfiedLinkError) {
281+
// Native library not loaded (e.g. standalone instrumentation).
282+
}
283+
}
268284
}

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: 74 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,23 @@ 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 =
100+
"EdgeLocationData(sourceFile=$sourceFile, methods=${methodNames.size}, edges=${edgeData.size / 2})"
101+
}
102+
85103
// An instance of EdgeCoverageInstrumentor should only be used to instrument a single class as it
86104
// internally tracks the edge IDs, which have to be globally unique.
87105
class EdgeCoverageInstrumentor(
@@ -106,10 +124,38 @@ class EdgeCoverageInstrumentor(
106124
),
107125
)
108126

127+
// ── Source location tracking ──────────────────────────────────
128+
private var sourceFile: String = ""
129+
private var simpleClassName: String = ""
130+
private val methodNameList = mutableListOf<String>()
131+
private val methodNameIndex = mutableMapOf<String, Int>()
132+
private val locationData = mutableListOf<Int>()
133+
134+
private fun internMethodName(name: String): Int =
135+
methodNameIndex.getOrPut(name) {
136+
methodNameList.add(name)
137+
methodNameList.size - 1
138+
}
139+
140+
/** Returns the collected edge locations, or null if no edges were instrumented. */
141+
fun buildEdgeLocations(): EdgeLocationData? {
142+
if (numEdges == 0) return null
143+
return EdgeLocationData(
144+
sourceFile,
145+
methodNameList.toTypedArray(),
146+
locationData.toIntArray(),
147+
)
148+
}
149+
109150
override fun instrument(
110151
internalClassName: String,
111152
bytecode: ByteArray,
112153
): ByteArray {
154+
val lastSlash = internalClassName.lastIndexOf('/')
155+
simpleClassName = if (lastSlash >= 0) internalClassName.substring(lastSlash + 1) else internalClassName
156+
// Fallback source file if the class has no SourceFile attribute.
157+
sourceFile = internalClassName
158+
113159
val reader = InstrSupport.classReaderFor(bytecode)
114160
val writer = ClassWriter(reader, 0)
115161
val version = InstrSupport.getMajorVersion(reader)
@@ -144,6 +190,8 @@ class EdgeCoverageInstrumentor(
144190
/**
145191
* A [ProbeInserter] that injects bytecode instrumentation at every control flow edge and
146192
* modifies the stack size and number of local variables accordingly.
193+
*
194+
* Also records the source location of each probe for PC symbolization.
147195
*/
148196
private inner class EdgeCoverageProbeInserter(
149197
access: Int,
@@ -152,7 +200,22 @@ class EdgeCoverageInstrumentor(
152200
mv: MethodVisitor,
153201
arrayStrategy: IProbeArrayStrategy,
154202
) : ProbeInserter(access, name, desc, mv, arrayStrategy) {
203+
private var currentLine = 0
204+
private var isFirstProbe = true
205+
private val methodIdx = internMethodName("$simpleClassName.$name")
206+
207+
override fun visitLineNumber(line: Int, start: Label) {
208+
currentLine = line
209+
super.visitLineNumber(line, start)
210+
}
211+
155212
override fun insertProbe(id: Int) {
213+
// Pack isFuncEntry into the sign bit of the line number.
214+
val packedLine = if (isFirstProbe) (currentLine or (1 shl 31)) else currentLine
215+
locationData.add(packedLine)
216+
locationData.add(methodIdx)
217+
isFirstProbe = false
218+
156219
strategy.instrumentControlFlowEdge(mv, id, variable, coverageMapInternalClassName)
157220
}
158221

@@ -182,6 +245,17 @@ class EdgeCoverageInstrumentor(
182245
) : ClassProbesAdapter(cpv, trackFrames) {
183246
override fun nextId(): Int = nextEdgeId()
184247

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

0 commit comments

Comments
 (0)