blob: 10799ec0aaffd5717268f5074216d20bbff495d0 [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.ddmuilib;
import com.android.ddmlib.Log;
import com.android.ddmlib.NativeLibraryMapInfo;
import com.android.ddmlib.NativeStackCallInfo;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
/**
* Represents an addr2line process to get filename/method information from a
* memory address.<br>
* Each process can only handle one library, which should be provided when
* creating a new process.<br>
* <br>
* The processes take some time to load as they need to parse the library files.
* For this reason, processes cannot be manually started. Instead the class
* keeps an internal list of processes and one asks for a process for a specific
* library, using <code>getProcess(String library)<code>.<br></br>
* Internally, the processes are started in pipe mode to be able to query them
* with multiple addresses.
*/
public class Addr2Line {
private static final String ANDROID_SYMBOLS_ENVVAR = "ANDROID_SYMBOLS";
private static final String LIBRARY_NOT_FOUND_MESSAGE_FORMAT =
"Unable to locate library %s on disk. Addresses mapping to this library "
+ "will not be resolved. In order to fix this, set the the library search path "
+ "in the UI, or set the environment variable " + ANDROID_SYMBOLS_ENVVAR + ".";
/**
* Loaded processes list. This is also used as a locking object for any
* methods dealing with starting/stopping/creating processes/querying for
* method.
*/
private static final HashMap<String, Addr2Line> sProcessCache =
new HashMap<String, Addr2Line>();
/**
* byte array representing a carriage return. Used to push addresses in the
* process pipes.
*/
private static final byte[] sCrLf = {
'\n'
};
/** Path to the library */
private NativeLibraryMapInfo mLibrary;
/** the command line process */
private Process mProcess;
/** buffer to read the result of the command line process from */
private BufferedReader mResultReader;
/**
* output stream to provide new addresses to decode to the command line
* process
*/
private BufferedOutputStream mAddressWriter;
private static final String DEFAULT_LIBRARY_SYMBOLS_FOLDER;
static {
String symbols = System.getenv(ANDROID_SYMBOLS_ENVVAR);
if (symbols == null) {
DEFAULT_LIBRARY_SYMBOLS_FOLDER = DdmUiPreferences.getSymbolDirectory();
} else {
DEFAULT_LIBRARY_SYMBOLS_FOLDER = symbols;
}
}
private static List<String> mLibrarySearchPaths = new ArrayList<String>();
/**
* Set the search path where libraries should be found.
* @param path search path to use, can be a colon separated list of paths if multiple folders
* should be searched
*/
public static void setSearchPath(String path) {
mLibrarySearchPaths.clear();
mLibrarySearchPaths.addAll(Arrays.asList(path.split(":")));
}
/**
* Returns the instance of a Addr2Line process for the specified library.
* <br>The library should be in a format that makes<br>
* <code>$ANDROID_PRODUCT_OUT + "/symbols" + library</code> a valid file.
*
* @param library the library in which to look for addresses.
* @return a new Addr2Line object representing a started process, ready to
* be queried for addresses. If any error happened when launching a
* new process, <code>null</code> will be returned.
*/
public static Addr2Line getProcess(final NativeLibraryMapInfo library) {
String libName = library.getLibraryName();
// synchronize around the hashmap object
if (libName != null) {
synchronized (sProcessCache) {
// look for an existing process
Addr2Line process = sProcessCache.get(libName);
// if we don't find one, we create it
if (process == null) {
process = new Addr2Line(library);
// then we start it
boolean status = process.start();
if (status) {
// if starting the process worked, then we add it to the
// list.
sProcessCache.put(libName, process);
} else {
// otherwise we just drop the object, to return null
process = null;
}
}
// return the process
return process;
}
}
return null;
}
/**
* Construct the object with a library name. The library should be present
* in the search path as provided by ANDROID_SYMBOLS, ANDROID_OUT/symbols, or in the user
* provided search path.
*
* @param library the library in which to look for address.
*/
private Addr2Line(final NativeLibraryMapInfo library) {
mLibrary = library;
}
/**
* Search for the library in the library search path and obtain the full path to where it
* is found.
* @return fully resolved path to the library if found in search path, null otherwise
*/
private String getLibraryPath(String library) {
// first check the symbols folder
String path = DEFAULT_LIBRARY_SYMBOLS_FOLDER + library;
if (new File(path).exists()) {
return path;
}
for (String p : mLibrarySearchPaths) {
// try appending the full path on device
String fullPath = p + "/" + library;
if (new File(fullPath).exists()) {
return fullPath;
}
// try appending basename(library)
fullPath = p + "/" + new File(library).getName();
if (new File(fullPath).exists()) {
return fullPath;
}
}
return null;
}
/**
* Starts the command line process.
*
* @return true if the process was started, false if it failed to start, or
* if there was any other errors.
*/
private boolean start() {
// because this is only called from getProcess() we know we don't need
// to synchronize this code.
String addr2Line = System.getenv("ANDROID_ADDR2LINE");
if (addr2Line == null) {
addr2Line = DdmUiPreferences.getAddr2Line();
}
// build the command line
String[] command = new String[5];
command[0] = addr2Line;
command[1] = "-C";
command[2] = "-f";
command[3] = "-e";
String fullPath = getLibraryPath(mLibrary.getLibraryName());
if (fullPath == null) {
String msg = String.format(LIBRARY_NOT_FOUND_MESSAGE_FORMAT, mLibrary.getLibraryName());
Log.e("ddm-Addr2Line", msg);
return false;
}
command[4] = fullPath;
try {
// attempt to start the process
mProcess = Runtime.getRuntime().exec(command);
if (mProcess != null) {
// get the result reader
InputStreamReader is = new InputStreamReader(mProcess
.getInputStream());
mResultReader = new BufferedReader(is);
// get the outstream to write the addresses
mAddressWriter = new BufferedOutputStream(mProcess
.getOutputStream());
// check our streams are here
if (mResultReader == null || mAddressWriter == null) {
// not here? stop the process and return false;
mProcess.destroy();
mProcess = null;
return false;
}
// return a success
return true;
}
} catch (IOException e) {
// log the error
String msg = String.format(
"Error while trying to start %1$s process for library %2$s",
DdmUiPreferences.getAddr2Line(), mLibrary);
Log.e("ddm-Addr2Line", msg);
// drop the process just in case
if (mProcess != null) {
mProcess.destroy();
mProcess = null;
}
}
// we can be here either cause the allocation of mProcess failed, or we
// caught an exception
return false;
}
/**
* Stops the command line process.
*/
public void stop() {
synchronized (sProcessCache) {
if (mProcess != null) {
// remove the process from the list
sProcessCache.remove(mLibrary);
// then stops the process
mProcess.destroy();
// set the reference to null.
// this allows to make sure another thread calling getAddress()
// will not query a stopped thread
mProcess = null;
}
}
}
/**
* Stops all current running processes.
*/
public static void stopAll() {
// because of concurrent access (and our use of HashMap.values()), we
// can't rely on the synchronized inside stop(). We need to put one
// around the whole loop.
synchronized (sProcessCache) {
// just a basic loop on all the values in the hashmap and call to
// stop();
Collection<Addr2Line> col = sProcessCache.values();
for (Addr2Line a2l : col) {
a2l.stop();
}
}
}
/**
* Looks up an address and returns method name, source file name, and line
* number.
*
* @param addr the address to look up
* @return a BacktraceInfo object containing the method/filename/linenumber
* or null if the process we stopped before the query could be
* processed, or if an IO exception happened.
*/
public NativeStackCallInfo getAddress(long addr) {
long offset = addr - mLibrary.getStartAddress();
// even though we don't access the hashmap object, we need to
// synchronized on it to prevent
// another thread from stopping the process we're going to query.
synchronized (sProcessCache) {
// check the process is still alive/allocated
if (mProcess != null) {
// prepare to the write the address to the output buffer.
// first, conversion to a string containing the hex value.
String tmp = Long.toString(offset, 16);
try {
// write the address to the buffer
mAddressWriter.write(tmp.getBytes());
// add CR-LF
mAddressWriter.write(sCrLf);
// flush it all.
mAddressWriter.flush();
// read the result. We need to read 2 lines
String method = mResultReader.readLine();
String source = mResultReader.readLine();
// make the backtrace object and return it
if (method != null && source != null) {
return new NativeStackCallInfo(addr, mLibrary.getLibraryName(), method, source);
}
} catch (IOException e) {
// log the error
Log.e("ddms",
"Error while trying to get information for addr: "
+ tmp + " in library: " + mLibrary);
// we'll return null later
}
}
}
return null;
}
}