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.Native2JavaCompletionEvent; import xtc.lang.blink.Event.EventConsumer; import xtc.lang.blink.Event.J2CBreakPointHitEvent; import xtc.lang.blink.Event.Native2JavaReturnEvent; import xtc.lang.blink.Event.NativeJNIWarningEvent; import xtc.lang.blink.Event.RawTextMessageEvent; import xtc.lang.blink.Event.NativeBreakPointHitEvent; import xtc.lang.blink.Event.NativeStepCompletionEvent; import xtc.lang.blink.EventLoop.ReplyHandler; import xtc.lang.blink.SymbolMapper.SourceFileAndLine; import xtc.lang.blink.agent.AgentNativeDeclaration; /** * GNU GDB implementation as the native debugger. */ public class NativeGDB extends StdIOProcess implements NativeDebugger, AgentNativeDeclaration { private static class GDBAttachEvent extends Event { GDBAttachEvent(NativeGDB g) { super(g, EventConsumer.BlinkController); } public String getName() {return "GDBAttached";} } /** * Debug agent's internal break point hit event. This internal event could be * interpreted as on one of the DebugContextSwitching events and Language * transition events. */ private static class BDA_CBP_BreakpointHitEvent extends Event { private final int bpID; // CDB break point identifier. private final String message; // CDB's detailed message. BDA_CBP_BreakpointHitEvent(NativeGDB g, int bpID, String message) { super(g, EventConsumer.NativerDebugger); this.bpID = bpID; this.message = message; } public int getBpID() {return bpID;} public String getMessage() {return message;} public String getName() {return "AgentInternalBreakPointHit";} } private static class InternalStepCompletionEvent extends Event { InternalStepCompletionEvent(NativeGDB g) { super(g, EventConsumer.NativerDebugger); } public String getName() { return "InternalStepCompletionEvent"; } } private static class GDBRawMessageEvent extends Event { private final String message; GDBRawMessageEvent(NativeGDB g, String message) { super(g, EventConsumer.BlinkController); this.message = message; } public String getMessage() {return message;} public String getName() {return "GDBRawMessageEvent";} public String toString() {return super.toString() + message;} } /** GDB prompt. */ private static final String PROMPT = "(gdb) "; /** The pattern cache. */ private static final HashMap<String,Pattern> patternCache = new HashMap<String,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; } private final StringBuffer sbStdout = new StringBuffer(); /** A primitive break point inside the Blink debugging agent. */ private int cbpBreakPointId; private long agent_address_begin; private long agent_address_end; private boolean gdbAttached = false; private volatile boolean native2JavaCallRequested = false; private volatile boolean callJavaDummyRequested = false; private volatile boolean steppingRequested = false; /** * @param dbg The Blink debugger. * @param name The user friendly name. */ public NativeGDB(Blink dbg, String name) { super(dbg, name); } /** * Attach the GDB to the debugee JVM process. * * @param pid The process identifier. */ public void attach(int pid) throws IOException { final String[] gdbCommandArray = new String[] { "gdb", "-nw", "-quiet", "--pid", Integer.toString(pid), "-ex", "echo blinkgdbready\\n", }; begin(gdbCommandArray); EventLoop.subLoop(dbg, new ReplyHandler() { public boolean dispatch(Event e) { if (e instanceof GDBAttachEvent) { return true; } else { return false; } } }); if (dbg.options.getVerboseLevel() >= 1) { dbg.out("gdb is initialized.\n"); } // do gdb initialization. raeGDB("set language c\n"); raeGDB("set width 0\n"); cbpBreakPointId = createSymbolBreakPoint(BDA_CBP); // the JVM uses this signal, so do not stop. raeGDB("handle SIGSEGV nostop \n"); String sharedLibries = raeGDB("info shar\n"); Pattern p = Pattern.compile("^0x([0-9a-f]+) +0x([0-9a-f]+) +\\S(.+)$"); 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.endsWith("libjdwp.so")||name.endsWith("jdwp.dll")) { String begin = m.group(1); String end = m.group(2); setVariable(BDA_JDWP_REGION_BEGIN_VARIABLE, "0x"+begin); setVariable(BDA_JDWP_REGION_END_VARIABLE, "0x"+end); } else if (name.endsWith("lib" + BDA_SHARED_LIBRARY_NAME + ".so") ||name.endsWith(BDA_SHARED_LIBRARY_NAME + ".dll")) { String begin = m.group(1); String end = m.group(2); agent_address_begin = Long.parseLong(begin, 16); agent_address_end = Long.parseLong(end, 16); } } } assert agent_address_begin > 0 && (agent_address_begin < agent_address_end); sendMessage("continue\n"); } public void callNative2Java() throws IOException { native2JavaCallRequested = true; sendMessage("call " + BDA_C2J + "()\n"); } public void step() throws IOException { steppingRequested = true; sendMessage("step\n"); } public void next() throws IOException { steppingRequested = true; sendMessage("next\n"); } /** trigger detach sequence. */ public void detach() throws IOException { sendMessage("detach\nquit\n"); } public void quit() throws IOException { sendMessage("kill\nquit\n"); } /** Continue the debuggee's execution. */ public void cont() throws IOException { sendMessage("continue\n"); } public String getJNIEnv() throws IOException { Matcher m = raeGDB("print/x " + BDA_ENSURE_JNIENV + "()\n", "\\$\\d+ = (0x[0-9a-f]+)\\n"); return m.group(1); } public String eval(NativeCallFrame f, String expr) throws IOException { raeGDB("frame " + f.getFrameID() + "\n"); Matcher m = raeGDB("print " + expr + "\n", "\\$\\d+ = (.+)\\n"); String rst = m.group(1); return rst; } public void callJavaDummy() throws IOException { callJavaDummyRequested = true; sendMessage("call " + BDA_DUMMY_JAVA + "()" + "\n"); } private void setVariable(String name, String value) throws IOException { raeGDB("set " + name + " = " + value + "\n"); } public void setVariable(NativeCallFrame f, String name, String value) throws IOException { raeGDB("frame " + f.getFrameID() + "\n"); raeGDB("set " + name + " = " + value + "\n"); } public int createBreakpoint(String sourceFile, int line) throws IOException { Matcher m = raeGDB("break " + sourceFile + ":" + line + "\n", "Breakpoint ([0-9]+) .*\\n"); int bpid = Integer.valueOf(m.group(1)); return bpid; } public int createBreakpoint(String symbol) throws IOException { Matcher m = raeGDB("break " + symbol + "\n", "Breakpoint ([0-9]+) .*\\n"); int bpid = Integer.valueOf(m.group(1)); return bpid; } public int createRawAddressBreakPoint(String address) throws IOException { Matcher m = raeGDB("break *" + address + "\n", "Breakpoint ([0-9]+) .*\\n"); int bpid = Integer.valueOf(m.group(1)); return bpid; } private int createSymbolBreakPoint(String symbol) throws IOException { Matcher m = raeGDB("break " + symbol + "\n", "(?:.*\n)*Breakpoint (\\d+) at.*\\n"); int bpid = Integer.valueOf(m.group(1)); return bpid; } public void enableBreakpoint(int bpid) throws IOException { raeGDB("enable " + bpid + "\n"); } public void disableBreakpoint(int bpid) throws IOException { raeGDB("disable " + bpid + "\n"); } public void deleteBreakpoint(int bpid) throws IOException { raeGDB("delete " + bpid + "\n"); } 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 int getLanguageTransitionCount() throws IOException { String s = raeGDB("call " + BDA_GET_CURRENT_TRANSITION_COUNT + "()\n", "\\$\\d+ = (\\d+)\n" + "(?:.*\n)*").group(1); return Integer.parseInt(s); } public void setTransitionBreakPoint(LanguageTransitionEventType bptype, int transitionCount) throws IOException { String controlVariable = getBreakpointControlVariable(bptype); raeGDB("set " + controlVariable + " = 1\n"); raeGDB("set " + BDA_TRANSITION_COUNT + " = " + transitionCount +"\n"); } public void clearTransitionBreakPoint(LanguageTransitionEventType bptype) throws IOException { String controlVariable = getBreakpointControlVariable(bptype); raeGDB("set " + controlVariable + " = 0\n"); } public String whatis(NativeCallFrame f, String expr) throws IOException { raeGDB("frame " + f.getFrameID() + "\n"); String type = raeGDB("whatis " + expr + "\n", ".+ = (.+)\\n").group(1); return type; } public void dispatch(Event e) { assert e.consumer == EventConsumer.NativerDebugger; if (e instanceof BDA_CBP_BreakpointHitEvent) { dispatch((BDA_CBP_BreakpointHitEvent) e); } else if (e instanceof InternalStepCompletionEvent) { try { String pc_str = raeGDB("p/x $pc\n", "\\s*\\$\\d+ = 0x([0-9a-f]+)\n").group(1); long pc = Long.parseLong(pc_str, 16); if (isInAgentLibrary(pc)) { sendMessage("continue\n"); } else { dbg.enqueEvent(new NativeStepCompletionEvent(this)); } } catch(IOException ioe) { dbg.err("can not correctly handle step completion."); } } } private boolean isInAgentLibrary(long addr) { return agent_address_begin <= addr && addr < agent_address_end; } private String readEnum(String name) throws IOException { return raeGDB("print " +name + "\n", "\\s*\\$\\d+ = (.+)\n").group(1); } private String readAddressValue(String name) throws IOException { return raeGDB( "print " + name + "\n", "\\s*\\$\\d+ = .*(0x[0-9a-f]+)\n").group(1); } private String readStringValue(String name) throws IOException { return raeGDB("print " + name + "\n", "\\s*\\$\\d+ = 0x.+ \"(.+)\"\n").group(1); } private int readIntValue(String name) throws IOException { String value = raeGDB("print " + name + "\n", "\\s*\\$\\d+ = (\\d+)\n").group(1); return Integer.parseInt(value); } /** process internal event in the event handler thread. */ private void dispatch(BDA_CBP_BreakpointHitEvent 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)) { String native_target_address = readAddressValue(BDA_CBP_TARGET_NATIVE_ADDRESS); // move the control to the prologue of the native method. raeGDB("finish\n"); //bda_cbp -> jni_state_j2c_call raeGDB("finish\n"); //jni_state_j2c_call -> j2c_proxy_xxx raeGDB("advance *" + native_target_address + "\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)) { raeGDB("finish\n"); // bda_cbp -> jni_state_c2j_return raeGDB("finish\n"); // jni_state_c2j_return -> c2j_proxyCallXXXMethod raeGDB("finish\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."); } } /** * Internally process the raw message to generate macro event. * * @param e The raw text message event. */ protected void processMessageEvent(RawTextMessageEvent e) { sbStdout.append(e.getMessage()); int begin = 0; int match = sbStdout.indexOf(PROMPT); if (match == -1) { return; } while (match != -1) { assert begin <= match; String s = sbStdout.substring(begin, match); processMessage(s); begin = match + PROMPT.length(); match = sbStdout.indexOf(PROMPT, begin); } int bufLen = sbStdout.length(); if (begin < bufLen) { String remainder = sbStdout.substring(begin, bufLen - 1); sbStdout.setLength(0); sbStdout.append(remainder); } else { sbStdout.setLength(0); } } private void processMessage(String msg) { if (!gdbAttached) { gdbAttached = true; dbg.enqueEvent(new GDBAttachEvent(this)); } if (!checkBreakPointHitPattern(msg) && !checkCallCompletionPattern(msg) && !checkStepCompletion(msg)) { dbg.enqueEvent(new GDBRawMessageEvent(this, msg)); } Pattern exitPattern = p("Program exited normally.\\n"); Matcher exitMatcher = exitPattern.matcher(msg); if (exitMatcher.find()) { try { sendMessage("quit\n"); } catch (IOException ioe) { } } } private boolean checkBreakPointHitPattern(String msg) { Pattern p = p("(?:.*\n)*" + "Breakpoint (\\d+), (.*\n(?:.*\\n)*)"); Matcher mgdbbp = p.matcher(msg); if (mgdbbp.matches()) { int bpid = Integer.valueOf(mgdbbp.group(1)); String message = mgdbbp.group(2); steppingRequested = false; if (bpid ==cbpBreakPointId) { dbg.enqueEvent(new BDA_CBP_BreakpointHitEvent(this, bpid, message)); } else { dbg.enqueEvent(new NativeBreakPointHitEvent(this, bpid, message)); } return true; } return false; } private boolean checkCallCompletionPattern(String msg) { if (!native2JavaCallRequested && !callJavaDummyRequested) { return false; } Pattern p = p("(?:.*\n)*" + "\\$\\d+ = \\d+\n" + "(?:.*\n)*"); Matcher m = p.matcher(msg); if (!m.matches()) { return false; } assert native2JavaCallRequested ^ callJavaDummyRequested; if (native2JavaCallRequested) { native2JavaCallRequested = false; dbg.enqueEvent(new Native2JavaCompletionEvent(this)); } else { assert callJavaDummyRequested; callJavaDummyRequested = false; dbg.enqueEvent(new DummyCallCompletionEvent(this)); } return true; } private boolean checkStepCompletion(String msg) { if (!steppingRequested) { return false; } dbg.enqueEvent(new InternalStepCompletionEvent(this)); steppingRequested = false; return true; } /** * Let the Blink debug to talk to gdb and obtain native stack frame list. * Then, parse the output messages and constuct a list of native stack frames. * * @return The list of native stack frames. */ public LinkedList<NativeCallFrame> getFrames() throws IOException { String output = raeGDB("where\n"); Pattern frameLinePattern = Pattern.compile("^#([0-9]+)" + "\\s+" // id + "(0x[a-f0-9]+)?" + "\\s+" // address + "(in\\s+(\\S+)|(\\S+))" + "\\s+" // function name + "\\(.*\\)" + "\\s*" // argument --> ignore + "(at (.+):([0-9]+))?" // source file and line + "(from \\S+)?" + "$"); LinkedList<NativeCallFrame> frames = new LinkedList<NativeCallFrame>(); for (final String line : output.split("\n")) { Matcher m = frameLinePattern.matcher(line); if (!m.find()) { continue; } int id = Integer.valueOf(m.group(1)); String func = m.group(4) != null ? m.group(4) : m.group(5); String srcFile = m.group(7); int lineno = m.group(8) == null ? -1 : Integer.parseInt(m.group(8)); NativeFrameType frameType; if (func.startsWith("bda_j2c_proxy")) { frameType = NativeFrameType.J2C_PROXY; } else if (func.startsWith("bda_c2j_proxy")) { frameType = NativeFrameType.C2J_PROXY; } else { frameType = NativeFrameType.NORMAL; } NativeCallFrame frame = new NativeCallFrame(id, func, srcFile, lineno, frameType); frames.addLast(frame); } return frames; } public SourceFileAndLine getCurrentLocation() throws IOException { raeGDB("frame 0\n"); String frameText = raeGDB("bt 1\n"); String firstLine = new StringTokenizer(frameText, "\n").nextToken(); Pattern frameLineWithSourceFileAndLinePattern = Pattern .compile("^#0.+at (.+):([0-9]+).*$"); // source file and line Matcher m = frameLineWithSourceFileAndLinePattern.matcher(firstLine); SourceFileAndLine loc = null; if (m.matches()) { String sourceFile = m.group(1); int lineNumber = Integer.parseInt(m.group(2)); loc = new SourceFileAndLine(sourceFile, lineNumber); } return loc; } public List<LocalVariable> getLocals(NativeCallFrame f) throws IOException { LinkedList<LocalVariable> localList = new LinkedList<LocalVariable>(); raeGDB("frame " + f.getFrameID() + "\n"); String args = raeGDB("info args\n"); String locals = raeGDB("info locals\n"); String argsAndLocals = args.concat(locals); Pattern p = Pattern.compile("^(.+) = (.+)$"); for (final String line : argsAndLocals.split("\n")) { Matcher m = p.matcher(line); if (m.matches()) { String name = m.group(1); String value = m.group(2); localList.add(new LocalVariable(name, value)); } else if (line.equals("No locals.")) { continue; // skip } else { dbg.err("can not recognize this GDB output: " + line + "\n"); } } return localList; } public String getSourceLines(String filename, int line, int count) throws IOException { raeGDB("set listsize " + count + "\n"); return raeGDB("list " + filename + ":" + line + "\n"); } public String runCommand(String command) throws IOException { return raeGDB(command + "\n"); } /** * Send a message to the gdb process, and wait until the gdb sends back an * expected message. Here, the expected message is a regular expression, and * its corresponding matcher obejct will be returned. * * @param msg The message to send. * @param expect The message to be expected from the gdb. * @return The matched string for the regular expression. */ private Matcher raeGDB(final String msg, String expect) throws IOException { final Pattern p = Pattern.compile(expect); sendMessage(msg); return (Matcher) EventLoop.subLoop(dbg, new ReplyHandler() { public boolean dispatch(Event e) { if (e instanceof GDBRawMessageEvent) { GDBRawMessageEvent ge = (GDBRawMessageEvent) e; Matcher tm = p.matcher(ge.getMessage()); if (tm.matches()) { setResult(tm); return true; } } return false; } }); } /** * Issue a gdb command, and get text message from the gdb. * @param cmd The command. * @return The result message. */ private String raeGDB(final String cmd) throws IOException { sendMessage(cmd); String lines = (String) EventLoop.subLoop(dbg, new ReplyHandler() { public boolean dispatch(Event e) { if (e instanceof GDBRawMessageEvent) { setResult(((GDBRawMessageEvent) e).getMessage()); return true; } else { return false; } } }); return lines; } }