blob: 3d36aed7734e48d3cebb621e518c2ffd42d8f13b [file] [log] [blame]
/*
* 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 com.android.dx.dex.file;
import com.android.dx.dex.code.LocalList;
import com.android.dx.dex.code.PositionList;
import com.android.dx.rop.code.RegisterSpec;
import com.android.dx.rop.code.SourcePosition;
import com.android.dx.rop.cst.CstMethodRef;
import com.android.dx.rop.cst.CstType;
import com.android.dx.rop.cst.CstUtf8;
import com.android.dx.rop.type.Prototype;
import com.android.dx.rop.type.StdTypeList;
import com.android.dx.rop.type.Type;
import com.android.dx.util.ByteArrayAnnotatedOutput;
import com.android.dx.util.AnnotatedOutput;
import com.android.dx.util.ExceptionWithContext;
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 static com.android.dx.dex.file.DebugInfoConstants.*;
/**
* An encoder for the dex debug info state machine format. The format
* for each method enrty is as follows:
* <ol>
* <li> signed LEB128: initial value for line register.
* <li> n instances of signed LEB128: string indicies (offset by 1)
* for each method argument in left-to-right order
* with <code>this</code> excluded. A value of '0' indicates "no name"
* <li> A sequence of special or normal opcodes as defined in
* <code>DebugInfoConstants</code>.
* <li> A single terminating <code>OP_END_SEQUENCE</code>
* </ol>
*/
public final class DebugInfoEncoder {
private static final boolean DEBUG = false;
/** null-ok; positions (line numbers) to encode */
private final PositionList positions;
/** 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 pl null-ok; positions (line numbers) to encode
* @param ll null-ok; local variables to encode
* @param file null-ok; may only be <code>null</code> if simply using
* this class to do a debug print
* @param codeSize
* @param regSize
* @param isStatic
* @param ref
*/
public DebugInfoEncoder(PositionList pl, LocalList ll,
DexFile file, int codeSize, int regSize,
boolean isStatic, CstMethodRef ref) {
this.positions = pl;
this.locals = ll;
this.file = file;
output = new ByteArrayAnnotatedOutput();
this.desc = ref.getPrototype();
this.isStatic = isStatic;
this.codeSize = codeSize;
this.regSize = regSize;
lastEntryForReg = new LocalList.Entry[regSize];
}
/**
* Annotates or writes a message to the <code>debugPrint</code> 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 encoded byte sequence without padding and
* terminated with a <code>'\00'</code>
*/
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</code>.
*
* @param prefix null-ok; prefix to attach to each line of output
* @param debugPrint null-ok; if specified, an alternate output for
* annotations
* @param out null-ok; if specified, where annotations should go
* @param consume whether to claim to have consumed output for
* <code>out</code>
* @return output sequence
*/
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<PositionList.Entry> sortedPositions = buildSortedPositions();
ArrayList<LocalList.Entry> 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 szp = sortedPositions.size();
int szl = locals.size();
// Current index in sortedPositions
int curp = 0;
// Current index in locals
int curl = 0;
for (;;) {
/*
* Emit any information for the current address.
*/
curl = emitLocalsAtAddress(curl);
curp = emitPositionsAtAddress(curp, 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 (curl < szl) {
nextAddrL = locals.get(curl).getAddress();
}
if (curp < szp) {
nextAddrP = sortedPositions.get(curp).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(curp++));
} 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 curl Current index in locals
* @return new value for <code>curl</code>
* @throws IOException
*/
private int emitLocalsAtAddress(int curl)
throws IOException {
int sz = locals.size();
// TODO: Don't emit ends implied by starts.
while ((curl < sz)
&& (locals.get(curl).getAddress() == address)) {
LocalList.Entry lle = locals.get(curl++);
int reg = lle.getRegister();
LocalList.Entry prevlle = lastEntryForReg[reg];
if (lle == prevlle) {
/*
* 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] = lle;
if (lle.isStart()) {
if ((prevlle != null) && lle.matches(prevlle)) {
/*
* 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 (prevlle.isStart()) {
/*
* We should never be handed a start when a
* a matching local is already active.
*/
throw new RuntimeException("shouldn't happen");
}
emitLocalRestart(lle);
} else {
emitLocalStart(lle);
}
} 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 (lle.getDisposition()
!= LocalList.Disposition.END_REPLACED) {
emitLocalEnd(lle);
}
}
}
return curl;
}
/**
* Emits all positions that occur at the current <code>address</code>
*
* @param curp Current index in sortedPositions
* @param sortedPositions positions, sorted by ascending address
* @return new value for <code>curp</code>
* @throws IOException
*/
private int emitPositionsAtAddress(int curp,
ArrayList<PositionList.Entry> sortedPositions)
throws IOException {
int szp = sortedPositions.size();
while (curp < szp
&& sortedPositions.get(curp).getAddress() == address) {
emitPosition(sortedPositions.get(curp++));
}
return curp;
}
/**
* 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<PositionList.Entry> sortedPositions,
ArrayList<LocalList.Entry> 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.writeUnsignedLeb128(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.writeUnsignedLeb128(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)
? "<unnamed>" : 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;
}
CstUtf8 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<PositionList.Entry> buildSortedPositions() {
int sz = (positions == null) ? 0 : positions.size();
ArrayList<PositionList.Entry> 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<PositionList.Entry>() {
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</code>
*
* @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</code> method argument locals,
* sorted by ascending register
*/
private ArrayList<LocalList.Entry> extractMethodArguments() {
ArrayList<LocalList.Entry> 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<LocalList.Entry>() {
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 non-null; entry
* @return non-null; annotation string
*/
private String entryAnnotationString(LocalList.Entry e) {
StringBuilder sb = new StringBuilder();
sb.append(RegisterSpec.PREFIX);
sb.append(e.getRegister());
sb.append(' ');
CstUtf8 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());
}
CstUtf8 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 null-ok; string to emit
* @throws IOException
*/
private void emitStringIndex(CstUtf8 string) throws IOException {
if ((string == null) || (file == null)) {
output.writeUnsignedLeb128(0);
} else {
output.writeUnsignedLeb128(
1 + file.getStringIds().indexOf(string));
}
if (DEBUG) {
System.err.printf("Emit string %s\n",
string == null ? "<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 null-ok; type to emit
* @throws IOException
*/
private void emitTypeIndex(CstType type) throws IOException {
if ((type == null) || (file == null)) {
output.writeUnsignedLeb128(0);
} else {
output.writeUnsignedLeb128(
1 + file.getTypeIds().indexOf(type));
}
if (DEBUG) {
System.err.printf("Emit type %s\n",
type == null ? "<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 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.writeUnsignedLeb128(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 &gt;= DBG_LINE_BASE and &lt;= DBG_LINE_BASE +
* DBG_LINE_RANGE, the line change to encode
* @param deltaAddress &gt;= 0; the address change to encode
* @return &lt;= 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.writeSignedLeb128(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 &gt;= 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.writeUnsignedLeb128(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 &gt= 0 vallue 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.writeUnsignedLeb128(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");
}
}
}