wasm gc: support writing debug info, support it in disassembler

This commit is contained in:
Alexey Andreev 2024-10-04 18:49:58 +02:00
parent 7aec0763fa
commit c2eb11e056
9 changed files with 368 additions and 10 deletions

View File

@ -0,0 +1,22 @@
/*
* Copyright 2024 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;
public enum WasmDebugInfoLevel {
NONE,
DEOBFUSCATION,
FULL
}

View File

@ -0,0 +1,21 @@
/*
* Copyright 2024 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;
public enum WasmDebugInfoLocation {
EMBEDDED,
EXTERNAL
}

View File

@ -21,6 +21,9 @@ import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.teavm.backend.wasm.debug.DebugLines;
import org.teavm.backend.wasm.debug.ExternalDebugFile;
import org.teavm.backend.wasm.debug.GCDebugInfoBuilder;
import org.teavm.backend.wasm.gc.TeaVMWasmGCHost;
import org.teavm.backend.wasm.gc.WasmGCDependencies;
import org.teavm.backend.wasm.generate.gc.WasmGCDeclarationsGenerator;
@ -63,6 +66,8 @@ public class WasmGCTarget implements TeaVMTarget, TeaVMWasmGCHost {
private BoundCheckInsertion boundCheckInsertion = new BoundCheckInsertion();
private boolean strict;
private boolean obfuscated;
private WasmDebugInfoLocation debugLocation;
private WasmDebugInfoLevel debugLevel;
private List<WasmGCIntrinsicFactory> intrinsicFactories = new ArrayList<>();
private Map<MethodReference, WasmGCIntrinsic> customIntrinsics = new HashMap<>();
private List<WasmGCCustomTypeMapperFactory> customTypeMapperFactories = new ArrayList<>();
@ -77,6 +82,14 @@ public class WasmGCTarget implements TeaVMTarget, TeaVMWasmGCHost {
this.strict = strict;
}
public void setDebugLevel(WasmDebugInfoLevel debugLevel) {
this.debugLevel = debugLevel;
}
public void setDebugLocation(WasmDebugInfoLocation debugLocation) {
this.debugLocation = debugLocation;
}
@Override
public void addIntrinsicFactory(WasmGCIntrinsicFactory intrinsicFactory) {
intrinsicFactories.add(intrinsicFactory);
@ -172,6 +185,7 @@ public class WasmGCTarget implements TeaVMTarget, TeaVMWasmGCHost {
customGeneratorFactories, customCustomGenerators,
controller.getProperties());
var intrinsics = new WasmGCIntrinsics(classes, controller.getServices(), intrinsicFactories, customIntrinsics);
var debugInfoBuilder = new GCDebugInfoBuilder();
var declarationsGenerator = new WasmGCDeclarationsGenerator(
module,
classes,
@ -231,7 +245,7 @@ public class WasmGCTarget implements TeaVMTarget, TeaVMWasmGCHost {
customGenerators.contributeToModule(module);
adjustModuleMemory(module);
emitWasmFile(module, buildTarget, outputName);
emitWasmFile(module, buildTarget, outputName, debugInfoBuilder);
}
private void adjustModuleMemory(WasmModule module) {
@ -248,13 +262,22 @@ public class WasmGCTarget implements TeaVMTarget, TeaVMWasmGCHost {
module.setMaxMemorySize(pages);
}
private void emitWasmFile(WasmModule module, BuildTarget buildTarget, String outputName) throws IOException {
private void emitWasmFile(WasmModule module, BuildTarget buildTarget, String outputName,
GCDebugInfoBuilder debugInfoBuilder) throws IOException {
var binaryWriter = new WasmBinaryWriter();
DebugLines debugLines = null;
if (debugLevel != WasmDebugInfoLevel.NONE) {
debugLines = debugInfoBuilder.lines();
}
var binaryRenderer = new WasmBinaryRenderer(binaryWriter, WasmBinaryVersion.V_0x1, obfuscated,
null, null, null, null, WasmBinaryStatsCollector.EMPTY);
null, null, debugLines, null, WasmBinaryStatsCollector.EMPTY);
optimizeIndexes(module);
module.prepareForRendering();
if (debugLocation == WasmDebugInfoLocation.EMBEDDED) {
binaryRenderer.render(module, debugInfoBuilder::build);
} else {
binaryRenderer.render(module);
}
var data = binaryWriter.getData();
if (!outputName.endsWith(".wasm")) {
outputName += ".wasm";
@ -262,6 +285,14 @@ public class WasmGCTarget implements TeaVMTarget, TeaVMWasmGCHost {
try (var output = buildTarget.createResource(outputName)) {
output.write(data);
}
if (debugLocation == WasmDebugInfoLocation.EXTERNAL) {
var debugInfoData = ExternalDebugFile.write(debugInfoBuilder.build());
if (debugInfoData != null) {
try (var output = buildTarget.createResource(outputName + ".tdbg")) {
output.write(debugInfoData);
}
}
}
}
private void optimizeIndexes(WasmModule module) {

View File

@ -0,0 +1,41 @@
/*
* Copyright 2024 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;
import java.util.List;
import org.teavm.backend.wasm.model.WasmCustomSection;
import org.teavm.backend.wasm.render.WasmBinaryWriter;
public final class ExternalDebugFile {
private ExternalDebugFile() {
}
public static byte[] write(List<WasmCustomSection> sections) {
if (sections.isEmpty()) {
return null;
}
var writer = new WasmBinaryWriter();
writer.writeInt32(0x67626474);
writer.writeInt32(1);
for (var section : sections) {
var data = section.getData();
writer.writeAsciiString(section.getName());
writer.writeLEB(data.length);
writer.writeBytes(data);
}
return writer.getData();
}
}

View File

@ -0,0 +1,80 @@
/*
* Copyright 2024 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;
import java.util.ArrayList;
import java.util.List;
import org.teavm.backend.wasm.model.WasmCustomSection;
public class GCDebugInfoBuilder {
private DebugStringsBuilder strings;
private DebugFilesBuilder files;
private DebugPackagesBuilder packages;
private DebugClassesBuilder classes;
private DebugMethodsBuilder methods;
private DebugLinesBuilder lines;
public GCDebugInfoBuilder() {
strings = new DebugStringsBuilder();
files = new DebugFilesBuilder(strings);
packages = new DebugPackagesBuilder(strings);
classes = new DebugClassesBuilder(packages, strings);
methods = new DebugMethodsBuilder(classes, strings);
lines = new DebugLinesBuilder(files, methods);
}
public DebugStrings strings() {
return strings;
}
public DebugFiles files() {
return files;
}
public DebugPackages packages() {
return packages;
}
public DebugClasses classes() {
return classes;
}
public DebugMethods methods() {
return methods;
}
public DebugLines lines() {
return lines;
}
public List<WasmCustomSection> build() {
var result = new ArrayList<WasmCustomSection>();
addSection(result, strings);
addSection(result, files);
addSection(result, packages);
addSection(result, classes);
addSection(result, methods);
addSection(result, lines);
return result;
}
private void addSection(List<WasmCustomSection> sections, DebugSectionBuilder builder) {
if (builder.isEmpty()) {
return;
}
sections.add(new WasmCustomSection(builder.name(), builder.build()));
}
}

View File

@ -21,7 +21,17 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
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.parser.DebugClassParser;
import org.teavm.backend.wasm.debug.parser.DebugFileParser;
import org.teavm.backend.wasm.debug.parser.DebugLinesParser;
import org.teavm.backend.wasm.debug.parser.DebugMethodParser;
import org.teavm.backend.wasm.debug.parser.DebugPackageParser;
import org.teavm.backend.wasm.debug.parser.DebugSectionParser;
import org.teavm.backend.wasm.debug.parser.DebugStringParser;
import org.teavm.backend.wasm.parser.AddressListener;
import org.teavm.backend.wasm.parser.CodeSectionParser;
import org.teavm.backend.wasm.parser.FunctionSectionListener;
@ -41,9 +51,27 @@ public final class Disassembler {
private WasmHollowFunctionType[] functionTypes;
private int[] functionTypeRefs;
private int importFunctionCount;
private Map<String, DebugSectionParser> debugSectionParsers = new HashMap<>();
private DebugLinesParser debugLines;
private LineInfo lineInfo;
public Disassembler(DisassemblyWriter writer) {
this.writer = writer;
installDebugParsers();
}
private void installDebugParsers() {
var strings = addDebugSection(new DebugStringParser());
var files = addDebugSection(new DebugFileParser(strings));
var packages = addDebugSection(new DebugPackageParser(strings));
var classes = addDebugSection(new DebugClassParser(strings, packages));
var methods = addDebugSection(new DebugMethodParser(strings, classes));
debugLines = addDebugSection(new DebugLinesParser(files, methods));
}
private <T extends DebugSectionParser> T addDebugSection(T section) {
debugSectionParsers.put(section.name(), section);
return section;
}
public void startModule() {
@ -65,18 +93,25 @@ public final class Disassembler {
public void read(byte[] bytes) {
var nameAccumulator = new NameAccumulatingSectionListener();
var input = new ByteArrayAsyncInputStream(bytes);
var nameParser = createNameParser(input, nameAccumulator);
input.readFully(nameParser::parse);
var preparationParser = createPreparationParser(input, nameAccumulator);
input.readFully(preparationParser::parse);
lineInfo = debugLines.getLineInfo();
input = new ByteArrayAsyncInputStream(bytes);
var parser = createParser(input, nameAccumulator.buildProvider());
input.readFully(parser::parse);
}
public ModuleParser createNameParser(AsyncInputStream input, NameSectionListener listener) {
public ModuleParser createPreparationParser(AsyncInputStream input, NameSectionListener listener) {
return new ModuleParser(input) {
@Override
protected Consumer<byte[]> getSectionConsumer(int code, int pos, String name) {
if (code == 0) {
var debugSection = debugSectionParsers.get(name);
if (debugSection != null) {
return debugSection::parse;
}
}
return Disassembler.this.getNameSectionConsumer(code, name, listener);
}
};
@ -139,11 +174,14 @@ public final class Disassembler {
disassembler.setFunctionTypes(functionTypes);
disassembler.setFunctionTypeRefs(functionTypeRefs);
writer.setAddressOffset(pos);
writer.setDebugLines(lineInfo);
writer.startSection();
writer.write("(; code section size: " + bytes.length + " ;)").eol();
var sectionParser = new CodeSectionParser(disassembler);
sectionParser.setFunctionIndexOffset(importFunctionCount);
sectionParser.parse(writer.addressListener, bytes);
writer.flush();
writer.setDebugLines(null);
};
} else {
return null;

View File

@ -24,7 +24,11 @@ public class DisassemblyHTMLWriter extends DisassemblyWriter {
@Override
public DisassemblyWriter prologue() {
return writeExact("<html><body><pre>");
writeExact("<html>\n<head>");
writeExact("<style>\n");
writeExact("em { color: gray; }\n");
writeExact("</style></head>\n");
return writeExact("<body><pre>");
}
@Override
@ -82,4 +86,14 @@ public class DisassemblyHTMLWriter extends DisassemblyWriter {
writeExact(s);
return this;
}
@Override
protected void startAnnotation() {
writeExact("<em>");
}
@Override
protected void endAnnotation() {
writeExact("</em>");
}
}

View File

@ -16,16 +16,27 @@
package org.teavm.backend.wasm.disasm;
import java.io.PrintWriter;
import org.teavm.backend.wasm.debug.info.LineInfo;
import org.teavm.backend.wasm.debug.info.LineInfoCommandVisitor;
import org.teavm.backend.wasm.debug.info.LineInfoEnterCommand;
import org.teavm.backend.wasm.debug.info.LineInfoExitCommand;
import org.teavm.backend.wasm.debug.info.LineInfoFileCommand;
import org.teavm.backend.wasm.debug.info.LineInfoLineCommand;
import org.teavm.backend.wasm.parser.AddressListener;
public abstract class DisassemblyWriter {
private PrintWriter out;
private boolean withAddress;
private int indentLevel;
private int addressWithinSection;
private int address;
private boolean hasAddress;
private boolean lineStarted;
private int addressOffset;
private LineInfo debugLines;
private int currentSequenceIndex;
private int currentCommandIndex = -1;
private int lineInfoIndent;
public DisassemblyWriter(PrintWriter out) {
this.out = out;
@ -39,6 +50,15 @@ public abstract class DisassemblyWriter {
this.addressOffset = addressOffset;
}
public void setDebugLines(LineInfo debugLines) {
this.debugLines = debugLines;
}
public void startSection() {
addressWithinSection = -1;
currentSequenceIndex = 0;
}
public DisassemblyWriter address() {
hasAddress = true;
return this;
@ -63,6 +83,9 @@ public abstract class DisassemblyWriter {
private void startLine() {
if (!lineStarted) {
lineStarted = true;
if (debugLines != null) {
printDebugLine();
}
if (withAddress) {
if (hasAddress) {
hasAddress = false;
@ -77,6 +100,85 @@ public abstract class DisassemblyWriter {
}
}
private void printDebugLine() {
if (currentSequenceIndex >= debugLines.sequences().size()) {
return;
}
var force = false;
if (currentCommandIndex < 0) {
if (addressWithinSection < debugLines.sequences().get(currentSequenceIndex).startAddress()) {
return;
}
currentCommandIndex = 0;
force = true;
} else {
if (addressWithinSection >= debugLines.sequences().get(currentSequenceIndex).endAddress()) {
printSingleDebugAnnotation("<end debug line sequence>");
++currentSequenceIndex;
currentCommandIndex = -1;
lineInfoIndent = 0;
return;
}
}
var sequence = debugLines.sequences().get(currentSequenceIndex);
if (currentCommandIndex >= sequence.commands().size()) {
return;
}
var command = sequence.commands().get(currentCommandIndex);
if (!force) {
if (currentCommandIndex + 1 < sequence.commands().size()
&& addressWithinSection >= sequence.commands().get(currentCommandIndex + 1).address()) {
command = sequence.commands().get(++currentCommandIndex);
} else {
return;
}
}
command.acceptVisitor(new LineInfoCommandVisitor() {
@Override
public void visit(LineInfoEnterCommand command) {
printSingleDebugAnnotation(" at " + command.method().fullName());
++lineInfoIndent;
}
@Override
public void visit(LineInfoExitCommand command) {
--lineInfoIndent;
}
@Override
public void visit(LineInfoFileCommand command) {
if (command.file() == null) {
printSingleDebugAnnotation("at <unknown>:" + command.line());
} else {
printSingleDebugAnnotation(" at " + command.file().name() + ":" + command.line());
}
}
@Override
public void visit(LineInfoLineCommand command) {
printSingleDebugAnnotation(" at " + command.line());
}
});
}
private void printSingleDebugAnnotation(String text) {
out.print(" ");
for (int i = 0; i < indentLevel; ++i) {
out.print(" ");
}
for (int i = 0; i < lineInfoIndent; ++i) {
out.print(" ");
}
startAnnotation();
out.print("(;");
write(text);
out.print(" ;)");
endAnnotation();
out.print("\n");
}
private void printAddress() {
out.print("(; ");
for (int i = 7; i >= 0; --i) {
@ -108,15 +210,20 @@ public abstract class DisassemblyWriter {
public abstract DisassemblyWriter epilogue();
protected void startAnnotation() {
}
protected void endAnnotation() {
}
public void flush() {
out.flush();
}
public final AddressListener addressListener = new AddressListener() {
@Override
public void address(int address) {
addressWithinSection = address;
DisassemblyWriter.this.address = address + addressOffset;
}
};

View File

@ -32,6 +32,8 @@ import java.util.Map;
import java.util.StringTokenizer;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.teavm.backend.wasm.WasmDebugInfoLevel;
import org.teavm.backend.wasm.WasmDebugInfoLocation;
import org.teavm.backend.wasm.WasmGCTarget;
import org.teavm.backend.wasm.disasm.Disassembler;
import org.teavm.backend.wasm.disasm.DisassemblyHTMLWriter;
@ -69,6 +71,8 @@ class WebAssemblyGCPlatformSupport extends TestPlatformSupport<WasmGCTarget> {
var target = new WasmGCTarget();
target.setObfuscated(false);
target.setStrict(true);
target.setDebugLevel(WasmDebugInfoLevel.DEOBFUSCATION);
target.setDebugLocation(WasmDebugInfoLocation.EMBEDDED);
var sourceDirs = System.getProperty(SOURCE_DIRS);
if (sourceDirs != null) {
var dirs = new ArrayList<File>();