package xtc.lang.blink; import java.io.IOException; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.StringTokenizer; import java.util.regex.Matcher; import java.util.regex.Pattern; import xtc.lang.blink.CallStack.LocalVariable; import xtc.lang.blink.CallStack.NativeCallFrame; import xtc.lang.blink.CallStack.NativeCallFrame.NativeFrameType; import xtc.lang.blink.Event.DummyCallCompletionEvent; import xtc.lang.blink.Event.Java2NativeCallEvent; import xtc.lang.blink.Event.Java2NativeReturnEvent; import xtc.lang.blink.Event.Native2JavaCallEvent; import xtc.lang.blink.Event.J2CBreakPointHitEvent; import xtc.lang.blink.Event.Native2JavaCompletionEvent; import xtc.lang.blink.Event.Native2JavaReturnEvent; import xtc.lang.blink.Event.NativeBreakPointHitEvent; import xtc.lang.blink.Event.NativeJNIWarningEvent; import xtc.lang.blink.Event.NativeStepCompletionEvent; import xtc.lang.blink.Event.RawTextMessageEvent; import xtc.lang.blink.EventLoop.ReplyHandler; import xtc.lang.blink.SymbolMapper.SourceFileAndLine; import xtc.lang.blink.agent.AgentNativeDeclaration; /** Microsoft CDB driver as a native component debugger. */ public class NativeCDB extends StdIOProcess implements NativeDebugger, AgentNativeDeclaration { /** CDB user command prompt. */ private static final String PROMPT_PATTERN = "\\d+\\:\\d+\\> "; /** The pattern cache. */ private static final HashMap<String,Pattern> patternCache = new HashMap<String,Pattern>(); /** * Find or create regular expression pattern. * @param patternString The pattern string. * @return The regular expression pattern. */ private static final Pattern p(String patternString) { Pattern pattern = patternCache.get(patternString); if (pattern == null) { pattern = Pattern.compile(patternString); patternCache.put(patternString, pattern); } return pattern; } /** CDB internal event to notify that CDB is attached to the target JVM. */ private static class CDBAttachEvent extends Event { CDBAttachEvent(NativeCDB g) { super(g, EventConsumer.BlinkController); } public String getName() {return "CDBAttachEvent";} } /** CDB internal output message event. */ private static class CDBRawMessageEvent extends Event { private final String message; CDBRawMessageEvent(NativeCDB g, String message) { super(g, EventConsumer.BlinkController); this.message = message; } public String getMessage() {return message;} public String getName() {return "CDBRawMessageEvent";} public String toString() { return super.toString() + message; } } /** * Debug agent's internal breakpoint hit event. This internal event could be * interpreted as on one of the DebugContextSwitching events and Language * transition events. */ private static class AgentBreakPointHitEvent extends Event { private final int bpID; //CDB break point identifier. private final String message; //CDB's detailed message. AgentBreakPointHitEvent(NativeCDB g, int bpID, String message) { super(g, EventConsumer.NativerDebugger); this.bpID = bpID; this.message = message; } /** Getters. */ public int getBpID() {return bpID;} public String getMessage() {return message;} public String getName() {return "AgentInternalBreakPointHit";} } /** Internal step completion event. */ private static class InternalStepCompletionEvent extends Event { InternalStepCompletionEvent(NativeCDB g) { super(g, EventConsumer.NativerDebugger); } public String getName() {return "InternalStepCompletionEvent";} } /** The internal native breakpoint within Blink agent. */ private int cbpBreakPointId; /** Beginning of the Agent native shared library. */ private long agent_address_begin; /** Ending of the Agent native shared library. */ private long agent_address_end; /** The next break point identifier. */ private int nextBreakPointIdentifier; /** Whether or not the CDB is attached to the JVM debugee. */ private boolean cdbAttached = false; /** Whether or not this CDB driver activated xtc.lang.agent.Agent.dummyJava. */ private volatile boolean callDummyRequested = false; /** * Whether or not this CDB driver yielded to Java debugger by activating a * call to xtc.lang.agent.Agent.c2j(). */ private volatile boolean callNative2JavaRequested = false; /** * Whether or not the CDB source-level single-stepping was requested and is * being processed. */ private volatile boolean stepRequested = false; /** * Internal raw message buffer to capture interesting event such as break * point hit. See processMeaageEvent. This buffer chops CDB's raw output text * message by CDB's prompt. */ private final StringBuffer sbStdout = new StringBuffer(); /** * Constructor. * * @param dbg The Blink debugger. * @param name The user friendly name. */ public NativeCDB(Blink dbg, String name) { super(dbg, name); } /** * Process message for internal processing. * * @param e The event. */ void processMessageEvent(RawTextMessageEvent e) { sbStdout.append(e.getMessage()); Pattern p1 = Pattern.compile(PROMPT_PATTERN); Matcher m1 = p1.matcher(sbStdout); if (m1.find()) { boolean hasReminder = !Pattern.compile("(?:.*\n)*" + PROMPT_PATTERN + "$") .matcher(sbStdout).matches(); String[] frags = p1.split(sbStdout); for(int i = 0; i < frags.length-1;i++) { processCDBRawMessage(frags[i]); } String lastOne = frags.length > 0 ? frags[frags.length-1] :""; if (hasReminder) { sbStdout.setLength(0); sbStdout.append(lastOne); } else { processCDBRawMessage(lastOne); sbStdout.setLength(0); } } } /** * Process a CDB raw message. * * @param s The message. */ private void processCDBRawMessage(String s) { if (!cdbAttached) { dbg.enqueEvent(new CDBAttachEvent(this)); cdbAttached = true; return; } //now check any interesting event. Pattern breakpointHitPattern = p("(?:.*\n)*Breakpoint (\\d+) hit\n" + "((?:.*\n)*)"); Matcher breakpointHitMatcher = breakpointHitPattern.matcher(s); if (breakpointHitMatcher.find()) { int bpid = Integer.parseInt(breakpointHitMatcher.group(1)); String msg = breakpointHitMatcher.group(2); stepRequested = false; //check internal break point hit. if (bpid == cbpBreakPointId) { dbg.enqueEvent(new AgentBreakPointHitEvent(this, bpid, msg)); } else { // visible native break point. dbg.enqueEvent(new NativeBreakPointHitEvent(this, bpid, msg)); } return; } if (callDummyRequested || callNative2JavaRequested) { Pattern callReturnPattern = p(".call returns:\n" + "(.+)\\n" + "(?:.*\n)*"); Matcher callReturnMatcher = callReturnPattern.matcher(s); if (callReturnMatcher.find()) { assert callDummyRequested ^ callNative2JavaRequested; if (callDummyRequested) { callDummyRequested = false; dbg.enqueEvent(new DummyCallCompletionEvent(this)); } else if (callNative2JavaRequested) { callNative2JavaRequested = false; dbg.enqueEvent(new Native2JavaCompletionEvent(this)); } return; } } if (stepRequested) { dbg.enqueEvent(new InternalStepCompletionEvent(this)); stepRequested = false; return; } dbg.enqueEvent(new CDBRawMessageEvent(this, s)); } /** * Process an event in the main thread. * * @param e The event. */ public void dispatch(Event e) { if (e instanceof AgentBreakPointHitEvent) { dispatch((AgentBreakPointHitEvent)e); } else if (e instanceof InternalStepCompletionEvent) { try { String eipString = raeLine("r eip\n", "eip=([0-9a-f]+)")[1]; long eip = Long.parseLong(eipString, 16); assert agent_address_begin != 0L && agent_address_end != 0L; if (isInAgentLibrary(eip)) { sendMessage("G\n"); } else { dbg.enqueEvent(new NativeStepCompletionEvent(this)); } } catch(IOException ioe) { dbg.err("can not correctly handle step completion.\n"); } } } /** * Process an internal agent breakpoint hit event in the event handler * thread. * * @param e The event. */ private void dispatch(AgentBreakPointHitEvent e) { try { if (e.getBpID() == cbpBreakPointId) { String bptype = readEnum(BDA_CBP_BPTYPE); if (bptype.equals(BDA_BPTYPE_J2C_DEBUGGER)) { dbg.enqueEvent(new J2CBreakPointHitEvent(this)); } else if (bptype.equals(BDA_BPTYPE_J2C_JNI_CALL)) { long native_target_address = readAddressValue(BDA_CBP_TARGET_NATIVE_ADDRESS); advance(native_target_address); rae("t\n"); dbg.enqueEvent(new Java2NativeCallEvent(this)); } else if (bptype.equals(BDA_BPTYPE_J2C_JNI_RETURN)) { String cname = readStringValue(BDA_CBP_TARGET_CNAME); int lineNumber = readIntValue(BDA_CBP_TARGET_LINE_NUMBER); dbg.enqueEvent(new Java2NativeReturnEvent(this, cname, lineNumber)); } else if (bptype.equals(BDA_BPTYPE_C2J_JNI_CALL)) { String cname = readStringValue(BDA_CBP_TARGET_CNAME); int lineNumber = readIntValue(BDA_CBP_TARGET_LINE_NUMBER); dbg.enqueEvent(new Native2JavaCallEvent(this, cname, lineNumber)); } else if (bptype.equals(BDA_BPTYPE_C2J_JNI_RETURN)) { rae("gu\n"); // bda_cbp -> jni_state_c2j_return rae("gu\n"); // jni_state_c2j_return -> c2j_proxyCallXXXMethod rae("gu\n"); // caller of the (*env)->CallXXXMethod dbg.enqueEvent(new Native2JavaReturnEvent(this)); } else if (bptype.equals(BDA_BPTYPE_JNI_WARNING)) { String message = readStringValue(BDA_CBP_STATE_MESSAGE); dbg.enqueEvent(new NativeJNIWarningEvent(this, message)); } else { assert false : "can not recognize an internal break point"; } } else { assert false : "can not recognize an internal break point"; } } catch(IOException ioe) { dbg.err("can not correctly handle internal break point."); } } /** * Read a string typed value. * @param name The variable name. * @return The string value. */ private String readStringValue(String name) throws IOException { String s = raeLine("?? " + name + "\n", " \"(.+)\"")[1]; return s; } /** * Read an integer typed value. * @param name The variable name. * @return The integer value. */ private int readIntValue(String name) throws IOException { String s = raeLine("?? " + name + "\n", "int ([0-9]+)")[1]; return Integer.parseInt(s); } /** * Read an address typed value. * @param name The variable name. * @return The address. */ private long readAddressValue(String name) throws IOException { String s = raeLine("?? " + name + "\n", "^.+0x([0-9a-f]+)$")[1]; return Long.parseLong(s, 16); } /** * Read an enumeration typed value. * @param name The variable name. * @return The enumeration name. */ private String readEnum(String name) throws IOException { return raeLine("?? " + name + "\n", "^.+ (.+) .*$")[1]; } /** * Attach CDB to the debugee JVM. * @param pid The process id of the debbugee. */ public void attach(int pid) throws IOException { final String[] cdbCommandArray = new String[] { "cdb", "-lines", "-G", "-pid", String.valueOf(pid), }; begin(cdbCommandArray); EventLoop.subLoop(dbg, new ReplyHandler() { public boolean dispatch(Event e) { if (e instanceof CDBAttachEvent) { return true; } else { return false; } } }); if (dbg.options.getVerboseLevel() >= 1) { dbg.out("cdb is initialized.\n"); } cbpBreakPointId = createSymbolBreakPoint(BDA_CBP); nextBreakPointIdentifier = cbpBreakPointId + 1; Pattern p = Pattern.compile("([0-9a-f]+) ([0-9a-f]+) +([^ ]+) +.+"); String sharedLibries = rae("lm\n"); for (StringTokenizer t = new StringTokenizer(sharedLibries, "\n"); t .hasMoreElements();) { Matcher m = p.matcher(t.nextToken()); if (m.matches()) { String name = m.group(3); if (name.equals("jdwp")) { String begin = m.group(1); String end = m.group(2); setVariable(BDA_JDWP_REGION_BEGIN_VARIABLE, begin); setVariable(BDA_JDWP_REGION_END_VARIABLE, end); } else if (name.equals(BDA_SHARED_LIBRARY_NAME)) { String begin = m.group(1); String end = m.group(2); agent_address_begin = Long.parseLong(begin, 16); agent_address_end = Long.parseLong(end, 16); } } } sendMessage("G\n"); } /** Detach and terminate the CDB. */ public void detach() throws IOException { sendMessage(".detach\nQ\n"); } /** Active a native-to-Java transition. */ public void callNative2Java() throws IOException { String callCommand = ".call " + BDA_C2J +"()\n"; rae(callCommand); callNative2JavaRequested = true; sendMessage("G\n"); } /** Continue the debugee JVM. */ public void cont() throws IOException { sendMessage("G\n"); } /** Abruptly termindate the debugee JVM. */ public void quit() throws IOException { sendMessage("Q\n"); } /** * Run a CDB command and return the output message. This is for * internal-debugging purpose. * * @param command The CDB command. */ public String runCommand(String command) throws IOException { return rae(command + "\n"); } /** * Create a native breakpoint, and return the breaakpoint identifier. * * @param sourceFile The source file. * @param line The line number. * @return The breakpoint identifier. */ public int createBreakpoint(String sourceFile, int line) throws IOException { int bpid = nextBreakPointID(); String cmd ="bp" + bpid + " `" + sourceFile + ":" + line +"`\n"; String rst = rae(cmd); if (rst != null && rst.length() > 0) { dbg.out(rst); // perhaps some problem. } return bpid; } public int createBreakpoint(String symbol) throws IOException { int bpid = nextBreakPointID(); String cmd ="bp" + bpid + " " + symbol + "\n"; String rst = rae(cmd); if (rst != null && rst.length() > 0) { dbg.out(rst); // perhaps some problem. } return bpid; } /** * Create a native breakpiont, and return the breakpoint identifier. * * @param symbol The symbol name. * @return The breakpoint identifier. */ private int createSymbolBreakPoint(String symbol) throws IOException { String[] rsts = raeLine( "bm " + symbol + "\n", "^\\s*(\\d+)\\: \\p{XDigit}+ \\@\\!\"(.+)\"$" ); int bpid = Integer.valueOf(rsts[1]); return bpid; } /** * Get a new breakpiont identifier. * * @return The breakpint identifier. */ private int nextBreakPointID() { return nextBreakPointIdentifier++; } /** * Advance the program counter to a specified address. * * @param address The target native address. */ private void advance(long address) throws IOException { final int bpid = nextBreakPointID(); rae("bp" + bpid + " /1 " + Long.toHexString(address) + "\n"); sendMessage("g\n"); EventLoop.subLoop(dbg, new ReplyHandler() { public boolean dispatch(Event e) { if (e instanceof NativeBreakPointHitEvent) { NativeBreakPointHitEvent ne = (NativeBreakPointHitEvent)e; if (bpid == ne.getDebuggerBreakpointID()) { return true; } } return false; } }); } /** * Enable a breakpoint. * * @param bpid The breakpoint identifier. */ public void enableBreakpoint(int bpid) throws IOException { rae("be " + bpid + "\n"); } /** * Disable a breakpoint. * * @param bpid The breakpoint identifier. */ public void disableBreakpoint(int bpid) throws IOException { rae("bd " + bpid + "\n"); } /** * Delete a breakpoint. * @param bpid The breakpoint identifier. */ public void deleteBreakpoint(int bpid) throws IOException { assert false : "Not implemented"; } public String getJNIEnv() throws IOException { rae(".call " + BDA_ENSURE_JNIENV + "()\n"); String jnienv = raeLine("g\n", "struct JNINativeInterface_ \\*\\* (0x\\p{XDigit}+)")[1]; return jnienv; } /** Followings are for inspecting calling context and program state.*/ public List<NativeCallFrame> getFrames() throws IOException { // extract frames String framesText = rae("kn\n"); LinkedList<NativeCallFrame> frames = new LinkedList<NativeCallFrame>(); for (StringTokenizer t = new StringTokenizer(framesText, "\n"); t .hasMoreElements();) { String l = t.nextToken(); Pattern p = p("^(\\p{XDigit}+) (\\p{XDigit}+) (\\p{XDigit}+) (\\S+)(?: (\\[.+\\]))?"); Matcher m = p.matcher(l); if (m.matches()) { int frameID = Integer.valueOf(m.group(1), 16); String position = m.group(4); String sourceInfo = m.group(5); //parse position String functionName; NativeFrameType frameType = NativeFrameType.NORMAL; Pattern posPattern = p("(.+)\\!(.+)(?:\\+0x(\\p{XDigit}+))?|(.+)\\+0x(\\p{XDigit}+)|0x(\\p{XDigit}+)"); Matcher posMatcher = posPattern.matcher(position); boolean matched = posMatcher.matches(); assert matched == true : "can not parse position: " + position + "\n"; if (posMatcher.group(1) != null) { String moduleName = posMatcher.group(1); String funcName = posMatcher.group(2); functionName = funcName; if (moduleName.equals(BDA_SHARED_LIBRARY_NAME)) { if (funcName.startsWith("bda_j2c_proxy")) { frameType = NativeFrameType.J2C_PROXY; } else if (funcName.startsWith("bda_c2j_proxy")) { frameType = NativeFrameType.C2J_PROXY; } } } else if (posMatcher.group(4) != null) { String moduleName = posMatcher.group(4); int moduleOffset = Integer.parseInt(posMatcher.group(5), 16); functionName = moduleName + "+0x" + Integer.toHexString(moduleOffset); } else { //raw address assert posMatcher.group(6) != null; functionName = "0x" + posMatcher.group(6); } //parse sourceInfo String sourceFile = null; int lineno = 1; if (sourceInfo!= null) { Pattern frameSourceInfoPattern = p("\\[(.+) \\@ ([0-9]+)\\]"); Matcher frameSourceInfoMatcher = frameSourceInfoPattern.matcher(sourceInfo); if (frameSourceInfoMatcher.matches()) { sourceFile = frameSourceInfoMatcher.group(1); lineno = Integer.parseInt(frameSourceInfoMatcher.group(2)); } } NativeCallFrame frame = new NativeCallFrame(frameID, functionName, sourceFile, lineno, frameType); frames.addLast(frame); } else if (l.matches("^WARNING: .+$")) { if (dbg.options.getVerboseLevel() >= 1) { dbg.err("ignoring CDB output: " + l + "\n"); } } else if (l.matches("\\s+#\\s+ChildEBP\\s+RetAddr\\s*")) { //skip header. } else { assert false :"can not recognize CDB output: " + l; } } return frames; } public SourceFileAndLine getCurrentLocation() throws IOException { SourceFileAndLine loc = null; String frames = rae("kn1\n"); Pattern p = Pattern.compile("0+ [0-9a-f]+ [0-9a-f]+ .+ \\[(.+) @ ([0-9]+)\\]"); StringTokenizer t = new StringTokenizer(frames, "\n"); while(t.hasMoreTokens()) { String s = t.nextToken(); Matcher m = p.matcher(s); if (m.matches()) { String file = m.group(1); int line = Integer.parseInt(m.group(2)); loc = new SourceFileAndLine(file, line); } } return loc; } public String getSourceLines(String filename, int line, int count) throws IOException { assert filename != null && line >=0 && count >=0; rae("lsf " + filename + "\n"); String lines = rae("ls " + line + ", " + count + "\n"); return lines; } public List<LocalVariable> getLocals(NativeCallFrame f) throws IOException { LinkedList<LocalVariable> localList = new LinkedList<LocalVariable>(); rae(".frame " + f.getFrameID() + "\n"); String localsText = rae("dv\n"); Pattern localVariablePattern = Pattern.compile("^\\s*(.+) = (.+)$"); for(StringTokenizer t = new StringTokenizer(localsText, "\n"); t.hasMoreElements();) { String l = t.nextToken(); Matcher m = localVariablePattern.matcher(l); if (m.matches()) { String name = m.group(1); String value = m.group(2); LocalVariable v = new LocalVariable(name, value); localList.addLast(v); } else assert false : "CDB: can not recognize: " + l + "\n"; } return localList; } /* Followings are for inter-language stepping. */ public void step() throws IOException { rae("l+t\n"); stepRequested = true; sendMessage("t\n"); } public void next() throws IOException { rae("l+t\n"); stepRequested = true; sendMessage("p\n"); } public int getLanguageTransitionCount() throws IOException { rae(".call " + BDA_GET_CURRENT_TRANSITION_COUNT + "()\n"); String countString = raeLine("g\n", "int (\\d+)")[1]; return Integer.parseInt(countString); } private static String getBreakpointControlVariable(LanguageTransitionEventType bptype) { switch (bptype) { case J2C_CALL: return BDA_J2C_CALL_BREAKPOINT_VARIABLE; case J2C_RETURN: return BDA_J2C_RETURN_BREAKPOINT_VARIABLE; case C2J_CALL: return BDA_C2J_CALL_BREAKPOINT_VARIABLE; case C2J_RETURN: return BDA_C2J_RETURN_BREAKPOINT_VARIABLE; default: assert false : "not reachable"; return ""; } } public void setTransitionBreakPoint(LanguageTransitionEventType bptype, int transitionCount) throws IOException { String controlVariable = getBreakpointControlVariable(bptype); rae("ed " + controlVariable + " 1\n"); rae("ed " + BDA_TRANSITION_COUNT + " " + transitionCount +"\n"); } public void clearTransitionBreakPoint(LanguageTransitionEventType bptype) throws IOException { String controlVariable = getBreakpointControlVariable(bptype); rae("ed " + controlVariable + " 0\n"); } public void callJavaDummy() throws IOException { rae(".call " + BDA_DUMMY_JAVA + "()" + "\n"); callDummyRequested = true; sendMessage("g\n"); } private boolean isInAgentLibrary(long addr) { return agent_address_begin <= addr && addr < agent_address_end; } /** Following are for expression evaluation. */ public String eval(NativeCallFrame f, String expr) throws IOException { assert false : "Not implemented"; return null; } public void setVariable(NativeCallFrame f, String name, String expr) throws IOException { assert false : "Not implemented"; } public String whatis(NativeCallFrame f, String expr) throws IOException { assert false : "Not implemented"; return null; } private void setVariable(String name, String value) throws IOException { rae("ed " + name + " " + value +"\n"); } /** * Run a CDB command, and get text message from the CDB. * * @param cmd The command. * @return The result message. */ private String rae(final String cmd) throws IOException { sendMessage(cmd); return (String) EventLoop.subLoop(dbg, new ReplyHandler() { public boolean dispatch(Event e) { if (e instanceof CDBRawMessageEvent) { CDBRawMessageEvent ge =(CDBRawMessageEvent)e; setResult(ge.getMessage()); return true; } return false; } }); } /** * Run a CDB command and search for a expected line until the CDB prompt. At * the end, return an array of Strings from the matching regular expression. * If multiple CDB output lines match the expected message, take the last one. * * @param cmd The command. * @param expect The expected line. * @return The matcher object. */ private String[] raeLine(final String cmd, final String expect) throws IOException { final Pattern p = Pattern.compile(expect); sendMessage(cmd); return (String[]) EventLoop.subLoop(dbg, new ReplyHandler() { public boolean dispatch(Event e) { if (e instanceof CDBRawMessageEvent) { CDBRawMessageEvent ge =(CDBRawMessageEvent)e; String[] lines = ge.getMessage().split("\n"); for(String l :lines) { Matcher m = p.matcher(l); if (m.matches()) { String[] frags = new String[m.groupCount()+1]; for(int i = 0; i <= m.groupCount();i++) { frags[i] = m.group(i); } setResult(frags); return true; } } setResult(null); return true; } return false; } }); } }