Wasm: working on control flow analyzer for debugger

This commit is contained in:
Alexey Andreev 2022-12-05 20:03:57 +01:00
parent 44204b952d
commit 87d63168d2
15 changed files with 569 additions and 60 deletions

View File

@ -0,0 +1,71 @@
/*
* 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 java.io.PrintStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.teavm.common.CollectionUtil;
public class ControlFlowInfo {
private List<? extends FunctionControlFlow> functions;
public ControlFlowInfo(FunctionControlFlow[] functions) {
this.functions = Collections.unmodifiableList(Arrays.asList(functions));
}
public List<? extends FunctionControlFlow> functions() {
return functions;
}
public FunctionControlFlow find(int address) {
var index = CollectionUtil.binarySearch(functions, address, FunctionControlFlow::end);
if (index < 0) {
index = -index - 1;
}
if (index > functions.size()) {
return null;
}
var fn = functions.get(index);
if (fn.start() > address) {
return fn;
}
return fn;
}
public void dump(PrintStream out) {
for (int i = 0; i < functions.size(); ++i) {
var range = functions.get(i);
out.println("Range #" + i + ": [" + range.start() + ".." + range.end() + ")");
for (var iter = range.iterator(); iter.hasNext(); iter.next()) {
out.print(" " + Integer.toHexString(iter.address()));
if (iter.isCall()) {
out.print(" (call)");
}
out.print(" -> ");
var followers = iter.targets();
for (var j = 0; j < followers.length; ++j) {
if (j > 0) {
out.print(", ");
}
out.print(Integer.toHexString(followers[j]));
}
out.println();
}
}
}
}

View File

@ -0,0 +1,56 @@
/*
* 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 java.io.PrintStream;
public class DebugInfo {
private LineInfo lines;
private ControlFlowInfo controlFlow;
private int offset;
public DebugInfo(LineInfo lines, ControlFlowInfo controlFlow, int offset) {
this.lines = lines;
this.controlFlow = controlFlow;
this.offset = offset;
}
public LineInfo lines() {
return lines;
}
public ControlFlowInfo controlFlow() {
return controlFlow;
}
public int offset() {
return offset;
}
public void dump(PrintStream out) {
if (offset != 0) {
out.println("Code section offset: " + Integer.toHexString(offset));
}
if (lines != null) {
out.println("LINES");
lines.dump(out);
}
if (controlFlow != null) {
out.println("CONTROL FLOW");
controlFlow.dump(out);
}
}
}

View File

@ -0,0 +1,40 @@
/*
* 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;
public class FunctionControlFlow {
private int start;
private int end;
int[] offsets;
int[] data;
FunctionControlFlow(int[] offsets, int[] data) {
this.offsets = offsets;
this.data = data;
}
public int start() {
return start;
}
public int end() {
return end;
}
public FunctionControlFlowIterator iterator() {
return new FunctionControlFlowIterator(this);
}
}

View File

@ -0,0 +1,43 @@
/*
* 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.IntArrayList;
public class FunctionControlFlowBuilder {
private IntArrayList offsets = new IntArrayList();
private IntArrayList data = new IntArrayList();
public void addBranch(int position, int[] targets) {
offsets.add(data.size() << 1);
data.add(position);
data.add(targets);
}
public void addCall(int position, int[] targets) {
offsets.add((data.size() << 1) | 1);
data.add(position);
data.add(targets);
}
public boolean isEmpty() {
return offsets.isEmpty();
}
public FunctionControlFlow build() {
return new FunctionControlFlow(offsets.toArray(), data.toArray());
}
}

View File

@ -0,0 +1,66 @@
/*
* 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 java.util.Arrays;
public class FunctionControlFlowIterator {
private FunctionControlFlow controlFlow;
private int index;
private boolean valid;
private int offset;
private boolean isCall;
FunctionControlFlowIterator(FunctionControlFlow controlFlow) {
this.controlFlow = controlFlow;
}
public boolean hasNext() {
return index < controlFlow.offsets.length;
}
public void next() {
++index;
valid = false;
}
private void fill() {
if (!valid) {
valid = true;
var n = controlFlow.offsets[index];
offset = n >>> 1;
isCall = (n & 1) != 0;
}
}
public int address() {
fill();
return controlFlow.data[offset];
}
public int[] targets() {
fill();
var nextOffset = index < controlFlow.offsets.length - 1
? controlFlow.offsets[index + 1] >>> 1
: controlFlow.data.length;
return Arrays.copyOfRange(controlFlow.data, offset + 1, nextOffset);
}
public boolean isCall() {
fill();
return isCall;
}
}

View File

@ -22,20 +22,14 @@ import java.util.List;
import org.teavm.common.CollectionUtil;
public class LineInfo {
private int offset;
private LineInfoSequence[] sequences;
private List<? extends LineInfoSequence> sequenceList;
public LineInfo(int offset, LineInfoSequence[] sequences) {
this.offset = offset;
public LineInfo(LineInfoSequence[] sequences) {
this.sequences = sequences.clone();
sequenceList = Collections.unmodifiableList(Arrays.asList(this.sequences));
}
public int offset() {
return offset;
}
public List<? extends LineInfoSequence> sequences() {
return sequenceList;
}

View File

@ -0,0 +1,206 @@
/*
* 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.parser;
import com.carrotsearch.hppc.IntArrayList;
import java.util.ArrayList;
import java.util.List;
import org.teavm.backend.wasm.debug.info.ControlFlowInfo;
import org.teavm.backend.wasm.debug.info.FunctionControlFlow;
import org.teavm.backend.wasm.debug.info.FunctionControlFlowBuilder;
import org.teavm.backend.wasm.model.WasmType;
import org.teavm.backend.wasm.parser.AddressListener;
import org.teavm.backend.wasm.parser.BranchOpcode;
import org.teavm.backend.wasm.parser.CodeListener;
import org.teavm.backend.wasm.parser.CodeSectionListener;
import org.teavm.backend.wasm.parser.Opcode;
public class ControlFlowParser implements CodeSectionListener, CodeListener, AddressListener {
private int previousAddress;
private int address;
private FunctionControlFlowBuilder cfb;
private List<Branch> branches = new ArrayList<>();
private List<FunctionControlFlow> ranges = new ArrayList<>();
private List<Branch> pendingBranches = new ArrayList<>();
private List<Block> blocks = new ArrayList<>();
public ControlFlowInfo build() {
return new ControlFlowInfo(ranges.toArray(new FunctionControlFlow[0]));
}
@Override
public void address(int address) {
previousAddress = this.address;
this.address = address;
flush();
}
@Override
public boolean functionStart(int index, int size) {
cfb = new FunctionControlFlowBuilder();
return true;
}
@Override
public CodeListener code() {
return this;
}
@Override
public int startBlock(boolean loop, WasmType type) {
return startBlock(loop);
}
@Override
public int startConditionalBlock(WasmType type) {
return startBlock(false);
}
private int startBlock(boolean loop) {
var token = blocks.size();
var branch = !loop ? newBranch(false) : null;
var block = new Block(branch, address);
blocks.add(block);
if (branch != null) {
block.pendingBranches.add(branch);
}
return token;
}
@Override
public void startElseSection(int token) {
var block = blocks.get(blocks.size() - 1);
var lastBranch = branches.get(branches.size() - 1);
if (lastBranch.address != previousAddress) {
lastBranch = new Branch(previousAddress, false);
branches.add(lastBranch);
}
block.pendingBranches.add(lastBranch);
block.branch.targets.add(address);
}
@Override
public void endBlock(int token, boolean loop) {
var block = blocks.remove(blocks.size() - 1);
pendingBranches.addAll(block.pendingBranches);
if (loop) {
var branch = newBranch(false);
branch.targets.add(block.address);
}
}
@Override
public void call(int functionIndex) {
call();
}
@Override
public void indirectCall(int typeIndex, int tableIndex) {
call();
}
private void call() {
newPendingBranch(true);
}
@Override
public void opcode(Opcode opcode) {
switch (opcode) {
case RETURN:
case UNREACHABLE: {
newBranch(false);
break;
}
default:
break;
}
}
@Override
public void branch(BranchOpcode opcode, int depth, int target) {
var branch = newBranch(false);
if (opcode == BranchOpcode.BR_IF) {
pendingBranches.add(branch);
}
var block = blocks.get(target);
block.pendingBranches.add(branch);
}
@Override
public void tableBranch(int[] depths, int[] targets, int defaultDepth, int defaultTarget) {
var branch = newPendingBranch(false);
for (var target : targets) {
blocks.get(target).pendingBranches.add(branch);
}
blocks.get(defaultTarget).pendingBranches.add(branch);
}
private Branch newPendingBranch(boolean isCall) {
var branch = newBranch(isCall);
pendingBranches.add(branch);
return branch;
}
private Branch newBranch(boolean isCall) {
var branch = new Branch(address, isCall);
branches.add(branch);
return branch;
}
private void flush() {
for (var branch : pendingBranches) {
branch.targets.add(address);
}
pendingBranches.clear();
}
@Override
public void functionEnd() {
for (var branch : branches) {
if (branch.isCall) {
cfb.addCall(branch.address, branch.targets.toArray());
} else {
cfb.addBranch(branch.address, branch.targets.toArray());
}
}
ranges.add(cfb.build());
branches.clear();
pendingBranches.clear();
blocks.clear();
}
private static class Block {
Branch branch;
final int address;
List<Branch> pendingBranches = new ArrayList<>();
Block(Branch branch, int address) {
this.branch = branch;
this.address = address;
}
}
private static class Branch {
final int address;
final IntArrayList targets = new IntArrayList();
final boolean isCall;
Branch(int address, boolean isCall) {
this.address = address;
this.isCall = isCall;
}
}
}

View File

@ -21,7 +21,9 @@ import java.nio.file.Files;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import org.teavm.backend.wasm.debug.info.LineInfo;
import org.teavm.backend.wasm.debug.info.ControlFlowInfo;
import org.teavm.backend.wasm.debug.info.DebugInfo;
import org.teavm.backend.wasm.parser.CodeSectionParser;
import org.teavm.backend.wasm.parser.ModuleParser;
import org.teavm.common.AsyncInputStream;
import org.teavm.common.ByteArrayAsyncInputStream;
@ -29,6 +31,8 @@ import org.teavm.common.ByteArrayAsyncInputStream;
public class DebugInfoParser extends ModuleParser {
private Map<String, DebugSectionParser> sectionParsers = new HashMap<>();
private DebugLinesParser lines;
private ControlFlowInfo controlFlow;
private int offset;
public DebugInfoParser(AsyncInputStream reader) {
super(reader);
@ -45,8 +49,8 @@ public class DebugInfoParser extends ModuleParser {
return section;
}
public LineInfo getLineInfo() {
return lines.getLineInfo();
public DebugInfo getDebugInfo() {
return new DebugInfo(lines.getLineInfo(), controlFlow, offset);
}
@Override
@ -55,11 +59,19 @@ public class DebugInfoParser extends ModuleParser {
var parser = sectionParsers.get(name);
return parser != null ? parser::parse : null;
} else if (code == 10) {
lines.setOffset(pos);
this.offset = pos;
return this::parseCode;
}
return null;
}
private void parseCode(byte[] data) {
var builder = new ControlFlowParser();
var codeParser = new CodeSectionParser(builder, builder);
codeParser.parse(data);
controlFlow = builder.build();
}
public static void main(String[] args) throws IOException {
if (args.length != 1) {
System.err.println("Pass single argument - path to wasm file");
@ -69,9 +81,9 @@ public class DebugInfoParser extends ModuleParser {
var input = new ByteArrayAsyncInputStream(Files.readAllBytes(file.toPath()));
var parser = new DebugInfoParser(input);
input.readFully(parser::parse);
var lineInfo = parser.getLineInfo();
if (lineInfo != null) {
lineInfo.dump(System.out);
var debugInfo = parser.getDebugInfo();
if (debugInfo != null) {
debugInfo.dump(System.out);
} else {
System.out.println("No debug information found");
}

View File

@ -42,7 +42,6 @@ public class DebugLinesParser extends DebugSectionParser {
private int address;
private MethodInfo currentMethod;
private int sequenceStartAddress;
private int offset;
public DebugLinesParser(
DebugFileParser files,
@ -57,13 +56,8 @@ public class DebugLinesParser extends DebugSectionParser {
return lineInfo;
}
public void setOffset(int offset) {
this.offset = offset;
}
@Override
protected void doParse() {
address = offset;
while (ptr < data.length) {
var cmd = data[ptr++] & 0xFF;
switch (cmd) {
@ -91,7 +85,7 @@ public class DebugLinesParser extends DebugSectionParser {
break;
}
}
lineInfo = new LineInfo(offset, sequences.toArray(new LineInfoSequence[0]));
lineInfo = new LineInfo(sequences.toArray(new LineInfoSequence[0]));
sequences = null;
commands = null;
stateStack = null;

View File

@ -152,7 +152,7 @@ public class DisassemblyCodeSectionListener implements AddressListener, CodeSect
}
@Override
public void endBlock(int token) {
public void endBlock(int token, boolean loop) {
writer.address(address).outdent().write("end (; $label_" + token + " ;)").eol();
}

View File

@ -40,7 +40,7 @@ public interface CodeListener {
default void startElseSection(int token) {
}
default void endBlock(int token) {
default void endBlock(int token, boolean loop) {
}
default void branch(BranchOpcode opcode, int depth, int target) {

View File

@ -18,17 +18,26 @@ package org.teavm.backend.wasm.parser;
import org.teavm.backend.wasm.model.WasmType;
public interface CodeSectionListener {
void sectionStart(int functionCount);
default void sectionStart(int functionCount) {
}
boolean functionStart(int index, int size);
default boolean functionStart(int index, int size) {
return false;
}
void localsStart(int count);
default void localsStart(int count) {
}
void local(int start, int count, WasmType type);
default void local(int start, int count, WasmType type) {
}
CodeListener code();
default CodeListener code() {
return null;
}
void functionEnd();
default void functionEnd() {
}
void sectionEnd();
default void sectionEnd() {
}
}

View File

@ -68,10 +68,10 @@ public class CodeSectionParser {
var end = ptr + functionSize;
if (listener.functionStart(index, functionSize)) {
parseLocals();
}
codeListener = listener.code();
if (codeListener != null) {
parseCode();
codeListener = listener.code();
if (codeListener != null) {
parseCode();
}
}
ptr = end;
reportAddress();
@ -625,7 +625,7 @@ public class CodeSectionParser {
}
blockStack.remove(blockStack.size() - 1);
reportAddress();
codeListener.endBlock(token);
codeListener.endBlock(token, isLoop);
++ptr;
return true;
}
@ -656,7 +656,7 @@ public class CodeSectionParser {
}
blockStack.remove(blockStack.size() - 1);
reportAddress();
codeListener.endBlock(token);
codeListener.endBlock(token, false);
++ptr;
return true;
}

View File

@ -25,7 +25,7 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.teavm.backend.wasm.debug.info.LineInfo;
import org.teavm.backend.wasm.debug.info.DebugInfo;
import org.teavm.backend.wasm.debug.info.LineInfoFileCommand;
import org.teavm.backend.wasm.debug.info.MethodInfo;
import org.teavm.backend.wasm.debug.parser.DebugInfoParser;
@ -55,9 +55,9 @@ public class Debugger {
private List<JavaScriptBreakpoint> temporaryBreakpoints = new ArrayList<>();
private Map<JavaScriptScript, DebugInformation> debugInformationMap = new HashMap<>();
private Map<String, Set<DebugInformation>> debugInformationFileMap = new HashMap<>();
private Map<JavaScriptScript, LineInfo> wasmLineInfoMap = new HashMap<>();
private Map<LineInfo, JavaScriptScript> wasmScriptMap = new HashMap<>();
private Map<String, Set<LineInfo>> wasmInfoFileMap = new HashMap<>();
private Map<JavaScriptScript, DebugInfo> wasmDebugInfoMap = new HashMap<>();
private Map<DebugInfo, JavaScriptScript> wasmScriptMap = new HashMap<>();
private Map<String, Set<DebugInfo>> wasmInfoFileMap = new HashMap<>();
private Map<DebugInformation, JavaScriptScript> scriptMap = new HashMap<>();
private Map<JavaScriptBreakpoint, Breakpoint> breakpointMap = new HashMap<>();
private Set<Breakpoint> breakpoints = new LinkedHashSet<>();
@ -130,12 +130,14 @@ public class Debugger {
}
break;
case WASM: {
var info = wasmLineInfoMap.get(script);
var info = wasmDebugInfoMap.get(script);
if (info != null) {
return enterMethod ? javaScriptDebugger.stepInto() : javaScriptDebugger.stepOver();
}
break;
}
case UNKNOWN:
break;
}
enterMethod = false;
first = false;
@ -240,7 +242,7 @@ public class Debugger {
return list != null ? new ArrayList<>(list) : Collections.emptyList();
}
private List<LineInfo> wasmLineInfoBySource(String sourceFile) {
private List<DebugInfo> wasmLineInfoBySource(String sourceFile) {
var list = wasmInfoFileMap.get(sourceFile);
return list != null ? new ArrayList<>(list) : Collections.emptyList();
}
@ -315,16 +317,19 @@ public class Debugger {
}));
}
}
for (var wasmLineInfo : wasmLineInfoBySource(location.getFileName())) {
for (var sequence : wasmLineInfo.sequences()) {
for (var wasmDebugInfo : wasmLineInfoBySource(location.getFileName())) {
if (wasmDebugInfo.lines() == null) {
continue;
}
for (var sequence : wasmDebugInfo.lines().sequences()) {
for (var loc : sequence.unpack().locations()) {
if (loc.location() == null) {
continue;
}
if (loc.location().line() == location.getLine()
&& loc.location().file().fullName().equals(location.getFileName())) {
var jsLocation = new JavaScriptLocation(wasmScriptMap.get(wasmLineInfo),
0, loc.address());
var jsLocation = new JavaScriptLocation(wasmScriptMap.get(wasmDebugInfo),
0, loc.address() + wasmDebugInfo.offset());
promises.add(javaScriptDebugger.createBreakpoint(jsLocation).thenVoid(jsBreakpoint -> {
jsBreakpoints.add(jsBreakpoint);
breakpointMap.put(jsBreakpoint, breakpoint);
@ -427,15 +432,20 @@ public class Debugger {
}
private List<SourceLocationWithMethod> mapWasmFrames(JavaScriptCallFrame frame) {
var lineInfo = wasmLineInfoMap.get(frame.getLocation().getScript());
var debugInfo = wasmDebugInfoMap.get(frame.getLocation().getScript());
if (debugInfo == null) {
return Collections.emptyList();
}
var lineInfo = debugInfo.lines();
if (lineInfo == null) {
return Collections.emptyList();
}
var sequence = lineInfo.find(frame.getLocation().getColumn());
var address = frame.getLocation().getColumn() - debugInfo.offset();
var sequence = lineInfo.find(address);
if (sequence == null) {
return Collections.emptyList();
}
var instructionLocation = sequence.unpack().find(frame.getLocation().getColumn());
var instructionLocation = sequence.unpack().find(address);
if (instructionLocation == null) {
return Collections.emptyList();
}
@ -525,15 +535,18 @@ public class Debugger {
} catch (Throwable e) {
e.printStackTrace();
}
if (parser.getLineInfo() != null) {
wasmLineInfoMap.put(script, parser.getLineInfo());
wasmScriptMap.put(parser.getLineInfo(), script);
for (var sequence : parser.getLineInfo().sequences()) {
for (var command : sequence.commands()) {
if (command instanceof LineInfoFileCommand) {
var file = ((LineInfoFileCommand) command).file();
if (file != null) {
addWasmInfoFile(file.fullName(), parser.getLineInfo());
var debugInfo = parser.getDebugInfo();
if (debugInfo != null) {
wasmDebugInfoMap.put(script, debugInfo);
wasmScriptMap.put(debugInfo, script);
if (debugInfo.lines() != null) {
for (var sequence : debugInfo.lines().sequences()) {
for (var command : sequence.commands()) {
if (command instanceof LineInfoFileCommand) {
var file = ((LineInfoFileCommand) command).file();
if (file != null) {
addWasmInfoFile(file.fullName(), debugInfo);
}
}
}
}
@ -542,13 +555,13 @@ public class Debugger {
});
}
private void addWasmInfoFile(String sourceFile, LineInfo wasmLineInfo) {
private void addWasmInfoFile(String sourceFile, DebugInfo debugInfo) {
var list = wasmInfoFileMap.get(sourceFile);
if (list == null) {
list = new HashSet<>();
wasmInfoFileMap.put(sourceFile, list);
}
list.add(wasmLineInfo);
list.add(debugInfo);
allSourceFiles.add(sourceFile);
}

View File

@ -50,6 +50,11 @@
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
</dependency>
<dependency>
<groupId>com.carrotsearch</groupId>
<artifactId>hppc</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>