Wasm: working on step-in and step-over in debugger

This commit is contained in:
Alexey Andreev 2022-12-09 19:13:08 +01:00
parent 159db29757
commit a6add26aaa
8 changed files with 363 additions and 59 deletions

View File

@ -51,7 +51,7 @@ public class ControlFlowInfo {
for (int i = 0; i < functions.size(); ++i) { for (int i = 0; i < functions.size(); ++i) {
var range = functions.get(i); var range = functions.get(i);
out.println("Range #" + i + ": [" + range.start() + ".." + range.end() + ")"); out.println("Range #" + i + ": [" + range.start() + ".." + range.end() + ")");
for (var iter = range.iterator(); iter.hasNext(); iter.next()) { for (var iter = range.iterator(0); iter.hasNext(); iter.next()) {
out.print(" " + Integer.toHexString(iter.address())); out.print(" " + Integer.toHexString(iter.address()));
if (iter.isCall()) { if (iter.isCall()) {
out.print(" (call)"); out.print(" (call)");

View File

@ -21,7 +21,9 @@ public class FunctionControlFlow {
int[] offsets; int[] offsets;
int[] data; int[] data;
FunctionControlFlow(int[] offsets, int[] data) { FunctionControlFlow(int start, int end, int[] offsets, int[] data) {
this.start = start;
this.end = end;
this.offsets = offsets; this.offsets = offsets;
this.data = data; this.data = data;
} }
@ -34,7 +36,33 @@ public class FunctionControlFlow {
return end; return end;
} }
public FunctionControlFlowIterator iterator() { public FunctionControlFlowIterator iterator(int index) {
return new FunctionControlFlowIterator(this); return new FunctionControlFlowIterator(this, index);
}
public int count() {
return offsets.length;
}
public int findIndex(int address) {
var l = 0;
var u = offsets.length;
while (true) {
var i = (l + u) / 2;
var t = data[offsets[i]];
if (address == t) {
return i;
} else if (address > t) {
l = i + 1;
if (l > u) {
return i + 1;
}
} else {
u = i - 1;
if (u < l) {
return i;
}
}
}
} }
} }

View File

@ -18,9 +18,16 @@ package org.teavm.backend.wasm.debug.info;
import com.carrotsearch.hppc.IntArrayList; import com.carrotsearch.hppc.IntArrayList;
public class FunctionControlFlowBuilder { public class FunctionControlFlowBuilder {
private int start;
private int end;
private IntArrayList offsets = new IntArrayList(); private IntArrayList offsets = new IntArrayList();
private IntArrayList data = new IntArrayList(); private IntArrayList data = new IntArrayList();
public FunctionControlFlowBuilder(int start, int end) {
this.start = start;
this.end = end;
}
public void addBranch(int position, int[] targets) { public void addBranch(int position, int[] targets) {
offsets.add(data.size() << 1); offsets.add(data.size() << 1);
data.add(position); data.add(position);
@ -38,6 +45,6 @@ public class FunctionControlFlowBuilder {
} }
public FunctionControlFlow build() { public FunctionControlFlow build() {
return new FunctionControlFlow(offsets.toArray(), data.toArray()); return new FunctionControlFlow(start, end, offsets.toArray(), data.toArray());
} }
} }

View File

@ -24,8 +24,9 @@ public class FunctionControlFlowIterator {
private int offset; private int offset;
private boolean isCall; private boolean isCall;
FunctionControlFlowIterator(FunctionControlFlow controlFlow) { FunctionControlFlowIterator(FunctionControlFlow controlFlow, int index) {
this.controlFlow = controlFlow; this.controlFlow = controlFlow;
this.index = 0;
} }
public boolean hasNext() { public boolean hasNext() {
@ -37,6 +38,10 @@ public class FunctionControlFlowIterator {
valid = false; valid = false;
} }
public void rewind(int index) {
this.index = index;
}
private void fill() { private void fill() {
if (!valid) { if (!valid) {
valid = true; valid = true;

View File

@ -49,13 +49,18 @@ public class LineInfoUnpackedSequence {
} }
public InstructionLocation find(int address) { public InstructionLocation find(int address) {
var index = findIndex(address);
return index >= 0 ? locations.get(index) : null;
}
public int findIndex(int address) {
if (address < startAddress || address >= endAddress) { if (address < startAddress || address >= endAddress) {
return null; return -1;
} }
var index = CollectionUtil.binarySearch(locations, address, InstructionLocation::address); var index = CollectionUtil.binarySearch(locations, address, InstructionLocation::address);
if (index < 0) { if (index < 0) {
index = -index; index = -index;
} }
return locations.get(Math.min(locations.size() - 1, index)); return Math.min(locations.size() - 1, index);
} }
} }

View File

@ -0,0 +1,223 @@
/*
* Copyright 2022 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.backend.wasm.debug.info;
import com.carrotsearch.hppc.IntArrayDeque;
import com.carrotsearch.hppc.IntHashSet;
import com.carrotsearch.hppc.IntSet;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.teavm.backend.wasm.debug.parser.DebugInfoParser;
import org.teavm.common.ByteArrayAsyncInputStream;
import org.teavm.common.CollectionUtil;
public class StepLocationsFinder {
public final DebugInfo debugInfo;
private LineInfoSequence lines;
private FunctionControlFlow graph;
private List<Point> points;
private IntArrayDeque queue = new IntArrayDeque();
private String currentFileName;
private int currentLine;
private boolean enterMethod;
private IntSet breakpointAddresses = new IntHashSet();
private IntSet callAddresses = new IntHashSet();
private IntSet visited = new IntHashSet();
public StepLocationsFinder(DebugInfo debugInfo) {
this.debugInfo = debugInfo;
}
public boolean step(String fileName, int line, int address, boolean enterMethod) {
address -= debugInfo.offset();
updatePoints(address);
if (points == null) {
return false;
}
var index = CollectionUtil.binarySearch(points, address, p -> p.address);
if (index < 0) {
index = -index - 2;
}
if (index < 0) {
return false;
}
this.enterMethod = enterMethod;
currentFileName = fileName;
currentLine = line;
queue.addLast(index);
callAddresses.clear();
breakpointAddresses.clear();
while (!queue.isEmpty()) {
processTask();
}
visited.clear();
currentFileName = null;
return true;
}
public int[] getBreakpointAddresses() {
return breakpointAddresses.toArray();
}
public int[] getCallAddresses() {
return callAddresses.toArray();
}
private void updatePoints(int address) {
var lines = debugInfo.lines().find(address);
var graph = debugInfo.controlFlow().find(address);
if (lines == null || graph == null) {
this.lines = null;
this.graph = null;
points = null;
return;
}
if (lines != this.lines || graph != this.graph) {
this.lines = lines;
this.graph = graph;
points = null;
}
if (points == null) {
points = createPoints();
}
}
private List<Point> createPoints() {
var list = new ArrayList<Point>();
var indexInLines = 0;
var graphIter = graph.iterator(0);
var commandExecutor = new LineInfoCommandExecutor();
while (indexInLines < lines.commands().size() && graphIter.hasNext()) {
boolean nextInGraph;
if (graphIter == null) {
nextInGraph = false;
} else if (indexInLines >= lines.commands().size()) {
nextInGraph = true;
} else {
nextInGraph = lines.commands().get(indexInLines).address() >= graphIter.address();
}
if (nextInGraph) {
var point = new Point(graphIter.address());
list.add(point);
point.isCall = graphIter.isCall();
point.next = graphIter.targets();
graphIter.next();
} else {
var cmd = lines.commands().get(indexInLines++);
cmd.acceptVisitor(commandExecutor);
var location = commandExecutor.createLocation();
if (location != null && location.location() != null) {
Point point;
if (!list.isEmpty() && list.get(list.size() - 1).address == cmd.address()) {
point = list.get(list.size() - 1);
} else {
point = new Point(cmd.address());
list.add(point);
}
point.location = location.location();
}
}
}
for (var point : list) {
var next = point.next;
if (next == null) {
continue;
}
int j = 0;
for (int i = 0; i < next.length; ++i) {
var foundIndex = CollectionUtil.binarySearch(list, next[i], p -> p.address);
if (foundIndex < 0) {
foundIndex = -foundIndex - 1;
}
if (foundIndex >= list.size()) {
continue;
}
next[j++] = foundIndex;
}
if (j != next.length) {
if (j == 0) {
point.next = null;
} else {
point.next = Arrays.copyOf(next, j);
}
}
}
list.trimToSize();
return list;
}
private void processTask() {
var index = queue.removeFirst();
if (!visited.add(index)) {
return;
}
var point = points.get(index);
if (point.location != null && !isCurrent(point.location)) {
breakpointAddresses.add(point.address + debugInfo.offset());
return;
}
if (enterMethod) {
breakpointAddresses.add(point.address + debugInfo.offset());
callAddresses.add(point.address + debugInfo.offset());
}
if (point.next != null) {
for (var nextIndex : point.next) {
queue.addLast(nextIndex);
}
} else if (index < points.size() - 1) {
queue.addLast(index + 1);
}
}
private boolean isCurrent(Location loc) {
return loc.line() == currentLine && loc.file().fullName().equals(currentFileName);
}
private static class Point {
int address;
int[] next;
boolean isCall;
Location location;
Point(int address) {
this.address = address;
}
}
public static void main(String[] args) throws IOException {
var file = new File("/home/konsoletyper/prog/apache-tomcat-10.0.4/webapps/wasm/classes.wasm");
var input = new ByteArrayAsyncInputStream(Files.readAllBytes(file.toPath()));
var parser = new DebugInfoParser(input);
input.readFully(parser::parse);
var debugInfo = parser.getDebugInfo();
var finder = new StepLocationsFinder(debugInfo);
finder.step("org/teavm/samples/wasi/WasiTest2.java", 9, 0x943e + debugInfo.offset(), false);
System.out.println(Arrays.toString(finder.getBreakpointAddresses()));
}
}

View File

@ -31,7 +31,7 @@ import org.teavm.backend.wasm.parser.Opcode;
public class ControlFlowParser implements CodeSectionListener, CodeListener, AddressListener { public class ControlFlowParser implements CodeSectionListener, CodeListener, AddressListener {
private int previousAddress; private int previousAddress;
private int address; private int address;
private FunctionControlFlowBuilder cfb; private int startAddress;
private List<Branch> branches = new ArrayList<>(); private List<Branch> branches = new ArrayList<>();
private List<FunctionControlFlow> ranges = new ArrayList<>(); private List<FunctionControlFlow> ranges = new ArrayList<>();
private List<Branch> pendingBranches = new ArrayList<>(); private List<Branch> pendingBranches = new ArrayList<>();
@ -50,7 +50,7 @@ public class ControlFlowParser implements CodeSectionListener, CodeListener, Add
@Override @Override
public boolean functionStart(int index, int size) { public boolean functionStart(int index, int size) {
cfb = new FunctionControlFlowBuilder(); startAddress = address;
return true; return true;
} }
@ -71,7 +71,7 @@ public class ControlFlowParser implements CodeSectionListener, CodeListener, Add
private int startBlock(boolean loop) { private int startBlock(boolean loop) {
var token = blocks.size(); var token = blocks.size();
var branch = !loop ? newBranch(false) : null; var branch = !loop ? newPendingBranch(false) : null;
var block = new Block(branch, address); var block = new Block(branch, address);
blocks.add(block); blocks.add(block);
if (branch != null) { if (branch != null) {
@ -169,6 +169,7 @@ public class ControlFlowParser implements CodeSectionListener, CodeListener, Add
@Override @Override
public void functionEnd() { public void functionEnd() {
var cfb = new FunctionControlFlowBuilder(startAddress, address);
for (var branch : branches) { for (var branch : branches) {
if (branch.isCall) { if (branch.isCall) {
cfb.addCall(branch.address, branch.targets.toArray()); cfb.addCall(branch.address, branch.targets.toArray());

View File

@ -15,6 +15,7 @@
*/ */
package org.teavm.debugging; package org.teavm.debugging;
import com.carrotsearch.hppc.IntHashSet;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Base64; import java.util.Base64;
import java.util.Collection; import java.util.Collection;
@ -25,9 +26,11 @@ import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.Predicate;
import org.teavm.backend.wasm.debug.info.DebugInfo; import org.teavm.backend.wasm.debug.info.DebugInfo;
import org.teavm.backend.wasm.debug.info.LineInfoFileCommand; import org.teavm.backend.wasm.debug.info.LineInfoFileCommand;
import org.teavm.backend.wasm.debug.info.MethodInfo; import org.teavm.backend.wasm.debug.info.MethodInfo;
import org.teavm.backend.wasm.debug.info.StepLocationsFinder;
import org.teavm.backend.wasm.debug.parser.DebugInfoParser; import org.teavm.backend.wasm.debug.parser.DebugInfoParser;
import org.teavm.common.ByteArrayAsyncInputStream; import org.teavm.common.ByteArrayAsyncInputStream;
import org.teavm.common.Promise; import org.teavm.common.Promise;
@ -53,6 +56,7 @@ public class Debugger {
private JavaScriptDebugger javaScriptDebugger; private JavaScriptDebugger javaScriptDebugger;
private DebugInformationProvider debugInformationProvider; private DebugInformationProvider debugInformationProvider;
private List<JavaScriptBreakpoint> temporaryBreakpoints = new ArrayList<>(); private List<JavaScriptBreakpoint> temporaryBreakpoints = new ArrayList<>();
private Predicate<JavaScriptBreakpoint> temporaryBreakpointHandler;
private Map<JavaScriptScript, DebugInformation> debugInformationMap = new HashMap<>(); private Map<JavaScriptScript, DebugInformation> debugInformationMap = new HashMap<>();
private Map<String, Set<DebugInformation>> debugInformationFileMap = new HashMap<>(); private Map<String, Set<DebugInformation>> debugInformationFileMap = new HashMap<>();
private Map<JavaScriptScript, DebugInfo> wasmDebugInfoMap = new HashMap<>(); private Map<JavaScriptScript, DebugInfo> wasmDebugInfoMap = new HashMap<>();
@ -65,6 +69,7 @@ public class Debugger {
private CallFrame[] callStack; private CallFrame[] callStack;
private Set<String> scriptNames = new LinkedHashSet<>(); private Set<String> scriptNames = new LinkedHashSet<>();
private Set<String> allSourceFiles = new LinkedHashSet<>(); private Set<String> allSourceFiles = new LinkedHashSet<>();
private StepLocationsFinder wasmStepLocationsFinder;
public Debugger(JavaScriptDebugger javaScriptDebugger, DebugInformationProvider debugInformationProvider) { public Debugger(JavaScriptDebugger javaScriptDebugger, DebugInformationProvider debugInformationProvider) {
this.javaScriptDebugger = javaScriptDebugger; this.javaScriptDebugger = javaScriptDebugger;
@ -113,51 +118,57 @@ public class Debugger {
if (callStack == null || callStack.length == 0) { if (callStack == null || callStack.length == 0) {
return jsStep(enterMethod); return jsStep(enterMethod);
} }
var recentFrame = callStack[0]; var frame = callStack[0];
if (recentFrame.getLocation() == null || recentFrame.getLocation().getFileName() == null if (frame.getLocation() == null || frame.getLocation().getFileName() == null
|| recentFrame.getLocation().getLine() < 0) { || frame.getLocation().getLine() < 0) {
return jsStep(enterMethod); return jsStep(enterMethod);
} }
var successors = new HashSet<JavaScriptLocation>(); var successors = new HashSet<JavaScriptLocation>();
boolean first = true;
loop:
for (var frame : callStack) {
var script = frame.getOriginalLocation().getScript(); var script = frame.getOriginalLocation().getScript();
var hasSuccessors = false;
if (frame.getLocation() != null && frame.getLocation().getFileName() != null
&& frame.getLocation().getLine() >= 0) {
switch (script.getLanguage()) { switch (script.getLanguage()) {
case JS: case JS:
if (!addJsBreakpoints(frame, script, enterMethod, first, successors)) { hasSuccessors = addJsBreakpoints(frame, script, enterMethod, successors);
break loop;
}
break; break;
case WASM: { case WASM: {
var info = wasmDebugInfoMap.get(script); var promise = stepWasm(frame, enterMethod);
if (info != null) { if (promise != null) {
return enterMethod ? javaScriptDebugger.stepInto() : javaScriptDebugger.stepOver(); return promise;
} }
break; break;
} }
case UNKNOWN: default:
break; break;
} }
enterMethod = false;
first = false;
} }
if (hasSuccessors) {
return jsStep(enterMethod);
} else {
return createTemporaryBreakpoints(successors, null).thenAsync(v -> javaScriptDebugger.stepOut());
}
}
private Promise<Void> createTemporaryBreakpoints(Collection<JavaScriptLocation> locations,
Predicate<JavaScriptBreakpoint> handler) {
var jsBreakpointPromises = new ArrayList<Promise<Void>>(); var jsBreakpointPromises = new ArrayList<Promise<Void>>();
for (var successor : successors) { for (var location : locations) {
jsBreakpointPromises.add(javaScriptDebugger.createBreakpoint(successor) jsBreakpointPromises.add(javaScriptDebugger.createBreakpoint(location)
.thenVoid(temporaryBreakpoints::add)); .thenVoid(temporaryBreakpoints::add));
} }
return Promise.allVoid(jsBreakpointPromises).thenAsync(v -> javaScriptDebugger.resume()); temporaryBreakpointHandler = handler;
return Promise.allVoid(jsBreakpointPromises);
} }
private boolean addJsBreakpoints(CallFrame frame, JavaScriptScript script, boolean enterMethod, boolean first, private boolean addJsBreakpoints(CallFrame frame, JavaScriptScript script, boolean enterMethod,
Set<JavaScriptLocation> successors) { Set<JavaScriptLocation> successors) {
var debugInfo = debugInformationMap.get(script); var debugInfo = debugInformationMap.get(script);
boolean exits; if (debugInfo == null) {
if (frame.getLocation() != null && frame.getLocation().getFileName() != null return false;
&& frame.getLocation().getLine() >= 0 && debugInfo != null) { }
exits = addFollowing(debugInfo, frame.getLocation(), script, new HashSet<>(), successors); addFollowing(debugInfo, frame.getLocation(), script, new HashSet<>(), successors);
if (enterMethod) { if (enterMethod) {
var successorFinder = new CallSiteSuccessorFinder(debugInfo, script, successors); var successorFinder = new CallSiteSuccessorFinder(debugInfo, script, successors);
var callSites = debugInfo.getCallSites(frame.getLocation()); var callSites = debugInfo.getCallSites(frame.getLocation());
@ -165,18 +176,33 @@ public class Debugger {
callSite.acceptVisitor(successorFinder); callSite.acceptVisitor(successorFinder);
} }
} }
} else { return true;
exits = true;
} }
if (!exits) {
private Promise<Void> stepWasm(CallFrame frame, boolean enterMethod) {
var debugInfo = wasmDebugInfoMap.get(frame.getOriginalLocation().getScript());
if (debugInfo == null || debugInfo.controlFlow() == null || debugInfo.lines() == null) {
return null;
}
if (wasmStepLocationsFinder == null || wasmStepLocationsFinder.debugInfo != debugInfo) {
wasmStepLocationsFinder = new StepLocationsFinder(debugInfo);
}
wasmStepLocationsFinder.step(frame.getLocation().getFileName(), frame.getLocation().getLine(),
frame.getOriginalLocation().getColumn(), enterMethod);
var locations = new ArrayList<JavaScriptLocation>();
for (var breakpointAddress : wasmStepLocationsFinder.getBreakpointAddresses()) {
locations.add(new JavaScriptLocation(frame.getOriginalLocation().getScript(), 0, breakpointAddress));
}
var callAddresses = IntHashSet.from(wasmStepLocationsFinder.getCallAddresses());
var result = createTemporaryBreakpoints(locations, br -> {
if (br != null && br.isValid() && callAddresses.contains(br.getLocation().getColumn())) {
javaScriptDebugger.stepInto();
return false; return false;
} }
if (!first && frame.getLocation() != null) { return true;
for (var location : debugInfo.getGeneratedLocations(frame.getLocation())) { });
successors.add(new JavaScriptLocation(script, location.getLine(), location.getColumn())); return result.thenVoid(x -> javaScriptDebugger.stepOut());
}
}
return false;
} }
static class CallSiteSuccessorFinder implements DebuggerCallSiteVisitor { static class CallSiteSuccessorFinder implements DebuggerCallSiteVisitor {
@ -599,21 +625,30 @@ public class Debugger {
} }
private void firePaused(JavaScriptBreakpoint breakpoint) { private void firePaused(JavaScriptBreakpoint breakpoint) {
List<JavaScriptBreakpoint> temporaryBreakpoints = new ArrayList<>(this.temporaryBreakpoints); var temporaryBreakpoints = new ArrayList<>(this.temporaryBreakpoints);
var handler = temporaryBreakpointHandler;
this.temporaryBreakpoints.clear(); this.temporaryBreakpoints.clear();
List<Promise<Void>> promises = new ArrayList<>(); temporaryBreakpointHandler = null;
for (JavaScriptBreakpoint jsBreakpoint : temporaryBreakpoints) { var promises = new ArrayList<Promise<Void>>();
for (var jsBreakpoint : temporaryBreakpoints) {
promises.add(jsBreakpoint.destroy()); promises.add(jsBreakpoint.destroy());
} }
callStack = null; callStack = null;
Promise.allVoid(promises).thenVoid(v -> { Promise.allVoid(promises).thenVoid(v -> {
Breakpoint javaBreakpoint = null; Breakpoint javaBreakpoint = null;
if (breakpoint != null && !temporaryBreakpoints.contains(breakpoint)) { JavaScriptBreakpoint tmpBreakpoint = null;
if (breakpoint != null) {
if (temporaryBreakpoints.contains(breakpoint)) {
tmpBreakpoint = breakpoint;
} else {
javaBreakpoint = breakpointMap.get(breakpoint); javaBreakpoint = breakpointMap.get(breakpoint);
} }
for (DebuggerListener listener : getListeners()) { }
if (handler == null || !handler.test(tmpBreakpoint)) {
for (var listener : getListeners()) {
listener.paused(javaBreakpoint); listener.paused(javaBreakpoint);
} }
}
}); });
} }