/*
* Copyright (c) 2013, 2014 Chris Newland.
* Licensed under https://github.com/AdoptOpenJDK/jitwatch/blob/master/LICENSE-BSD
* Instructions: https://github.com/AdoptOpenJDK/jitwatch/wiki
*/
package org.adoptopenjdk.jitwatch.loader;
import com.sun.tools.javap.JavapTask;
import com.sun.tools.javap.JavapTask.BadArgs;
import org.adoptopenjdk.jitwatch.model.MemberSignatureParts;
import org.adoptopenjdk.jitwatch.model.bytecode.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.*;
public final class BytecodeLoader
{
private static final Logger logger = LoggerFactory.getLogger(BytecodeLoader.class);
private static final Pattern PATTERN_BYTECODE_INSTRUCTION = Pattern
.compile("^([0-9]+):\\s([0-9a-z_]+)\\s?([#0-9a-z,\\- ]+)?\\s?\\{?\\s?(//.*)?");
enum BytecodeSection
{
NONE, CONSTANT_POOL, CODE, EXCEPTIONS, LINETABLE, RUNTIMEVISIBLEANNOTATIONS, LOCALVARIABLETABLE, STACKMAPTABLE
}
private static final Map<String, BytecodeSection> sectionLabelMap = new HashMap<>();
static
{
sectionLabelMap.put(S_BYTECODE_CONSTANT_POOL, BytecodeSection.CONSTANT_POOL);
sectionLabelMap.put(S_BYTECODE_CODE, BytecodeSection.CODE);
sectionLabelMap.put(S_BYTECODE_LINENUMBERTABLE, BytecodeSection.LINETABLE);
sectionLabelMap.put(S_BYTECODE_LOCALVARIABLETABLE, BytecodeSection.LOCALVARIABLETABLE);
sectionLabelMap.put(S_BYTECODE_RUNTIMEVISIBLEANNOTATIONS, BytecodeSection.RUNTIMEVISIBLEANNOTATIONS);
sectionLabelMap.put(S_BYTECODE_EXCEPTIONS, BytecodeSection.EXCEPTIONS);
sectionLabelMap.put(S_BYTECODE_STACKMAPTABLE, BytecodeSection.STACKMAPTABLE);
}
private BytecodeLoader()
{
}
public static ClassBC fetchBytecodeForClass(Collection<String> classLocations, String fqClassName)
{
if (DEBUG_LOGGING_BYTECODE)
{
logger.debug("fetchBytecodeForClass: {}", fqClassName);
}
String[] args = buildClassPathFromClassLocations(classLocations, fqClassName);
if (DEBUG_LOGGING_BYTECODE)
{
for (String arg : args)
{
logger.debug("arg: {}", arg);
}
}
String byteCodeString = createJavapTaskFromArguments(fqClassName, args);
return parsedByteCodeFrom(fqClassName, byteCodeString);
}
private static ClassBC parsedByteCodeFrom(String fqClassName, String byteCodeString)
{
ClassBC result = null;
if (byteCodeString != null)
{
try
{
result = parse(fqClassName, byteCodeString);
}
catch (Exception ex)
{
logger.error("Exception parsing bytecode", ex);
}
}
return result;
}
private static String createJavapTaskFromArguments(String fqClassName, String[] args)
{
String byteCodeString = null;
try (ByteArrayOutputStream baos = new ByteArrayOutputStream(65536))
{
JavapTask task = new JavapTask();
task.setLog(baos);
task.handleOptions(args);
task.call();
byteCodeString = baos.toString();
}
catch (BadArgs ba)
{
logger.error("Could not obtain bytecode for class: {}", fqClassName, ba);
}
catch (IOException ioe)
{
logger.error("", ioe);
}
return byteCodeString;
}
private static String[] buildClassPathFromClassLocations(Collection<String> classLocations, String fqClassName)
{
String[] args;
if (classLocations.size() == 0)
{
args = new String[] { "-c", "-p", "-v", fqClassName };
}
else
{
StringBuilder classPathBuilder = new StringBuilder();
for (String cp : classLocations)
{
classPathBuilder.append(cp).append(File.pathSeparatorChar);
}
classPathBuilder.deleteCharAt(classPathBuilder.length() - 1);
args = new String[] { "-c", "-p", "-v", "-classpath", classPathBuilder.toString(), fqClassName };
}
return args;
}
// TODO refactor this class - better stateful than all statics
public static ClassBC parse(String fqClassName, String bytecode)
{
ClassBC classBytecode = new ClassBC();
String[] lines = bytecode.split(S_NEWLINE);
int pos = 0;
StringBuilder builder = new StringBuilder();
BytecodeSection section = BytecodeSection.NONE;
String memberSignature = null;
MemberBytecode memberBytecode = null;
while (pos < lines.length)
{
String line = lines[pos].trim();
if (DEBUG_LOGGING_BYTECODE)
{
logger.debug("Line: {}", line);
}
BytecodeSection nextSection = getNextSection(line);
if (nextSection != null)
{
sectionFinished(fqClassName, section, memberSignature, builder, memberBytecode, classBytecode);
section = changeSection(nextSection);
pos++;
if (pos < lines.length)
{
line = lines[pos].trim();
}
}
if (DEBUG_LOGGING_BYTECODE)
{
logger.debug("{} Line: {}", section, line);
}
switch (section)
{
case NONE:
if (couldBeMemberSignature(line))
{
memberSignature = cleanBytecodeMemberSignature(line);
if (DEBUG_LOGGING_BYTECODE)
{
logger.debug("New signature: {}", memberSignature);
}
memberBytecode = new MemberBytecode();
if (DEBUG_LOGGING_BYTECODE)
{
logger.debug("Initialised new MemberBytecode");
}
}
else if (line.startsWith(S_BYTECODE_MINOR_VERSION))
{
int minorVersion = getVersionPart(line);
classBytecode.setMinorVersion(minorVersion);
}
else if (line.startsWith(S_BYTECODE_MAJOR_VERSION))
{
int majorVersion = getVersionPart(line);
classBytecode.setMajorVersion(majorVersion);
}
break;
case CODE:
section = performCODE(fqClassName, classBytecode, builder, section, memberSignature, memberBytecode, line);
break;
case CONSTANT_POOL:
section = performConstantPool(fqClassName, classBytecode, builder, section, memberSignature, memberBytecode, line);
break;
case LINETABLE:
section = performLINETABLE(fqClassName, classBytecode, builder, section, memberSignature, memberBytecode, line);
break;
case RUNTIMEVISIBLEANNOTATIONS:
if (!isRunTimeVisibleAnnotation(line))
{
section = changeSection(BytecodeSection.NONE);
pos--;
}
break;
case LOCALVARIABLETABLE:
if (!isLocalVariableLine(line))
{
section = changeSection(BytecodeSection.NONE);
pos--;
}
break;
case STACKMAPTABLE:
if (!isStackMapTable(line))
{
section = changeSection(BytecodeSection.NONE);
pos--;
}
break;
case EXCEPTIONS:
break;
}
pos++;
}
return classBytecode;
}
private static BytecodeSection performLINETABLE(String fqClassName, ClassBC classBytecode, StringBuilder builder, BytecodeSection section,
String memberSignature, MemberBytecode memberBytecode, String line)
{
if (line.startsWith("line "))
{
builder.append(line).append(C_NEWLINE);
}
else
{
sectionFinished(fqClassName, BytecodeSection.LINETABLE, memberSignature, builder, memberBytecode, classBytecode);
section = changeSection(BytecodeSection.NONE);
}
return section;
}
private static BytecodeSection performConstantPool(String fqClassName, ClassBC classBytecode, StringBuilder builder, BytecodeSection section,
String memberSignature, MemberBytecode memberBytecode, String line)
{
if (!line.startsWith(S_HASH))
{
sectionFinished(fqClassName, BytecodeSection.CONSTANT_POOL, memberSignature, builder, memberBytecode, classBytecode);
section = changeSection(BytecodeSection.NONE);
}
return section;
}
private static BytecodeSection performCODE(String fqClassName, ClassBC classBytecode, StringBuilder builder, BytecodeSection section,
String memberSignature, MemberBytecode memberBytecode, final String line)
{
int firstColonIndex = line.indexOf(C_COLON);
if (firstColonIndex != -1)
{
String beforeColon = line.substring(0, firstColonIndex);
try
{
// line number ?
Integer.parseInt(beforeColon);
builder.append(line).append(C_NEWLINE);
}
catch (NumberFormatException nfe)
{
if ( S_DEFAULT.equals(beforeColon))
{
// possibly inside a tableswitch or lookupswitch
builder.append(line).append(C_NEWLINE);
}
else
{
sectionFinished(fqClassName, BytecodeSection.CODE, memberSignature, builder, memberBytecode, classBytecode);
section = changeSection(BytecodeSection.NONE);
}
}
}
else if (S_CLOSE_BRACE.equals(line.trim()))
{
// end of a tableswitch or lookupswitch
builder.append(line).append(C_NEWLINE);
}
return section;
}
private static boolean isRunTimeVisibleAnnotation(final String line)
{
return line.contains(": #");
}
private static boolean isLocalVariableLine(final String line)
{
return line.startsWith("Start") || (line.length() > 0 && Character.isDigit(line.charAt(0)));
}
private static boolean isStackMapTable(final String line)
{
String trimmedLine = line.trim();
return trimmedLine.startsWith("frame_type") || trimmedLine.startsWith("offset_delta") || trimmedLine.startsWith("locals")
|| trimmedLine.startsWith("stack");
}
private static boolean couldBeMemberSignature(String line)
{
return line.endsWith(");") || line.contains(" throws ") && line.endsWith(S_SEMICOLON)
|| line.startsWith(S_BYTECODE_STATIC_INITIALISER_SIGNATURE);
}
private static void sectionFinished(String fqClassName, BytecodeSection lastSection, String memberSignature, StringBuilder builder,
MemberBytecode memberBytecode, ClassBC classBytecode)
{
if (DEBUG_LOGGING_BYTECODE)
{
logger.debug("sectionFinished: {}", lastSection);
}
if (lastSection == BytecodeSection.CODE)
{
List<BytecodeInstruction> instructions = parseInstructions(builder.toString());
if (memberBytecode != null)
{
memberBytecode.setInstructions(instructions);
classBytecode.putMemberBytecode(memberSignature, memberBytecode);
if (DEBUG_LOGGING_BYTECODE)
{
logger.debug("stored bytecode for : {}", memberSignature);
}
}
else
{
logger.error("No member for these instructions");
for (BytecodeInstruction instr : instructions)
{
logger.error("{}", instr);
}
}
}
else if (lastSection == BytecodeSection.LINETABLE)
{
storeLineNumberTable(fqClassName, memberBytecode, builder.toString(), memberSignature);
if (DEBUG_LOGGING_BYTECODE)
{
logger.debug("stored line number table for : {}", memberSignature);
}
}
builder.delete(0, builder.length());
}
private static BytecodeSection changeSection(BytecodeSection nextSection)
{
if (DEBUG_LOGGING_BYTECODE)
{
logger.debug("Changing section to: {}", nextSection);
}
return nextSection;
}
private static BytecodeSection getNextSection(final String line)
{
BytecodeSection nextSection = null;
if (line != null)
{
if (line.length() == 0)
{
nextSection = BytecodeSection.NONE;
}
for (Map.Entry<String, BytecodeSection> entry : sectionLabelMap.entrySet())
{
if (entry.getKey().startsWith(line.trim()))
{
nextSection = entry.getValue();
break;
}
}
}
return nextSection;
}
private static int getVersionPart(final String line)
{
int version = 0;
int colonPos = line.indexOf(C_COLON);
if (colonPos != -1 && colonPos != line.length() - 1)
{
String versionPart = line.substring(colonPos + 1).trim();
try
{
version = Integer.parseInt(versionPart);
}
catch (NumberFormatException nfe)
{
logger.error("Could not parse version part {}", versionPart, nfe);
}
}
return version;
}
private static String cleanBytecodeMemberSignature(final String signature)
{
if (DEBUG_LOGGING_BYTECODE)
{
logger.debug("cleanBytecodeMemberSignature: {}", signature);
}
String result = null;
if (signature != null)
{
if (signature.startsWith(S_BYTECODE_STATIC_INITIALISER_SIGNATURE))
{
result = S_BYTECODE_STATIC_INITIALISER_SIGNATURE;
}
else
{
// remove spaces between multiple method parameters
int openParentheses = signature.lastIndexOf(S_OPEN_PARENTHESES);
if (openParentheses != -1)
{
int closeParentheses = signature.indexOf(S_CLOSE_PARENTHESES, openParentheses);
if (closeParentheses != -1)
{
String params = signature.substring(openParentheses, closeParentheses);
params = params.replace(S_SPACE, S_EMPTY);
result = signature.substring(0, openParentheses) + params + S_CLOSE_PARENTHESES;
}
}
}
}
return result;
}
public static List<BytecodeInstruction> parseInstructions(final String bytecode)
{
List<BytecodeInstruction> bytecodeInstructions = new ArrayList<>();
if (DEBUG_LOGGING_BYTECODE)
{
logger.debug("Raw bytecode: '{}'", bytecode);
}
String[] lines = bytecode.split(S_NEWLINE);
boolean inSwitch = false;
BCParamSwitch table = new BCParamSwitch();
BytecodeInstruction instruction = null;
for (String line : lines)
{
line = line.trim();
if (DEBUG_LOGGING_BYTECODE)
{
logger.debug("parsing bytecode line: '{}' inSwitch: {}", line, inSwitch);
}
if (inSwitch)
{
if (S_CLOSE_BRACE.equals(line))
{
instruction.addParameter(table);
bytecodeInstructions.add(instruction);
inSwitch = false;
if (DEBUG_LOGGING_BYTECODE)
{
logger.debug("finished switch");
}
}
else
{
String[] parts = line.split(S_COLON);
if (parts.length == 2)
{
table.put(parts[0].trim(), parts[1].trim());
}
else
{
logger.error("Unexpected tableswitch entry: " + line);
}
}
}
else
{
try
{
Matcher matcher = PATTERN_BYTECODE_INSTRUCTION.matcher(line);
if (matcher.find())
{
instruction = new BytecodeInstruction();
String offset = matcher.group(1);
String mnemonic = matcher.group(2);
String paramString = matcher.group(3);
String comment = matcher.group(4);
instruction.setOffset(Integer.parseInt(offset));
instruction.setOpcode(Opcode.getOpcodeForMnemonic(mnemonic));
if (comment != null && comment.trim().length() > 0)
{
instruction.setComment(comment.trim());
}
if (instruction.getOpcode() == Opcode.TABLESWITCH || instruction.getOpcode() == Opcode.LOOKUPSWITCH)
{
if (DEBUG_LOGGING_BYTECODE)
{
logger.debug("Found a table or lookup switch");
}
inSwitch = true;
}
else
{
if (paramString != null && paramString.trim().length() > 0)
{
processParameters(paramString.trim(), instruction);
}
bytecodeInstructions.add(instruction);
}
}
else
{
logger.error("could not parse bytecode: '" + line + "'");
}
}
catch (Exception e)
{
logger.error("Error parsing bytecode line: '" + line + "'", e);
}
}
}
return bytecodeInstructions;
}
private static void storeLineNumberTable(String fqClassName, MemberBytecode memberBytecode, String tableLines, String memberSignature)
{
String[] lines = tableLines.split(S_NEWLINE);
for (String line : lines)
{
// strip off 'line '
line = line.trim().substring(5);
String[] parts = line.split(S_COLON);
if (parts.length == 2)
{
String source = parts[0].trim();
String offset = parts[1].trim();
try
{
MemberSignatureParts msp = MemberSignatureParts.fromBytecodeSignature(fqClassName, memberSignature);
LineTableEntry entry = new LineTableEntry(msp, Integer.parseInt(source), Integer.parseInt(offset));
memberBytecode.getLineTable().add(entry);
}
catch (NumberFormatException nfe)
{
logger.error("Could not parse line number {}", line, nfe);
}
}
else
{
logger.error("Could not split line: {}", line);
}
}
}
private static void processParameters(String paramString, BytecodeInstruction instruction)
{
String[] parts = paramString.split(S_COMMA);
for (String part : parts)
{
IBytecodeParam parameter;
part = part.trim();
if (part.charAt(0) == C_HASH)
{
parameter = new BCParamConstant(part);
}
else
{
try
{
int value = Integer.parseInt(part);
parameter = new BCParamNumeric(value);
}
catch (NumberFormatException nfe)
{
parameter = new BCParamString(part);
}
}
instruction.addParameter(parameter);
}
}
}