diff --git a/compiler/compiler/src/main/java/org/robovm/compiler/plugin/debug/DebugInformationPlugin.java b/compiler/compiler/src/main/java/org/robovm/compiler/plugin/debug/DebugInformationPlugin.java index 38d367ed8..5e72d3ebd 100644 --- a/compiler/compiler/src/main/java/org/robovm/compiler/plugin/debug/DebugInformationPlugin.java +++ b/compiler/compiler/src/main/java/org/robovm/compiler/plugin/debug/DebugInformationPlugin.java @@ -23,7 +23,7 @@ import org.robovm.compiler.config.Config; import org.robovm.compiler.config.OS; import org.robovm.compiler.llvm.Alloca; -import org.robovm.compiler.llvm.ArrayType; +import org.robovm.compiler.llvm.ArrayConstantBuilder; import org.robovm.compiler.llvm.BasicBlock; import org.robovm.compiler.llvm.Call; import org.robovm.compiler.llvm.Constant; @@ -337,11 +337,29 @@ public void afterMethod(Config config, Clazz clazz, SootMethod method, ModuleBui } } + if (hookInstructionLines.isEmpty()) { + // there was no hook instructions identified, no need to bpTable, debug info and locals information + return; + } + // instrument hooks call, there is known line range, create global for method breakpoints int arraySize = ((methodEndLineNumber - methodLineNumber + 1) + 7) / 8; - // global value to this array (without values as zeroinit) - Global bpTable = new Global(Symbols.bptableSymbol(method), Linkage.internal, - new ZeroInitializer(new ArrayType(arraySize, Type.I8))); + // use same data to tell debugger what lines are available for break points: + // idea is to set all not available (not instrumented) line with bit set to 1 + // these bits will not be used for hooks as there is no corresponding injection + // but debugger will be able to read it and respond to JDWP + byte[] bpTableValue = new byte[arraySize]; + // set all lines as breakpoint armed + Arrays.fill(bpTableValue, (byte)0xff); + for (Integer line : hookInstructionLines.keySet()) { + // reset armed for valid lines + int lineOffset = line - methodLineNumber; + int idx = lineOffset >> 3; + int mask = ~(1 << (lineOffset & 7)); + bpTableValue[idx] &= mask; + } + // global value to this array + Global bpTable = new Global(Symbols.bptableSymbol(method), Linkage.internal, new ArrayConstantBuilder(Type.I8).add(bpTableValue).build()); mb.addGlobal(bpTable); // cast to byte pointer ConstantBitcast bpTableRef = new ConstantBitcast(bpTable.ref(), Type.I8_PTR); diff --git a/compiler/vm/core/src/hooks.c b/compiler/vm/core/src/hooks.c index 834aba3c2..fb6205d69 100755 --- a/compiler/vm/core/src/hooks.c +++ b/compiler/vm/core/src/hooks.c @@ -1177,9 +1177,6 @@ static inline char getSuspendedEvent(DebugEnv* debugEnv, jint lineNumberOffset, if (debugEnv->suspended) { return EVT_THREAD_SUSPENDED; } - if (debugEnv->stepping && ((pc >= debugEnv->pclow && pc < debugEnv->pchigh) || (pc >= debugEnv->pclow2 && pc < debugEnv->pchigh2))) { - return EVT_THREAD_STEPPED; - } // we only check for breakpoints if we aren't invoking // lineNumberOffset may be < 0 if the instrumented unit @@ -1189,6 +1186,12 @@ static inline char getSuspendedEvent(DebugEnv* debugEnv, jint lineNumberOffset, return EVT_BREAKPOINT; } } + + // check stepping only after breakpoint check. Otherwise breakpoint in one line loops will be ignored + if (debugEnv->stepping && ((pc >= debugEnv->pclow && pc < debugEnv->pchigh) || (pc >= debugEnv->pclow2 && pc < debugEnv->pchigh2))) { + return EVT_THREAD_STEPPED; + } + return 0; } diff --git a/plugins/debugger/src/main/java/org/robovm/debugger/delegates/InstanceUtils.java b/plugins/debugger/src/main/java/org/robovm/debugger/delegates/InstanceUtils.java index cf11962a9..d0968bf1c 100644 --- a/plugins/debugger/src/main/java/org/robovm/debugger/delegates/InstanceUtils.java +++ b/plugins/debugger/src/main/java/org/robovm/debugger/delegates/InstanceUtils.java @@ -675,7 +675,8 @@ private VmThread createThreadInstance(ClassInfo ci, long objectPtr, Object threa String name = readStringValue(nameInstance); VmThreadGroup threadGroup = getFieldValue(objectPtr, ci, ClassDataConsts.fields.JAVA_LANG_THREAD_GROUP); - return new VmThread(objectPtr, (Long)threadPtr, ci, name, threadGroup); + // allow null threadPtr, when thread object is created but thread is not started (attached) + return new VmThread(objectPtr, threadPtr != null ? (Long)threadPtr : 0L, ci, name, threadGroup); } private VmThreadGroup createThreadGroupInstance(ClassInfo ci, long objectPtr, @SuppressWarnings("unused") Object unused) { diff --git a/plugins/debugger/src/main/java/org/robovm/debugger/delegates/JdwpEventCenterDelegate.java b/plugins/debugger/src/main/java/org/robovm/debugger/delegates/JdwpEventCenterDelegate.java index c3a3c5fc4..dca6dcde2 100644 --- a/plugins/debugger/src/main/java/org/robovm/debugger/delegates/JdwpEventCenterDelegate.java +++ b/plugins/debugger/src/main/java/org/robovm/debugger/delegates/JdwpEventCenterDelegate.java @@ -23,6 +23,7 @@ import org.robovm.debugger.hooks.payloads.HooksThreadEventPayload; import org.robovm.debugger.hooks.payloads.HooksThreadStoppedEventPayload; import org.robovm.debugger.jdwp.JdwpConsts; +import org.robovm.debugger.jdwp.handlers.eventrequest.events.EventData; import org.robovm.debugger.jdwp.handlers.eventrequest.events.IJdwpEventDelegate; import org.robovm.debugger.jdwp.handlers.eventrequest.events.JdwpClassLoadedEventData; import org.robovm.debugger.jdwp.handlers.eventrequest.events.JdwpEventData; @@ -53,6 +54,7 @@ import java.util.List; import java.util.Set; import java.util.concurrent.LinkedBlockingQueue; +import java.util.function.Predicate; /** * @author Demyan Kimitsa @@ -326,9 +328,24 @@ private void applyRequestToTarget(JdwpEventRequest request) { if (thread.suspendCount() == 0) throw new DebuggerException(JdwpConsts.Error.THREAD_NOT_SUSPENDED); + // simple event data and validator for step out/over operations to properly find back-stack to stop + // and make sure it is not in ignored class + class StackTraceValidator extends EventData implements Predicate { + String className; + public String getClassName() { return className; } + public boolean test(VmStackTrace vmStackTrace) { + if (vmStackTrace.classInfo() instanceof ClassInfoImpl) { + className = ((ClassInfoImpl) vmStackTrace.classInfo()).className().replace('/', '.'); + return request.test(this, JdwpConsts.EventModifier.CLASS_MATCH) && + request.test(this, JdwpConsts.EventModifier.CLASS_EXCLUDE) ; + } + return false; // can't get class name + } + } + // apply it to target // size modifier is ignored as stepping in hooks implemented as line only - RuntimeUtils.RuntimeStepReference ref = delegates.runtime().step(thread, stepMod.depth()); + RuntimeUtils.RuntimeStepReference ref = delegates.runtime().step(thread, stepMod.depth(), new StackTraceValidator()); // remember the request to be able to resume it in case some of criteria doesn't pass (e.g. class is filtered out) // or execution is interrupted and stepping is canceled by exception event @@ -632,13 +649,16 @@ private JdwpEventData processThreadEvent(HooksThreadEventPayload event) { // get corresponding thread object VmThread thread = delegates.state().referenceRefIdHolder().instanceByAddr(event.threadObj()); if (event.eventId() == HookConsts.events.THREAD_ATTACHED || event.eventId() == HookConsts.events.THREAD_STARTED) { - if (thread != null) - throw new DebuggerException("Thread " + Long.toHexString(event.threadObj()) + " already attached/started!"); + if (thread != null) { + if (thread.threadPtr() != 0) + throw new DebuggerException("Thread " + Long.toHexString(event.threadObj()) + " already attached/started!"); + // thread object was resolved before it was started, just attach thread + thread.attach(event.thread()); + } else { + thread = delegates.instances().instanceByPointer(event.threadObj(), event.thread(), true); + } - // attach thread - thread = delegates.instances().instanceByPointer(event.threadObj(), event.thread(), true); delegates.state().threads().add(thread); - log.debug("THREAD_STARTED: " + thread); return new JdwpEventData(JdwpConsts.EventKind.THREAD_START, thread); @@ -729,6 +749,10 @@ private VmStackTrace convertStackTrace(int eventId, HooksCallStackEntry payload) return null; } + // skip synthetic methods from being visible to debugger + if (methodInfo.isBridge() || methodInfo.isBroCallback() || methodInfo.isBroBridge()) + return null; + return new VmStackTrace(classInfo, methodInfo, payload.lineNumber(), payload.fp(), payload.pc() - payload.impl()); } diff --git a/plugins/debugger/src/main/java/org/robovm/debugger/delegates/RuntimeUtils.java b/plugins/debugger/src/main/java/org/robovm/debugger/delegates/RuntimeUtils.java index 09fd6b1b2..20e926fc1 100644 --- a/plugins/debugger/src/main/java/org/robovm/debugger/delegates/RuntimeUtils.java +++ b/plugins/debugger/src/main/java/org/robovm/debugger/delegates/RuntimeUtils.java @@ -23,6 +23,7 @@ import java.util.HashSet; import java.util.Set; +import java.util.function.Predicate; /** * @author Demyan Kimitsa @@ -119,7 +120,7 @@ public void clearBreakPoint(MethodInfo methodInfo, int line) { * @param thread to step * @param depth of step, {@link org.robovm.debugger.jdwp.JdwpConsts.StepDepth} */ - public RuntimeStepReference step(VmThread thread, int depth) { + public RuntimeStepReference step(VmThread thread, int depth, Predicate stackTraceValidator) { long pclow; long pchigh; long pclow2; @@ -137,7 +138,7 @@ public RuntimeStepReference step(VmThread thread, int depth) { // find out previous not native entry VmStackTrace prevStackEntry = null; for (int idx = 1; idx < stack.length; idx++) { - if (stack[idx].methodInfo().isNative()) + if (stack[idx].methodInfo().isNative() || !stackTraceValidator.test(stack[idx])) continue; prevStackEntry = stack[idx]; break; diff --git a/plugins/debugger/src/main/java/org/robovm/debugger/delegates/ThreadDelegate.java b/plugins/debugger/src/main/java/org/robovm/debugger/delegates/ThreadDelegate.java index 7200c529d..71de48962 100644 --- a/plugins/debugger/src/main/java/org/robovm/debugger/delegates/ThreadDelegate.java +++ b/plugins/debugger/src/main/java/org/robovm/debugger/delegates/ThreadDelegate.java @@ -50,7 +50,7 @@ public void suspendThread(VmThread thread) throws DebuggerException { int suspendCount = thread.markSuspended(); if (suspendCount == 1) { delegates.hooksApi().threadSuspend(thread.threadPtr()); - thread.setStatus(VmThread.Status.SUPENDED); + thread.setStatus(VmThread.Status.SUSPENDED); } } @@ -103,7 +103,7 @@ public void onThreadSuspended(VmThread thread, VmStackTrace[] stack, boolean kee if (keepSuspended) { setThreadStack(thread, stack); thread.markSuspended(); - thread.setStatus(VmThread.Status.SUPENDED); + thread.setStatus(VmThread.Status.SUSPENDED); } else { if (thread.suspendCount() == 0) { // thread is not suspended and this event is filtered out, so resume thread @@ -166,7 +166,7 @@ public void resumeAllOtherThreads(VmThread stoppedThread) { */ public VmThread anySuspendedThread() { for (VmThread thread : delegates.state().threads()) { - if (thread.status() == VmThread.Status.SUPENDED) + if (thread.status() == VmThread.Status.SUSPENDED) return thread; } return null; diff --git a/plugins/debugger/src/main/java/org/robovm/debugger/jdwp/handlers/eventrequest/events/JdwpEventRequest.java b/plugins/debugger/src/main/java/org/robovm/debugger/jdwp/handlers/eventrequest/events/JdwpEventRequest.java index c55ad9218..c23b9b1ff 100644 --- a/plugins/debugger/src/main/java/org/robovm/debugger/jdwp/handlers/eventrequest/events/JdwpEventRequest.java +++ b/plugins/debugger/src/main/java/org/robovm/debugger/jdwp/handlers/eventrequest/events/JdwpEventRequest.java @@ -48,6 +48,14 @@ public boolean test(EventData data) { return true; } + public boolean test(EventData data, int modifierKind) { + // test through specific predicates + for (EventPredicate predicate : predicates) + if (predicate.modifierKind() == modifierKind && !predicate.test(data)) + return false; + return true; + } + public int requestId() { return requestId; } diff --git a/plugins/debugger/src/main/java/org/robovm/debugger/jdwp/handlers/method/JdwpMethodLineTableHandler.java b/plugins/debugger/src/main/java/org/robovm/debugger/jdwp/handlers/method/JdwpMethodLineTableHandler.java index 73a379d58..2ace8a5c9 100644 --- a/plugins/debugger/src/main/java/org/robovm/debugger/jdwp/handlers/method/JdwpMethodLineTableHandler.java +++ b/plugins/debugger/src/main/java/org/robovm/debugger/jdwp/handlers/method/JdwpMethodLineTableHandler.java @@ -57,12 +57,37 @@ public short handle(ByteBufferPacket payload, ByteBufferPacket output) { output.writeLong(startLine); // start code index output.writeLong(finalLine); // end code index - output.writeInt32(finalLine - startLine + 1); // count map entrie + // read bptable from mach-o executable, it contains bits set where lines are not available + int arraySize = ((finalLine - startLine + 1) + 7) / 8; + state.appFileDataMemoryReader().setAddress(methodInfo.bpTableAddr()); + byte[] bpTable = state.appFileDataMemoryReader().readBytes(arraySize); + + int savedCntPos = output.position(); + output.writeInt32(0); // count map entries -- will be set once calculated + int cnt = 0; + int idx = 0; + byte mask = 0; for (int lineNo = startLine; lineNo <= finalLine; lineNo++) { - output.writeLong(lineNo); // code index - output.writeInt32(lineNo); // line number + // pick mask from bp table each 8 bits + if ((idx & 7) == 0) + mask = bpTable[idx >> 3]; + + if ((mask & 1) == 0) { + // line is available + output.writeLong(lineNo); // code index + output.writeInt32(lineNo); // line number + cnt += 1; + } + + idx += 1; + mask >>= 1; } + // update cnt + output.setPosition(savedCntPos); + output.writeInt32(cnt); + output.setPosition(output.size()); + return JdwpConsts.Error.NONE; } } diff --git a/plugins/debugger/src/main/java/org/robovm/debugger/state/classdata/ClassInfoImpl.java b/plugins/debugger/src/main/java/org/robovm/debugger/state/classdata/ClassInfoImpl.java index 3207a9593..9f5bdcda6 100644 --- a/plugins/debugger/src/main/java/org/robovm/debugger/state/classdata/ClassInfoImpl.java +++ b/plugins/debugger/src/main/java/org/robovm/debugger/state/classdata/ClassInfoImpl.java @@ -214,7 +214,7 @@ private void readMethods(ByteBufferMemoryReader reader, int methodCount, ClassIn MethodInfo methodInfo = new MethodInfo(); methods[idx] = methodInfo; methodInfo.readMethodInfo(reader); - if (methodInfo.isBridge() || methodInfo.isBroCallback() || methodInfo.isBroCallback() || methodInfo.isBroBridge()) { + if (methodInfo.isBridge() || methodInfo.isBroCallback() || methodInfo.isBroBridge()) { // mark bridge and callbacks as native just to keep debugger away from attempts to get information about // these methods(line tables etc) methodInfo.markAsNative(); diff --git a/plugins/debugger/src/main/java/org/robovm/debugger/state/classdata/MethodInfo.java b/plugins/debugger/src/main/java/org/robovm/debugger/state/classdata/MethodInfo.java index c9ff72b7c..9393d0083 100644 --- a/plugins/debugger/src/main/java/org/robovm/debugger/state/classdata/MethodInfo.java +++ b/plugins/debugger/src/main/java/org/robovm/debugger/state/classdata/MethodInfo.java @@ -166,11 +166,11 @@ public int spFpAlign() { return spFpAlign; } - boolean isBroCallback() { + public boolean isBroCallback() { return (flags & ClassDataConsts.methodinfo.BRO_CALLBACK) != 0; } - boolean isBroBridge() { + public boolean isBroBridge() { return (flags & ClassDataConsts.methodinfo.BRO_BRIDGE) != 0; } diff --git a/plugins/debugger/src/main/java/org/robovm/debugger/state/instances/VmThread.java b/plugins/debugger/src/main/java/org/robovm/debugger/state/instances/VmThread.java index f552dea7a..335c22c7a 100644 --- a/plugins/debugger/src/main/java/org/robovm/debugger/state/instances/VmThread.java +++ b/plugins/debugger/src/main/java/org/robovm/debugger/state/instances/VmThread.java @@ -24,12 +24,12 @@ public class VmThread extends VmInstance { public enum Status { - SUPENDED, + SUSPENDED, RUNNING } /** native (not java) thread object pointer */ - private final long threadPtr; + private long threadPtr; /** thread name */ private final String name; @@ -81,6 +81,10 @@ public long threadPtr() { return threadPtr; } + public void attach(long threadPtr) { + this.threadPtr = threadPtr; + } + public VmStackTrace[] stack() { return stack; } diff --git a/plugins/debugger/src/main/java/org/robovm/debugger/utils/bytebuffer/ByteBufferReader.java b/plugins/debugger/src/main/java/org/robovm/debugger/utils/bytebuffer/ByteBufferReader.java index 0277833e2..95cfc1be9 100644 --- a/plugins/debugger/src/main/java/org/robovm/debugger/utils/bytebuffer/ByteBufferReader.java +++ b/plugins/debugger/src/main/java/org/robovm/debugger/utils/bytebuffer/ByteBufferReader.java @@ -260,7 +260,13 @@ public byte[] readBytes(int numBytes) { throw new DebuggerException("Cant read zero number of bytes!"); expects(numBytes); - return Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.position() + numBytes); + if (byteBuffer.hasArray()) + return Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.position() + numBytes); + else { + byte[] res = new byte[numBytes]; + byteBuffer.get(res); + return res; + } } diff --git a/plugins/debugger/src/main/java/org/robovm/debugger/utils/macho/MachOLoader.java b/plugins/debugger/src/main/java/org/robovm/debugger/utils/macho/MachOLoader.java index 3bbc4557f..c75c36369 100755 --- a/plugins/debugger/src/main/java/org/robovm/debugger/utils/macho/MachOLoader.java +++ b/plugins/debugger/src/main/java/org/robovm/debugger/utils/macho/MachOLoader.java @@ -209,7 +209,7 @@ public ByteBufferMemoryReader memoryReader() { return null; return new ByteBufferMemoryReader(rootReader, 0, rootReader.size(), isPatform64Bit(), - regions.toArray(new ByteBufferMemoryReader.MemoryRegion[regions.size()])); + regions.toArray(new ByteBufferMemoryReader.MemoryRegion[0])); } public static void main(String[] argv) { diff --git a/plugins/idea/src/main/java/org/robovm/idea/components/RoboVmApplicationComponent.java b/plugins/idea/src/main/java/org/robovm/idea/components/RoboVmApplicationComponent.java index d3d94c720..5db1005e4 100755 --- a/plugins/idea/src/main/java/org/robovm/idea/components/RoboVmApplicationComponent.java +++ b/plugins/idea/src/main/java/org/robovm/idea/components/RoboVmApplicationComponent.java @@ -16,9 +16,11 @@ */ package org.robovm.idea.components; +import com.intellij.debugger.settings.DebuggerSettings; import com.intellij.ide.util.PropertiesComponent; import com.intellij.openapi.components.ApplicationComponent; import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.ui.classFilter.ClassFilter; import org.jetbrains.annotations.NotNull; import org.robovm.compiler.util.ToolchainUtil; import org.robovm.idea.RoboVmPlugin; @@ -28,6 +30,10 @@ import org.robovm.idea.sdk.RoboVmSdkType; import java.awt.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.BiPredicate; /** * Call on app startup, responsible for extracting/updating the RoboVM SDK and @@ -40,6 +46,7 @@ public class RoboVmApplicationComponent implements ApplicationComponent { public void initComponent() { displaySetupWizard(); RoboVmPlugin.extractSdk(); + setupDebugger(); } private void displaySetupWizard() { @@ -77,6 +84,40 @@ private void displaySetupWizard() { } } + /** + ** setup stepping filters to disable enter into dalvik.* and libcore.* classes + */ + private void setupDebugger() { + // tests if filter is configured + BiPredicate patternConfiguredPredicate = (classFilters, pattern) -> { + for (ClassFilter filter : classFilters) { + // exact match + String fp = filter.getPattern(); + if (fp.equals(pattern)) + return true; + // match as low level pattern + if (fp.endsWith(".*") && pattern.startsWith(fp.substring(0, fp.length() - 1))) + return true; + } + return false; + }; + + String[] robovmFilters = {"dalvik.*", "libcore.*"}; + ClassFilter[] filters = DebuggerSettings.getInstance().getSteppingFilters(); + List modifiedFilters = null; + for (String pattern : robovmFilters) { + if (patternConfiguredPredicate.test(filters, pattern)) + continue; + // not configured + if (modifiedFilters == null) + modifiedFilters =new ArrayList<>(Arrays.asList(filters)); + modifiedFilters.add(new ClassFilter(pattern)); + } + + if (modifiedFilters != null) + DebuggerSettings.getInstance().setSteppingFilters(modifiedFilters.toArray(new ClassFilter[0])); + } + @Override public void disposeComponent() {}