From 9ddc65d7e50bcb0323b12d3c362321fe8e998686 Mon Sep 17 00:00:00 2001 From: ghidra1 Date: Wed, 24 Jul 2024 10:43:03 -0400 Subject: [PATCH] GP-4324 Improved Function Editor for Decompiler use to limit full commit and added checkbox to control full commit --- .../help/topics/FunctionPlugin/Variables.htm | 45 +- .../FunctionPlugin/images/FunctionEditor.png | Bin 34128 -> 38281 bytes .../core/function/editor/FunctionData.java | 354 ++++++ .../function/editor/FunctionDataView.java | 248 ++++ .../function/editor/FunctionEditorDialog.java | 93 +- .../function/editor/FunctionEditorModel.java | 1016 +++++++---------- .../function/editor/FunctionVariableData.java | 2 + .../core/function/editor/ParamInfo.java | 139 ++- .../function/editor/ParameterTableModel.java | 10 + .../editor/StorageAddressEditorDialog.java | 5 + .../editor/FunctionEditorModelTest.java | 50 +- .../actions/SpecifyCPrototypeAction.java | 12 +- .../program/model/listing/Function.java | 2 +- .../model/listing/VariableUtilities.java | 29 +- 14 files changed, 1287 insertions(+), 718 deletions(-) create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/FunctionData.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/FunctionDataView.java diff --git a/Ghidra/Features/Base/src/main/help/help/topics/FunctionPlugin/Variables.htm b/Ghidra/Features/Base/src/main/help/help/topics/FunctionPlugin/Variables.htm index fbdeb90b49..5b3c50ee17 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/FunctionPlugin/Variables.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/FunctionPlugin/Variables.htm @@ -29,10 +29,16 @@
  • Ordered list of named parameters and associated data types.
  • +
    +

    Note: + While certain changes may be made without commiting all return and parameter details, those + changes which are made that require a full commit will cause the + Commit all return/parameter details checkbox to be enabled. + The user may choose to alter this full commit by checking or unchecking the corresponding + checkbox. +

    +
    - - -

    Function Attributes

    @@ -289,11 +295,9 @@
    1. Place the cursor on a function signature
    2. -
    3. Right-mouse-click, and select Edit Function.
    4. -
    5. Edit any attributes of the function using the dialog.
    6. - +
    7. Consider committing all return/parameter details
    8. Press OK to save your changes.
    @@ -396,6 +400,35 @@ the compiler specification (*.cspec file) associated with a program. This feature is typically used when the effects of calling a well-known function need to be simplified so that the caller can be more easily analyzed and/or understood.

    + +

    Commit all return/parameter details

    + +

    This checkbox controls whether a complete function update will be performed, including + datatype and optional custom-storage details, for the return and all parameters. + This checkbox will enable itself when + specific changes are made which require a complete commit in order to preserve what is seen + in the editor. Use of custom storage or altering return or parameter datatypes will require + a full commit in order to retain such changes. It is important to note that a full Commit + with storage and/or datatype changes will impose a USER_DEFINED Signature Source + which will lock-in parameter details within the Decompiler. The Signature Source + Function Listing Field can be useful for monitoring this state.

    + +
    +

    + If the signature has been autogenerated by the decompiler and changes have been made to + things other than the signature, like calling convention or callfixup, that may affect the + generated signature. Consider changing the calling convention only to check if the + decompiler would generate the same signature unless you are sure the generated signature + is correct.

    +
    + +

    Prior to clicking the dialog's OK button the user may uncheck this control to + prevent return/parameter changes from being committed. Once datatype or custom-storage + choices have been changed, any parameter name changes will also require a full commit to + preserve such changes. A full commit is not required if only parameter names have been + changed. The exception to this is when the Function Editor is used within the Decompiler + and parameters have not previously be committed.

    +

    Edit Parameter Storage Dialog

    diff --git a/Ghidra/Features/Base/src/main/help/help/topics/FunctionPlugin/images/FunctionEditor.png b/Ghidra/Features/Base/src/main/help/help/topics/FunctionPlugin/images/FunctionEditor.png index 93d8eff837df4443aeaa7898256394da0ba3742c..9913c311e2f2addbb9c0d833c6a8575b6deabb46 100644 GIT binary patch literal 38281 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 WEJp!ZcOWaklao@umn&iN{Qm$w2|DTk diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/FunctionData.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/FunctionData.java new file mode 100644 index 0000000000..a66dc66688 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/FunctionData.java @@ -0,0 +1,354 @@ +/* ### + * 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.function.editor; + +import java.util.*; + +import ghidra.program.model.data.*; +import ghidra.program.model.lang.CompilerSpec; +import ghidra.program.model.lang.PrototypeModel; +import ghidra.program.model.listing.*; +import ghidra.util.exception.AssertException; +import ghidra.util.exception.InvalidInputException; + +class FunctionData extends FunctionDataView { + + public FunctionData(Function function) { + super(function); + } + + /** + * Determine if return/parameter datatype and/or storage has changed and should be comitted. + * Auto params and calling-convention imposed changes are not considered. + * @param originalFunctionData original function data prior to edits + * @return true if non-auto parameters have been modified, else false + */ + boolean hasParameterChanges(FunctionDataView originalFunctionData) { + + boolean checkStorage = false; + if (canCustomizeStorage()) { +// if (!originalFunctionData.canCustomizeStorage()) { +// // switched to using custom storage +// return true; +// } + checkStorage = true; + } + + if (!returnInfo.getFormalDataType() + .equals(originalFunctionData.returnInfo.getFormalDataType())) { + return true; + } + if (checkStorage && + !returnInfo.getStorage().equals(originalFunctionData.returnInfo.getStorage())) { + return true; + } + + int paramStartIndex = autoParamCount; + int nonAutoParamCount = parameters.size() - paramStartIndex; + int originalParamStartIndex = originalFunctionData.autoParamCount; + int originalNonAutoParamCount = + originalFunctionData.parameters.size() - originalParamStartIndex; + + if (nonAutoParamCount != originalNonAutoParamCount) { + return true; + } + + for (int i = 0; i < nonAutoParamCount; i++) { + ParamInfo param = parameters.get(paramStartIndex + i); + ParamInfo originalParam = + originalFunctionData.parameters.get(originalParamStartIndex + i); + if (!param.getFormalDataType().equals(originalParam.getFormalDataType())) { + return true; + } + if (checkStorage && !param.getStorage().equals(originalParam.getStorage())) { + return true; + } + + // Check for name change without stored param + if (!Objects.equals(param.getName(true), originalParam.getName(true))) { + if (function.getParameter(param.getOrdinal()) == null) { + return true; + } + } + } + + return false; + } + + /** + * Determine if parameter name(s) have been modified. Note that this method will return false + * if the number of non-auto parameters has changed. + * @param originalFunctionData original function data prior to edits + * @return true if one or more parameter names has changed, else false + */ + boolean hasParameterNamesChanged(FunctionDataView originalFunctionData) { + + int paramStartIndex = autoParamCount; + int nonAutoParamCount = parameters.size() - paramStartIndex; + int originalParamStartIndex = originalFunctionData.autoParamCount; + int originalNonAutoParamCount = + originalFunctionData.parameters.size() - originalParamStartIndex; + + if (nonAutoParamCount != originalNonAutoParamCount) { + return false; + } + + for (int i = 0; i < nonAutoParamCount; i++) { + ParamInfo param = parameters.get(paramStartIndex + i); + ParamInfo originalParam = + originalFunctionData.parameters.get(originalParamStartIndex + i); + if (!Objects.equals(param.getName(true), originalParam.getName(true))) { + return true; + } + } + + return false; + } + + ParamInfo addNewParameter() { + ParamInfo param = new ParamInfo(this, null, DataType.DEFAULT, + VariableStorage.UNASSIGNED_STORAGE, canCustomizeStorage(), parameters.size()); + parameters.add(param); + fixupOrdinals(); + updateParameterAndReturnStorage(); + return param; + } + + void removeAllParameters() { + parameters.clear(); + autoParamCount = 0; + } + + void removeParameters(List paramsToRemove) { + Iterator it = parameters.iterator(); + while (it.hasNext()) { + ParamInfo p = it.next(); + if (paramsToRemove.contains(p)) { + it.remove(); + } + } + fixupOrdinals(); + updateParameterAndReturnStorage(); + } + + void moveParameterUp(int paramIndex) { + ParamInfo param = parameters.remove(paramIndex); + parameters.add(paramIndex - 1, param); + fixupOrdinals(); + updateParameterAndReturnStorage(); + } + + void moveParameterDown(int paramIndex) { + ParamInfo param = parameters.remove(paramIndex); + parameters.add(paramIndex + 1, param); + fixupOrdinals(); + updateParameterAndReturnStorage(); + } + + void setName(String n) { + this.name = n; + } + + void setInline(boolean enable) { + this.isInLine = enable; + } + + void setHasNoReturn(boolean enable) { + this.hasNoReturn = enable; + } + + void clearCallFixup() { + callFixupName = null; + } + + void setCallFixupName(String cfuName) { + this.callFixupName = cfuName; + } + + void setCallingConventionName(String ccName) { + if (Objects.equals(ccName, callingConventionName)) { + return; + } + this.callingConventionName = ccName; + removeExplicitThisParameter(); + updateParameterAndReturnStorage(); + } + + void setVarArgs(boolean enable) { + this.hasVarArgs = enable; + } + + /** + * Update dynamic storage and auto-params when custom storage is disasbled. + * Returns immediately if custom storage is enabled. + */ + void updateParameterAndReturnStorage() { + if (allowCustomStorage) { + autoParamCount = 0; + return; + } + PrototypeModel effectiveCallingConvention = getEffectiveCallingConvention(); + + if (effectiveCallingConvention == null) { + for (ParamInfo info : parameters) { + info.setStorage(VariableStorage.UNASSIGNED_STORAGE); + } + return; + } + + DataType[] dataTypes = new DataType[parameters.size() - autoParamCount + 1]; + dataTypes[0] = returnInfo.getFormalDataType(); + + int index = 1; + for (int i = autoParamCount; i < parameters.size(); i++) { + dataTypes[index++] = parameters.get(i).getFormalDataType(); + } + + VariableStorage[] paramStorage = + effectiveCallingConvention.getStorageLocations(getProgram(), dataTypes, true); + + returnInfo.setStorage(paramStorage[0]); + + List oldParams = parameters; + int oldAutoCount = autoParamCount; + + parameters = new ArrayList<>(); + autoParamCount = 0; + + int ordinal = 0; + for (int i = 1; i < paramStorage.length; i++) { + VariableStorage storage = paramStorage[i]; + ParamInfo info; + if (storage.isAutoStorage()) { + DataType dt = VariableUtilities.getAutoDataType(function, + returnInfo.getFormalDataType(), storage); + try { + info = new ParamInfo(this, + new AutoParameterImpl(dt, ++autoParamCount, storage, function)); + } + catch (InvalidInputException e) { + throw new AssertException(e); // unexpected + } + } + else { + info = oldParams.get(oldAutoCount + ordinal); + info.setStorage(storage); + ++ordinal; + } + parameters.add(info); + } + fixupOrdinals(); + } + + void clearAutoParams() { + autoParamCount = 0; + } + + /** + * Change the enablement of custom storage + * @param enable true if custom storage should be enable, else false to + */ + void setUseCustomStorage(boolean enable) { + if (enable == allowCustomStorage) { + return; + } + allowCustomStorage = enable; + if (!enable) { + removeExplicitThisParameter(); + DataType returnDt = removeExplicitReturnStoragePtrParameter(); + if (returnDt != null) { + returnInfo.setFormalDataType(returnDt); + returnInfo.setStorage(VariableStorage.UNASSIGNED_STORAGE); + } + updateParameterAndReturnStorage(); + } + else { + switchToCustomStorage(); + } + } + + /** + * Switch to custom storage and perform required transformations + */ + private void switchToCustomStorage() { + try { + VariableStorage returnStorage = returnInfo.getStorage(); + DataType returnType = returnInfo.getDataType(); + if (returnStorage.isForcedIndirect() && returnStorage.isVoidStorage()) { + returnType = VoidDataType.dataType; + } + returnInfo.setFormalDataType(returnType); + returnInfo.setStorage(returnStorage.clone(getProgram())); + + autoParamCount = 0; + for (ParamInfo paramInfo : parameters) { + DataType dt = paramInfo.getDataType(); + VariableStorage storage = paramInfo.getStorage(); + paramInfo.setFormalDataType(dt); + paramInfo.setStorage(storage.clone(getProgram())); + } + } + catch (InvalidInputException e) { + throw new AssertException(e); // unexpected + } + } + + void removeExplicitThisParameter() { + if (!allowCustomStorage && + CompilerSpec.CALLING_CONVENTION_thiscall.equals(callingConventionName)) { + int thisIndex = findExplicitThisParameter(); + if (thisIndex >= 0) { + parameters.remove(thisIndex); // remove explicit 'this' parameter + } + } + } + + private DataType removeExplicitReturnStoragePtrParameter() { + int index = findExplicitReturnStoragePtrParameter(); + if (index >= 0) { + // remove explicit '__return_storage_ptr__' parameter - should always be a pointer + ParamInfo returnStoragePtrParameter = parameters.remove(index); + DataType dt = returnStoragePtrParameter.getDataType(); + if (dt instanceof Pointer ptr) { + return ptr.getDataType(); + } + } + return null; + } + + private int findExplicitThisParameter() { + for (int i = 0; i < parameters.size(); i++) { + ParamInfo p = parameters.get(i); + if (!p.isAutoParameter() && Function.THIS_PARAM_NAME.equals(p.getName()) && + (p.getDataType() instanceof Pointer)) { + return i; + } + } + return -1; + } + + private int findExplicitReturnStoragePtrParameter() { + for (int i = 0; i < parameters.size(); i++) { + ParamInfo p = parameters.get(i); + if (!p.isAutoParameter() && Function.RETURN_PTR_PARAM_NAME.equals(p.getName()) && + (p.getDataType() instanceof Pointer)) { + return i; + } + } + return -1; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/FunctionDataView.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/FunctionDataView.java new file mode 100644 index 0000000000..bfc48ca0ef --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/FunctionDataView.java @@ -0,0 +1,248 @@ +/* ### + * 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.function.editor; + +import java.util.*; + +import ghidra.program.model.data.VoidDataType; +import ghidra.program.model.lang.PrototypeModel; +import ghidra.program.model.listing.*; +import ghidra.program.model.symbol.SymbolUtilities; + +/** + * {@link FunctionDataView} provides an immutable view of function data used by the + * Function Editor model. + */ +class FunctionDataView { + + Function function; + + String name; + boolean hasVarArgs; + ParamInfo returnInfo; + List parameters = new ArrayList<>(); + int autoParamCount = 0; + boolean isInLine; + boolean hasNoReturn; + String callingConventionName; + boolean allowCustomStorage; + String callFixupName; + + /** + * Construct instance from {@link Function} details. + * @param function program function + */ + FunctionDataView(Function function) { + this.function = function; + this.name = function.getName(); + allowCustomStorage = function.hasCustomVariableStorage(); + hasVarArgs = function.hasVarArgs(); + isInLine = function.isInline(); + hasNoReturn = function.hasNoReturn(); + callingConventionName = function.getCallingConventionName(); + callFixupName = function.getCallFixup(); + initializeParametersAndReturn(); + } + + /** + * Construct a duplicate instance from another {@link FunctionDataView} instance. + * @param otherFunctionData function data + */ + FunctionDataView(FunctionDataView otherFunctionData) { + name = otherFunctionData.name; + hasVarArgs = otherFunctionData.hasVarArgs; + returnInfo = otherFunctionData.returnInfo.copy(); + for (ParamInfo p : otherFunctionData.parameters) { + parameters.add(p.copy()); + } + autoParamCount = otherFunctionData.autoParamCount; + isInLine = otherFunctionData.isInLine; + hasNoReturn = otherFunctionData.hasNoReturn; + callingConventionName = otherFunctionData.callingConventionName; + allowCustomStorage = otherFunctionData.allowCustomStorage; + callFixupName = otherFunctionData.callFixupName; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof FunctionDataView otherFunctionData)) { + return false; + } + if (!Objects.equals(name, otherFunctionData.name) || + !Objects.equals(callingConventionName, otherFunctionData.callingConventionName) || + hasVarArgs != otherFunctionData.hasVarArgs || + parameters.size() != otherFunctionData.parameters.size() || + autoParamCount != otherFunctionData.autoParamCount || + isInLine != otherFunctionData.isInLine || + hasNoReturn != otherFunctionData.hasNoReturn || + allowCustomStorage != otherFunctionData.allowCustomStorage || + !Objects.equals(callFixupName, otherFunctionData.callFixupName) || + !returnInfo.isSame(otherFunctionData.returnInfo)) { + return false; + } + + int paramCount = parameters.size(); + for (int i = 0; i < paramCount; i++) { + ParamInfo param = parameters.get(i); + ParamInfo otherParam = otherFunctionData.parameters.get(i); + if (!param.isSame(otherParam)) { + return false; + } + } + + return true; + } + + @Override + public int hashCode() { + return getNameString().hashCode(); + } + + private void initializeParametersAndReturn() { + + returnInfo = new ParamInfo(this, function.getReturn()); + + // check for void storage correction + if (VoidDataType.isVoidDataType(returnInfo.getDataType()) && + returnInfo.getStorage() != VariableStorage.VOID_STORAGE) { + returnInfo.setStorage(VariableStorage.VOID_STORAGE); + } + + autoParamCount = 0; + Parameter[] params = function.getParameters(); + for (Parameter parameter : params) { + if (parameter.isAutoParameter()) { + ++autoParamCount; + } + parameters.add(new ParamInfo(this, parameter)); + } + + fixupOrdinals(); + } + + void fixupOrdinals() { + for (int i = 0; i < parameters.size(); i++) { + parameters.get(i).setOrdinal(i); + } + } + + String getFunctionSignatureText() { + StringBuilder buf = new StringBuilder(); + buf.append(returnInfo.getFormalDataType().getName()).append(" "); + buf.append(getNameString()); + buf.append(" ("); + int skipCount = autoParamCount; + int ordinal = 0; + for (ParamInfo param : parameters) { + if (skipCount > 0) { + --skipCount; + continue; + } + if (ordinal++ != 0) { + buf.append(", "); + } + buf.append(param.getFormalDataType().getName()); + + buf.append(" "); + + buf.append(param.getName()); + + } + if (hasVarArgs()) { + if (!parameters.isEmpty()) { + buf.append(", "); + } + buf.append(FunctionSignature.VAR_ARGS_DISPLAY_STRING); + } + else if (parameters.size() == 0) { + buf.append(FunctionSignature.VOID_PARAM_DISPLAY_STRING); + } + buf.append(')'); + return buf.toString(); + } + + public Program getProgram() { + return function.getProgram(); + } + + boolean canCustomizeStorage() { + return allowCustomStorage; + } + + int getAutoParamCount() { + return autoParamCount; + } + + int getParamCount() { + return parameters.size(); + } + + public String getName() { + return name; + } + + String getNameString() { + return name.length() == 0 ? SymbolUtilities.getDefaultFunctionName(function.getEntryPoint()) + : name; + } + + boolean isInline() { + return isInLine; + } + + boolean hasNoReturn() { + return hasNoReturn; + } + + String getCallFixupName() { + return callFixupName; + } + + boolean hasCallFixup() { + return callFixupName != null; + } + + List getParameters() { + return parameters; + } + + ParamInfo getReturnInfo() { + return returnInfo; + } + + PrototypeModel getEffectiveCallingConvention() { + FunctionManager functionManager = getProgram().getFunctionManager(); + PrototypeModel effectiveCallingConvention = + functionManager.getCallingConvention(getCallingConventionName()); + if (effectiveCallingConvention == null) { + effectiveCallingConvention = functionManager.getDefaultCallingConvention(); + } + return effectiveCallingConvention; + } + + String getCallingConventionName() { + return callingConventionName; + } + + boolean hasVarArgs() { + return hasVarArgs; + } + + boolean hasParameters() { + return !parameters.isEmpty(); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/FunctionEditorDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/FunctionEditorDialog.java index bde202fe30..6ccc82ef56 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/FunctionEditorDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/FunctionEditorDialog.java @@ -57,6 +57,11 @@ import resources.Icons; public class FunctionEditorDialog extends DialogComponentProvider implements ModelChangeListener { + private static final String COMMIT_FULL_SIGNATURE_WARNING = + "All signature details will be commited (see Commit checkbox above)"; + private static final String SIGNATURE_LOSS_WARNING = + "Return/Parameter changes will not be applied (see Commit checkbox above)"; + private FunctionEditorModel model; private DocumentListener nameFieldDocumentListener; private GTable parameterTable; @@ -77,6 +82,7 @@ public class FunctionEditorDialog extends DialogComponentProvider implements Mod private JCheckBox storageCheckBox; private JScrollPane scroll; private JPanel previewPanel; + private JCheckBox commitFullParamDetailsCheckBox; // optional: may be null private FunctionSignatureTextField signatureTextField; private UndoRedoKeeper signatureFieldUndoRedoKeeper; @@ -84,11 +90,22 @@ public class FunctionEditorDialog extends DialogComponentProvider implements Mod private MyGlassPane glassPane; private JPanel centerPanel; + /** + * Construct Function Editor dialog for a specified Function and associated DataType service. + * @param service DataType service + * @param function Function to be modified + */ public FunctionEditorDialog(DataTypeManagerService service, Function function) { - this(new FunctionEditorModel(service, function)); + this(new FunctionEditorModel(service, function), true); } - public FunctionEditorDialog(FunctionEditorModel model) { + /** + * Construct Function Editor dialog using a specified model + * @param model function detail model + * @param hasOptionalSignatureCommit if true an optional control will be included which + * controls commit of parameter/return details, including Varargs and use of custom storage. + */ + public FunctionEditorDialog(FunctionEditorModel model, boolean hasOptionalSignatureCommit) { super(createTitle(model.getFunction())); this.service = model.getDataTypeManagerService(); setRememberLocation(true); @@ -96,7 +113,7 @@ public class FunctionEditorDialog extends DialogComponentProvider implements Mod setHelpLocation(new HelpLocation("FunctionPlugin", "Edit_Function")); this.model = model; model.setModelChangeListener(this); - addWorkPanel(buildMainPanel()); + addWorkPanel(buildMainPanel(hasOptionalSignatureCommit)); addOKButton(); addCancelButton(); glassPane = new MyGlassPane(); @@ -157,7 +174,13 @@ public class FunctionEditorDialog extends DialogComponentProvider implements Mod return; } } - if (model.apply()) { + + boolean fullCommit = true; + if (commitFullParamDetailsCheckBox != null && + !commitFullParamDetailsCheckBox.isSelected()) { + fullCommit = false; + } + if (model.apply(fullCommit)) { close(); } } @@ -168,24 +191,24 @@ public class FunctionEditorDialog extends DialogComponentProvider implements Mod super.close(); } - private JComponent buildMainPanel() { + private JComponent buildMainPanel(boolean hasOptionalSignatureCommit) { JPanel panel = new JPanel(new BorderLayout()); panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); panel.add(buildPreview(), BorderLayout.NORTH); - panel.add(buildCenterPanel(), BorderLayout.CENTER); + panel.add(buildCenterPanel(hasOptionalSignatureCommit), BorderLayout.CENTER); return panel; } - private JComponent buildCenterPanel() { + private JComponent buildCenterPanel(boolean hasOptionalSignatureCommit) { centerPanel = new JPanel(new BorderLayout()); centerPanel.setBorder(BorderFactory.createEmptyBorder(10, 0, 10, 0)); centerPanel.add(buildAttributePanel(), BorderLayout.NORTH); centerPanel.add(buildTable(), BorderLayout.CENTER); - centerPanel.add(buildBottomPanel(), BorderLayout.SOUTH); + centerPanel.add(buildBottomPanel(hasOptionalSignatureCommit), BorderLayout.SOUTH); return centerPanel; } - private Component buildBottomPanel() { + private Component buildBottomPanel(boolean hasOptionalSignatureCommit) { JPanel panel = new JPanel(new BorderLayout()); Border b = BorderFactory.createEmptyBorder(0, 0, 0, 0); @@ -203,6 +226,29 @@ public class FunctionEditorDialog extends DialogComponentProvider implements Mod else { panel.add(new JPanel(), BorderLayout.CENTER); } + + if (hasOptionalSignatureCommit) { + commitFullParamDetailsCheckBox = new JCheckBox("Commit all return/parameter details"); + commitFullParamDetailsCheckBox.addActionListener(e -> { + if (!model.isValid()) { + return; + } + if (!commitFullParamDetailsCheckBox.isSelected()) { + if (model.hasSignificantParameterChanges()) { + setStatusText(SIGNATURE_LOSS_WARNING, MessageType.WARNING); + } + else { + clearStatusText(); + } + } + else { + setStatusText(COMMIT_FULL_SIGNATURE_WARNING, MessageType.WARNING); + setOkEnabled(true); + } + }); + panel.add(commitFullParamDetailsCheckBox, BorderLayout.SOUTH); + } + return panel; } @@ -250,7 +296,7 @@ public class FunctionEditorDialog extends DialogComponentProvider implements Mod signatureTextField.setEscapeListener(e -> { - if (!model.hasChanges()) { + if (!model.hasSignatureTextChanges()) { // no changes; user wish to close the dialog cancelCallback(); return; @@ -400,7 +446,7 @@ public class FunctionEditorDialog extends DialogComponentProvider implements Mod callFixupComboBox.addItem(element); } callFixupComboBox.addItemListener( - e -> model.setCallFixupName((String) callFixupComboBox.getSelectedItem())); + e -> model.setCallFixupChoice((String) callFixupComboBox.getSelectedItem())); } else { callFixupComboBox.setToolTipText("No call-fixups defined by compiler specification"); @@ -414,11 +460,11 @@ public class FunctionEditorDialog extends DialogComponentProvider implements Mod private Component buildTable() { JPanel panel = new JPanel(new BorderLayout()); panel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEmptyBorder(), - "Function Variables")); + "Function Return/Parameters")); paramTableModel = new ParameterTableModel(model); parameterTable = new ParameterTable(paramTableModel); - selectionListener = e -> model.setSelectedParameterRow(parameterTable.getSelectedRows()); + selectionListener = e -> model.setSelectedParameterRows(parameterTable.getSelectedRows()); JScrollPane tableScroll = new JScrollPane(parameterTable); panel.add(tableScroll, BorderLayout.CENTER); @@ -497,11 +543,18 @@ public class FunctionEditorDialog extends DialogComponentProvider implements Mod updateTableSelection(); updateTableButtonEnablement(); updateStorageEditingEnabled(); + updateOptionalParamCommit(); + } + } + + private void updateOptionalParamCommit() { + if (commitFullParamDetailsCheckBox != null) { + commitFullParamDetailsCheckBox.setSelected(model.hasSignificantParameterChanges()); } } private void updateStorageEditingEnabled() { - boolean canCustomizeStorage = model.canCustomizeStorage(); + boolean canCustomizeStorage = model.canUseCustomStorage(); if (storageCheckBox.isSelected() != canCustomizeStorage) { storageCheckBox.setSelected(canCustomizeStorage); } @@ -522,7 +575,9 @@ public class FunctionEditorDialog extends DialogComponentProvider implements Mod selectionModel.removeListSelectionListener(selectionListener); parameterTable.clearSelection(); for (int i : selectedRows) { - parameterTable.addRowSelectionInterval(i, i); + if (i < parameterTable.getRowCount()) { + parameterTable.addRowSelectionInterval(i, i); + } } parameterTable.scrollToSelectedRow(); selectionModel.addListSelectionListener(selectionListener); @@ -539,7 +594,7 @@ public class FunctionEditorDialog extends DialogComponentProvider implements Mod } private void updateCallFixupCombo() { - String callFixupName = model.getCallFixupName(); + String callFixupName = model.getCallFixupChoice(); if (!callFixupComboBox.getSelectedItem().equals(callFixupName)) { callFixupComboBox.setSelectedItem(callFixupName); if (!callFixupComboBox.getSelectedItem().equals(callFixupName)) { @@ -574,7 +629,7 @@ public class FunctionEditorDialog extends DialogComponentProvider implements Mod } private void updateOkButton() { - setOkEnabled(model.isValid()); + setOkEnabled(model.isValid() && model.hasChanges()); } private void updateStatusText() { @@ -778,6 +833,10 @@ public class FunctionEditorDialog extends DialogComponentProvider implements Mod setForeground(getErrorForegroundColor(isSelected)); setToolTipText("Invalid Parameter Storage"); } + else if (rowData.hasStorageConflict()) { + setForeground(getErrorForegroundColor(isSelected)); + setToolTipText("Conflicting Parameter Storage"); + } else { setForeground(isSelected ? table.getSelectionForeground() : Colors.FOREGROUND); setToolTipText(""); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/FunctionEditorModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/FunctionEditorModel.java index f4d63443d4..8cb92e5acb 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/FunctionEditorModel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/FunctionEditorModel.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. @@ -22,12 +22,12 @@ import ghidra.app.util.cparser.C.ParseException; import ghidra.app.util.parser.FunctionSignatureParser; import ghidra.program.model.address.Address; import ghidra.program.model.data.*; -import ghidra.program.model.lang.*; +import ghidra.program.model.lang.Register; import ghidra.program.model.listing.*; import ghidra.program.model.listing.Function.FunctionUpdateType; +import ghidra.program.model.listing.VariableUtilities.VariableConflictHandler; import ghidra.program.model.pcode.Varnode; -import ghidra.program.model.symbol.SourceType; -import ghidra.program.model.symbol.SymbolUtilities; +import ghidra.program.model.symbol.*; import ghidra.util.*; import ghidra.util.exception.*; @@ -36,45 +36,33 @@ public class FunctionEditorModel { HTMLUtilities.escapeHTML(" or to commit edits, to abort"); static final String NONE_CHOICE = "-NONE-"; - private String name; + private FunctionData functionData; + private FunctionDataView originalFunctionData; + + private String signatureFieldText; + private ModelChangeListener listener; - private boolean hasVarArgs; - private ParamInfo returnInfo; - private List parameters = new ArrayList<>(); - private int autoParamCount = 0; + + private Function function; + private Program program; + + private List selectedParams = new ArrayList<>(); + + private boolean isInParsingMode; + + private DataTypeManagerService dataTypeManagerService; + private String statusText = ""; private boolean isValid = true; - private boolean signatureTransformed = false; - private boolean isInLine; - private boolean isNoReturn; - private String callingConventionName; - private Function function; - private FunctionManager functionManager; - private String callFixupName; - private int[] selectedFunctionRows = new int[0]; - private Program program; - private boolean allowCustomStorage; - private boolean isInParsingMode; - private String signatureFieldText; - private DataTypeManagerService dataTypeManagerService; - private boolean modelChanged = false; + private boolean isSignatureTransformed = false; + private boolean hasSignificantParameterChanges = false; public FunctionEditorModel(DataTypeManagerService service, Function function) { this.dataTypeManagerService = service; this.function = function; this.program = function.getProgram(); - this.name = function.getName(); - this.functionManager = program.getFunctionManager(); - allowCustomStorage = function.hasCustomVariableStorage(); - hasVarArgs = function.hasVarArgs(); - isInLine = function.isInline(); - isNoReturn = function.hasNoReturn(); - callingConventionName = function.getCallingConventionName(); - callFixupName = function.getCallFixup(); - if (callFixupName == null) { - callFixupName = NONE_CHOICE; - } - initializeParametersAndReturn(); + functionData = new FunctionData(function); + this.originalFunctionData = new FunctionDataView(functionData); validate(); } @@ -82,59 +70,21 @@ public class FunctionEditorModel { this.listener = listener; } + boolean hasChanges() { + return !functionData.equals(originalFunctionData); + } + + boolean hasSignificantParameterChanges() { + return hasSignificantParameterChanges; + } + // Returns the current calling convention or the default calling convention if current unknown - private PrototypeModel getEffectiveCallingConvention() { - PrototypeModel effectiveCallingConvention = - functionManager.getCallingConvention(callingConventionName); - if (effectiveCallingConvention == null) { - effectiveCallingConvention = functionManager.getDefaultCallingConvention(); - } - return effectiveCallingConvention; - } - - private void initializeParametersAndReturn() { - - returnInfo = new ParamInfo(this, function.getReturn()); - - // check for void storage correction - if (VoidDataType.isVoidDataType(returnInfo.getDataType()) && - returnInfo.getStorage() != VariableStorage.VOID_STORAGE) { - returnInfo.setStorage(VariableStorage.VOID_STORAGE); - modelChanged = true; - } - - autoParamCount = 0; - Parameter[] params = function.getParameters(); - for (Parameter parameter : params) { - if (parameter.isAutoParameter()) { - ++autoParamCount; - } - parameters.add(new ParamInfo(this, parameter)); - } - - fixupOrdinals(); - } - - private boolean hasModifiedParametersOrReturn() { - if (returnInfo.isModified()) { - return true; - } - if ((function.getParameterCount() - - function.getAutoParameterCount()) != (parameters.size() - autoParamCount)) { - return true; - } - for (ParamInfo info : parameters) { - if (!info.isAutoParameter() && info.isModified()) { - return true; - } - } - return false; - } List getCallingConventionNames() { Collection names = function.getProgram().getFunctionManager().getCallingConventionNames(); List list = new ArrayList<>(names); + String callingConventionName = getCallingConventionName(); if (callingConventionName != null && !names.contains(callingConventionName)) { list.add(callingConventionName); Collections.sort(list); @@ -148,34 +98,6 @@ public class FunctionEditorModel { return program.getCompilerSpec().getPcodeInjectLibrary().getCallFixupNames(); } - void setName(String name) { - if (this.name.equals(name)) { - return; - } - this.name = name.trim(); - notifyDataChanged(); - } - - public void setCallingConventionName(String callingConventionName) { - this.callingConventionName = callingConventionName; - removeExplicitThisParameter(); - updateParameterAndReturnStorage(); - notifyDataChanged(); - } - - String getCallingConventionName() { - return callingConventionName; - } - - void setHasVarArgs(boolean b) { - hasVarArgs = b; - notifyDataChanged(); - } - - String getName() { - return name; - } - void dispose() { listener = new ModelChangeListener() { @Override @@ -191,11 +113,6 @@ public class FunctionEditorModel { } private void notifyDataChanged() { - notifyDataChanged(true); - } - - private void notifyDataChanged(boolean functionDataChanged) { - this.modelChanged |= functionDataChanged; validate(); if (listener != null) { Swing.runLater(() -> listener.dataChanged()); @@ -204,23 +121,27 @@ public class FunctionEditorModel { private void validate() { statusText = ""; - if (signatureTransformed) { - statusText = "Signature transformed due to auto-params and/or forced-indirect storage"; - signatureTransformed = false; // one-shot message + if (isSignatureTransformed) { + statusText = + "Signature transformed due to auto-params and/or forced-indirect storage change"; + isSignatureTransformed = false; // one-shot message } isValid = hasValidName() && hasValidReturnType() && hasValidReturnStorage() && hasValidParams(); + hasSignificantParameterChanges = false; if (isValid) { + hasSignificantParameterChanges = functionData.hasParameterChanges(originalFunctionData); checkUnassignedStorage(); } } private boolean hasValidReturnStorage() { - if (!allowCustomStorage) { + if (!functionData.canCustomizeStorage()) { return true; } + ParamInfo returnInfo = functionData.getReturnInfo(); VariableStorage returnStorage = returnInfo.getStorage(); DataType returnType = returnInfo.getDataType(); @@ -243,23 +164,19 @@ public class FunctionEditorModel { returnDataTypeSize + "-bytes)"; return false; } - else if (storageSize > returnDataTypeSize && storageSize <= 8 && returnDataTypeSize <= 8 && - Undefined.isUndefined(returnInfo.getDataType())) { - // grow undefined type size if needed - returnInfo.setFormalDataType(Undefined.getUndefinedDataType(storageSize)); - } return true; } private void checkUnassignedStorage() { + ParamInfo returnInfo = functionData.getReturnInfo(); VariableStorage returnStorage = returnInfo.getStorage(); DataType returnType = returnInfo.getFormalDataType(); boolean hasUnassignedStorage = returnStorage != null && returnStorage.isUnassignedStorage(); if (!hasUnassignedStorage) { - for (ParamInfo param : parameters) { + for (ParamInfo param : functionData.getParameters()) { if (param.getStorage().isUnassignedStorage()) { hasUnassignedStorage = true; break; @@ -270,9 +187,9 @@ public class FunctionEditorModel { if (hasUnassignedStorage) { statusText = "Warning: Return Storage and/or Parameter Storage is Unassigned"; } - else if (!allowCustomStorage && - Function.UNKNOWN_CALLING_CONVENTION_STRING.equals(callingConventionName)) { - if (!(VoidDataType.isVoidDataType(returnType) && parameters.isEmpty())) { + else if (!functionData.canCustomizeStorage() && Function.UNKNOWN_CALLING_CONVENTION_STRING + .equals(functionData.getCallingConventionName())) { + if (!(VoidDataType.isVoidDataType(returnType) && !functionData.hasParameters())) { statusText = "Warning: No calling convention specified. Ghidra may automatically assign one later."; } @@ -281,21 +198,82 @@ public class FunctionEditorModel { } private boolean hasValidParams() { - for (ParamInfo param : parameters) { + for (ParamInfo param : functionData.getParameters()) { if (!(isValidParamType(param) && isValidParamName(param) && isValidStorage(param))) { return false; } } - return checkForConflictingParameters(); + return hasNonConflictingStorage(); } - private boolean checkForConflictingParameters() { - // VARDO conflicting parameters + private void clearStorageConflicts() { + for (ParamInfo p : functionData.getParameters()) { + p.setHasStorageConflict(false); + } + } + + private void setStorageConflict(int ordinal) { + for (ParamInfo p : functionData.getParameters()) { + if (p.getOrdinal() == ordinal) { + p.setHasStorageConflict(true); + } + } + } + + private boolean hasNonConflictingStorage() { + clearStorageConflicts(); + if (!functionData.canCustomizeStorage()) { + return true; + } + ArrayList params = new ArrayList<>(); + for (ParamInfo paramInfo : functionData.getParameters()) { + params.add(paramInfo.getParameter(SourceType.USER_DEFINED)); + } + for (Parameter p : params) { + if (identifyStorageConflicts(p, params)) { + statusText = "One or more parameter storage conflicts exist"; + return false; + } + } return true; } + /** + * Scan the list of function parameters for those whose storage conflicts with the specified + * parameter. + * @param p parameter whose storage should be examined for conflict with others + * @param params list of function parameters to search + * @return true if storage conflict detected, else false + */ + private boolean identifyStorageConflicts(Parameter p, ArrayList params) { + try { + VariableUtilities.checkVariableConflict(params, p, p.getVariableStorage(), + conflicts -> handleConflicts(conflicts)); + } + catch (VariableSizeException e) { + // This exception occurs when any parameter storage conflict is detected/ + // Mark the tested parameter as being in conflict. + setStorageConflict(p.getOrdinal()); + return true; + } + return false; + } + + /** + * Mark all storage conflicts identified by + * {@link VariableUtilities#checkVariableConflict(List, Variable, VariableStorage, VariableConflictHandler)} + * @param conflicts parameters whose storage conflicts + * @return return false to indicate conflicts have not been resolved and additional checks + * should be disconctinued. + * @see VariableConflictHandler + */ + private boolean handleConflicts(List conflicts) { + conflicts.forEach(var -> setStorageConflict(((Parameter) var).getOrdinal())); + return false; + } + private boolean isValidStorage(ParamInfo param) { - if (!allowCustomStorage) { + if (!functionData.canCustomizeStorage()) { return true; } @@ -321,18 +299,19 @@ public class FunctionEditorModel { paramSize + "-bytes) assigned to parameter " + (param.getOrdinal() + 1); return false; } - else if (paramSize == 0) { - // assume 0-sized structure which we need to allow - } - else if (storageSize > paramSize && storageSize <= 8 && paramSize <= 8 && - Undefined.isUndefined(param.getDataType())) { - // grow undefined type size if needed - param.setFormalDataType(Undefined.getUndefinedDataType(storageSize)); - } +// else if (paramSize == 0) { +// // assume 0-sized structure which we need to allow +// } +// else if (storageSize > paramSize && storageSize <= 8 && paramSize <= 8 && +// Undefined.isUndefined(param.getDataType())) { +// // grow undefined type size if needed +// param.setFormalDataType(Undefined.getUndefinedDataType(storageSize)); +// } return true; } boolean hasValidName() { + String name = functionData.getName(); if (name.length() == 0) { statusText = "Missing function name"; return false; @@ -346,7 +325,7 @@ public class FunctionEditorModel { } private boolean hasValidReturnType() { - DataType returnType = returnInfo.getDataType(); + DataType returnType = functionData.getReturnInfo().getDataType(); if (VoidDataType.isVoidDataType(returnType)) { return true; } @@ -365,7 +344,7 @@ public class FunctionEditorModel { "Invalid name for parameter " + (param.getOrdinal() + 1) + ": " + paramName; return false; } - for (ParamInfo info : parameters) { + for (ParamInfo info : functionData.getParameters()) { if (info != param && info.getName().equals(paramName)) { statusText = "Duplicate parameter name: " + paramName; return false; @@ -393,65 +372,6 @@ public class FunctionEditorModel { return isValid; } - String getFunctionSignatureTextFromModel() { - StringBuilder buf = new StringBuilder(); - buf.append(returnInfo.getFormalDataType().getName()).append(" "); - buf.append(getNameString()); - buf.append(" ("); - int skipCount = autoParamCount; - int ordinal = 0; - for (ParamInfo param : parameters) { - if (skipCount > 0) { - --skipCount; - continue; - } - if (ordinal++ != 0) { - buf.append(", "); - } - buf.append(param.getFormalDataType().getName()); - - buf.append(" "); - - buf.append(getParamNameString(param)); - - } - if (hasVarArgs()) { - if (!parameters.isEmpty()) { - buf.append(", "); - } - buf.append(FunctionSignature.VAR_ARGS_DISPLAY_STRING); - } - else if (parameters.size() == 0) { - buf.append(FunctionSignature.VOID_PARAM_DISPLAY_STRING); - } - buf.append(')'); - return buf.toString(); - } - - String getNameString() { - return name.length() == 0 ? "?" : name; - } - - private String getParamNameString(ParamInfo param) { - return param.getName(); - } - - boolean hasVarArgs() { - return hasVarArgs; - } - - DataType getReturnType() { - return returnInfo.getDataType(); - } - - DataType getFormalReturnType() { - return returnInfo.getFormalDataType(); - } - - public boolean setFormalReturnType(DataType formalReturnType) { - return setParameterFormalDataType(returnInfo, formalReturnType); - } - String getStatusText() { if (isInParsingMode) { return PARSING_MODE_STATUS_TEXT; @@ -459,22 +379,6 @@ public class FunctionEditorModel { return statusText; } - void setIsInLine(boolean isInLine) { - if (isInLine == this.isInLine) { - return; - } - this.isInLine = isInLine; - if (isInLine) { - callFixupName = NONE_CHOICE; - } - notifyDataChanged(); - } - - void setNoReturn(boolean isNoReturn) { - this.isNoReturn = isNoReturn; - notifyDataChanged(); - } - boolean isInlineAllowed() { return !getAffectiveFunction().isExternal(); } @@ -489,204 +393,156 @@ public class FunctionEditorModel { return function.isThunk() ? function.getThunkedFunction(true) : function; } + String getFunctionSignatureTextFromModel() { + return functionData.getFunctionSignatureText(); + } + + String getNameString() { + return functionData.getNameString(); + } + + String getName() { + return functionData.getName(); + } + + void setName(String name) { + if (getName().equals(name)) { + return; + } + functionData.setName(name.trim()); + notifyDataChanged(); + } + + String getCallingConventionName() { + return functionData.getCallingConventionName(); + } + + public void setCallingConventionName(String callingConventionName) { + if (Objects.equals(getCallingConventionName(), callingConventionName)) { + return; + } + functionData.setCallingConventionName(callingConventionName); + notifyDataChanged(); + } + + boolean hasVarArgs() { + return functionData.hasVarArgs(); + } + + void setHasVarArgs(boolean b) { + if (b == hasVarArgs()) { + return; + } + functionData.setVarArgs(b); + notifyDataChanged(); + } + + DataType getReturnType() { + return functionData.getReturnInfo().getDataType(); + } + + DataType getFormalReturnType() { + return functionData.getReturnInfo().getFormalDataType(); + } + + public boolean setFormalReturnType(DataType formalReturnType) { + return setParameterFormalDataType(functionData.getReturnInfo(), formalReturnType); + } + boolean isInLine() { - return isInLine; + return functionData.isInline(); + } + + void setIsInLine(boolean isInLine) { + if (isInLine == functionData.isInline()) { + return; + } + functionData.setInline(isInLine); + if (isInLine && functionData.hasCallFixup()) { + functionData.clearCallFixup(); + } + notifyDataChanged(); } boolean isNoReturn() { - return isNoReturn; + return functionData.hasNoReturn(); } - String getCallFixupName() { - return callFixupName; - } - - void setCallFixupName(String callFixupName) { - if (callFixupName.equals(this.callFixupName)) { + void setNoReturn(boolean hasNoReturn) { + if (hasNoReturn == functionData.hasNoReturn()) { return; } - this.callFixupName = callFixupName; - if (!callFixupName.equals(NONE_CHOICE)) { - isInLine = false; + functionData.setHasNoReturn(hasNoReturn); + notifyDataChanged(); + } + + String getCallFixupChoice() { + String fixupName = functionData.getCallFixupName(); + return fixupName == null ? NONE_CHOICE : fixupName; + } + + void setCallFixupChoice(String callFixupName) { + if (callFixupName.equals(getCallFixupChoice())) { + return; + } + if (NONE_CHOICE.equals(callFixupName)) { + callFixupName = null; + } + functionData.setCallFixupName(callFixupName); + if (isInLine() && functionData.hasCallFixup()) { + functionData.setInline(false); } notifyDataChanged(); } - public void setSelectedParameterRow(int[] selectedRows) { - selectedFunctionRows = selectedRows; + public void setSelectedParameterRows(int[] selectedRows) { + selectedParams.clear(); + List parameters = functionData.getParameters(); + for (int i : selectedRows) { + ParamInfo p; + if (i == 0) { + p = functionData.getReturnInfo(); + } + else { + p = parameters.get(i - 1); + } + selectedParams.add(p); + } + Collections.sort(selectedParams); notifyDataChanged(); } - private void setSelectedRow(int row) { - selectedFunctionRows = new int[] { row }; + private ParamInfo getSelectedParam() { + return selectedParams.iterator().next(); } - private void adjustSelectionForRowRemoved(int row) { - // adjust selectedParamRows - if (selectedFunctionRows.length == 0) { - return; - } - List rows = new ArrayList<>(); - for (int i : selectedFunctionRows) { - if (i < row) { - rows.add(i); - } - if (i > row) { - rows.add(i - 1); - } - } - selectedFunctionRows = new int[rows.size()]; - int index = 0; - for (int i : rows) { - selectedFunctionRows[index++] = i; - } - } - - private void adjustSelectionForRowAdded(int row) { - // adjust selectedParamRows - if (selectedFunctionRows.length == 0) { - return; - } - List rows = new ArrayList<>(); - for (int i : selectedFunctionRows) { - if (i < row) { - rows.add(i); - } - if (i >= row) { - rows.add(i + 1); - } - } - selectedFunctionRows = new int[rows.size()]; - int index = 0; - for (int i : rows) { - selectedFunctionRows[index++] = i; - } + private void setSelectedParam(ParamInfo p) { + selectedParams.clear(); + selectedParams.add(p); } int[] getSelectedParameterRows() { - return selectedFunctionRows; + List list = new ArrayList<>(); + for (ParamInfo p : selectedParams) { + list.add(p.getOrdinal() + 1); + } + Collections.sort(list); + int[] selectedRows = new int[list.size()]; + for (int i = 0; i < selectedRows.length; i++) { + selectedRows[i] = list.get(i); + } + return selectedRows; } void addParameter() { if (listener != null) { listener.tableRowsChanged(); } - ParamInfo param = new ParamInfo(this, null, DataType.DEFAULT, - VariableStorage.UNASSIGNED_STORAGE, parameters.size()); - parameters.add(param); - fixupOrdinals(); - updateParameterAndReturnStorage(); - setSelectedRow(parameters.size()); + ParamInfo p = functionData.addNewParameter(); + setSelectedParam(p); notifyDataChanged(); } - /** - * Switch to custom storage and perform required transformations - */ - private void switchToCustomStorage() { - try { - VariableStorage returnStorage = returnInfo.getStorage(); - if (returnStorage.isForcedIndirect()) { - DataType returnType = returnInfo.getDataType(); - if (returnStorage.isVoidStorage()) { - returnType = VoidDataType.dataType; - } - returnInfo.setFormalDataType(returnType); - returnInfo.setStorage(returnStorage.clone(program)); - signatureTransformed = true; - } - autoParamCount = 0; - int paramCnt = parameters.size(); - for (int i = 0; i < paramCnt; i++) { - ParamInfo paramInfo = parameters.get(i); - DataType dt = paramInfo.getDataType(); - VariableStorage storage = paramInfo.getStorage(); - signatureTransformed |= storage.isAutoStorage(); - paramInfo.setFormalDataType(dt); - paramInfo.setStorage(storage.clone(program)); - } - } - catch (InvalidInputException e) { - throw new AssertException(e); // unexpected - } - } - - private void updateParameterAndReturnStorage() { - if (allowCustomStorage) { - return; - } - PrototypeModel effectiveCallingConvention = getEffectiveCallingConvention(); - - if (effectiveCallingConvention == null) { - for (ParamInfo info : parameters) { - info.setStorage(VariableStorage.UNASSIGNED_STORAGE); - } - return; - } - - DataType[] dataTypes = new DataType[parameters.size() - autoParamCount + 1]; - dataTypes[0] = returnInfo.getFormalDataType(); - - int index = 1; - for (int i = autoParamCount; i < parameters.size(); i++) { - dataTypes[index++] = parameters.get(i).getFormalDataType(); - } - - VariableStorage[] paramStorage = - effectiveCallingConvention.getStorageLocations(program, dataTypes, true); - - returnInfo.setStorage(paramStorage[0]); - - List oldParams = parameters; - int oldAutoCount = autoParamCount; - - parameters = new ArrayList<>(); - autoParamCount = 0; - - int ordinal = 0; - for (int i = 1; i < paramStorage.length; i++) { - VariableStorage storage = paramStorage[i]; - ParamInfo info; - if (storage.isAutoStorage()) { - DataType dt = VariableUtilities.getAutoDataType(function, - returnInfo.getFormalDataType(), storage); - try { - if (autoParamCount < oldAutoCount) { - if (oldParams.get(autoParamCount) - .getStorage() - .getAutoParameterType() != storage.getAutoParameterType()) { - adjustSelectionForRowRemoved(i); - } - } - else { - adjustSelectionForRowAdded(i); - } - info = new ParamInfo(this, - new AutoParameterImpl(dt, ++autoParamCount, storage, function)); - } - catch (InvalidInputException e) { - throw new AssertException(e); // unexpected - } - } - else { - info = oldParams.get(oldAutoCount + ordinal); - info.setStorage(storage); - ++ordinal; - } - parameters.add(info); - } - for (int i = oldAutoCount; i > autoParamCount; i--) { - adjustSelectionForRowRemoved(i); - } - fixupOrdinals(); - } - - private void fixupOrdinals() { - for (int i = 0; i < parameters.size(); i++) { - parameters.get(i).setOrdinal(i); - } - } - public void removeParameters() { if (!canRemoveParameters()) { throw new AssertException("Attempted to remove parameters when not allowed."); @@ -694,20 +550,19 @@ public class FunctionEditorModel { if (listener != null) { listener.tableRowsChanged(); } - Arrays.sort(selectedFunctionRows); - for (int i = selectedFunctionRows.length - 1; i >= 0; i--) { - int index = selectedFunctionRows[i]; - parameters.remove(index - 1); + int ordinal = selectedParams.get(0).getOrdinal(); + functionData.removeParameters(selectedParams); + selectedParams.clear(); + ParamInfo selectParam = null; + for (ParamInfo p : functionData.getParameters()) { + selectParam = p; + if (ordinal == p.getOrdinal()) { + break; + } } - if (parameters.isEmpty()) { - selectedFunctionRows = new int[0]; + if (selectParam != null) { + setSelectedParam(selectParam); } - else { - int selectRow = Math.min(selectedFunctionRows[0], parameters.size()); - selectedFunctionRows = new int[] { selectRow }; - } - fixupOrdinals(); - updateParameterAndReturnStorage(); notifyDataChanged(); } @@ -718,12 +573,8 @@ public class FunctionEditorModel { if (listener != null) { listener.tableRowsChanged(); } - int paramIndex = selectedFunctionRows[0] - 1; // first row is return value - ParamInfo param = parameters.remove(paramIndex); - parameters.add(paramIndex - 1, param); - fixupOrdinals(); - setSelectedRow(selectedFunctionRows[0] - 1); // move selection up one row - updateParameterAndReturnStorage(); + ParamInfo p = getSelectedParam(); + functionData.moveParameterUp(p.getOrdinal()); notifyDataChanged(); } @@ -734,25 +585,22 @@ public class FunctionEditorModel { if (listener != null) { listener.tableRowsChanged(); } - int paramIndex = selectedFunctionRows[0] - 1; - ParamInfo param = parameters.remove(paramIndex); - parameters.add(paramIndex + 1, param); - fixupOrdinals(); - setSelectedRow(selectedFunctionRows[0] + 1); - updateParameterAndReturnStorage(); + ParamInfo p = getSelectedParam(); + functionData.moveParameterDown(p.getOrdinal()); notifyDataChanged(); } + // TODO: Exposing this method is inappropriate public List getParameters() { - return parameters; + return functionData.getParameters(); } boolean canRemoveParameters() { - if (selectedFunctionRows.length == 0) { + if (selectedParams.size() == 0) { return false; } - for (int row : selectedFunctionRows) { - if (row <= autoParamCount) { + for (ParamInfo p : selectedParams) { + if (p.isAutoParameter() || p.isReturnParameter()) { return false; } } @@ -760,25 +608,31 @@ public class FunctionEditorModel { } boolean canMoveParameterUp() { - // remember first row (return type) and auto-params cannot be moved. - int minRowToMoveUp = 2 + autoParamCount; - if (parameters.size() > 0 && parameters.get(0).getName().equals("this")) { - minRowToMoveUp++; + if (selectedParams.size() != 1) { + return false; } - return selectedFunctionRows.length == 1 && selectedFunctionRows[0] >= minRowToMoveUp; + ParamInfo p = selectedParams.iterator().next(); + if (p.getOrdinal() <= getAutoParamCount()) { + return false; + } + return true; } boolean canMoveParameterDown() { - if (selectedFunctionRows.length != 1) { + if (selectedParams.size() != 1) { return false; } - // remember first row (return type) and auto-params cannot be moved. - int minRowToMoveDown = 1 + autoParamCount; - if (parameters.size() > 0 && parameters.get(0).getName().equals("this")) { - minRowToMoveDown++; + ParamInfo p = selectedParams.iterator().next(); + if (p.getOrdinal() < getAutoParamCount() || p.getOrdinal() >= (getParamCount() - 1)) { + return false; } - int selectedRow = selectedFunctionRows[0]; - return selectedRow >= minRowToMoveDown && selectedRow < parameters.size(); + if (canUseCustomStorage() && functionData.hasParameters()) { + List parameters = getParameters(); + if ("this".equals(parameters.get(0).getName())) { + return false; + } + } + return true; } void setParameterName(ParamInfo param, String newName) { @@ -801,7 +655,8 @@ public class FunctionEditorModel { } param.setFormalDataType(formalDataType.clone(program.getDataTypeManager())); - if (allowCustomStorage) { + + if (canUseCustomStorage()) { if (isReturn && VoidDataType.isVoidDataType(formalDataType)) { param.setStorage(VariableStorage.VOID_STORAGE); } @@ -818,7 +673,7 @@ public class FunctionEditorModel { } } else { - updateParameterAndReturnStorage(); + functionData.updateParameterAndReturnStorage(); } notifyDataChanged(); return true; @@ -851,7 +706,7 @@ public class FunctionEditorModel { } VariableStorage getReturnStorage() { - return returnInfo.getStorage(); + return functionData.getReturnInfo().getStorage(); } Function getFunction() { @@ -862,167 +717,150 @@ public class FunctionEditorModel { if (storage == null) { storage = VariableStorage.UNASSIGNED_STORAGE; } - returnInfo.setStorage(storage); + if (storage.equals(getReturnStorage())) { + return; + } + functionData.getReturnInfo().setStorage(storage); notifyDataChanged(); } + // TODO: Exposing this method is inappropriate public void setParameterStorage(ParamInfo param, VariableStorage storage) { + if (storage == null) { + storage = VariableStorage.UNASSIGNED_STORAGE; + } + if (storage.equals(param.getStorage())) { + return; + } param.setStorage(storage); notifyDataChanged(); } - private int findExplicitThisParameter() { - for (int i = 0; i < parameters.size(); i++) { - ParamInfo p = parameters.get(i); - if (!p.isAutoParameter() && Function.THIS_PARAM_NAME.equals(p.getName()) && - (p.getDataType() instanceof Pointer)) { - return i; - } - } - return -1; - } - - private void removeExplicitThisParameter() { - if (!allowCustomStorage && - CompilerSpec.CALLING_CONVENTION_thiscall.equals(callingConventionName)) { - int thisIndex = findExplicitThisParameter(); - if (thisIndex >= 0) { - parameters.remove(thisIndex); // remove explicit 'this' parameter - adjustSelectionForRowRemoved(thisIndex); - } - } - } - - private int findExplicitReturnStoragePtrParameter() { - for (int i = 0; i < parameters.size(); i++) { - ParamInfo p = parameters.get(i); - if (!p.isAutoParameter() && Function.RETURN_PTR_PARAM_NAME.equals(p.getName()) && - (p.getDataType() instanceof Pointer)) { - return i; - } - } - return -1; - } - - private DataType removeExplicitReturnStoragePtrParameter() { - int index = findExplicitReturnStoragePtrParameter(); - if (index >= 0) { - // remove explicit '__return_storage_ptr__' parameter - should always be a pointer - ParamInfo returnStoragePtrParameter = parameters.remove(index); - DataType dt = returnStoragePtrParameter.getDataType(); - adjustSelectionForRowRemoved(index); - if (dt instanceof Pointer ptr) { - return ptr.getDataType(); - } - } - return null; - } - /** * Change custom storage enablement * @param b enablement state */ public void setUseCustomizeStorage(boolean b) { - if (b == allowCustomStorage) { + if (b == canUseCustomStorage()) { return; } - allowCustomStorage = b; - if (!allowCustomStorage) { - removeExplicitThisParameter(); - DataType returnDt = removeExplicitReturnStoragePtrParameter(); - if (returnDt != null) { - returnInfo.setFormalDataType(returnDt); - returnInfo.setStorage(VariableStorage.UNASSIGNED_STORAGE); - } - updateParameterAndReturnStorage(); - } - else { - switchToCustomStorage(); - } + functionData.setUseCustomStorage(b); + isSignatureTransformed = !functionData.getFunctionSignatureText() + .equals(originalFunctionData.getFunctionSignatureText()); notifyDataChanged(); } - public boolean canCustomizeStorage() { - return allowCustomStorage; + public boolean canUseCustomStorage() { + return functionData.canCustomizeStorage(); } boolean apply() { - if (!modelChanged) { - return true; - } + return apply(true); + } + + boolean apply(boolean commitFullParamDetails) { int id = program.startTransaction("Edit Function"); try { - return applyFunctionData(); + if (applyFunctionData(commitFullParamDetails)) { + setModelUnchanged(); + return true; + } + return false; } finally { program.endTransaction(id, true); } } - private boolean applyFunctionData() { + private boolean applyFunctionData(boolean commitFullParamDetails) { try { + String name = functionData.getName(); if (!name.equals(function.getName())) { function.setName(name, SourceType.USER_DEFINED); } - boolean paramsOrReturnModified = hasModifiedParametersOrReturn(); - if (!paramsOrReturnModified) { + boolean isInline = functionData.isInline(); + if (function.isInline() != isInline) { + function.setInline(isInline); + } - // change param names without impacting signature source - for (ParamInfo paramInfo : parameters) { - if (!paramInfo.isAutoParameter() && paramInfo.isNameModified()) { - Parameter param = paramInfo.getOriginalParameter(); + boolean hasNoReturn = functionData.hasNoReturn(); + if (function.hasNoReturn() != hasNoReturn) { + function.setNoReturn(hasNoReturn); + } + + String fixupName = functionData.getCallFixupName(); + if (!SystemUtilities.isEqual(fixupName, function.getCallFixup())) { + function.setCallFixup(fixupName); + } + + boolean hasVarArgs = hasVarArgs(); + if (function.hasVarArgs() != hasVarArgs) { + function.setVarArgs(hasVarArgs); + } + + String callingConventionName = functionData.getCallingConventionName(); + boolean isCallingConventionChanged = + !Objects.equals(callingConventionName, originalFunctionData.callingConventionName); + + boolean useCustomStorage = functionData.canCustomizeStorage(); + + if (!commitFullParamDetails) { + + // Partial commit without return/parameter details - no need for source type change + + if (useCustomStorage != function.hasCustomVariableStorage()) { + function.setCustomVariableStorage(useCustomStorage); + } + + if (isCallingConventionChanged) { + function.setCallingConvention(callingConventionName); + } + + if (!hasSignificantParameterChanges && + functionData.hasParameterNamesChanged(originalFunctionData)) { + + for (ParamInfo paramInfo : functionData.getParameters()) { + Parameter param = function.getParameter(paramInfo.getOrdinal()); if (param != null) { if (param.getSymbol().isDeleted()) { // concurrent removal of param - must do full update - paramsOrReturnModified = true; break; } param.setName(paramInfo.getName(), SourceType.USER_DEFINED); } } } + + return true; } - if (paramsOrReturnModified) { - List params = new ArrayList<>(); - for (ParamInfo paramInfo : parameters) { - if (paramInfo.isAutoParameter()) { - continue; - } - params.add(paramInfo.getParameter(allowCustomStorage)); - } + SourceType sigSource = + hasSignificantParameterChanges ? SourceType.USER_DEFINED : SourceType.ANALYSIS; - // TODO: How should we handle conflicts with locals? - function.updateFunction(callingConventionName, - returnInfo.getParameter(allowCustomStorage), params, - allowCustomStorage ? FunctionUpdateType.CUSTOM_STORAGE - : FunctionUpdateType.DYNAMIC_STORAGE_FORMAL_PARAMS, - true, SourceType.USER_DEFINED); - } - else { - boolean changed = false; - if (allowCustomStorage != function.hasCustomVariableStorage()) { - function.setCustomVariableStorage(allowCustomStorage); - changed = true; - } - if (!function.getCallingConventionName().equals(callingConventionName)) { - try { - function.setCallingConvention(callingConventionName); - } - catch (InvalidInputException e) { - // user had to choose from list, so can't happen - throw new AssertException("Unexpected exception", e); - } - } - if (changed && function.getSignatureSource() == SourceType.DEFAULT && - parameters.size() == 0 && - !Function.UNKNOWN_CALLING_CONVENTION_STRING.equals(callingConventionName)) { - function.setSignatureSource(SourceType.USER_DEFINED); - } + SymbolTable symbolTable = program.getSymbolTable(); + + List params = new ArrayList<>(); + for (ParamInfo paramInfo : functionData.getParameters()) { + if (paramInfo.isAutoParameter()) { + continue; + } + SourceType source = SourceType.USER_DEFINED; + Symbol var = symbolTable.getLocalVariableSymbol(paramInfo.getName(), function); + if (var instanceof Parameter) { + source = var.getSource(); + } + params.add(paramInfo.getParameter(source)); } + + // TODO: How should we handle conflicts with locals? + function.updateFunction(callingConventionName, + functionData.getReturnInfo().getParameter(SourceType.DEFAULT), params, + useCustomStorage ? FunctionUpdateType.CUSTOM_STORAGE + : FunctionUpdateType.DYNAMIC_STORAGE_FORMAL_PARAMS, + true, sigSource); } catch (DuplicateNameException e) { Msg.showError(this, null, "Function Edit Error", e.getMessage()); @@ -1033,32 +871,15 @@ public class FunctionEditorModel { return false; } - if (function.isInline() != isInLine) { - function.setInline(isInLine); - } - - if (function.hasVarArgs() != hasVarArgs) { - function.setVarArgs(hasVarArgs); - } - - if (function.hasNoReturn() != isNoReturn) { - function.setNoReturn(isNoReturn); - } - - String fixupName = callFixupName.equals(NONE_CHOICE) ? null : callFixupName; - if (!SystemUtilities.isEqual(fixupName, function.getCallFixup())) { - function.setCallFixup(fixupName); - } - return true; } private FunctionSignature getFunctionSignature() { - FunctionDefinitionDataType funDt = new FunctionDefinitionDataType(name); - funDt.setReturnType(returnInfo.getFormalDataType()); + FunctionDefinitionDataType funDt = new FunctionDefinitionDataType(getName()); + funDt.setReturnType(getFormalReturnType()); List params = new ArrayList<>(); - for (ParamInfo paramInfo : parameters) { + for (ParamInfo paramInfo : getParameters()) { if (paramInfo.isAutoParameter()) { continue; } @@ -1067,7 +888,7 @@ public class FunctionEditorModel { params.add(new ParameterDefinitionImpl(paramName, paramDt, null)); } funDt.setArguments(params.toArray(new ParameterDefinition[params.size()])); - funDt.setVarArgs(hasVarArgs); + funDt.setVarArgs(hasVarArgs()); return funDt; } @@ -1080,7 +901,11 @@ public class FunctionEditorModel { } int getAutoParamCount() { - return autoParamCount; + return functionData.getAutoParamCount(); + } + + int getParamCount() { + return functionData.getParamCount(); } private boolean isSameSize(DataType dt1, DataType dt2) { @@ -1092,38 +917,40 @@ public class FunctionEditorModel { public void setFunctionData(FunctionDefinitionDataType functionDefinition) { - name = functionDefinition.getName(); + setName(functionDefinition.getName()); setCallingConventionName(functionDefinition.getCallingConventionName()); DataType returnDt = functionDefinition.getReturnType(); - returnInfo.setFormalDataType(returnDt); + setFormalReturnType(returnDt); - List oldParams = parameters; - parameters = new ArrayList<>(); - autoParamCount = 0; - selectedFunctionRows = new int[0]; + List oldParams = new ArrayList<>(getParameters()); + functionData.removeAllParameters(); + + List parameters = functionData.getParameters(); for (ParameterDefinition paramDefinition : functionDefinition.getArguments()) { - parameters.add(new ParamInfo(this, paramDefinition)); + parameters.add(new ParamInfo(functionData, paramDefinition)); } - hasVarArgs = functionDefinition.hasVarArgs(); - fixupOrdinals(); + setHasVarArgs(functionDefinition.hasVarArgs()); - if (allowCustomStorage) { + functionData.fixupOrdinals(); + + if (canUseCustomStorage()) { if (VoidDataType.isVoidDataType(returnDt)) { - returnInfo.setStorage(VariableStorage.VOID_STORAGE); + setReturnStorage(VariableStorage.VOID_STORAGE); } - else if (!isSameSize(returnInfo.getFormalDataType(), - functionDefinition.getReturnType())) { - returnInfo.setStorage(VariableStorage.UNASSIGNED_STORAGE); + else if (!isSameSize(getFormalReturnType(), functionDefinition.getReturnType())) { + setReturnStorage(VariableStorage.UNASSIGNED_STORAGE); } reconcileCustomStorage(oldParams, parameters); } - else { - updateParameterAndReturnStorage(); - } + + selectedParams.clear(); + + functionData.updateParameterAndReturnStorage(); + notifyDataChanged(); } @@ -1178,8 +1005,7 @@ public class FunctionEditorModel { signatureFieldText.equals(getFunctionSignatureTextFromModel()); if (isInParsingMode == signatureTextFieldInSync) { isInParsingMode = !isInParsingMode; - notifyDataChanged(false); -// notifyParsingModeChanged(); + notifyDataChanged(); } } @@ -1187,7 +1013,7 @@ public class FunctionEditorModel { setSignatureFieldText(getFunctionSignatureTextFromModel()); } - boolean hasChanges() { + boolean hasSignatureTextChanges() { return !Objects.equals(getFunctionSignatureTextFromModel(), signatureFieldText); } @@ -1197,9 +1023,9 @@ public class FunctionEditorModel { FunctionDefinitionDataType f = parser.parse(getFunctionSignature(), signatureFieldText); // Preserve calling convention and noreturn flag from current model - f.setNoReturn(isNoReturn); + f.setNoReturn(functionData.hasNoReturn()); try { - f.setCallingConvention(callingConventionName); + f.setCallingConvention(getCallingConventionName()); } catch (InvalidInputException e) { // ignore @@ -1210,17 +1036,21 @@ public class FunctionEditorModel { } int getFunctionNameStartPosition() { - return returnInfo.getFormalDataType().getName().length() + 1; + return getFormalReturnType().getName().length() + 1; } /** - * Sets the change state of the model. Normally, the model sets the modelChanged variable to true - * every time something is changed. This provides a way to for applications to make some initial changes - * but make the dialog think that nothing has changed. - * @param b the new changeState for this model + * Sets the change state of the model to unchanged. Normally, the model sets the modelChanged + * variable to true every time something is changed. This provides a way to for applications + * to make some initial changes but make the dialog think that nothing has changed. */ - public void setModelChanged(boolean b) { - modelChanged = b; + public void setModelUnchanged() { + originalFunctionData = new FunctionDataView(functionData); + resetSignatureTextField(); + validate(); + if (listener != null) { + Swing.runLater(() -> listener.dataChanged()); + } } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/FunctionVariableData.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/FunctionVariableData.java index b59308611b..bce00eaef6 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/FunctionVariableData.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/FunctionVariableData.java @@ -36,4 +36,6 @@ interface FunctionVariableData { public String getName(); public DataType getFormalDataType(); + + public boolean hasStorageConflict(); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/ParamInfo.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/ParamInfo.java index 357b74d880..14a73ddc53 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/ParamInfo.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/ParamInfo.java @@ -15,63 +15,98 @@ */ package ghidra.app.plugin.core.function.editor; +import java.util.Objects; + import ghidra.program.model.data.*; import ghidra.program.model.listing.*; import ghidra.program.model.symbol.SourceType; import ghidra.program.model.symbol.SymbolUtilities; -import ghidra.util.SystemUtilities; import ghidra.util.exception.AssertException; import ghidra.util.exception.InvalidInputException; -public class ParamInfo { +public class ParamInfo implements Comparable { - private Parameter original; private String name; private DataType formalDataType; private VariableStorage storage; + private boolean isCustomStorage; private int ordinal; - private FunctionEditorModel model; - ParamInfo(FunctionEditorModel model, Parameter parameter) { - this(model, parameter.getName(), parameter.getFormalDataType(), - parameter.getVariableStorage(), parameter.getOrdinal()); - original = parameter; + private boolean hasStorageConflict = false; + + private FunctionDataView functionData; + private Program program; + + ParamInfo(FunctionDataView functionData, Parameter parameter) { + this(functionData, parameter.getName(), parameter.getFormalDataType(), + parameter.getVariableStorage(), functionData.canCustomizeStorage(), + parameter.getOrdinal()); } - ParamInfo(FunctionEditorModel model, ParameterDefinition paramDefinition) { - this(model, paramDefinition.getName(), paramDefinition.getDataType(), - VariableStorage.UNASSIGNED_STORAGE, paramDefinition.getOrdinal()); + ParamInfo(FunctionDataView functionData, ParameterDefinition paramDefinition) { + this(functionData, paramDefinition.getName(), paramDefinition.getDataType(), + VariableStorage.UNASSIGNED_STORAGE, false, paramDefinition.getOrdinal()); + this.functionData = functionData; } - ParamInfo(FunctionEditorModel model, String name, DataType formalDataType, - VariableStorage storage, - int ordinal) { - this.model = model; + ParamInfo(FunctionDataView functionData, String name, DataType formalDataType, + VariableStorage storage, boolean isCustomStorage, int ordinal) { + this.functionData = functionData; + this.program = functionData.getProgram(); this.name = SymbolUtilities.isDefaultParameterName(name) ? null : name; this.formalDataType = formalDataType; this.storage = storage; + this.isCustomStorage = isCustomStorage; this.ordinal = ordinal; } + ParamInfo copy() { + return new ParamInfo(functionData, name, formalDataType, storage, isCustomStorage, ordinal); + } + + public boolean isSame(ParamInfo otherParam) { + if (!Objects.equals(name, otherParam.name) || + isAutoParameter() != otherParam.isAutoParameter() || + !formalDataType.equals(otherParam.getFormalDataType())) { + return false; + } + return !isCustomStorage || storage.equals(otherParam.storage); + } + @Override - public boolean equals(Object obj) { + public int compareTo(ParamInfo o) { + int c = ordinal - o.ordinal; + if (c != 0) { + return c; + } + return getName().compareTo(o.getName()); + } + + @Override + public final boolean equals(Object obj) { return this == obj; } @Override - public int hashCode() { - return getName().hashCode(); + public final int hashCode() { + return super.hashCode(); + } + + public String getName(boolean returnNullForDefault) { + if (returnNullForDefault) { + return name; + } + return getName(); } public String getName() { - return name != null ? name : SymbolUtilities.getDefaultParamName(ordinal - - model.getAutoParamCount()); + return name != null ? name + : SymbolUtilities.getDefaultParamName(ordinal - functionData.getAutoParamCount()); } DataType getDataType() { DataType dt = formalDataType; if (storage.isForcedIndirect()) { - Program program = model.getProgram(); DataTypeManager dtm = program.getDataTypeManager(); int ptrSize = storage.size(); if (ptrSize != dtm.getDataOrganization().getPointerSize()) { @@ -96,6 +131,10 @@ public class ParamInfo { return storage.isAutoStorage(); } + boolean isReturnParameter() { + return ordinal == Parameter.RETURN_ORIDINAL; + } + boolean isForcedIndirect() { return storage.isForcedIndirect(); } @@ -110,14 +149,11 @@ public class ParamInfo { } void setOrdinal(int i) { - if (original != null && original.getOrdinal() != i) { - original = null; - } this.ordinal = i; } void setName(String name) { - if (name != null && name.length() == 0) { + if (name != null && (name.length() == 0 || SymbolUtilities.isDefaultParameterName(name))) { name = null; } this.name = name; @@ -125,48 +161,27 @@ public class ParamInfo { void setFormalDataType(DataType formalDataType) { this.formalDataType = formalDataType; - original = null; } void setStorage(VariableStorage storage) { + this.isCustomStorage = functionData.canCustomizeStorage(); this.storage = storage; - if (model.canCustomizeStorage()) { - original = null; - } } - boolean isModified() { - return original == null; - } + Parameter getParameter(SourceType source) { - boolean isNameModified() { - return original != null && !SystemUtilities.isEqual(original.getName(), getName()); - } - - /** - * @return unchanged original parameter or null if new or datatype was changed - */ - Parameter getOriginalParameter() { - return original; - } - - Parameter getParameter(boolean isCustom) { - if (original != null) { - return original; - } - - VariableStorage variableStorage = isCustom ? storage : VariableStorage.UNASSIGNED_STORAGE; + VariableStorage variableStorage = + isCustomStorage ? storage : VariableStorage.UNASSIGNED_STORAGE; try { if (ordinal == Parameter.RETURN_ORIDINAL) { - return new ReturnParameterImpl(formalDataType, variableStorage, true, - model.getProgram()); + return new ReturnParameterImpl(formalDataType, variableStorage, true, program); } - // preserve original source type if name unchanged - SourceType source = SourceType.USER_DEFINED; - if (original != null && original.getName().equals(name)) { - source = original.getSource(); + String n = name; + if (n == null) { + source = SourceType.DEFAULT; + n = SymbolUtilities.getDefaultParamName(ordinal); } - return new MyParameter(name, formalDataType, variableStorage, model.getProgram(), + return new ParameterImpl(n, ordinal, formalDataType, variableStorage, true, program, source); } catch (InvalidInputException e) { @@ -174,13 +189,11 @@ public class ParamInfo { } } - private static class MyParameter extends ParameterImpl { - - MyParameter(String name, DataType dataType, VariableStorage storage, Program program, - SourceType source) throws InvalidInputException { - super(name, UNASSIGNED_ORDINAL, dataType, storage, true, program, - SourceType.USER_DEFINED); - } + boolean hasStorageConflict() { + return hasStorageConflict; } + void setHasStorageConflict(boolean state) { + hasStorageConflict = state; + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/ParameterTableModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/ParameterTableModel.java index bf93155dbf..3d284d1010 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/ParameterTableModel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/ParameterTableModel.java @@ -291,6 +291,11 @@ class ParameterTableModel extends AbstractGTableModel { public void setStorage(VariableStorage storage) { functionModel.setParameterStorage(param, storage); } + + @Override + public boolean hasStorageConflict() { + return param.hasStorageConflict(); + } } private class ReturnRowData implements FunctionVariableData { @@ -336,5 +341,10 @@ class ParameterTableModel extends AbstractGTableModel { public void setName(String name) { // no name for return type } + + @Override + public boolean hasStorageConflict() { + return false; + } } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/StorageAddressEditorDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/StorageAddressEditorDialog.java index ecb59ff7d9..5008f00f06 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/StorageAddressEditorDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/StorageAddressEditorDialog.java @@ -388,6 +388,11 @@ public class StorageAddressEditorDialog extends DialogComponentProvider public DataType getFormalDataType() { return variable.getDataType(); } + + @Override + public boolean hasStorageConflict() { + return false; + } } } diff --git a/Ghidra/Features/Base/src/test/java/ghidra/app/plugin/core/function/editor/FunctionEditorModelTest.java b/Ghidra/Features/Base/src/test/java/ghidra/app/plugin/core/function/editor/FunctionEditorModelTest.java index cda7621120..18541d0844 100644 --- a/Ghidra/Features/Base/src/test/java/ghidra/app/plugin/core/function/editor/FunctionEditorModelTest.java +++ b/Ghidra/Features/Base/src/test/java/ghidra/app/plugin/core/function/editor/FunctionEditorModelTest.java @@ -104,7 +104,7 @@ public class FunctionEditorModelTest extends AbstractGuiTest { model.setName(""); assertDataChangedCallback(true); assertEquals("", model.getName()); - assertEquals("void ? (void)", getSignatureText()); + assertEquals("void FUN_00001000 (void)", getSignatureText()); assertTrue(!model.isValid()); assertEquals("Missing function name", model.getStatusText()); } @@ -142,11 +142,11 @@ public class FunctionEditorModelTest extends AbstractGuiTest { @Test public void testCustomStorage() { - assertFalse(model.canCustomizeStorage()); + assertFalse(model.canUseCustomStorage()); model.setUseCustomizeStorage(true); - assertTrue(model.canCustomizeStorage()); + assertTrue(model.canUseCustomStorage()); model.setUseCustomizeStorage(false); - assertFalse(model.canCustomizeStorage()); + assertFalse(model.canUseCustomStorage()); } @Test @@ -155,17 +155,17 @@ public class FunctionEditorModelTest extends AbstractGuiTest { model.setIsInLine(true); assertTrue(model.isInLine()); assertTrue(model.isValid()); - assertEquals("-NONE-", model.getCallFixupName()); + assertEquals("-NONE-", model.getCallFixupChoice()); String callFixupName = model.getCallFixupNames()[0]; - model.setCallFixupName(callFixupName); - assertEquals(callFixupName, model.getCallFixupName()); + model.setCallFixupChoice(callFixupName); + assertEquals(callFixupName, model.getCallFixupChoice()); assertTrue(!model.isInLine()); model.setIsInLine(true); assertTrue(model.isInLine()); assertTrue(model.isValid()); - assertEquals("-NONE-", model.getCallFixupName()); + assertEquals("-NONE-", model.getCallFixupChoice()); } @@ -204,14 +204,14 @@ public class FunctionEditorModelTest extends AbstractGuiTest { model.addParameter(); assertEquals(3, model.getParameters().size()); - model.setSelectedParameterRow(new int[0]); + model.setSelectedParameterRows(new int[0]); // none selected, so can't remove assertEquals(0, model.getSelectedParameterRows().length); assertTrue(!model.canRemoveParameters()); // select the first entry - model.setSelectedParameterRow(new int[] { 1 }); + model.setSelectedParameterRows(new int[] { 1 }); assertEquals(1, model.getSelectedParameterRows().length); assertTrue(model.canRemoveParameters()); @@ -234,7 +234,7 @@ public class FunctionEditorModelTest extends AbstractGuiTest { assertEquals(3, model.getParameters().size()); // select the last entry - model.setSelectedParameterRow(new int[] { 3 }); + model.setSelectedParameterRows(new int[] { 3 }); assertEquals(1, model.getSelectedParameterRows().length); assertTrue(model.canRemoveParameters()); @@ -257,7 +257,7 @@ public class FunctionEditorModelTest extends AbstractGuiTest { assertEquals(3, model.getParameters().size()); // select the first and last entry - model.setSelectedParameterRow(new int[] { 1, 3 }); + model.setSelectedParameterRows(new int[] { 1, 3 }); assertEquals(2, model.getSelectedParameterRows().length); assertTrue(model.canRemoveParameters()); @@ -279,7 +279,7 @@ public class FunctionEditorModelTest extends AbstractGuiTest { assertEquals(3, model.getParameters().size()); // select all params - model.setSelectedParameterRow(new int[] { 1, 2, 3 }); + model.setSelectedParameterRows(new int[] { 1, 2, 3 }); assertEquals(3, model.getSelectedParameterRows().length); assertTrue(model.canRemoveParameters()); @@ -299,22 +299,22 @@ public class FunctionEditorModelTest extends AbstractGuiTest { model.addParameter(); // no selection, both buttons disabled - model.setSelectedParameterRow(new int[0]); + model.setSelectedParameterRows(new int[0]); assertTrue(!model.canMoveParameterUp()); assertTrue(!model.canMoveParameterDown()); // multiple selection, both buttons disabled - model.setSelectedParameterRow(new int[] { 1, 2 }); + model.setSelectedParameterRows(new int[] { 1, 2 }); assertTrue(!model.canMoveParameterUp()); assertTrue(!model.canMoveParameterDown()); // select the first param, up button disabled, down button enabled - model.setSelectedParameterRow(new int[] { 1 }); + model.setSelectedParameterRows(new int[] { 1 }); assertTrue(!model.canMoveParameterUp()); assertTrue(model.canMoveParameterDown()); // select the middle row, both buttons enabled - model.setSelectedParameterRow(new int[] { 2 }); + model.setSelectedParameterRows(new int[] { 2 }); assertTrue(model.canMoveParameterUp()); assertTrue(model.canMoveParameterDown()); @@ -338,7 +338,7 @@ public class FunctionEditorModelTest extends AbstractGuiTest { assertEquals("p3", parameters.get(2).getName()); // select the last row - model.setSelectedParameterRow(new int[] { 3 }); + model.setSelectedParameterRows(new int[] { 3 }); model.moveSelectedParameterUp(); @@ -382,7 +382,7 @@ public class FunctionEditorModelTest extends AbstractGuiTest { assertEquals("p3", parameters.get(2).getName()); // select the first row - model.setSelectedParameterRow(new int[] { 1 }); + model.setSelectedParameterRows(new int[] { 1 }); model.moveSelectedParameterDown(); @@ -661,7 +661,7 @@ public class FunctionEditorModelTest extends AbstractGuiTest { public void testParsingSignatureWithForcedIndirect() throws Exception { setupX64windows(); - assertFalse(model.canCustomizeStorage()); + assertFalse(model.canUseCustomStorage()); assertEquals("void bob (void)", getSignatureText()); assertEquals(0, model.getParameters().size()); @@ -707,7 +707,7 @@ public class FunctionEditorModelTest extends AbstractGuiTest { public void testParsingSignatureWithForcedIndirectAndAuto() throws Exception { setupX64windows(); - assertFalse(model.canCustomizeStorage()); + assertFalse(model.canUseCustomStorage()); model.setCallingConventionName("__thiscall"); @@ -826,7 +826,7 @@ public class FunctionEditorModelTest extends AbstractGuiTest { assertEquals(1, fun.getParameterCount()); model = new FunctionEditorModel(null /* use default parser*/, fun); model.setModelChangeListener(new MyModelChangeListener()); - model.setSelectedParameterRow(new int[] { 1 }); + model.setSelectedParameterRows(new int[] { 1 }); model.removeParameters(); model.apply(); assertEquals(0, fun.getParameterCount()); @@ -1703,7 +1703,7 @@ public class FunctionEditorModelTest extends AbstractGuiTest { model.parseSignatureFieldText(); assertEquals(3, model.getParameters().size()); - model.setSelectedParameterRow(new int[] { 1, 3 }); + model.setSelectedParameterRows(new int[] { 1, 3 }); model.setCallingConventionName("__thiscall"); assertEquals(4, model.getParameters().size()); @@ -1910,14 +1910,14 @@ public class FunctionEditorModelTest extends AbstractGuiTest { @Test public void testCantDeleteReturnValueInTable() { - model.setSelectedParameterRow(new int[] { 0 });// select return value row + model.setSelectedParameterRows(new int[] { 0 });// select return value row assertFalse(model.canRemoveParameters()); } @Test public void testCantMoveThisParameterUpOrDown() { model.setCallingConventionName("__thiscall"); - model.setSelectedParameterRow(new int[] { 1 });// select this parameter row + model.setSelectedParameterRows(new int[] { 1 });// select this parameter row assertFalse(model.canMoveParameterUp()); assertFalse(model.canMoveParameterDown()); diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/SpecifyCPrototypeAction.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/SpecifyCPrototypeAction.java index acb079f956..aeffece704 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/SpecifyCPrototypeAction.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/SpecifyCPrototypeAction.java @@ -60,7 +60,7 @@ public class SpecifyCPrototypeAction extends AbstractDecompilerAction { int autoParamCnt = modelParamCnt - decompParamCnt; // make sure decomp params account for injected auto params - boolean useCustom = (decompParamCnt < autoParamCnt); + boolean useCustom = (decompParamCnt < autoParamCnt) | model.canUseCustomStorage(); for (int i = 0; i < autoParamCnt && !useCustom; i++) { if (i >= decompParamCnt) { @@ -79,7 +79,7 @@ public class SpecifyCPrototypeAction extends AbstractDecompilerAction { // remove original params which replicate auto params for (int i = 0; i < autoParamCnt; i++) { // be sure to select beyond auto-params. First auto-param is on row 1 - model.setSelectedParameterRow(new int[] { autoParamCnt + 1 }); + model.setSelectedParameterRows(new int[] { autoParamCnt + 1 }); model.removeParameters(); } @@ -167,14 +167,14 @@ public class SpecifyCPrototypeAction extends AbstractDecompilerAction { // If editing the decompiled function (i.e., not a subfunction) and function // is not fully locked update the model to reflect the decompiled results if (function.getEntryPoint().equals(hf.getFunction().getEntryPoint())) { + if (function.getSignatureSource() == SourceType.DEFAULT) { - model.setUseCustomizeStorage(false); model.setFunctionData(buildSignature(hf)); verifyDynamicEditorModel(hf, model); } else if (function.getReturnType() == DataType.DEFAULT) { model.setFormalReturnType(functionPrototype.getReturnType()); - if (model.canCustomizeStorage()) { + if (model.canUseCustomStorage()) { model.setReturnStorage(functionPrototype.getReturnStorage()); } } @@ -182,9 +182,9 @@ public class SpecifyCPrototypeAction extends AbstractDecompilerAction { // make the model think it is not changed, so if the user doesn't change anything, // we don't save the changes made above. - model.setModelChanged(false); + model.setModelUnchanged(); - FunctionEditorDialog dialog = new FunctionEditorDialog(model); + FunctionEditorDialog dialog = new FunctionEditorDialog(model, true); tool.showDialog(dialog, context.getComponentProvider()); } } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/Function.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/Function.java index 35b490b1f6..96f5a84c67 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/Function.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/Function.java @@ -111,7 +111,7 @@ public interface Function extends Namespace { /** * Returns the current call-fixup name set on this instruction or null if one has not been set - * @return the name + * @return the call fixup name or null */ public String getCallFixup(); diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/VariableUtilities.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/VariableUtilities.java index 89845b3c5d..043c0e7d43 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/VariableUtilities.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/listing/VariableUtilities.java @@ -614,7 +614,7 @@ public class VariableUtilities { } if (conflicts != null) { - generateConflictException(newStorage, conflicts, 4); + generateConflictException(var, newStorage, conflicts, 4); } } @@ -622,8 +622,8 @@ public class VariableUtilities { * Check for variable storage conflict and optionally remove conflicting variables. * @param existingVariables variables to check (may contain null entries) * @param var function variable - * @param conflictHandler variable conflict handler * @param newStorage variable storage + * @param conflictHandler variable conflict handler * @throws VariableSizeException if another variable conflicts */ public static void checkVariableConflict(List existingVariables, @@ -653,7 +653,7 @@ public class VariableUtilities { if (conflicts != null) { if (conflictHandler == null || !conflictHandler.resolveConflicts(conflicts)) { - generateConflictException(newStorage, conflicts, 4); + generateConflictException(var, newStorage, conflicts, 4); } } } @@ -667,18 +667,33 @@ public class VariableUtilities { boolean resolveConflicts(List conflicts); } - private static void generateConflictException(VariableStorage newStorage, + private static void appendVariableStorageDetails(Variable var, VariableStorage storage, + StringBuilder msg) { + if (var != null) { + msg.append(var.getName()); + msg.append("{"); + msg.append(storage); + msg.append("}"); + } + else { + msg.append(storage); + } + } + + private static void generateConflictException(Variable var, VariableStorage newStorage, List conflicts, int maxConflictVarDetails) throws VariableSizeException { maxConflictVarDetails = Math.min(conflicts.size(), maxConflictVarDetails); - StringBuffer msg = new StringBuffer(); - msg.append("Variable storage conflict between " + newStorage + " and: "); + StringBuilder msg = new StringBuilder("Variable storage conflict between "); + appendVariableStorageDetails(var, newStorage, msg); + msg.append(" and "); for (int i = 0; i < maxConflictVarDetails; i++) { if (i != 0) { msg.append(", "); } - msg.append(conflicts.get(i).getVariableStorage().toString()); + Variable v = conflicts.get(i); + appendVariableStorageDetails(v, v.getVariableStorage(), msg); } if (maxConflictVarDetails < conflicts.size()) { msg.append(" ... {");