/* * [The "BSD licence"] * Copyright (c) 2010 Ben Gruver (JesusFreke) * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. The name of the author may not be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.jf.dexlib; import org.jf.dexlib.Util.*; import java.io.*; import java.security.DigestException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.zip.Adler32; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; /** *
These are the main use cases that drove the design of this library
* *Annotate an existing dex file - In this case, the intent is to document the structure of * an existing dex file. We want to be able to read in the dex file, and then write out a dex file * that is exactly the same (while adding annotation information to an AnnotatedOutput object)
Canonicalize an existing dex file - In this case, the intent is to rewrite an existing dex file * so that it is in a canonical form. There is a certain amount of leeway in how various types of * tems in a dex file are ordered or represented. It is sometimes useful to be able to easily * compare a disassebled and reassembled dex file with the original dex file. If both dex-files are * written canonically, they "should" match exactly, barring any explicit changes to the reassembled * file.
* *Currently, there are a couple of pieces of information that probably won't match exactly *
EncodedCatchHandlerList
for a method{@link org.jf.dexlib.DebugInfoItem}
for a methodNote that the above discrepancies should typically only be "intra-item" differences. They * shouldn't change the size of the item, or affect how anything else is placed or laid out
Creating a dex file from scratch - In this case, a blank dex file is created and then classes * are added to it incrementally by calling the {@link org.jf.dexlib.Section#intern intern} method of * {@link DexFile#ClassDefsSection}, which will add all the information necessary to represent the given * class. For example, when assembling a dex file from a set of assembly text files.
* *In this case, we can choose to write the dex file in a canonical form or not. It is somewhat * slower to write it in a canonical format, due to the extra sorting and calculations that are * required.
Reading in the dex file - In this case, the intent is to read in a dex file and expose all the * data to the calling application. For example, when disassembling a dex file into a text based * assembly format, or doing other misc processing of the dex file.
These are other use cases that are possible, but did not drive the design of the library. * No effort was made to test these use cases or ensure that they work. Some of these could * probably be better achieved with a disassemble - modify - reassemble type process, using * smali/baksmali or another assembler/disassembler pair that are compatible with each other
* *getPreserveSignedRegisters()
*/
private DexFile(boolean preserveSignedRegisters, boolean skipInstructions) {
this.preserveSignedRegisters = preserveSignedRegisters;
this.skipInstructions = skipInstructions;
sectionsByType = new Section[] {
StringIdsSection,
TypeIdsSection,
ProtoIdsSection,
FieldIdsSection,
MethodIdsSection,
ClassDefsSection,
TypeListsSection,
AnnotationSetRefListsSection,
AnnotationSetsSection,
ClassDataSection,
CodeItemsSection,
AnnotationDirectoriesSection,
StringDataSection,
DebugInfoItemsSection,
AnnotationsSection,
EncodedArraysSection,
null,
null
};
indexedSections = new IndexedSection[] {
StringIdsSection,
TypeIdsSection,
ProtoIdsSection,
FieldIdsSection,
MethodIdsSection,
ClassDefsSection
};
offsettedSections = new OffsettedSection[] {
AnnotationSetRefListsSection,
AnnotationSetsSection,
CodeItemsSection,
AnnotationDirectoriesSection,
TypeListsSection,
StringDataSection,
AnnotationsSection,
EncodedArraysSection,
ClassDataSection,
DebugInfoItemsSection
};
}
/**
* Construct a new DexFile instance by reading in the given dex file.
* @param file The dex file to read in
* @throws IOException if an IOException occurs
*/
public DexFile(String file)
throws IOException {
this(new File(file), true, false);
}
/**
* Construct a new DexFile instance by reading in the given dex file,
* and optionally keep track of any registers in the debug information that are signed,
* so they will be written in the same format.
* @param file The dex file to read in
* @param preserveSignedRegisters If true, keep track of any registers in the debug information
* that are signed, so they will be written in the same format. See
* @param skipInstructions If true, skip the instructions in any code item.
* getPreserveSignedRegisters()
* @throws IOException if an IOException occurs
*/
public DexFile(String file, boolean preserveSignedRegisters, boolean skipInstructions)
throws IOException {
this(new File(file), preserveSignedRegisters, skipInstructions);
}
/**
* Construct a new DexFile instance by reading in the given dex file.
* @param file The dex file to read in
* @throws IOException if an IOException occurs
*/
public DexFile(File file)
throws IOException {
this(file, true, false);
}
/**
* Construct a new DexFile instance by reading in the given dex file,
* and optionally keep track of any registers in the debug information that are signed,
* so they will be written in the same format.
* @param file The dex file to read in
* @param preserveSignedRegisters If true, keep track of any registers in the debug information
* that are signed, so they will be written in the same format.
* @param skipInstructions If true, skip the instructions in any code item.
* @see #getPreserveSignedRegisters
* @throws IOException if an IOException occurs
*/
public DexFile(File file, boolean preserveSignedRegisters, boolean skipInstructions)
throws IOException {
this(preserveSignedRegisters, skipInstructions);
long fileLength;
byte[] magic = FileUtils.readFile(file, 0, 8);
InputStream inputStream = null;
Input in = null;
ZipFile zipFile = null;
try {
//do we have a zip file?
if (magic[0] == 0x50 && magic[1] == 0x4B) {
zipFile = new ZipFile(file);
ZipEntry zipEntry = zipFile.getEntry("classes.dex");
if (zipEntry == null) {
throw new NoClassesDexException("zip file " + file.getName() + " does not contain a classes.dex " +
"file");
}
fileLength = zipEntry.getSize();
if (fileLength < 40) {
throw new RuntimeException("The classes.dex file in " + file.getName() + " is too small to be a" +
" valid dex file");
} else if (fileLength > Integer.MAX_VALUE) {
throw new RuntimeException("The classes.dex file in " + file.getName() + " is too large to read in");
}
inputStream = new BufferedInputStream(zipFile.getInputStream(zipEntry));
inputStream.mark(8);
for (int i=0; i<8; i++) {
magic[i] = (byte)inputStream.read();
}
inputStream.reset();
} else {
fileLength = file.length();
if (fileLength < 40) {
throw new RuntimeException(file.getName() + " is too small to be a valid dex file");
}
if (fileLength < 40) {
throw new RuntimeException(file.getName() + " is too small to be a valid dex file");
} else if (fileLength > Integer.MAX_VALUE) {
throw new RuntimeException(file.getName() + " is too large to read in");
}
inputStream = new FileInputStream(file);
}
byte[] dexMagic, odexMagic;
boolean isDex = false;
this.isOdex = false;
if (Arrays.equals(magic, HeaderItem.MAGIC)) {
isDex = true;
} else if (Arrays.equals(magic, OdexHeader.MAGIC_35)) {
isOdex = true;
} else if (Arrays.equals(magic, OdexHeader.MAGIC_36)) {
isOdex = true;
}
if (isOdex) {
byte[] odexHeaderBytes = FileUtils.readStream(inputStream, 40);
Input odexHeaderIn = new ByteArrayInput(odexHeaderBytes);
odexHeader = new OdexHeader(odexHeaderIn);
int dependencySkip = odexHeader.depsOffset - odexHeader.dexOffset - odexHeader.dexLength;
if (dependencySkip < 0) {
throw new ExceptionWithContext("Unexpected placement of the odex dependency data");
}
if (odexHeader.dexOffset > 40) {
FileUtils.readStream(inputStream, odexHeader.dexOffset - 40);
}
in = new ByteArrayInput(FileUtils.readStream(inputStream, odexHeader.dexLength));
if (dependencySkip > 0) {
FileUtils.readStream(inputStream, dependencySkip);
}
odexDependencies = new OdexDependencies(
new ByteArrayInput(FileUtils.readStream(inputStream, odexHeader.depsLength)));
} else if (isDex) {
in = new ByteArrayInput(FileUtils.readStream(inputStream, (int)fileLength));
} else {
StringBuffer sb = new StringBuffer("bad magic value:");
for (int i=0; i<8; i++) {
sb.append(" ");
sb.append(Hex.u1(magic[i]));
}
throw new RuntimeException(sb.toString());
}
} finally {
if (inputStream != null) {
inputStream.close();
}
if (zipFile != null) {
zipFile.close();
}
}
ReadContext readContext = new ReadContext();
HeaderItem.readFrom(in, 0, readContext);
//the map offset was set while reading in the header item
int mapOffset = readContext.getSectionOffset(ItemType.TYPE_MAP_LIST);
in.setCursor(mapOffset);
MapItem.readFrom(in, 0, readContext);
//the sections are ordered in such a way that the item types
Section sections[] = new Section[] {
StringDataSection,
StringIdsSection,
TypeIdsSection,
TypeListsSection,
ProtoIdsSection,
FieldIdsSection,
MethodIdsSection,
AnnotationsSection,
AnnotationSetsSection,
AnnotationSetRefListsSection,
AnnotationDirectoriesSection,
DebugInfoItemsSection,
CodeItemsSection,
ClassDataSection,
EncodedArraysSection,
ClassDefsSection
};
for (Section section: sections) {
if (section == null) {
continue;
}
if (skipInstructions && (section == CodeItemsSection || section == DebugInfoItemsSection)) {
continue;
}
int sectionOffset = readContext.getSectionOffset(section.ItemType);
if (sectionOffset > 0) {
int sectionSize = readContext.getSectionSize(section.ItemType);
in.setCursor(sectionOffset);
section.readFrom(sectionSize, in, readContext);
}
}
}
/**
* Constructs a new, blank dex file. Classes can be added to this dex file by calling
* the Section.intern()
method of ClassDefsSection
*/
public DexFile() {
this(true, false);
}
/**
* Get the Section
containing items of the same type as the given item
* @param item Get the Section
that contains items of this type
* @param Section
containing items of the same type as the given item
*/
public Section
containing items of the given type
* @param itemType the type of item
* @return the Section
containing items of the given type
*/
public Section getSectionForType(ItemType itemType) {
return sectionsByType[itemType.SectionIndex];
}
/**
* Get a boolean value indicating whether this dex file preserved any signed
* registers in the debug info as it read the dex file in. By default, the dex file
* doesn't check whether the registers are encoded as unsigned or signed values.
*
* This does *not* affect the actual register value that is read in. The value is
* read correctly regardless
*
* This does affect whether any signed registers will retain the same encoding or be
* forced to the (correct) unsigned encoding when the dex file is written back out.
*
* See the discussion about signed register values in the documentation for
* DexFile
* @return a boolean indicating whether this dex file preserved any signed registers
* as it was read in
*/
public boolean getPreserveSignedRegisters() {
return preserveSignedRegisters;
}
/**
* Get a boolean value indicating whether to skip any instructions in a code item while reading in the dex file.
* This is useful when you only need the information about the classes and their methods, for example, when
* loading the BOOTCLASSPATH jars in order to analyze a dex file
* @return a boolean value indicating whether to skip any instructions in a code item
*/
public boolean skipInstructions() {
return skipInstructions;
}
/**
* Get a boolean value indicating whether all items should be placed into a
* (possibly arbitrary) "canonical" ordering. If false, then only the items
* that must be ordered per the dex specification are sorted.
*
* When true, writing the dex file involves somewhat more overhead
*
* If both SortAllItems and Inplace are true, Inplace takes precedence
* @return a boolean value indicating whether all items should be sorted
*/
public boolean getSortAllItems() {
return this.sortAllItems;
}
/**
* Set a boolean value indicating whether all items should be placed into a
* (possibly arbitrary) "canonical" ordering. If false, then only the items
* that must be ordered per the dex specification are sorted.
*
* When true, writing the dex file involves somewhat more overhead
*
* If both SortAllItems and Inplace are true, Inplace takes precedence
* @param value a boolean value indicating whether all items should be sorted
*/
public void setSortAllItems(boolean value) {
this.sortAllItems = value;
}
/**
* @return a boolean value indicating whether this dex file was created by reading in an odex file
*/
public boolean isOdex() {
return this.isOdex;
}
/**
* @return an OdexDependencies object that contains the dependencies for this odex, or null if this
* DexFile represents a dex file instead of an odex file
*/
public OdexDependencies getOdexDependencies() {
return odexDependencies;
}
/**
* @return An OdexHeader object containing the information from the odex header in this dex file, or null if there
* is no odex header
*/
public OdexHeader getOdexHeader() {
return odexHeader;
}
/**
* Get a boolean value indicating whether items in this dex file should be
* written back out "in-place", or whether the normal layout logic should be
* applied.
*
* This should only be used for a dex file that has been read from an existing
* dex file, and no modifications have been made to the dex file. Otherwise,
* there is a good chance that the resulting dex file will be invalid due to
* items that aren't placed correctly
*
* If both SortAllItems and Inplace are true, Inplace takes precedence
* @return a boolean value indicating whether items in this dex file should be
* written back out in-place.
*/
public boolean getInplace() {
return this.inplace;
}
/**
* @return the size of the file, in bytes
*/
public int getFileSize() {
return fileSize;
}
/**
* @return the size of the data section, in bytes
*/
public int getDataSize() {
return dataSize;
}
/**
* @return the offset where the data section begins
*/
public int getDataOffset() {
return dataOffset;
}
/**
* Set a boolean value indicating whether items in this dex file should be
* written back out "in-place", or whether the normal layout logic should be
* applied.
*
* This should only be used for a dex file that has been read from an existing
* dex file, and no modifications have been made to the dex file. Otherwise,
* there is a good chance that the resulting dex file will be invalid due to
* items that aren't placed correctly
*
* If both SortAllItems and Inplace are true, Inplace takes precedence
* @param value a boolean value indicating whether items in this dex file should be
* written back out in-place.
*/
public void setInplace(boolean value) {
this.inplace = value;
}
/**
* Get an array of Section objects that are sorted by offset.
* @return an array of Section objects that are sorted by offset.
*/
protected Section[] getOrderedSections() {
int sectionCount = 0;
for (Section section: sectionsByType) {
if (section != null && section.getItems().size() > 0) {
sectionCount++;
}
}
Section[] sections = new Section[sectionCount];
sectionCount = 0;
for (Section section: sectionsByType) {
if (section != null && section.getItems().size() > 0) {
sections[sectionCount++] = section;
}
}
Arrays.sort(sections, new ComparatorgetSortAllItems()
and getInplace()
,
* and then performs a pass through all of the items, finalizing the position (i.e.
* index and/or offset) of each item in the dex file.
*
* This step is needed primarily so that the indexes and offsets of all indexed and
* offsetted items are available when writing references to those items elsewhere.
*/
public void place() {
int offset = HeaderItem.placeAt(0, 0);
int sectionsPosition = 0;
Section[] sections;
if (this.inplace) {
sections = this.getOrderedSections();
} else {
sections = new Section[indexedSections.length + offsettedSections.length];
System.arraycopy(indexedSections, 0, sections, 0, indexedSections.length);
System.arraycopy(offsettedSections, 0, sections, indexedSections.length, offsettedSections.length);
}
while (sectionsPosition < sections.length && sections[sectionsPosition].ItemType.isIndexedItem()) {
Section section = sections[sectionsPosition];
if (!this.inplace) {
section.sortSection();
}
offset = section.placeAt(offset);
sectionsPosition++;
}
dataOffset = offset;
while (sectionsPosition < sections.length) {
Section section = sections[sectionsPosition];
if (this.sortAllItems && !this.inplace) {
section.sortSection();
}
offset = section.placeAt(offset);
sectionsPosition++;
}
offset = AlignmentUtils.alignOffset(offset, ItemType.TYPE_MAP_LIST.ItemAlignment);
offset = MapItem.placeAt(offset, 0);
fileSize = offset;
dataSize = offset - dataOffset;
}
/**
* Writes the dex file to the give AnnotatedOutput
object. If
* out.Annotates()
is true, then annotations that document the format
* of the dex file are written.
*
* You must call place()
on this dex file, before calling this method
* @param out the AnnotatedOutput object to write the dex file and annotations to
*
* After calling this method, you should call calcSignature()
and
* then calcChecksum()
on the resulting byte array, to calculate the
* signature and checksum in the header
*/
public void writeTo(AnnotatedOutput out) {
out.annotate(0, "-----------------------------");
out.annotate(0, "header item");
out.annotate(0, "-----------------------------");
out.annotate(0, " ");
HeaderItem.writeTo(out);
out.annotate(0, " ");
int sectionsPosition = 0;
Section[] sections;
if (this.inplace) {
sections = this.getOrderedSections();
} else {
sections = new Section[indexedSections.length + offsettedSections.length];
System.arraycopy(indexedSections, 0, sections, 0, indexedSections.length);
System.arraycopy(offsettedSections, 0, sections, indexedSections.length, offsettedSections.length);
}
while (sectionsPosition < sections.length) {
sections[sectionsPosition].writeTo(out);
sectionsPosition++;
}
out.alignTo(MapItem.getItemType().ItemAlignment);
out.annotate(0, " ");
out.annotate(0, "-----------------------------");
out.annotate(0, "map item");
out.annotate(0, "-----------------------------");
out.annotate(0, " ");
MapItem.writeTo(out);
}
public final HeaderItem HeaderItem = new HeaderItem(this);
public final MapItem MapItem = new MapItem(this);
/**
* The IndexedSection
containing StringIdItem
items
*/
public final IndexedSectionIndexedSection
containing TypeIdItem
items
*/
public final IndexedSectionIndexedSection
containing ProtoIdItem
items
*/
public final IndexedSectionIndexedSection
containing FieldIdItem
items
*/
public final IndexedSectionIndexedSection
containing MethodIdItem
items
*/
public final IndexedSectionIndexedSection
containing ClassDefItem
items
*/
public final IndexedSectionOffsettedSection
containing TypeListItem
items
*/
public final OffsettedSectionOffsettedSection
containing AnnotationSetRefList
items
*/
public final OffsettedSectionOffsettedSection
containing AnnotationSetItem
items
*/
public final OffsettedSectionOffsettedSection
containing ClassDataItem
items
*/
public final OffsettedSectionOffsettedSection
containing CodeItem
items
*/
public final OffsettedSectionOffsettedSection
containing StringDataItem
items
*/
public final OffsettedSectionOffsettedSection
containing DebugInfoItem
items
*/
public final OffsettedSectionOffsettedSection
containing AnnotationItem
items
*/
public final OffsettedSectionOffsettedSection
containing EncodedArrayItem
items
*/
public final OffsettedSectionOffsettedSection
containing AnnotationDirectoryItem
items
*/
public final OffsettedSection.dex
file in the
* given array, and modify the array to contain it.
*
* @param bytes non-null; the bytes of the file
*/
public static void calcChecksum(byte[] bytes) {
Adler32 a32 = new Adler32();
a32.update(bytes, 12, bytes.length - 12);
int sum = (int) a32.getValue();
bytes[8] = (byte) sum;
bytes[9] = (byte) (sum >> 8);
bytes[10] = (byte) (sum >> 16);
bytes[11] = (byte) (sum >> 24);
}
public static class NoClassesDexException extends ExceptionWithContext {
public NoClassesDexException(String message) {
super(message);
}
}
}