| /* |
| * 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.dexgen.dex.file; |
| |
| import com.android.dexgen.dex.code.DalvCode; |
| import com.android.dexgen.dex.code.DalvInsnList; |
| import com.android.dexgen.dex.code.LocalList; |
| import com.android.dexgen.dex.code.PositionList; |
| import com.android.dexgen.rop.cst.CstMethodRef; |
| import com.android.dexgen.rop.cst.CstUtf8; |
| import com.android.dexgen.rop.type.Prototype; |
| import com.android.dexgen.rop.type.StdTypeList; |
| import com.android.dexgen.rop.type.Type; |
| import com.android.dexgen.util.ExceptionWithContext; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| import static com.android.dexgen.dex.file.DebugInfoConstants.*; |
| |
| /** |
| * A decoder for the dex debug info state machine format. |
| * This code exists mostly as a reference implementation and test for |
| * for the {@code DebugInfoEncoder} |
| */ |
| public class DebugInfoDecoder { |
| /** encoded debug info */ |
| private final byte[] encoded; |
| |
| /** positions decoded */ |
| private final ArrayList<PositionEntry> positions; |
| |
| /** locals decoded */ |
| private final ArrayList<LocalEntry> locals; |
| |
| /** size of code block in code units */ |
| private final int codesize; |
| |
| /** indexed by register, the last local variable live in a reg */ |
| private final LocalEntry[] lastEntryForReg; |
| |
| /** method descriptor of method this debug info is for */ |
| private final Prototype desc; |
| |
| /** true if method is static */ |
| private final boolean isStatic; |
| |
| /** dex file this debug info will be stored in */ |
| private final DexFile file; |
| |
| /** |
| * register size, in register units, of the register space |
| * used by this method |
| */ |
| private final int regSize; |
| |
| /** current decoding state: line number */ |
| private int line = 1; |
| |
| /** current decoding state: bytecode address */ |
| private int address = 0; |
| |
| /** string index of the string "this" */ |
| private final int thisStringIdx; |
| |
| /** |
| * Constructs an instance. |
| * |
| * @param encoded encoded debug info |
| * @param codesize size of code block in code units |
| * @param regSize register size, in register units, of the register space |
| * used by this method |
| * @param isStatic true if method is static |
| * @param ref method descriptor of method this debug info is for |
| * @param file dex file this debug info will be stored in |
| */ |
| DebugInfoDecoder(byte[] encoded, int codesize, int regSize, |
| boolean isStatic, CstMethodRef ref, DexFile file) { |
| if (encoded == null) { |
| throw new NullPointerException("encoded == null"); |
| } |
| |
| this.encoded = encoded; |
| this.isStatic = isStatic; |
| this.desc = ref.getPrototype(); |
| this.file = file; |
| this.regSize = regSize; |
| |
| positions = new ArrayList<PositionEntry>(); |
| locals = new ArrayList<LocalEntry>(); |
| this.codesize = codesize; |
| lastEntryForReg = new LocalEntry[regSize]; |
| |
| int idx = -1; |
| |
| try { |
| idx = file.getStringIds().indexOf(new CstUtf8("this")); |
| } catch (IllegalArgumentException ex) { |
| /* |
| * Silently tolerate not finding "this". It just means that |
| * no method has local variable info that looks like |
| * a standard instance method. |
| */ |
| } |
| |
| thisStringIdx = idx; |
| } |
| |
| /** |
| * An entry in the resulting postions table |
| */ |
| static private class PositionEntry { |
| /** bytecode address */ |
| public int address; |
| |
| /** line number */ |
| public int line; |
| |
| public PositionEntry(int address, int line) { |
| this.address = address; |
| this.line = line; |
| } |
| } |
| |
| /** |
| * An entry in the resulting locals table |
| */ |
| static private class LocalEntry { |
| /** address of event */ |
| public int address; |
| |
| /** {@code true} iff it's a local start */ |
| public boolean isStart; |
| |
| /** register number */ |
| public int reg; |
| |
| /** index of name in strings table */ |
| public int nameIndex; |
| |
| /** index of type in types table */ |
| public int typeIndex; |
| |
| /** index of type signature in strings table */ |
| public int signatureIndex; |
| |
| public LocalEntry(int address, boolean isStart, int reg, int nameIndex, |
| int typeIndex, int signatureIndex) { |
| this.address = address; |
| this.isStart = isStart; |
| this.reg = reg; |
| this.nameIndex = nameIndex; |
| this.typeIndex = typeIndex; |
| this.signatureIndex = signatureIndex; |
| } |
| |
| public String toString() { |
| return String.format("[%x %s v%d %04x %04x %04x]", |
| address, isStart ? "start" : "end", reg, |
| nameIndex, typeIndex, signatureIndex); |
| } |
| } |
| |
| /** |
| * Gets the decoded positions list. |
| * Valid after calling {@code decode}. |
| * |
| * @return positions list in ascending address order. |
| */ |
| public List<PositionEntry> getPositionList() { |
| return positions; |
| } |
| |
| /** |
| * Gets the decoded locals list, in ascending start-address order. |
| * Valid after calling {@code decode}. |
| * |
| * @return locals list in ascending address order. |
| */ |
| public List<LocalEntry> getLocals() { |
| return locals; |
| } |
| |
| /** |
| * Decodes the debug info sequence. |
| */ |
| public void decode() { |
| try { |
| decode0(); |
| } catch (Exception ex) { |
| throw ExceptionWithContext.withContext(ex, |
| "...while decoding debug info"); |
| } |
| } |
| |
| /** |
| * Reads a string index. String indicies are offset by 1, and a 0 value |
| * in the stream (-1 as returned by this method) means "null" |
| * |
| * @param bs |
| * @return index into file's string ids table, -1 means null |
| * @throws IOException |
| */ |
| private int readStringIndex(InputStream bs) throws IOException { |
| int offsetIndex = readUnsignedLeb128(bs); |
| |
| return offsetIndex - 1; |
| } |
| |
| /** |
| * 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); |
| } |
| |
| private void decode0() throws IOException { |
| ByteArrayInputStream bs = new ByteArrayInputStream(encoded); |
| |
| line = readUnsignedLeb128(bs); |
| int szParams = readUnsignedLeb128(bs); |
| StdTypeList params = desc.getParameterTypes(); |
| int curReg = getParamBase(); |
| |
| if (szParams != params.size()) { |
| throw new RuntimeException( |
| "Mismatch between parameters_size and prototype"); |
| } |
| |
| if (!isStatic) { |
| // Start off with implicit 'this' entry |
| LocalEntry thisEntry = |
| new LocalEntry(0, true, curReg, thisStringIdx, 0, 0); |
| locals.add(thisEntry); |
| lastEntryForReg[curReg] = thisEntry; |
| curReg++; |
| } |
| |
| for (int i = 0; i < szParams; i++) { |
| Type paramType = params.getType(i); |
| LocalEntry le; |
| |
| int nameIdx = readStringIndex(bs); |
| |
| if (nameIdx == -1) { |
| /* |
| * Unnamed parameter; often but not always filled in by an |
| * extended start op after the prologue |
| */ |
| le = new LocalEntry(0, true, curReg, -1, 0, 0); |
| } else { |
| // TODO: Final 0 should be idx of paramType.getDescriptor(). |
| le = new LocalEntry(0, true, curReg, nameIdx, 0, 0); |
| } |
| |
| locals.add(le); |
| lastEntryForReg[curReg] = le; |
| curReg += paramType.getCategory(); |
| } |
| |
| for (;;) { |
| int opcode = bs.read(); |
| |
| if (opcode < 0) { |
| throw new RuntimeException |
| ("Reached end of debug stream without " |
| + "encountering end marker"); |
| } |
| |
| switch (opcode) { |
| case DBG_START_LOCAL: { |
| int reg = readUnsignedLeb128(bs); |
| int nameIdx = readStringIndex(bs); |
| int typeIdx = readStringIndex(bs); |
| LocalEntry le = new LocalEntry( |
| address, true, reg, nameIdx, typeIdx, 0); |
| |
| locals.add(le); |
| lastEntryForReg[reg] = le; |
| } |
| break; |
| |
| case DBG_START_LOCAL_EXTENDED: { |
| int reg = readUnsignedLeb128(bs); |
| int nameIdx = readStringIndex(bs); |
| int typeIdx = readStringIndex(bs); |
| int sigIdx = readStringIndex(bs); |
| LocalEntry le = new LocalEntry( |
| address, true, reg, nameIdx, typeIdx, sigIdx); |
| |
| locals.add(le); |
| lastEntryForReg[reg] = le; |
| } |
| break; |
| |
| case DBG_RESTART_LOCAL: { |
| int reg = readUnsignedLeb128(bs); |
| LocalEntry prevle; |
| LocalEntry le; |
| |
| try { |
| prevle = lastEntryForReg[reg]; |
| |
| if (prevle.isStart) { |
| throw new RuntimeException("nonsensical " |
| + "RESTART_LOCAL on live register v" |
| + reg); |
| } |
| |
| le = new LocalEntry(address, true, reg, |
| prevle.nameIndex, prevle.typeIndex, 0); |
| } catch (NullPointerException ex) { |
| throw new RuntimeException( |
| "Encountered RESTART_LOCAL on new v" + reg); |
| } |
| |
| locals.add(le); |
| lastEntryForReg[reg] = le; |
| } |
| break; |
| |
| case DBG_END_LOCAL: { |
| int reg = readUnsignedLeb128(bs); |
| LocalEntry prevle; |
| LocalEntry le; |
| |
| try { |
| prevle = lastEntryForReg[reg]; |
| |
| if (!prevle.isStart) { |
| throw new RuntimeException("nonsensical " |
| + "END_LOCAL on dead register v" + reg); |
| } |
| |
| le = new LocalEntry(address, false, reg, |
| prevle.nameIndex, prevle.typeIndex, |
| prevle.signatureIndex); |
| } catch (NullPointerException ex) { |
| throw new RuntimeException( |
| "Encountered END_LOCAL on new v" + reg); |
| } |
| |
| locals.add(le); |
| lastEntryForReg[reg] = le; |
| } |
| break; |
| |
| case DBG_END_SEQUENCE: |
| // all done |
| return; |
| |
| case DBG_ADVANCE_PC: |
| address += readUnsignedLeb128(bs); |
| break; |
| |
| case DBG_ADVANCE_LINE: |
| line += readSignedLeb128(bs); |
| break; |
| |
| case DBG_SET_PROLOGUE_END: |
| //TODO do something with this. |
| break; |
| |
| case DBG_SET_EPILOGUE_BEGIN: |
| //TODO do something with this. |
| break; |
| |
| case DBG_SET_FILE: |
| //TODO do something with this. |
| break; |
| |
| default: |
| if (opcode < DBG_FIRST_SPECIAL) { |
| throw new RuntimeException( |
| "Invalid extended opcode encountered " |
| + opcode); |
| } |
| |
| int adjopcode = opcode - DBG_FIRST_SPECIAL; |
| |
| address += adjopcode / DBG_LINE_RANGE; |
| line += DBG_LINE_BASE + (adjopcode % DBG_LINE_RANGE); |
| |
| positions.add(new PositionEntry(address, line)); |
| break; |
| |
| } |
| } |
| } |
| |
| /** |
| * Validates an encoded debug info stream against data used to encode it, |
| * throwing an exception if they do not match. Used to validate the |
| * encoder. |
| * |
| * @param info encoded debug info |
| * @param file {@code non-null;} file to refer to during decoding |
| * @param ref {@code non-null;} method whose info is being decoded |
| * @param code {@code non-null;} original code object that was encoded |
| * @param isStatic whether the method is static |
| */ |
| public static void validateEncode(byte[] info, DexFile file, |
| CstMethodRef ref, DalvCode code, boolean isStatic) { |
| PositionList pl = code.getPositions(); |
| LocalList ll = code.getLocals(); |
| DalvInsnList insns = code.getInsns(); |
| int codeSize = insns.codeSize(); |
| int countRegisters = insns.getRegistersSize(); |
| |
| try { |
| validateEncode0(info, codeSize, countRegisters, |
| isStatic, ref, file, pl, ll); |
| } catch (RuntimeException ex) { |
| System.err.println("instructions:"); |
| insns.debugPrint(System.err, " ", true); |
| System.err.println("local list:"); |
| ll.debugPrint(System.err, " "); |
| throw ExceptionWithContext.withContext(ex, |
| "while processing " + ref.toHuman()); |
| } |
| } |
| |
| private static void validateEncode0(byte[] info, int codeSize, |
| int countRegisters, boolean isStatic, CstMethodRef ref, |
| DexFile file, PositionList pl, LocalList ll) { |
| DebugInfoDecoder decoder |
| = new DebugInfoDecoder(info, codeSize, countRegisters, |
| isStatic, ref, file); |
| |
| decoder.decode(); |
| |
| /* |
| * Go through the decoded position entries, matching up |
| * with original entries. |
| */ |
| |
| List<PositionEntry> decodedEntries = decoder.getPositionList(); |
| |
| if (decodedEntries.size() != pl.size()) { |
| throw new RuntimeException( |
| "Decoded positions table not same size was " |
| + decodedEntries.size() + " expected " + pl.size()); |
| } |
| |
| for (PositionEntry entry : decodedEntries) { |
| boolean found = false; |
| for (int i = pl.size() - 1; i >= 0; i--) { |
| PositionList.Entry ple = pl.get(i); |
| |
| if (entry.line == ple.getPosition().getLine() |
| && entry.address == ple.getAddress()) { |
| found = true; |
| break; |
| } |
| } |
| |
| if (!found) { |
| throw new RuntimeException ("Could not match position entry: " |
| + entry.address + ", " + entry.line); |
| } |
| } |
| |
| /* |
| * Go through the original local list, in order, matching up |
| * with decoded entries. |
| */ |
| |
| List<LocalEntry> decodedLocals = decoder.getLocals(); |
| int thisStringIdx = decoder.thisStringIdx; |
| int decodedSz = decodedLocals.size(); |
| int paramBase = decoder.getParamBase(); |
| |
| /* |
| * Preflight to fill in any parameters that were skipped in |
| * the prologue (including an implied "this") but then |
| * identified by full signature. |
| */ |
| for (int i = 0; i < decodedSz; i++) { |
| LocalEntry entry = decodedLocals.get(i); |
| int idx = entry.nameIndex; |
| |
| if ((idx < 0) || (idx == thisStringIdx)) { |
| for (int j = i + 1; j < decodedSz; j++) { |
| LocalEntry e2 = decodedLocals.get(j); |
| if (e2.address != 0) { |
| break; |
| } |
| if ((entry.reg == e2.reg) && e2.isStart) { |
| decodedLocals.set(i, e2); |
| decodedLocals.remove(j); |
| decodedSz--; |
| break; |
| } |
| } |
| } |
| } |
| |
| int origSz = ll.size(); |
| int decodeAt = 0; |
| boolean problem = false; |
| |
| for (int i = 0; i < origSz; i++) { |
| LocalList.Entry origEntry = ll.get(i); |
| |
| if (origEntry.getDisposition() |
| == LocalList.Disposition.END_REPLACED) { |
| /* |
| * The encoded list doesn't represent replacements, so |
| * ignore them for the sake of comparison. |
| */ |
| continue; |
| } |
| |
| LocalEntry decodedEntry; |
| |
| do { |
| decodedEntry = decodedLocals.get(decodeAt); |
| if (decodedEntry.nameIndex >= 0) { |
| break; |
| } |
| /* |
| * A negative name index means this is an anonymous |
| * parameter, and we shouldn't expect to see it in the |
| * original list. So, skip it. |
| */ |
| decodeAt++; |
| } while (decodeAt < decodedSz); |
| |
| int decodedAddress = decodedEntry.address; |
| |
| if (decodedEntry.reg != origEntry.getRegister()) { |
| System.err.println("local register mismatch at orig " + i + |
| " / decoded " + decodeAt); |
| problem = true; |
| break; |
| } |
| |
| if (decodedEntry.isStart != origEntry.isStart()) { |
| System.err.println("local start/end mismatch at orig " + i + |
| " / decoded " + decodeAt); |
| problem = true; |
| break; |
| } |
| |
| /* |
| * The secondary check here accounts for the fact that a |
| * parameter might not be marked as starting at 0 in the |
| * original list. |
| */ |
| if ((decodedAddress != origEntry.getAddress()) |
| && !((decodedAddress == 0) |
| && (decodedEntry.reg >= paramBase))) { |
| System.err.println("local address mismatch at orig " + i + |
| " / decoded " + decodeAt); |
| problem = true; |
| break; |
| } |
| |
| decodeAt++; |
| } |
| |
| if (problem) { |
| System.err.println("decoded locals:"); |
| for (LocalEntry e : decodedLocals) { |
| System.err.println(" " + e); |
| } |
| throw new RuntimeException("local table problem"); |
| } |
| } |
| |
| /** |
| * Reads a DWARFv3-style signed LEB128 integer to the specified stream. |
| * See DWARF v3 section 7.6. An invalid sequence produces an IOException. |
| * |
| * @param bs stream to input from |
| * @return read value |
| * @throws IOException on invalid sequence in addition to |
| * those caused by the InputStream |
| */ |
| public static int readSignedLeb128(InputStream bs) throws IOException { |
| int result = 0; |
| int cur; |
| int count = 0; |
| int signBits = -1; |
| |
| do { |
| cur = bs.read(); |
| result |= (cur & 0x7f) << (count * 7); |
| signBits <<= 7; |
| count++; |
| } while (((cur & 0x80) == 0x80) && count < 5); |
| |
| if ((cur & 0x80) == 0x80) { |
| throw new IOException ("invalid LEB128 sequence"); |
| } |
| |
| // Sign extend if appropriate |
| if (((signBits >> 1) & result) != 0 ) { |
| result |= signBits; |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Reads a DWARFv3-style unsigned LEB128 integer to the specified stream. |
| * See DWARF v3 section 7.6. An invalid sequence produces an IOException. |
| * |
| * @param bs stream to input from |
| * @return read value, which should be treated as an unsigned value. |
| * @throws IOException on invalid sequence in addition to |
| * those caused by the InputStream |
| */ |
| public static int readUnsignedLeb128(InputStream bs) throws IOException { |
| int result = 0; |
| int cur; |
| int count = 0; |
| |
| do { |
| cur = bs.read(); |
| result |= (cur & 0x7f) << (count * 7); |
| count++; |
| } while (((cur & 0x80) == 0x80) && count < 5); |
| |
| if ((cur & 0x80) == 0x80) { |
| throw new IOException ("invalid LEB128 sequence"); |
| } |
| |
| return result; |
| } |
| } |