From 1cadb4a26fc5d758bf9ca9c04a870a7776f0b4fd Mon Sep 17 00:00:00 2001 From: dragonmacher <48328597+dragonmacher@users.noreply.github.com> Date: Wed, 18 Sep 2024 12:26:22 -0400 Subject: [PATCH] GP-4933 - Fixed function call trees incorrectly dropping thunks --- .../app/plugin/core/calltree/CallNode.java | 18 +- .../core/calltree/CallTreeProvider.java | 13 +- .../app/plugin/core/calltree/DeadEndNode.java | 8 +- .../core/calltree/IncomingCallNode.java | 40 +- .../core/calltree/OutgoingCallNode.java | 74 ++- .../core/calltree/OutgoingCallsRootNode.java | 6 +- .../calltree/OutgoingFunctionCallNode.java | 33 -- .../java/ghidra/test/ToyProgramBuilder.java | 32 +- .../core/calltree/IncomingCallNodeTest.java | 196 +++++++ .../core/calltree/OutgoingCallNodeTest.java | 498 ++++++++++++++++++ .../OutgoingFunctionCallNodeTest.java | 336 ------------ .../main/java/docking/widgets/tree/GTree.java | 18 +- 12 files changed, 828 insertions(+), 444 deletions(-) delete mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/OutgoingFunctionCallNode.java create mode 100644 Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/calltree/IncomingCallNodeTest.java create mode 100644 Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/calltree/OutgoingCallNodeTest.java delete mode 100644 Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/calltree/OutgoingFunctionCallNodeTest.java diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/CallNode.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/CallNode.java index 60e39ed27e..c1ecea33d7 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/CallNode.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/CallNode.java @@ -4,9 +4,9 @@ * 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. @@ -45,8 +45,8 @@ public abstract class CallNode extends GTreeSlowLoadingNode { } /** - * Returns this node's remote function, where remote is the source function for - * an incoming call or a destination function for an outgoing call. May return + * Returns this node's remote function, where remote is the source function for + * an incoming call or a destination function for an outgoing call. May return * null for nodes that do not have functions. * @return the function or null */ @@ -65,8 +65,8 @@ public abstract class CallNode extends GTreeSlowLoadingNode { public abstract Address getSourceAddress(); /** - * Called when this node needs to be reconstructed due to external changes, such as when - * functions are renamed. + * Called when this node needs to be reconstructed due to external changes, such as when + * functions are renamed. * * @return a new node that is the same type as 'this' node. */ @@ -93,12 +93,6 @@ public abstract class CallNode extends GTreeSlowLoadingNode { protected void addNode(LazyMap> nodesByFunction, CallNode node) { Function function = node.getRemoteFunction(); - if (function != null && function.isThunk()) { - if (!callTreeOptions.allowsThunks()) { - return; - } - } - List nodes = nodesByFunction.get(function); if (nodes.contains(node)) { return; // never add equal() nodes diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/CallTreeProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/CallTreeProvider.java index 98affa9c8c..8ee6fca8e6 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/CallTreeProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/CallTreeProvider.java @@ -4,9 +4,9 @@ * 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. @@ -681,18 +681,18 @@ public class CallTreeProvider extends ComponentProviderAdapter { "Show the Function Call Tree window for the function " + "selected in the call tree"); tool.addLocalAction(this, newCallTree); - // + // // Provider menu actions // //@formatter:off filterThunksAction = new ToggleActionBuilder("Filter Thunks", plugin.getName()) - .selected(false) + .selected(false) .description("Thunk functions will not be shown in the tree when selected") .helpLocation(new HelpLocation(plugin.getName(), "Call_Tree_Action_Filter_Thunks")) .menuPath("Filter Thunks") .onAction(c -> { callTreeOptions = callTreeOptions.withFilterThunks(filterThunksAction.isSelected()); - doUpdate(); + doUpdate(); }) .buildAndInstallLocal(this); //@formatter:on @@ -705,7 +705,7 @@ public class CallTreeProvider extends ComponentProviderAdapter { .menuPath("Show Namespace") .onAction(c -> { callTreeOptions = callTreeOptions.withShowNamespace(showNamespaceAction.isSelected()); - doUpdate(); + doUpdate(); }) .buildAndInstallLocal(this); //@formatter:on @@ -860,6 +860,7 @@ public class CallTreeProvider extends ComponentProviderAdapter { } }; tree.setPaintHandlesForLeafNodes(false); + tree.setDoubleClickExpansionEnabled(false); // reserve double-click for navigation // tree.setFilterVisible(false); return tree; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/DeadEndNode.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/DeadEndNode.java index 4a23e4b8dd..59cec189c7 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/DeadEndNode.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/DeadEndNode.java @@ -4,9 +4,9 @@ * 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. @@ -65,6 +65,10 @@ public class DeadEndNode extends CallNode { return reference.getFromAddress(); } + public Address getRemoteAddress() { + return reference.getToAddress(); + } + @Override public Icon getIcon(boolean expanded) { return ICON; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/IncomingCallNode.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/IncomingCallNode.java index c5634ab572..b3aed50701 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/IncomingCallNode.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/IncomingCallNode.java @@ -4,9 +4,9 @@ * 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. @@ -36,8 +36,8 @@ import resources.icons.TranslateIcon; public class IncomingCallNode extends CallNode { - private Icon INCOMING_ICON = Icons.ARROW_UP_LEFT_ICON; - private Icon INCOMING_FUNCTION_ICON; + private static final Icon INCOMING_ICON = Icons.ARROW_UP_LEFT_ICON; + private Icon incomingFunctionIcon; private Icon icon = null; private final Address functionAddress; @@ -55,10 +55,10 @@ public class IncomingCallNode extends CallNode { this.sourceAddress = sourceAddress; this.functionAddress = function.getEntryPoint(); - MultiIcon incomingFunctionIcon = new MultiIcon(INCOMING_ICON, false, 32, 16); + MultiIcon multiIcon = new MultiIcon(INCOMING_ICON, false, 32, 16); TranslateIcon translateIcon = new TranslateIcon(CallTreePlugin.FUNCTION_ICON, 16, 0); - incomingFunctionIcon.addIcon(translateIcon); - INCOMING_FUNCTION_ICON = incomingFunctionIcon; + multiIcon.addIcon(translateIcon); + incomingFunctionIcon = multiIcon; } @Override @@ -79,8 +79,19 @@ public class IncomingCallNode extends CallNode { @Override public List generateChildren(TaskMonitor monitor) throws CancelledException { + List children = new ArrayList<>(); + doGenerateChildren(functionAddress, children, monitor); + + Collections.sort(children, new CallNodeComparator()); + + return children; + } + + private void doGenerateChildren(Address address, List results, TaskMonitor monitor) + throws CancelledException { + FunctionSignatureFieldLocation location = - new FunctionSignatureFieldLocation(program, functionAddress); + new FunctionSignatureFieldLocation(program, address); Set
addresses = ReferenceUtils.getReferenceAddresses(location, monitor); LazyMap> nodesByFunction = @@ -93,6 +104,13 @@ public class IncomingCallNode extends CallNode { continue; } + // If we are not showing thunks, then replace each thunk with all calls to that thunk + if (callerFunction.isThunk() && !callTreeOptions.allowsThunks()) { + Address callerEntry = callerFunction.getEntryPoint(); + doGenerateChildren(callerEntry, results, monitor); + continue; + } + IncomingCallNode node = new IncomingCallNode(program, callerFunction, fromAddress, callTreeOptions); addNode(nodesByFunction, node); @@ -102,9 +120,7 @@ public class IncomingCallNode extends CallNode { .stream() .flatMap(list -> list.stream()) .collect(Collectors.toList()); - Collections.sort(children, new CallNodeComparator()); - - return children; + results.addAll(children); } @Override @@ -115,7 +131,7 @@ public class IncomingCallNode extends CallNode { @Override public Icon getIcon(boolean expanded) { if (icon == null) { - icon = INCOMING_FUNCTION_ICON; + icon = incomingFunctionIcon; if (functionIsInPath()) { icon = CallTreePlugin.RECURSIVE_ICON; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/OutgoingCallNode.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/OutgoingCallNode.java index b3c13e9732..e104b8193d 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/OutgoingCallNode.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/OutgoingCallNode.java @@ -4,9 +4,9 @@ * 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. @@ -36,31 +36,34 @@ import resources.Icons; import resources.MultiIcon; import resources.icons.TranslateIcon; -public abstract class OutgoingCallNode extends CallNode { +public class OutgoingCallNode extends CallNode { private static final Icon OUTGOING_ICON = Icons.ARROW_DOWN_RIGHT_ICON; - private final Icon OUTGOING_FUNCTION_ICON; + private final Icon outgoingFunctionIcon; private Icon icon = null; protected final Program program; protected final Function function; protected String name; private final Address sourceAddress; - private final Icon baseIcon; - OutgoingCallNode(Program program, Function function, Address sourceAddress, Icon baseIcon, + OutgoingCallNode(Program program, Function function, Address sourceAddress, CallTreeOptions callTreeOptions) { super(callTreeOptions); this.program = program; this.function = function; this.name = function.getName(callTreeOptions.showNamespace()); this.sourceAddress = sourceAddress; - this.baseIcon = baseIcon; - MultiIcon outgoingFunctionIcon = new MultiIcon(OUTGOING_ICON, false, 32, 16); - TranslateIcon translateIcon = new TranslateIcon(baseIcon, 16, 0); - outgoingFunctionIcon.addIcon(translateIcon); - OUTGOING_FUNCTION_ICON = outgoingFunctionIcon; + MultiIcon multiIcon = new MultiIcon(OUTGOING_ICON, false, 32, 16); + TranslateIcon translateIcon = new TranslateIcon(CallTreePlugin.FUNCTION_ICON, 16, 0); + multiIcon.addIcon(translateIcon); + outgoingFunctionIcon = multiIcon; + } + + @Override + CallNode recreate() { + return new OutgoingCallNode(program, function, sourceAddress, callTreeOptions); } @Override @@ -70,8 +73,23 @@ public abstract class OutgoingCallNode extends CallNode { @Override public List generateChildren(TaskMonitor monitor) throws CancelledException { - AddressSetView functionBody = function.getBody(); - Address entryPoint = function.getEntryPoint(); + + List children = new ArrayList<>(); + Address calledEntry = function.getEntryPoint(); + doGenerateChildren(calledEntry, children, monitor); + + Collections.sort(children, new CallNodeComparator()); + + return children; + } + + private void doGenerateChildren(Address address, List results, TaskMonitor monitor) + throws CancelledException { + + FunctionManager fm = program.getFunctionManager(); + Function currentFunction = fm.getFunctionContaining(address); + AddressSetView functionBody = currentFunction.getBody(); + Address entryPoint = currentFunction.getEntryPoint(); Set references = getReferencesFrom(program, functionBody, monitor); LazyMap> nodesByFunction = LazyMap.lazyMap(new HashMap<>(), k -> new ArrayList<>()); @@ -80,10 +98,22 @@ public abstract class OutgoingCallNode extends CallNode { monitor.checkCancelled(); Address toAddress = reference.getToAddress(); if (toAddress.equals(entryPoint)) { - continue; + continue; // recursive } Function calledFunction = functionManager.getFunctionAt(toAddress); + if (calledFunction == null) { + createNode(nodesByFunction, reference, calledFunction); + continue; + } + + // If we are not showing thunks, then replace the thunk with the thunked function + if (calledFunction.isThunk() && !callTreeOptions.allowsThunks()) { + Function thunkedFunction = calledFunction.getThunkedFunction(true); + createNode(nodesByFunction, reference, thunkedFunction); + continue; + } + createNode(nodesByFunction, reference, calledFunction); } @@ -91,9 +121,7 @@ public abstract class OutgoingCallNode extends CallNode { .stream() .flatMap(list -> list.stream()) .collect(Collectors.toList()); - Collections.sort(children, new CallNodeComparator()); - - return children; + results.addAll(children); } private void createNode(LazyMap> nodes, Reference reference, @@ -101,21 +129,21 @@ public abstract class OutgoingCallNode extends CallNode { Address fromAddress = reference.getFromAddress(); if (calledFunction != null) { if (isExternalCall(calledFunction)) { - CallNode node = - new ExternalCallNode(calledFunction, fromAddress, baseIcon, callTreeOptions); + CallNode node = new ExternalCallNode(calledFunction, fromAddress, + CallTreePlugin.FUNCTION_ICON, callTreeOptions); addNode(nodes, node); } else { addNode(nodes, - new OutgoingFunctionCallNode(program, calledFunction, fromAddress, callTreeOptions)); + new OutgoingCallNode(program, calledFunction, fromAddress, callTreeOptions)); } } else if (isCallReference(reference)) { Function externalFunction = getExternalFunctionTempHackWorkaround(reference); if (externalFunction != null) { - CallNode node = - new ExternalCallNode(externalFunction, fromAddress, baseIcon, callTreeOptions); + CallNode node = new ExternalCallNode(externalFunction, fromAddress, + CallTreePlugin.FUNCTION_ICON, callTreeOptions); addNode(nodes, node); } else { @@ -201,7 +229,7 @@ public abstract class OutgoingCallNode extends CallNode { @Override public Icon getIcon(boolean expanded) { if (icon == null) { - icon = OUTGOING_FUNCTION_ICON; + icon = outgoingFunctionIcon; if (functionIsInPath()) { icon = CallTreePlugin.RECURSIVE_ICON; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/OutgoingCallsRootNode.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/OutgoingCallsRootNode.java index 471b035fdc..df9aeb9e7f 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/OutgoingCallsRootNode.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/OutgoingCallsRootNode.java @@ -4,9 +4,9 @@ * 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. @@ -25,7 +25,7 @@ public class OutgoingCallsRootNode extends OutgoingCallNode { OutgoingCallsRootNode(Program program, Function function, Address sourceAddress, CallTreeOptions callTreeOptions) { - super(program, function, sourceAddress, CallTreePlugin.FUNCTION_ICON, callTreeOptions); + super(program, function, sourceAddress, callTreeOptions); } @Override diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/OutgoingFunctionCallNode.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/OutgoingFunctionCallNode.java deleted file mode 100644 index d8a694bbd1..0000000000 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/OutgoingFunctionCallNode.java +++ /dev/null @@ -1,33 +0,0 @@ -/* ### - * IP: GHIDRA - * - * 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 ghidra.app.plugin.core.calltree; - -import ghidra.program.model.address.Address; -import ghidra.program.model.listing.Function; -import ghidra.program.model.listing.Program; - -public class OutgoingFunctionCallNode extends OutgoingCallNode { - - OutgoingFunctionCallNode(Program program, Function function, Address sourceAddress, - CallTreeOptions callTreeOptions) { - super(program, function, sourceAddress, CallTreePlugin.FUNCTION_ICON, callTreeOptions); - } - - @Override - CallNode recreate() { - return new OutgoingFunctionCallNode(program, function, getSourceAddress(), callTreeOptions); - } -} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/test/ToyProgramBuilder.java b/Ghidra/Features/Base/src/main/java/ghidra/test/ToyProgramBuilder.java index 6334510753..24bc1852e0 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/test/ToyProgramBuilder.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/test/ToyProgramBuilder.java @@ -4,9 +4,9 @@ * 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. @@ -46,7 +46,7 @@ public class ToyProgramBuilder extends ProgramBuilder { Program program = getProgram(); addrFactory = program.getAddressFactory(); defaultSpace = addrFactory.getDefaultAddressSpace(); - definedInstrAddresses = new ArrayList
(); + definedInstrAddresses = new ArrayList<>(); } /** @@ -86,7 +86,7 @@ public class ToyProgramBuilder extends ProgramBuilder { Program program = getProgram(); addrFactory = program.getAddressFactory(); defaultSpace = addrFactory.getDefaultAddressSpace(); - definedInstrAddresses = new ArrayList
(); + definedInstrAddresses = new ArrayList<>(); } private static String getToyLanguageId(boolean bigEndian, boolean wordAligned) { @@ -149,8 +149,8 @@ public class ToyProgramBuilder extends ProgramBuilder { } int relDest = (int) dest.subtract(address); if (relDest > Byte.MAX_VALUE || relDest < Byte.MIN_VALUE) { - throw new IllegalArgumentException("targetAddr is out of range for instruction: " + - relDest); + throw new IllegalArgumentException( + "targetAddr is out of range for instruction: " + relDest); } return (short) (relDest & 0xff); } @@ -162,8 +162,8 @@ public class ToyProgramBuilder extends ProgramBuilder { } int relDest = (int) dest.subtract(address); if (relDest > Byte.MAX_VALUE || relDest < Byte.MIN_VALUE) { - throw new IllegalArgumentException("targetAddr is out of range for instruction: " + - relDest); + throw new IllegalArgumentException( + "targetAddr is out of range for instruction: " + relDest); } return (short) (relDest & 0xffff); } @@ -391,6 +391,16 @@ public class ToyProgramBuilder extends ProgramBuilder { addBytesCall(toHex(offset), toHex(dest)); } + /** + * Add call (consumes 2-bytes) + * @param offset instruction address offset + * @param dest call destination offset + * @throws MemoryAccessException + */ + public void addBytesCall(String offset, long dest) throws MemoryAccessException { + addBytesCall(offset, toHex(dest)); + } + /** * Add call (consumes 2-bytes) * @param addr instruction address @@ -499,8 +509,7 @@ public class ToyProgramBuilder extends ProgramBuilder { * @param offset instruction address offset * @throws MemoryAccessException */ - public void addBytesSkipConditional(long offset) - throws MemoryAccessException { + public void addBytesSkipConditional(long offset) throws MemoryAccessException { addBytesSkipConditional(toHex(offset)); } @@ -509,8 +518,7 @@ public class ToyProgramBuilder extends ProgramBuilder { * @param addr instruction address * @throws MemoryAccessException */ - public void addBytesSkipConditional(String addr) - throws MemoryAccessException { + public void addBytesSkipConditional(String addr) throws MemoryAccessException { Address address = addr(addr); addInstructionWords(address, (short) (0x8000)); // skeq } diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/calltree/IncomingCallNodeTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/calltree/IncomingCallNodeTest.java new file mode 100644 index 0000000000..ef697ce6f0 --- /dev/null +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/calltree/IncomingCallNodeTest.java @@ -0,0 +1,196 @@ +/* ### + * IP: GHIDRA + * + * 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 ghidra.app.plugin.core.calltree; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.*; + +import docking.widgets.tree.GTreeNode; +import generic.test.AbstractGenericTest; +import ghidra.program.model.address.Address; +import ghidra.program.model.data.DataType; +import ghidra.program.model.listing.*; +import ghidra.test.ToyProgramBuilder; +import ghidra.util.task.TaskMonitor; +import utility.function.ExceptionalCallback; + +public class IncomingCallNodeTest extends AbstractGenericTest { + + private ToyProgramBuilder builder; + private Program program; + private IncomingCallNode node1; + + private String firstCalledFunctionAddress = "0x0100"; + private String firstCalledFromAddress = "0x1100"; + + @Before + public void setUp() throws Exception { + builder = new ToyProgramBuilder("Call Node Test", true); + builder.createMemory(".text", "0x0", 0x3000); + + builder.createFunction(firstCalledFunctionAddress); + + program = builder.getProgram(); + + setUpCallNode(false); + + createThunkFunctionCallGraph(); + } + + @After + public void tearDown() { + builder.dispose(); + } + + private void setUpCallNode(boolean hideThunks) { + + FunctionManager fm = program.getFunctionManager(); + Function firstCalledFunction = fm.getFunctionAt(builder.addr(firstCalledFunctionAddress)); // 0x0100 + Address calledFromAddress = builder.addr(firstCalledFromAddress); // 0x0010 + CallTreeOptions callTreeOptions = new CallTreeOptions(); + callTreeOptions = callTreeOptions.withRecurseDepth(5); + callTreeOptions = callTreeOptions.withFilterThunks(hideThunks); + node1 = + new IncomingCallNode(program, firstCalledFunction, calledFromAddress, callTreeOptions); + + } + + @Test + public void testGenerateChildren_CalledFunctionIsThunk_ShowThunks() throws Exception { + + /* + + node1 @ 0100 + + node2 (Function_1100) + + node3 (Thunk_Function_1050) + + node4 + Call: 1200 -> 1050 + */ + + List children = node1.generateChildren(TaskMonitor.DUMMY); + assertEquals(1, children.size()); + IncomingCallNode node2 = (IncomingCallNode) children.get(0); + Address fromAddress = builder.addr("0x1100"); + assertEquals("Function_1100", node2.getName()); + assertEquals(fromAddress, node2.getSourceAddress()); + + children = node2.generateChildren(TaskMonitor.DUMMY); + assertEquals(1, children.size()); + IncomingCallNode node3 = (IncomingCallNode) children.get(0); + assertEquals("Thunk_Function_1050", node3.getName()); + fromAddress = builder.addr("0x1050"); + + assertEquals(fromAddress, node3.getSourceAddress()); + + children = node3.generateChildren(TaskMonitor.DUMMY); + assertEquals(1, children.size()); // node4; Call: 1200 -> 1050 + IncomingCallNode node4 = (IncomingCallNode) children.get(0); + fromAddress = addr("0x1200"); + assertEquals("Function_1200", node4.getName()); + assertEquals(fromAddress, node4.getSourceAddress()); + } + + @Test + public void testGenerateChildren_CalledFunctionIsThunk_HideThunks() throws Exception { + + /* + + node1 @ 0100 + + node2 (Function_1100) + + node3 (Thunk_Function_1050) // <------ This is removed from the tree + + node4 + Call: 1200 -> 1050 + */ + setUpCallNode(true); + + List children = node1.generateChildren(TaskMonitor.DUMMY); + assertEquals(1, children.size()); + IncomingCallNode node2 = (IncomingCallNode) children.get(0); + Address fromAddress = builder.addr("0x1100"); + assertEquals("Function_1100", node2.getName()); + assertEquals(fromAddress, node2.getSourceAddress()); + + children = node2.generateChildren(TaskMonitor.DUMMY); + assertEquals(1, children.size()); + IncomingCallNode node3 = (IncomingCallNode) children.get(0); + assertEquals("Function_1200", node3.getName()); + fromAddress = builder.addr("0x1200"); + } + + private void createThunkFunctionCallGraph() throws Exception { + + /* + + node1 @ 0100 + + node2 (Function_1100) + + node3 (Thunk_Function_1050) + + node4 + Call: 1200 -> 1050 + */ + + String thunkedAddress = "0x1100"; + Function thunkedFunction = + builder.createEmptyFunction("Function_1100", thunkedAddress, 10, DataType.DEFAULT); + builder.createMemoryCallReference("0x1100", "0x0100"); // call the root node, node1 + + String thunkAddress = "0x1050"; + Function thunkFunction = + builder.createEmptyFunction("Thunk_Function_1050", thunkAddress, 10, DataType.DEFAULT); + tx(() -> { + thunkFunction.setThunkedFunction(thunkedFunction); + }); + + // call from thunk to thunked, node3 -> node2 + builder.createMemoryCallReference(thunkAddress, thunkedAddress); + + // call from somewhere to thunked function, node3 + builder.createEmptyFunction("Function_1200", "0x1200", 10, DataType.DEFAULT); + builder.createMemoryCallReference("0x1200", thunkAddress); + } + + private void tx(ExceptionalCallback c) { + int txId = program.startTransaction("Test - Function in Transaction"); + boolean commit = true; + try { + c.call(); + program.flushEvents(); + } + catch (Exception e) { + commit = false; + failWithException("Exception modifying program '" + program.getName() + "'", e); + } + finally { + program.endTransaction(txId, commit); + } + } + + private Address addr(String addrString) { + return builder.addr(addrString); + } + +} diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/calltree/OutgoingCallNodeTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/calltree/OutgoingCallNodeTest.java new file mode 100644 index 0000000000..355eacd751 --- /dev/null +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/calltree/OutgoingCallNodeTest.java @@ -0,0 +1,498 @@ +/* ### + * IP: GHIDRA + * + * 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 ghidra.app.plugin.core.calltree; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.*; + +import docking.widgets.tree.GTreeNode; +import generic.test.AbstractGenericTest; +import ghidra.program.model.address.Address; +import ghidra.program.model.data.*; +import ghidra.program.model.listing.*; +import ghidra.program.model.symbol.*; +import ghidra.test.ToyProgramBuilder; +import ghidra.util.task.TaskMonitor; +import utility.function.ExceptionalCallback; + +/** + * This test class is covering the various types of 'calls' a function can make, such as a + * direct call instruction, or through a user-defined reference. External calls are also a bit + * tricky. This test was created by following the code paths in {@link OutgoingCallNode} + * and making sure that each path was followed. + */ +public class OutgoingCallNodeTest extends AbstractGenericTest { + + private ToyProgramBuilder builder; + private Program program; + private OutgoingCallNode node1; + + private String firstCallSourceAddress = "0x0010"; + private String firstCalledFunctionAddress = "0x0100"; + + @Before + public void setUp() throws Exception { + builder = new ToyProgramBuilder("Call Node Test", true); + builder.createMemory(".text", "0x0", 0x3000); + + Function firstCalledFunction = builder.createFunction(firstCalledFunctionAddress); + + program = builder.getProgram(); + + Address calledFromAddress = builder.addr(firstCallSourceAddress); // fake + CallTreeOptions callTreeOptions = new CallTreeOptions(); + callTreeOptions = callTreeOptions.withRecurseDepth(5); + node1 = + new OutgoingCallNode(program, firstCalledFunction, calledFromAddress, callTreeOptions); + } + + @After + public void tearDown() { + builder.dispose(); + } + + @Test + public void testGenerateChildren_SelfRecursiveCall() throws Exception { + + builder.createMemoryCallReference(firstCalledFunctionAddress, firstCalledFunctionAddress); + + List children = node1.generateChildren(TaskMonitor.DUMMY); + assertTrue(children.isEmpty()); + } + + @Test + public void testGenerateChildren_CalledFunctionExists() throws Exception { + String otherAddress = "0x1000"; + builder.createEmptyFunction("Function_1000", otherAddress, 10, DataType.DEFAULT); + builder.createMemoryCallReference(firstCalledFunctionAddress, otherAddress); + + List children = node1.generateChildren(TaskMonitor.DUMMY); + assertEquals(1, children.size()); + assertEquals("Function_1000", children.get(0).getName()); + } + + @Test + public void testGenerateChildren_CalledFunctionIsThunk_ShowThunks() throws Exception { + + /* + + node1 + 0010 -> Function @ 0100 + + node2 + Call: 0100 -> Thunk_Function_1050 @ 1050 + + node3 + Call: 1050 -> Function_1100 @ 1100 + + node4 + Call: 1100 -> 1200 + */ + createThunkFunctionCallGraph(); + + List children = node1.generateChildren(TaskMonitor.DUMMY); + assertEquals(1, children.size()); + OutgoingCallNode node2 = (OutgoingCallNode) children.get(0); + Address fromAddress = builder.addr("0x0100"); + Address toAddress = builder.addr("0x1050"); // thunk address + assertEquals("Thunk_Function_1050", node2.getName()); + assertEquals(fromAddress, node2.getSourceAddress()); + assertEquals(toAddress, node2.getRemoteFunction().getEntryPoint()); + + children = node2.generateChildren(TaskMonitor.DUMMY); + assertEquals(1, children.size()); + OutgoingCallNode node3 = (OutgoingCallNode) children.get(0); + assertEquals("Function_1100", node3.getName()); + + children = node2.generateChildren(TaskMonitor.DUMMY); + assertEquals(1, children.size()); // node4; Call: 1100 -> 1200 + } + + @Test + public void testGenerateChildren_CalledFunctionIsThunk_HideThunks() throws Exception { + + setUpCallNodeWithThunksHidden(); + + /* + + node1 + 0010 -> Function @ 0100 + + node2 + Call: 0100 -> Thunk_Function_1050 @ 1050 // <------ ignored thunk; omitted from tree + + node3 + Call: 1050 -> Function_1100 @ 1100 + + node4 + Call: 1100 -> 1200 + */ + createThunkFunctionCallGraph(); + + List children = node1.generateChildren(TaskMonitor.DUMMY); + assertEquals(1, children.size()); + + // second node from above should be skipped, since it is at thunk + OutgoingCallNode node3 = (OutgoingCallNode) children.get(0); + Address fromAddress = addr("0x0100"); + Address toAddress = addr("0x1100"); // thunked address; no thunk + + // verify the function for the child is not the thunk function; the thunk is ignored + assertEquals("Function_1100", node3.getName()); + assertEquals(fromAddress, node3.getSourceAddress()); + assertEquals(toAddress, node3.getRemoteFunction().getEntryPoint()); + + children = node3.generateChildren(TaskMonitor.DUMMY); + assertEquals(1, children.size()); // Call: 1100 -> 1200 + DeadEndNode node4 = (DeadEndNode) children.get(0); + fromAddress = addr("0x1100"); + toAddress = addr("0x1200"); + assertEquals(fromAddress, node4.getSourceAddress()); + assertEquals(toAddress, node4.getRemoteAddress()); + } + + @Test + public void testGenerateChildren_CalledFunctionExists_ExternalCall() throws Exception { + String otherAddress = "0x1000"; + + String externalFunctionName = "External_Function"; + ExternalLocation location = + builder.createExternalFunction(otherAddress, "ExternalLibrary", externalFunctionName); + + builder.createMemoryCallReference(firstCalledFunctionAddress, + location.getExternalSpaceAddress().toString()); + + List children = node1.generateChildren(TaskMonitor.DUMMY); + assertEquals(1, children.size()); + assertEquals(externalFunctionName, children.get(0).getName()); + } + + @Test + public void testGenerateChildren_CallReference_ExternalFunction_NoFunctionInMemory() + throws Exception { + + builder.createMemoryCallReference(firstCalledFunctionAddress, "EXTERNAL:00000001"); + + List children = node1.generateChildren(TaskMonitor.DUMMY); + assertEquals(1, children.size()); + assertEquals("EXTERNAL:00000001", children.get(0).getName()); + } + + @Test + public void testGenerateChildren_CallReference_ToPointer_ToExternalFunction() throws Exception { + + // + // Function A + // -> has memory reference to a pointer + // -> this pointer has an external reference to a function + // + + Reference ref = builder.createMemoryCallReference(firstCalledFunctionAddress, "0x2000"); + + Address toAddress = ref.getToAddress(); + builder.applyDataType(toAddress.toString(), new Pointer32DataType()); + + String externalFunctionName = "External_Function"; + ExternalLocation location = + builder.createExternalFunction("0x2020", "ExternalLibrary", externalFunctionName); + + builder.createMemoryReadReference(toAddress.toString(), + location.getExternalSpaceAddress().toString()); + + List children = node1.generateChildren(TaskMonitor.DUMMY); + assertEquals(1, children.size()); + assertEquals(externalFunctionName, children.get(0).getName()); + } + + @Test + public void testGenerateChildren_CallReference_ToPointer_ToNonExternalFunction() + throws Exception { + + // + // Function A + // -> has memory reference to a pointer + // -> this pointer has an non-external reference to a function + // + + Reference ref = builder.createMemoryCallReference(firstCalledFunctionAddress, "0x2000"); + + Address toAddress = ref.getToAddress(); + builder.applyDataType(toAddress.toString(), new Pointer32DataType()); + + String functionAddress = "0x2020"; + builder.createEmptyFunction("Function_2020", functionAddress, 1, new VoidDataType()); + + builder.createMemoryReadReference(toAddress.toString(), functionAddress); + + List children = node1.generateChildren(TaskMonitor.DUMMY); + assertEquals(1, children.size()); + assertTrue(children.get(0) instanceof DeadEndNode); + } + + @Test + public void testGenerateChildren_CallReference_ToPointer_Offcut() throws Exception { + + // + // Bad code case; expected reference to pointer, but no data there + // + + String dataAddress = "0x2000"; + String offcutAddress = "0x2001"; + builder.applyDataType(dataAddress, new Pointer32DataType()); + builder.createMemoryCallReference(firstCalledFunctionAddress, offcutAddress); + + List children = node1.generateChildren(TaskMonitor.DUMMY); + assertEquals(1, children.size()); + assertTrue(children.get(0) instanceof DeadEndNode); + } + + @Test + public void testGenerateChildren_WriteReference() throws Exception { + + // + // Have a reference in the function to a place that is not another function, and the + // reference is a write reference. No call node is created. + // + + builder.createMemoryReference(firstCalledFunctionAddress, "0x1000", RefType.WRITE, + SourceType.USER_DEFINED); + List children = node1.generateChildren(TaskMonitor.DUMMY); + assertTrue(children.isEmpty()); + } + + @Test + public void testGenerateChildren_ReadReference_NullInstruction() throws Exception { + + // + // Have a reference in the function to a place that is not another function, and the + // reference is a read reference. + // Note: since we did not have the builder put an instruction at the 'to' address, + // the instruction there is null. + // + + builder.createMemoryReference(firstCalledFunctionAddress, "0x1000", RefType.READ, + SourceType.USER_DEFINED); + List children = node1.generateChildren(TaskMonitor.DUMMY); + assertTrue(children.isEmpty()); + + } + + @Test + public void testGenerateChildren_ReadReference_NotCallInstruction() throws Exception { + + // + // Read reference to an instruction with a flow type that is not a call + // + + builder.addBytesFallthrough(firstCalledFunctionAddress); + builder.disassemble(firstCalledFunctionAddress, 2); + + builder.createMemoryReference(firstCalledFunctionAddress, "0x1000", RefType.READ, + SourceType.USER_DEFINED); + List children = node1.generateChildren(TaskMonitor.DUMMY); + assertTrue(children.isEmpty()); + } + + @Test + public void testGenerateChildren_ReadReference_CallInstruction_InstructionAtToAddress() + throws Exception { + + // + // Read reference from an instruction with a flow type of call to a place that is an + // instruction (and thus is not a pointer to a function) + // + + createCallInstruction(); + + // instruction at the other side + builder.addBytesNOP(0x1000, 2); + builder.disassemble("0x1000", 2); + + builder.createMemoryReference(firstCalledFunctionAddress, "0x1000", RefType.READ, + SourceType.USER_DEFINED); + List children = node1.generateChildren(TaskMonitor.DUMMY); + assertTrue(children.isEmpty()); + } + + @Test + public void testGenerateChildren_ReadReference_CallInstruction_ToData_NoReference() + throws Exception { + + // + // Read reference from an instruction with a flow type of call. + // + + createCallInstruction(); + + builder.createMemoryReference(firstCalledFunctionAddress, "0x1000", RefType.READ, + SourceType.USER_DEFINED); + List children = node1.generateChildren(TaskMonitor.DUMMY); + assertTrue(children.isEmpty()); + } + + @Test + public void testGenerateChildren_ReadReference_CallInstruction_ToData_NonExternalReference() + throws Exception { + + // + // Read reference from an instruction with a flow type that is a call, to a non-external place + // + + createCallInstruction(); + + builder.createMemoryReference(firstCalledFunctionAddress, "0x1000", RefType.READ, + SourceType.USER_DEFINED); + + // put a non-external reference on the data at the 'to' address + builder.createMemoryCallReference("0x1000", "0x1020"); + + List children = node1.generateChildren(TaskMonitor.DUMMY); + assertTrue(children.isEmpty()); + } + + @Test + public void testGenerateChildren_ReadReference_CallInstruction_ToData_ExternalReference_NonFunctionSymbol() + throws Exception { + + // + // Read reference from an instruction with a flow type that is a call, to a external place + // + + createCallInstruction(); + + builder.createMemoryReference(firstCalledFunctionAddress, "0x1000", RefType.READ, + SourceType.USER_DEFINED); + + // put a external reference on the data at the 'to' address + builder.createExternalReference("0x1000", "ExternalLib", "ExternalLabel", 0); + + List children = node1.generateChildren(TaskMonitor.DUMMY); + assertTrue(children.isEmpty()); + } + + @Test + public void testGenerateChildren_ReadReference_CallInstruction_ToData_ExternalReference_FunctionSymbol() + throws Exception { + + // + // Read reference from an instruction with a flow type that is a call, to an external + // function symbol + // + + createCallInstruction(); + + builder.createMemoryReference(firstCalledFunctionAddress, "0x1000", RefType.READ, + SourceType.USER_DEFINED); + + // put a external reference on the data at the 'to' address that calls the external function + ExternalLocation location = + builder.createExternalFunction("0x1020", "ExternalLib", "ExternalFunction_1"); + builder.createMemoryReadReference("0x1000", location.getExternalSpaceAddress().toString()); + + List children = node1.generateChildren(TaskMonitor.DUMMY); + assertEquals(1, children.size()); + assertTrue(children.get(0) instanceof DeadEndNode); + } + + private Address addr(String addrString) { + return builder.addr(addrString); + } + + private void createThunkFunctionCallGraph() throws Exception { + + /* + + node1 + 0010 -> Function @ 0100 + + node2 + Call: 0100 -> Thunk_Function_1050 @ 1050 + + node3 + Call: 1050 -> Function_1100 @ 1100 + + node4 + Call: 1100 -> 1200 + */ + + String thunkedAddress = "0x1100"; + Function thunkedFunction = + builder.createEmptyFunction("Function_1100", thunkedAddress, 10, DataType.DEFAULT); + builder.createMemoryCallReference("0x1100", "0x1200"); // ref so a node appears in the tree + + String thunkAddress = "0x1050"; + Function thunkFunction = + builder.createEmptyFunction("Thunk_Function_1050", thunkAddress, 10, DataType.DEFAULT); + tx(() -> { + thunkFunction.setThunkedFunction(thunkedFunction); + }); + + builder.createMemoryCallReference(firstCalledFunctionAddress, thunkAddress); + builder.createMemoryCallReference(thunkAddress, thunkedAddress); + } + + private void setUpCallNodeWithThunksHidden() { + + FunctionManager fm = program.getFunctionManager(); + Function firstCalledFunction = fm.getFunctionAt(builder.addr(firstCalledFunctionAddress)); // 0x0100 + Address calledFromAddress = builder.addr(firstCallSourceAddress); // 0x0010 + CallTreeOptions callTreeOptions = new CallTreeOptions(); + callTreeOptions = callTreeOptions.withRecurseDepth(5); + callTreeOptions = callTreeOptions.withFilterThunks(true); + node1 = + new OutgoingCallNode(program, firstCalledFunction, calledFromAddress, callTreeOptions); + + } + + private void tx(ExceptionalCallback c) { + int txId = program.startTransaction("Test - Function in Transaction"); + boolean commit = true; + try { + c.call(); + program.flushEvents(); + } + catch (Exception e) { + commit = false; + failWithException("Exception modifying program '" + program.getName() + "'", e); + } + finally { + program.endTransaction(txId, commit); + } + } + + private void createCallInstruction() throws Exception { + Address source = builder.addr(firstCalledFunctionAddress); + Address destination = source.add(1); + builder.addBytesCall(source.getOffset(), destination.getOffset()); + builder.disassemble(firstCalledFunctionAddress, 2); + + // + // Note: the tests that use this are creating their references to control the code's + // execution path, so get rid of the default reference created for this instruction + // + int txID = program.startTransaction("Remove References"); + try { + ReferenceManager rm = program.getReferenceManager(); + rm.removeAllReferencesFrom(builder.addr(firstCalledFunctionAddress)); + } + finally { + program.endTransaction(txID, true); + } + } +} diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/calltree/OutgoingFunctionCallNodeTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/calltree/OutgoingFunctionCallNodeTest.java deleted file mode 100644 index 362571be0b..0000000000 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/calltree/OutgoingFunctionCallNodeTest.java +++ /dev/null @@ -1,336 +0,0 @@ -/* ### - * IP: GHIDRA - * - * 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 ghidra.app.plugin.core.calltree; - -import static org.junit.Assert.*; - -import java.util.List; - -import org.junit.Before; -import org.junit.Test; - -import docking.widgets.tree.GTreeNode; -import generic.test.AbstractGenericTest; -import ghidra.program.model.address.Address; -import ghidra.program.model.data.*; -import ghidra.program.model.listing.Function; -import ghidra.program.model.listing.Program; -import ghidra.program.model.symbol.*; -import ghidra.test.ToyProgramBuilder; -import ghidra.util.task.TaskMonitor; - -/** - * This test class is covering the various types of 'calls' a function can make, such as a - * direct call instruction, or through a user-defined reference. External calls are also a bit - * tricky. This test was created by following the code paths in {@link OutgoingFunctionCallNode} - * and making sure that each path was followed. - */ -public class OutgoingFunctionCallNodeTest extends AbstractGenericTest { - - private ToyProgramBuilder builder; - private Program program; - private OutgoingFunctionCallNode node; - private String nodeAddress = "0x0000"; - - @Before - public void setUp() throws Exception { - builder = new ToyProgramBuilder("Call Node Test", true); - builder.createMemory(".text", "0x0", 0x3000); - - Function function = builder.createFunction(nodeAddress); - - program = builder.getProgram(); - - Address source = builder.addr("0x1000"); // fake - CallTreeOptions callTreeOptions = new CallTreeOptions(); - callTreeOptions = callTreeOptions.withRecurseDepth(5); - node = new OutgoingFunctionCallNode(program, function, source, callTreeOptions); - } - - @Test - public void testGenerateChildren_SelfRecursiveCall() throws Exception { - - builder.createMemoryCallReference(nodeAddress, nodeAddress); - - List children = node.generateChildren(TaskMonitor.DUMMY); - assertTrue(children.isEmpty()); - } - - @Test - public void testGenerateChildren_CalledFunctionExists() throws Exception { - String otherAddress = "0x1000"; - builder.createEmptyFunction("Function_2", otherAddress, 10, DataType.DEFAULT); - builder.createMemoryCallReference(nodeAddress, otherAddress); - - List children = node.generateChildren(TaskMonitor.DUMMY); - assertEquals(1, children.size()); - assertEquals("Function_2", children.get(0).getName()); - } - - @Test - public void testGenerateChildren_CalledFunctionExists_ExternalCall() throws Exception { - String otherAddress = "0x1000"; - - String externalFunctionName = "External_Function"; - ExternalLocation location = - builder.createExternalFunction(otherAddress, "ExternalLibrary", externalFunctionName); - - builder.createMemoryCallReference(nodeAddress, - location.getExternalSpaceAddress().toString()); - - List children = node.generateChildren(TaskMonitor.DUMMY); - assertEquals(1, children.size()); - assertEquals(externalFunctionName, children.get(0).getName()); - } - - @Test - public void testGenerateChildren_CallReference_ExternalFunction_NoFunctionInMemory() - throws Exception { - - builder.createMemoryCallReference(nodeAddress, "EXTERNAL:00000001"); - - List children = node.generateChildren(TaskMonitor.DUMMY); - assertEquals(1, children.size()); - assertEquals("EXTERNAL:00000001", children.get(0).getName()); - } - - @Test - public void testGenerateChildren_CallReference_ToPointer_ToExternalFunction() throws Exception { - - // - // Function A - // -> has memory reference to a pointer - // -> this pointer has an external reference to a function - // - - Reference ref = builder.createMemoryCallReference(nodeAddress, "0x2000"); - - Address toAddress = ref.getToAddress(); - builder.applyDataType(toAddress.toString(), new Pointer32DataType()); - - String externalFunctionName = "External_Function"; - ExternalLocation location = - builder.createExternalFunction("0x2020", "ExternalLibrary", externalFunctionName); - - builder.createMemoryReadReference(toAddress.toString(), - location.getExternalSpaceAddress().toString()); - - List children = node.generateChildren(TaskMonitor.DUMMY); - assertEquals(1, children.size()); - assertEquals(externalFunctionName, children.get(0).getName()); - } - - @Test - public void testGenerateChildren_CallReference_ToPointer_ToNonExternalFunction() - throws Exception { - - // - // Function A - // -> has memory reference to a pointer - // -> this pointer has an non-external reference to a function - // - - Reference ref = builder.createMemoryCallReference(nodeAddress, "0x2000"); - - Address toAddress = ref.getToAddress(); - builder.applyDataType(toAddress.toString(), new Pointer32DataType()); - - String functionAddress = "0x2020"; - builder.createEmptyFunction("Function_1", functionAddress, 1, new VoidDataType()); - - builder.createMemoryReadReference(toAddress.toString(), functionAddress); - - List children = node.generateChildren(TaskMonitor.DUMMY); - assertEquals(1, children.size()); - assertTrue(children.get(0) instanceof DeadEndNode); - } - - @Test - public void testGenerateChildren_CallReference_ToPointer_Offcut() throws Exception { - - // - // Bad code case; expected reference to pointer, but no data there - // - - String dataAddress = "0x2000"; - String offcutAddress = "0x2001"; - builder.applyDataType(dataAddress, new Pointer32DataType()); - builder.createMemoryCallReference(nodeAddress, offcutAddress); - - List children = node.generateChildren(TaskMonitor.DUMMY); - assertEquals(1, children.size()); - assertTrue(children.get(0) instanceof DeadEndNode); - } - - @Test - public void testGenerateChildren_WriteReference() throws Exception { - - // - // Have a reference in the function to a place that is not another function, and the - // reference is a write reference. No call node is created. - // - - builder.createMemoryReference(nodeAddress, "0x1000", RefType.WRITE, - SourceType.USER_DEFINED); - List children = node.generateChildren(TaskMonitor.DUMMY); - assertTrue(children.isEmpty()); - } - - @Test - public void testGenerateChildren_ReadReference_NullInstruction() throws Exception { - - // - // Have a reference in the function to a place that is not another function, and the - // reference is a read reference. - // Note: since we did not have the builder put an instruction at the 'to' address, - // the instruction there is null. - // - - builder.createMemoryReference(nodeAddress, "0x1000", RefType.READ, SourceType.USER_DEFINED); - List children = node.generateChildren(TaskMonitor.DUMMY); - assertTrue(children.isEmpty()); - - } - - @Test - public void testGenerateChildren_ReadReference_NotCallInstruction() throws Exception { - - // - // Read reference to an instruction with a flow type that is not a call - // - - builder.addBytesFallthrough(nodeAddress); - builder.disassemble(nodeAddress, 2); - - builder.createMemoryReference(nodeAddress, "0x1000", RefType.READ, SourceType.USER_DEFINED); - List children = node.generateChildren(TaskMonitor.DUMMY); - assertTrue(children.isEmpty()); - } - - @Test - public void testGenerateChildren_ReadReference_CallInstruction_InstructionAtToAddress() - throws Exception { - - // - // Read reference from an instruction with a flow type of call to a place that is an - // instruction (and thus is not a pointer to a function) - // - - createCallInstruction(); - - // instruction at the other side - builder.addBytesNOP(0x1000, 2); - builder.disassemble("0x1000", 2); - - builder.createMemoryReference(nodeAddress, "0x1000", RefType.READ, SourceType.USER_DEFINED); - List children = node.generateChildren(TaskMonitor.DUMMY); - assertTrue(children.isEmpty()); - } - - @Test - public void testGenerateChildren_ReadReference_CallInstruction_ToData_NoReference() - throws Exception { - - // - // Read reference from an instruction with a flow type of call. - // - - createCallInstruction(); - - builder.createMemoryReference(nodeAddress, "0x1000", RefType.READ, SourceType.USER_DEFINED); - List children = node.generateChildren(TaskMonitor.DUMMY); - assertTrue(children.isEmpty()); - } - - @Test - public void testGenerateChildren_ReadReference_CallInstruction_ToData_NonExternalReference() - throws Exception { - - // - // Read reference from an instruction with a flow type that is a call, to a non-external place - // - - createCallInstruction(); - - builder.createMemoryReference(nodeAddress, "0x1000", RefType.READ, SourceType.USER_DEFINED); - - // put a non-external reference on the data at the 'to' address - builder.createMemoryCallReference("0x1000", "0x1020"); - - List children = node.generateChildren(TaskMonitor.DUMMY); - assertTrue(children.isEmpty()); - } - - @Test - public void testGenerateChildren_ReadReference_CallInstruction_ToData_ExternalReference_NonFunctionSymbol() - throws Exception { - - // - // Read reference from an instruction with a flow type that is a call, to a external place - // - - createCallInstruction(); - - builder.createMemoryReference(nodeAddress, "0x1000", RefType.READ, SourceType.USER_DEFINED); - - // put a external reference on the data at the 'to' address - builder.createExternalReference("0x1000", "ExternalLib", "ExternalLabel", 0); - - List children = node.generateChildren(TaskMonitor.DUMMY); - assertTrue(children.isEmpty()); - } - - @Test - public void testGenerateChildren_ReadReference_CallInstruction_ToData_ExternalReference_FunctionSymbol() - throws Exception { - - // - // Read reference from an instruction with a flow type that is a call, to an external - // function symbol - // - - createCallInstruction(); - - builder.createMemoryReference(nodeAddress, "0x1000", RefType.READ, SourceType.USER_DEFINED); - - // put a external reference on the data at the 'to' address that calls the external function - ExternalLocation location = - builder.createExternalFunction("0x1020", "ExternalLib", "ExternalFunction_1"); - builder.createMemoryReadReference("0x1000", location.getExternalSpaceAddress().toString()); - - List children = node.generateChildren(TaskMonitor.DUMMY); - assertEquals(1, children.size()); - assertTrue(children.get(0) instanceof DeadEndNode); - } - - private void createCallInstruction() throws Exception { - builder.addBytesCall(0x0000, 0x1); - builder.disassemble(nodeAddress, 2); - - // - // Note: the tests that use this are creating their references to control the code's - // execution path, so get rid of the default reference created for this instruction - // - int txID = program.startTransaction("Remove References"); - try { - ReferenceManager rm = program.getReferenceManager(); - rm.removeAllReferencesFrom(builder.addr("0x0000")); - } - finally { - program.endTransaction(txID, true); - } - } -} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTree.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTree.java index 516a39c1de..416f717200 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTree.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTree.java @@ -4,9 +4,9 @@ * 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. @@ -250,9 +250,9 @@ public class GTree extends JPanel implements BusyListener { /** * Sets an accessible name on the GTree. This prefix will be used to assign * meaningful accessible names to the tree, filter text field and the filter options button such - * that screen readers will properly describe them. + * that screen readers will properly describe them. *

- * This prefix should be the base name that describes the type of items in the tree. + * This prefix should be the base name that describes the type of items in the tree. * This method will then append the necessary information to name the text field and the button. * * @param namePrefix the accessible name prefix to assign to the filter component. For @@ -385,7 +385,7 @@ public class GTree extends JPanel implements BusyListener { } /** - * Sets the filter restore state. This method is a way to override the tree's filtering + * Sets the filter restore state. This method is a way to override the tree's filtering * behavior, which is usually set by a call to {@link #saveFilterRestoreState()}. Most clients * will never need to call this method. * @@ -1445,6 +1445,14 @@ public class GTree extends JPanel implements BusyListener { return true; } + /** + * Enable or disable using double-click to open and close tree nodes. The default is true. + * @param b true to enable + */ + public void setDoubleClickExpansionEnabled(boolean b) { + tree.setToggleClickCount(b ? 2 : 0); + } + //================================================================================================== // Inner Classes //==================================================================================================