org.snapscript.dx.dex.file.DebugInfoEncoder Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of snap-all Show documentation
Show all versions of snap-all Show documentation
Dynamic scripting for the JVM
/*
* Copyright (C) 2007 The Android Open Source Project
*
* 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.snapscript.dx.dex.file;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.BitSet;
import org.snapscript.dx.dex.code.LocalList;
import org.snapscript.dx.dex.code.PositionList;
import org.snapscript.dx.rop.code.RegisterSpec;
import org.snapscript.dx.rop.code.SourcePosition;
import org.snapscript.dx.rop.cst.CstMethodRef;
import org.snapscript.dx.rop.cst.CstString;
import org.snapscript.dx.rop.cst.CstType;
import org.snapscript.dx.rop.type.Prototype;
import org.snapscript.dx.rop.type.StdTypeList;
import org.snapscript.dx.rop.type.Type;
import org.snapscript.dx.util.AnnotatedOutput;
import org.snapscript.dx.util.ByteArrayAnnotatedOutput;
import org.snapscript.dx.util.ExceptionWithContext;
import static org.snapscript.dx.dex.file.DebugInfoConstants.*;
/**
* An encoder for the dex debug info state machine format. The format
* for each method enrty is as follows:
*
* - signed LEB128: initial value for line register.
*
- n instances of signed LEB128: string indicies (offset by 1)
* for each method argument in left-to-right order
* with {@code this} excluded. A value of '0' indicates "no name"
*
- A sequence of special or normal opcodes as defined in
* {@code DebugInfoConstants}.
*
- A single terminating {@code OP_END_SEQUENCE}
*
*/
public final class DebugInfoEncoder {
private static final boolean DEBUG = false;
/** {@code null-ok;} positions (line numbers) to encode */
private final PositionList positions;
/** {@code null-ok;} local variables to encode */
private final LocalList locals;
private final ByteArrayAnnotatedOutput output;
private final DexFile file;
private final int codeSize;
private final int regSize;
private final Prototype desc;
private final boolean isStatic;
/** current encoding state: bytecode address */
private int address = 0;
/** current encoding state: line number */
private int line = 1;
/**
* if non-null: the output to write annotations to. No normal
* output is written to this.
*/
private AnnotatedOutput annotateTo;
/** if non-null: another possible output for annotations */
private PrintWriter debugPrint;
/** if non-null: the prefix for each annotation or debugPrint line */
private String prefix;
/** true if output should be consumed during annotation */
private boolean shouldConsume;
/** indexed by register; last local alive in register */
private final LocalList.Entry[] lastEntryForReg;
/**
* Creates an instance.
*
* @param positions {@code null-ok;} positions (line numbers) to encode
* @param locals {@code null-ok;} local variables to encode
* @param file {@code null-ok;} may only be {@code null} if simply using
* this class to do a debug print
* @param codeSize
* @param regSize
* @param isStatic
* @param ref
*/
public DebugInfoEncoder(PositionList positions, LocalList locals,
DexFile file, int codeSize, int regSize,
boolean isStatic, CstMethodRef ref) {
this.positions = positions;
this.locals = locals;
this.file = file;
this.desc = ref.getPrototype();
this.isStatic = isStatic;
this.codeSize = codeSize;
this.regSize = regSize;
output = new ByteArrayAnnotatedOutput();
lastEntryForReg = new LocalList.Entry[regSize];
}
/**
* Annotates or writes a message to the {@code debugPrint} writer
* if applicable.
*
* @param length the number of bytes associated with this message
* @param message the message itself
*/
private void annotate(int length, String message) {
if (prefix != null) {
message = prefix + message;
}
if (annotateTo != null) {
annotateTo.annotate(shouldConsume ? length : 0, message);
}
if (debugPrint != null) {
debugPrint.println(message);
}
}
/**
* Converts this (PositionList, LocalList) pair into a state machine
* sequence.
*
* @return {@code non-null;} encoded byte sequence without padding and
* terminated with a {@code 0x00} byte
*/
public byte[] convert() {
try {
byte[] ret;
ret = convert0();
if (DEBUG) {
for (int i = 0 ; i < ret.length; i++) {
System.err.printf("byte %02x\n", (0xff & ret[i]));
}
}
return ret;
} catch (IOException ex) {
throw ExceptionWithContext
.withContext(ex, "...while encoding debug info");
}
}
/**
* Converts and produces annotations on a stream. Does not write
* actual bits to the {@code AnnotatedOutput}.
*
* @param prefix {@code null-ok;} prefix to attach to each line of output
* @param debugPrint {@code null-ok;} if specified, an alternate output for
* annotations
* @param out {@code null-ok;} if specified, where annotations should go
* @param consume whether to claim to have consumed output for
* {@code out}
* @return {@code non-null;} encoded output
*/
public byte[] convertAndAnnotate(String prefix, PrintWriter debugPrint,
AnnotatedOutput out, boolean consume) {
this.prefix = prefix;
this.debugPrint = debugPrint;
annotateTo = out;
shouldConsume = consume;
byte[] result = convert();
return result;
}
private byte[] convert0() throws IOException {
ArrayList sortedPositions = buildSortedPositions();
ArrayList methodArgs = extractMethodArguments();
emitHeader(sortedPositions, methodArgs);
// TODO: Make this mark be the actual prologue end.
output.writeByte(DBG_SET_PROLOGUE_END);
if (annotateTo != null || debugPrint != null) {
annotate(1, String.format("%04x: prologue end",address));
}
int positionsSz = sortedPositions.size();
int localsSz = locals.size();
// Current index in sortedPositions
int curPositionIdx = 0;
// Current index in locals
int curLocalIdx = 0;
for (;;) {
/*
* Emit any information for the current address.
*/
curLocalIdx = emitLocalsAtAddress(curLocalIdx);
curPositionIdx =
emitPositionsAtAddress(curPositionIdx, sortedPositions);
/*
* Figure out what the next important address is.
*/
int nextAddrL = Integer.MAX_VALUE; // local variable
int nextAddrP = Integer.MAX_VALUE; // position (line number)
if (curLocalIdx < localsSz) {
nextAddrL = locals.get(curLocalIdx).getAddress();
}
if (curPositionIdx < positionsSz) {
nextAddrP = sortedPositions.get(curPositionIdx).getAddress();
}
int next = Math.min(nextAddrP, nextAddrL);
// No next important address == done.
if (next == Integer.MAX_VALUE) {
break;
}
/*
* If the only work remaining are local ends at the end of the
* block, stop here. Those are implied anyway.
*/
if (next == codeSize
&& nextAddrL == Integer.MAX_VALUE
&& nextAddrP == Integer.MAX_VALUE) {
break;
}
if (next == nextAddrP) {
// Combined advance PC + position entry
emitPosition(sortedPositions.get(curPositionIdx++));
} else {
emitAdvancePc(next - address);
}
}
emitEndSequence();
return output.toByteArray();
}
/**
* Emits all local variable activity that occurs at the current
* {@link #address} starting at the given index into {@code
* locals} and including all subsequent activity at the same
* address.
*
* @param curLocalIdx Current index in locals
* @return new value for {@code curLocalIdx}
* @throws IOException
*/
private int emitLocalsAtAddress(int curLocalIdx)
throws IOException {
int sz = locals.size();
// TODO: Don't emit ends implied by starts.
while ((curLocalIdx < sz)
&& (locals.get(curLocalIdx).getAddress() == address)) {
LocalList.Entry entry = locals.get(curLocalIdx++);
int reg = entry.getRegister();
LocalList.Entry prevEntry = lastEntryForReg[reg];
if (entry == prevEntry) {
/*
* Here we ignore locals entries for parameters,
* which have already been represented and placed in the
* lastEntryForReg array.
*/
continue;
}
// At this point we have a new entry one way or another.
lastEntryForReg[reg] = entry;
if (entry.isStart()) {
if ((prevEntry != null) && entry.matches(prevEntry)) {
/*
* The previous local in this register has the same
* name and type as the one being introduced now, so
* use the more efficient "restart" form.
*/
if (prevEntry.isStart()) {
/*
* We should never be handed a start when a
* a matching local is already active.
*/
throw new RuntimeException("shouldn't happen");
}
emitLocalRestart(entry);
} else {
emitLocalStart(entry);
}
} else {
/*
* Only emit a local end if it is *not* due to a direct
* replacement. Direct replacements imply an end of the
* previous local in the same register.
*
* TODO: Make sure the runtime can deal with implied
* local ends from category-2 interactions, and when so,
* also stop emitting local ends for those cases.
*/
if (entry.getDisposition()
!= LocalList.Disposition.END_REPLACED) {
emitLocalEnd(entry);
}
}
}
return curLocalIdx;
}
/**
* Emits all positions that occur at the current {@code address}
*
* @param curPositionIdx Current index in sortedPositions
* @param sortedPositions positions, sorted by ascending address
* @return new value for {@code curPositionIdx}
* @throws IOException
*/
private int emitPositionsAtAddress(int curPositionIdx,
ArrayList sortedPositions)
throws IOException {
int positionsSz = sortedPositions.size();
while ((curPositionIdx < positionsSz)
&& (sortedPositions.get(curPositionIdx).getAddress()
== address)) {
emitPosition(sortedPositions.get(curPositionIdx++));
}
return curPositionIdx;
}
/**
* Emits the header sequence, which consists of LEB128-encoded initial
* line number and string indicies for names of all non-"this" arguments.
*
* @param sortedPositions positions, sorted by ascending address
* @param methodArgs local list entries for method argumens arguments,
* in left-to-right order omitting "this"
* @throws IOException
*/
private void emitHeader(ArrayList sortedPositions,
ArrayList methodArgs) throws IOException {
boolean annotate = (annotateTo != null) || (debugPrint != null);
int mark = output.getCursor();
// Start by initializing the line number register.
if (sortedPositions.size() > 0) {
PositionList.Entry entry = sortedPositions.get(0);
line = entry.getPosition().getLine();
}
output.writeUleb128(line);
if (annotate) {
annotate(output.getCursor() - mark, "line_start: " + line);
}
int curParam = getParamBase();
// paramTypes will not include 'this'
StdTypeList paramTypes = desc.getParameterTypes();
int szParamTypes = paramTypes.size();
/*
* Initialize lastEntryForReg to have an initial
* entry for the 'this' pointer.
*/
if (!isStatic) {
for (LocalList.Entry arg : methodArgs) {
if (curParam == arg.getRegister()) {
lastEntryForReg[curParam] = arg;
break;
}
}
curParam++;
}
// Write out the number of parameter entries that will follow.
mark = output.getCursor();
output.writeUleb128(szParamTypes);
if (annotate) {
annotate(output.getCursor() - mark,
String.format("parameters_size: %04x", szParamTypes));
}
/*
* Then emit the string indicies of all the method parameters.
* Note that 'this', if applicable, is excluded.
*/
for (int i = 0; i < szParamTypes; i++) {
Type pt = paramTypes.get(i);
LocalList.Entry found = null;
mark = output.getCursor();
for (LocalList.Entry arg : methodArgs) {
if (curParam == arg.getRegister()) {
found = arg;
if (arg.getSignature() != null) {
/*
* Parameters with signatures will be re-emitted
* in complete as LOCAL_START_EXTENDED's below.
*/
emitStringIndex(null);
} else {
emitStringIndex(arg.getName());
}
lastEntryForReg[curParam] = arg;
break;
}
}
if (found == null) {
/*
* Emit a null symbol for "unnamed." This is common
* for, e.g., synthesized methods and inner-class
* this$0 arguments.
*/
emitStringIndex(null);
}
if (annotate) {
String parameterName
= (found == null || found.getSignature() != null)
? "" : found.getName().toHuman();
annotate(output.getCursor() - mark,
"parameter " + parameterName + " "
+ RegisterSpec.PREFIX + curParam);
}
curParam += pt.getCategory();
}
/*
* If anything emitted above has a type signature, emit it again as
* a LOCAL_RESTART_EXTENDED
*/
for (LocalList.Entry arg : lastEntryForReg) {
if (arg == null) {
continue;
}
CstString signature = arg.getSignature();
if (signature != null) {
emitLocalStartExtended(arg);
}
}
}
/**
* Builds a list of position entries, sorted by ascending address.
*
* @return A sorted positions list
*/
private ArrayList buildSortedPositions() {
int sz = (positions == null) ? 0 : positions.size();
ArrayList result = new ArrayList(sz);
for (int i = 0; i < sz; i++) {
result.add(positions.get(i));
}
// Sort ascending by address.
Collections.sort (result, new Comparator() {
public int compare (PositionList.Entry a, PositionList.Entry b) {
return a.getAddress() - b.getAddress();
}
public boolean equals (Object obj) {
return obj == this;
}
});
return result;
}
/**
* Gets the register that begins the method's parameter range (including
* the 'this' parameter for non-static methods). The range continues until
* {@code regSize}
*
* @return register as noted above
*/
private int getParamBase() {
return regSize
- desc.getParameterTypes().getWordCount() - (isStatic? 0 : 1);
}
/**
* Extracts method arguments from a locals list. These will be collected
* from the input list and sorted by ascending register in the
* returned list.
*
* @return list of non-{@code this} method argument locals,
* sorted by ascending register
*/
private ArrayList extractMethodArguments() {
ArrayList result
= new ArrayList(desc.getParameterTypes().size());
int argBase = getParamBase();
BitSet seen = new BitSet(regSize - argBase);
int sz = locals.size();
for (int i = 0; i < sz; i++) {
LocalList.Entry e = locals.get(i);
int reg = e.getRegister();
if (reg < argBase) {
continue;
}
// only the lowest-start-address entry is included.
if (seen.get(reg - argBase)) {
continue;
}
seen.set(reg - argBase);
result.add(e);
}
// Sort by ascending register.
Collections.sort(result, new Comparator() {
public int compare(LocalList.Entry a, LocalList.Entry b) {
return a.getRegister() - b.getRegister();
}
public boolean equals(Object obj) {
return obj == this;
}
});
return result;
}
/**
* Returns a string representation of this LocalList entry that is
* appropriate for emitting as an annotation.
*
* @param e {@code non-null;} entry
* @return {@code non-null;} annotation string
*/
private String entryAnnotationString(LocalList.Entry e) {
StringBuilder sb = new StringBuilder();
sb.append(RegisterSpec.PREFIX);
sb.append(e.getRegister());
sb.append(' ');
CstString name = e.getName();
if (name == null) {
sb.append("null");
} else {
sb.append(name.toHuman());
}
sb.append(' ');
CstType type = e.getType();
if (type == null) {
sb.append("null");
} else {
sb.append(type.toHuman());
}
CstString signature = e.getSignature();
if (signature != null) {
sb.append(' ');
sb.append(signature.toHuman());
}
return sb.toString();
}
/**
* Emits a {@link DebugInfoConstants#DBG_RESTART_LOCAL DBG_RESTART_LOCAL}
* sequence.
*
* @param entry entry associated with this restart
* @throws IOException
*/
private void emitLocalRestart(LocalList.Entry entry)
throws IOException {
int mark = output.getCursor();
output.writeByte(DBG_RESTART_LOCAL);
emitUnsignedLeb128(entry.getRegister());
if (annotateTo != null || debugPrint != null) {
annotate(output.getCursor() - mark,
String.format("%04x: +local restart %s",
address, entryAnnotationString(entry)));
}
if (DEBUG) {
System.err.println("emit local restart");
}
}
/**
* Emits a string index as an unsigned LEB128. The actual value written
* is shifted by 1, so that the '0' value is reserved for "null". The
* null symbol is used in some cases by the parameter name list
* at the beginning of the sequence.
*
* @param string {@code null-ok;} string to emit
* @throws IOException
*/
private void emitStringIndex(CstString string) throws IOException {
if ((string == null) || (file == null)) {
output.writeUleb128(0);
} else {
output.writeUleb128(
1 + file.getStringIds().indexOf(string));
}
if (DEBUG) {
System.err.printf("Emit string %s\n",
string == null ? "" : string.toQuoted());
}
}
/**
* Emits a type index as an unsigned LEB128. The actual value written
* is shifted by 1, so that the '0' value is reserved for "null".
*
* @param type {@code null-ok;} type to emit
* @throws IOException
*/
private void emitTypeIndex(CstType type) throws IOException {
if ((type == null) || (file == null)) {
output.writeUleb128(0);
} else {
output.writeUleb128(
1 + file.getTypeIds().indexOf(type));
}
if (DEBUG) {
System.err.printf("Emit type %s\n",
type == null ? "" : type.toHuman());
}
}
/**
* Emits a {@link DebugInfoConstants#DBG_START_LOCAL DBG_START_LOCAL} or
* {@link DebugInfoConstants#DBG_START_LOCAL_EXTENDED
* DBG_START_LOCAL_EXTENDED} sequence.
*
* @param entry entry to emit
* @throws IOException
*/
private void emitLocalStart(LocalList.Entry entry)
throws IOException {
if (entry.getSignature() != null) {
emitLocalStartExtended(entry);
return;
}
int mark = output.getCursor();
output.writeByte(DBG_START_LOCAL);
emitUnsignedLeb128(entry.getRegister());
emitStringIndex(entry.getName());
emitTypeIndex(entry.getType());
if (annotateTo != null || debugPrint != null) {
annotate(output.getCursor() - mark,
String.format("%04x: +local %s", address,
entryAnnotationString(entry)));
}
if (DEBUG) {
System.err.println("emit local start");
}
}
/**
* Emits a {@link DebugInfoConstants#DBG_START_LOCAL_EXTENDED
* DBG_START_LOCAL_EXTENDED} sequence.
*
* @param entry entry to emit
* @throws IOException
*/
private void emitLocalStartExtended(LocalList.Entry entry)
throws IOException {
int mark = output.getCursor();
output.writeByte(DBG_START_LOCAL_EXTENDED);
emitUnsignedLeb128(entry.getRegister());
emitStringIndex(entry.getName());
emitTypeIndex(entry.getType());
emitStringIndex(entry.getSignature());
if (annotateTo != null || debugPrint != null) {
annotate(output.getCursor() - mark,
String.format("%04x: +localx %s", address,
entryAnnotationString(entry)));
}
if (DEBUG) {
System.err.println("emit local start");
}
}
/**
* Emits a {@link DebugInfoConstants#DBG_END_LOCAL DBG_END_LOCAL} sequence.
*
* @param entry {@code entry non-null;} entry associated with end.
* @throws IOException
*/
private void emitLocalEnd(LocalList.Entry entry)
throws IOException {
int mark = output.getCursor();
output.writeByte(DBG_END_LOCAL);
output.writeUleb128(entry.getRegister());
if (annotateTo != null || debugPrint != null) {
annotate(output.getCursor() - mark,
String.format("%04x: -local %s", address,
entryAnnotationString(entry)));
}
if (DEBUG) {
System.err.println("emit local end");
}
}
/**
* Emits the necessary byte sequences to emit the given position table
* entry. This will typically be a single special opcode, although
* it may also require DBG_ADVANCE_PC or DBG_ADVANCE_LINE.
*
* @param entry position entry to emit.
* @throws IOException
*/
private void emitPosition(PositionList.Entry entry)
throws IOException {
SourcePosition pos = entry.getPosition();
int newLine = pos.getLine();
int newAddress = entry.getAddress();
int opcode;
int deltaLines = newLine - line;
int deltaAddress = newAddress - address;
if (deltaAddress < 0) {
throw new RuntimeException(
"Position entries must be in ascending address order");
}
if ((deltaLines < DBG_LINE_BASE)
|| (deltaLines > (DBG_LINE_BASE + DBG_LINE_RANGE -1))) {
emitAdvanceLine(deltaLines);
deltaLines = 0;
}
opcode = computeOpcode (deltaLines, deltaAddress);
if ((opcode & ~0xff) > 0) {
emitAdvancePc(deltaAddress);
deltaAddress = 0;
opcode = computeOpcode (deltaLines, deltaAddress);
if ((opcode & ~0xff) > 0) {
emitAdvanceLine(deltaLines);
deltaLines = 0;
opcode = computeOpcode (deltaLines, deltaAddress);
}
}
output.writeByte(opcode);
line += deltaLines;
address += deltaAddress;
if (annotateTo != null || debugPrint != null) {
annotate(1,
String.format("%04x: line %d", address, line));
}
}
/**
* Computes a special opcode that will encode the given position change.
* If the return value is > 0xff, then the request cannot be fulfilled.
* Essentially the same as described in "DWARF Debugging Format Version 3"
* section 6.2.5.1.
*
* @param deltaLines {@code >= DBG_LINE_BASE, <= DBG_LINE_BASE +
* DBG_LINE_RANGE;} the line change to encode
* @param deltaAddress {@code >= 0;} the address change to encode
* @return {@code <= 0xff} if in range, otherwise parameters are out
* of range
*/
private static int computeOpcode(int deltaLines, int deltaAddress) {
if (deltaLines < DBG_LINE_BASE
|| deltaLines > (DBG_LINE_BASE + DBG_LINE_RANGE -1)) {
throw new RuntimeException("Parameter out of range");
}
return (deltaLines - DBG_LINE_BASE)
+ (DBG_LINE_RANGE * deltaAddress) + DBG_FIRST_SPECIAL;
}
/**
* Emits an {@link DebugInfoConstants#DBG_ADVANCE_LINE DBG_ADVANCE_LINE}
* sequence.
*
* @param deltaLines amount to change line number register by
* @throws IOException
*/
private void emitAdvanceLine(int deltaLines) throws IOException {
int mark = output.getCursor();
output.writeByte(DBG_ADVANCE_LINE);
output.writeSleb128(deltaLines);
line += deltaLines;
if (annotateTo != null || debugPrint != null) {
annotate(output.getCursor() - mark,
String.format("line = %d", line));
}
if (DEBUG) {
System.err.printf("Emitting advance_line for %d\n", deltaLines);
}
}
/**
* Emits an {@link DebugInfoConstants#DBG_ADVANCE_PC DBG_ADVANCE_PC}
* sequence.
*
* @param deltaAddress {@code >= 0;} amount to change program counter by
* @throws IOException
*/
private void emitAdvancePc(int deltaAddress) throws IOException {
int mark = output.getCursor();
output.writeByte(DBG_ADVANCE_PC);
output.writeUleb128(deltaAddress);
address += deltaAddress;
if (annotateTo != null || debugPrint != null) {
annotate(output.getCursor() - mark,
String.format("%04x: advance pc", address));
}
if (DEBUG) {
System.err.printf("Emitting advance_pc for %d\n", deltaAddress);
}
}
/**
* Emits an unsigned LEB128 value.
*
* @param n {@code >= 0;} value to emit. Note that, although this can
* represent integers larger than Integer.MAX_VALUE, we currently don't
* allow that.
* @throws IOException
*/
private void emitUnsignedLeb128(int n) throws IOException {
// We'll never need the top end of the unsigned range anyway.
if (n < 0) {
throw new RuntimeException(
"Signed value where unsigned required: " + n);
}
output.writeUleb128(n);
}
/**
* Emits the {@link DebugInfoConstants#DBG_END_SEQUENCE DBG_END_SEQUENCE}
* bytecode.
*/
private void emitEndSequence() {
output.writeByte(DBG_END_SEQUENCE);
if (annotateTo != null || debugPrint != null) {
annotate(1, "end sequence");
}
}
}