mirror of
https://github.com/fadden/6502bench.git
synced 2024-12-05 19:50:33 +00:00
5c6d1b5016
The address table formatter tries to map the calculated addresses to offsets within the file. When that fails, it currently just gives up. We now try to resolve external addresses to project/platform symbols. (issue #160)
576 lines
23 KiB
C#
576 lines
23 KiB
C#
/*
|
|
* Copyright 2019 faddenSoft
|
|
*
|
|
* 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.
|
|
*/
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
|
|
using Asm65;
|
|
using CommonUtil;
|
|
|
|
namespace SourceGen.WpfGui {
|
|
/// <summary>
|
|
/// Split-address table generator.
|
|
/// </summary>
|
|
public partial class FormatAddressTable : Window, INotifyPropertyChanged {
|
|
/// Format descriptors to apply.
|
|
/// </summary>
|
|
public SortedList<int, FormatDescriptor> NewFormatDescriptors { get; private set; }
|
|
|
|
/// <summary>
|
|
/// User labels to apply.
|
|
/// </summary>
|
|
public Dictionary<int, Symbol> NewUserLabels { get; private set; }
|
|
|
|
/// <summary>
|
|
/// All target offsets found. The list may contain redundant entries.
|
|
/// </summary>
|
|
public List<int> AllTargetOffsets { get; private set; }
|
|
|
|
/// <summary>
|
|
/// If set, targets are offset by one for RTS/RTL.
|
|
/// </summary>
|
|
public bool IsAdjustedForReturn {
|
|
get { return mIsAdjustedForReturn; }
|
|
set { mIsAdjustedForReturn = value; OnPropertyChanged(); UpdateControls(); }
|
|
}
|
|
private bool mIsAdjustedForReturn;
|
|
|
|
/// <summary>
|
|
/// If set, this is a split-address table, e.g. all of the low bytes are followed
|
|
/// by all of the high bytes.
|
|
/// </summary>
|
|
public bool IsSplitTable {
|
|
get { return mIsSplitTable; }
|
|
set { mIsSplitTable = value; OnPropertyChanged(); UpdateControls(); }
|
|
}
|
|
private bool mIsSplitTable;
|
|
|
|
/// <summary>
|
|
/// If set, caller will add code start points to targets.
|
|
/// </summary>
|
|
public bool WantCodeStartPoints {
|
|
get { return mWantCodeStartPoints; }
|
|
set {
|
|
mWantCodeStartPoints = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
private bool mWantCodeStartPoints;
|
|
|
|
/// <summary>
|
|
/// Set to true to make the "incompatible with selection" message visible.
|
|
/// </summary>
|
|
public bool IncompatibleSelectionVisibility {
|
|
get { return mIncompatibleSelectionVisibility; }
|
|
set {
|
|
mIncompatibleSelectionVisibility = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
private bool mIncompatibleSelectionVisibility;
|
|
|
|
/// <summary>
|
|
/// Set to true to make the "invalid constant" message visible.
|
|
/// </summary>
|
|
public bool InvalidConstantVisibility {
|
|
get { return mInvalidConstantVisibility; }
|
|
set {
|
|
mInvalidConstantVisibility = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
private bool mInvalidConstantVisibility;
|
|
|
|
/// <summary>
|
|
/// Set to true when valid output is available.
|
|
/// </summary>
|
|
private bool mOutputReady;
|
|
|
|
/// <summary>
|
|
/// Set to true when input is valid. Controls whether the OK button is enabled.
|
|
/// </summary>
|
|
public bool IsValid {
|
|
get { return mIsValid; }
|
|
set { mIsValid = value; OnPropertyChanged(); }
|
|
}
|
|
private bool mIsValid;
|
|
|
|
public class OutputPreviewItem {
|
|
public string Addr { get; private set; }
|
|
public string Offset { get; private set; }
|
|
public string Symbol { get; private set; }
|
|
|
|
public OutputPreviewItem(string addr, string offset, string symbol) {
|
|
Addr = addr;
|
|
Offset = offset;
|
|
Symbol = symbol;
|
|
}
|
|
}
|
|
public ObservableCollection<OutputPreviewItem> OutputPreviewList { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Selected offsets. An otherwise contiguous range of offsets can be broken up
|
|
/// by user-specified labels and address discontinuities, so this needs to be
|
|
/// processed by range.
|
|
/// </summary>
|
|
private TypedRangeSet mSelection;
|
|
|
|
/// <summary>
|
|
/// Project reference.
|
|
/// </summary>
|
|
private DisasmProject mProject;
|
|
|
|
/// <summary>
|
|
/// Formatter to use when displaying addresses and hex values.
|
|
/// </summary>
|
|
private Formatter mFormatter;
|
|
|
|
/// <summary>
|
|
/// Reentrancy block for UpdateControls().
|
|
/// </summary>
|
|
private bool mUpdating;
|
|
|
|
// INotifyPropertyChanged implementation
|
|
public event PropertyChangedEventHandler PropertyChanged;
|
|
private void OnPropertyChanged([CallerMemberName] string propertyName = "") {
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
|
}
|
|
|
|
|
|
public FormatAddressTable(Window owner, DisasmProject project, TypedRangeSet selection,
|
|
Formatter formatter) {
|
|
InitializeComponent();
|
|
Owner = owner;
|
|
DataContext = this;
|
|
|
|
mProject = project;
|
|
mFormatter = formatter;
|
|
mSelection = selection;
|
|
IsValid = false;
|
|
|
|
OutputPreviewList = new ObservableCollection<OutputPreviewItem>();
|
|
}
|
|
|
|
private void Window_Loaded(object sender, RoutedEventArgs e) {
|
|
mUpdating = true;
|
|
|
|
string fmt, infoStr;
|
|
if (mSelection.RangeCount == 1 && mSelection.Count == 1) {
|
|
infoStr = (string)FindResource("str_SingleByte");
|
|
} else if (mSelection.RangeCount == 1) {
|
|
fmt = (string)FindResource("str_SingleGroup");
|
|
infoStr = string.Format(fmt, mSelection.Count);
|
|
} else {
|
|
fmt = (string)FindResource("str_MultiGroup");
|
|
infoStr = string.Format(fmt, mSelection.Count, mSelection.RangeCount);
|
|
}
|
|
selectionInfoLabel.Text = infoStr;
|
|
|
|
width16Radio.IsChecked = true;
|
|
lowFirstPartRadio.IsChecked = true;
|
|
highSecondPartRadio.IsChecked = true;
|
|
bankNthPartRadio.IsChecked = true;
|
|
|
|
IncompatibleSelectionVisibility = InvalidConstantVisibility = false;
|
|
|
|
if (mProject.CpuDef.HasAddr16) {
|
|
// Disable the 24-bit option. Having 16-bit selected will disable the rest.
|
|
width24Radio.IsEnabled = false;
|
|
}
|
|
|
|
mUpdating = false;
|
|
UpdateControls();
|
|
}
|
|
|
|
private void OkButton_Click(object sender, RoutedEventArgs e) {
|
|
DialogResult = true;
|
|
}
|
|
|
|
private void UpdateControls() {
|
|
if (mUpdating) {
|
|
return;
|
|
}
|
|
mUpdating = true; // no re-entry
|
|
|
|
// handled with XAML bindings
|
|
//lowThirdPartRadio.Enabled = width24Radio.Checked;
|
|
//highThirdPartRadio.Enabled = width24Radio.Checked;
|
|
//bankByteGroupBox.Enabled = width24Radio.Checked;
|
|
|
|
lowSecondPartRadio.IsEnabled = true;
|
|
|
|
// If the user selects "constant" for high byte or bank byte, then there is no
|
|
// 3rd part available for low/high, so we need to turn those back off.
|
|
if (width24Radio.IsChecked == true) {
|
|
bool haveThree = !(highConstantRadio.IsChecked == true ||
|
|
bankConstantRadio.IsChecked == true);
|
|
lowThirdPartRadio.IsEnabled = haveThree;
|
|
highThirdPartRadio.IsEnabled = haveThree;
|
|
|
|
// If "constant" is selected for high byte *and* bank byte, then there's no
|
|
// 2nd part available for low.
|
|
if (highConstantRadio.IsChecked == true && bankConstantRadio.IsChecked == true) {
|
|
lowSecondPartRadio.IsEnabled = false;
|
|
}
|
|
} else {
|
|
// For 16-bit address, if high byte is constant, then there's no second
|
|
// part for the low byte.
|
|
if (highConstantRadio.IsChecked == true) {
|
|
lowSecondPartRadio.IsEnabled = false;
|
|
}
|
|
}
|
|
|
|
// Was a now-invalidated radio button selected before?
|
|
if (!lowThirdPartRadio.IsEnabled && lowThirdPartRadio.IsChecked == true) {
|
|
// low now invalid, switch to whatever high isn't using
|
|
if (highFirstPartRadio.IsChecked == true) {
|
|
lowSecondPartRadio.IsChecked = true;
|
|
} else {
|
|
lowFirstPartRadio.IsChecked = true;
|
|
}
|
|
}
|
|
if (width16Radio.IsChecked == true && highThirdPartRadio.IsChecked == true) {
|
|
// high now invalid, switch to whatever low isn't using
|
|
if (lowFirstPartRadio.IsChecked == true) {
|
|
highSecondPartRadio.IsChecked = true;
|
|
} else {
|
|
highFirstPartRadio.IsChecked = true;
|
|
}
|
|
}
|
|
if (!lowSecondPartRadio.IsEnabled && lowSecondPartRadio.IsChecked == true) {
|
|
// Should only happen when high part is constant.
|
|
Debug.Assert(highFirstPartRadio.IsChecked == false);
|
|
lowFirstPartRadio.IsChecked = true;
|
|
}
|
|
|
|
mUpdating = false;
|
|
UpdatePreview();
|
|
|
|
IsValid = mOutputReady;
|
|
}
|
|
|
|
private void WidthRadio_CheckedChanged(object sender, RoutedEventArgs e) {
|
|
UpdateControls();
|
|
}
|
|
|
|
private void LowByte_CheckedChanged(object sender, RoutedEventArgs e) {
|
|
// If we conflict with the high byte, change the high byte.
|
|
if (lowFirstPartRadio.IsChecked == true && highFirstPartRadio.IsChecked == true) {
|
|
highSecondPartRadio.IsChecked = true;
|
|
} else if (lowSecondPartRadio.IsChecked == true && highSecondPartRadio.IsChecked == true) {
|
|
highFirstPartRadio.IsChecked = true;
|
|
} else if (lowThirdPartRadio.IsChecked == true && highThirdPartRadio.IsChecked == true) {
|
|
highFirstPartRadio.IsChecked = true;
|
|
}
|
|
UpdateControls();
|
|
}
|
|
|
|
private void HighByte_CheckedChanged(object sender, RoutedEventArgs e) {
|
|
// If we conflict with the low byte, change the low byte.
|
|
if (lowFirstPartRadio.IsChecked == true && highFirstPartRadio.IsChecked == true) {
|
|
lowSecondPartRadio.IsChecked = true;
|
|
} else if (lowSecondPartRadio.IsChecked == true && highSecondPartRadio.IsChecked == true) {
|
|
lowFirstPartRadio.IsChecked = true;
|
|
} else if (lowThirdPartRadio.IsChecked == true && highThirdPartRadio.IsChecked == true) {
|
|
lowFirstPartRadio.IsChecked = true;
|
|
}
|
|
UpdateControls();
|
|
}
|
|
|
|
private void BankByte_CheckedChanged(object sender, EventArgs e) {
|
|
UpdateControls();
|
|
}
|
|
|
|
private void HighConstantTextBox_TextChanged(object sender, TextChangedEventArgs e) {
|
|
highConstantRadio.IsChecked = true;
|
|
UpdateControls();
|
|
}
|
|
|
|
private void BankConstantTextBox_TextChanged(object sender, TextChangedEventArgs e) {
|
|
bankConstantRadio.IsChecked = true;
|
|
UpdateControls();
|
|
}
|
|
|
|
private void UpdatePreview() {
|
|
mOutputReady = false;
|
|
|
|
int minDiv;
|
|
|
|
if (width16Radio.IsChecked == true) {
|
|
if (highConstantRadio.IsChecked == true) {
|
|
minDiv = 1;
|
|
} else {
|
|
minDiv = 2;
|
|
}
|
|
} else {
|
|
if (highConstantRadio.IsChecked == true) {
|
|
if (bankConstantRadio.IsChecked == true) {
|
|
minDiv = 1;
|
|
} else {
|
|
minDiv = 2;
|
|
}
|
|
} else {
|
|
if (bankConstantRadio.IsChecked == true) {
|
|
minDiv = 2;
|
|
} else {
|
|
minDiv = 3;
|
|
}
|
|
}
|
|
}
|
|
|
|
IncompatibleSelectionVisibility = InvalidConstantVisibility = false;
|
|
|
|
// Start by clearing the previous contents of the list. If something goes
|
|
// wrong, we want to show the error messages on an empty list.
|
|
OutputPreviewList.Clear();
|
|
|
|
if ((mSelection.Count % minDiv) != 0) {
|
|
IncompatibleSelectionVisibility = true;
|
|
return;
|
|
}
|
|
|
|
int highConstant = -1;
|
|
if (highConstantRadio.IsChecked == true) {
|
|
if (!Number.TryParseInt(highConstantTextBox.Text, out highConstant,
|
|
out int unused) || (highConstant != (byte) highConstant)) {
|
|
InvalidConstantVisibility = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
int bankConstant = -1;
|
|
if (bankConstantRadio.IsEnabled && bankConstantRadio.IsChecked == true) {
|
|
if (!Number.TryParseInt(bankConstantTextBox.Text, out bankConstant,
|
|
out int unused) || (bankConstant != (byte) bankConstant)) {
|
|
InvalidConstantVisibility = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Looks valid, generate format list.
|
|
GenerateFormats(minDiv, highConstant, bankConstant);
|
|
}
|
|
|
|
private void GenerateFormats(int div, int highConst, int bankConst) {
|
|
SortedList<int, FormatDescriptor> newDfds = new SortedList<int, FormatDescriptor>();
|
|
Dictionary<int, Symbol> newLabels = new Dictionary<int, Symbol>();
|
|
List<int> targetOffsets = new List<int>();
|
|
bool isBigEndian;
|
|
|
|
// Identify the offset where each set of data starts.
|
|
Debug.Assert(mSelection.Count % div == 0);
|
|
int span = mSelection.Count / div;
|
|
int lowOff, highOff, bankOff;
|
|
int stride;
|
|
|
|
if (lowFirstPartRadio.IsChecked == true) {
|
|
lowOff = 0;
|
|
isBigEndian = false;
|
|
} else if (lowSecondPartRadio.IsChecked == true) {
|
|
lowOff = 1;
|
|
isBigEndian = true;
|
|
} else if (lowThirdPartRadio.IsChecked == true) {
|
|
lowOff = 2;
|
|
isBigEndian = true;
|
|
} else {
|
|
Debug.Assert(false);
|
|
lowOff = -1;
|
|
isBigEndian = false;
|
|
}
|
|
if (highFirstPartRadio.IsChecked == true) {
|
|
highOff = 0;
|
|
} else if (highSecondPartRadio.IsChecked == true) {
|
|
highOff = 1;
|
|
} else if (highThirdPartRadio.IsChecked == true) {
|
|
highOff = 2;
|
|
} else {
|
|
highOff = -1; // use constant
|
|
}
|
|
if (width24Radio.IsChecked == true) {
|
|
if (bankNthPartRadio.IsChecked == true) {
|
|
// Use whichever part isn't being used by the other two.
|
|
if (lowOff != 0 && highOff != 0) {
|
|
bankOff = 0;
|
|
} else if (lowOff != 1 && highOff != 1) {
|
|
bankOff = 1;
|
|
} else {
|
|
Debug.Assert(lowOff != 2 && highOff != 2);
|
|
bankOff = 2;
|
|
}
|
|
} else {
|
|
bankOff = -1; // use constant
|
|
}
|
|
} else {
|
|
bankOff = -1; // use constant
|
|
|
|
// Set the constant bank value equal to whichever bank the table lives in.
|
|
// This is correct for "JMP (addr,X)". We use the offset of the first item
|
|
// in the range set, which is a little awkward to get to.
|
|
int offset = 0;
|
|
foreach (TypedRangeSet.Tuple tup in mSelection) {
|
|
offset = tup.Value;
|
|
break;
|
|
}
|
|
int addr = mProject.AddrMap.OffsetToAddress(offset);
|
|
bankConst = (byte)(addr >> 16);
|
|
}
|
|
|
|
if (IsSplitTable) {
|
|
// Split table, so stride is 1 and each section start is determined by the span.
|
|
stride = 1;
|
|
lowOff *= span;
|
|
highOff *= span;
|
|
bankOff *= span;
|
|
} else {
|
|
// For non-split table, the stride is the width of each entry.
|
|
stride = 1;
|
|
if (highOff >= 0) {
|
|
stride++;
|
|
}
|
|
if (bankOff >= 0) {
|
|
stride++;
|
|
}
|
|
}
|
|
|
|
Debug.WriteLine("FormatAddressTable: stride=" + stride + " span=" + span +
|
|
" count=" + mSelection.Count);
|
|
Debug.WriteLine(" low=" + lowOff + " high=" + highOff + " bank=" + bankOff);
|
|
|
|
// The TypedRangeSet doesn't have an index operation, so copy the values into
|
|
// an array.
|
|
int[] offsets = new int[mSelection.Count];
|
|
int index = 0;
|
|
foreach (TypedRangeSet.Tuple tup in mSelection) {
|
|
offsets[index++] = tup.Value;
|
|
}
|
|
|
|
int adj = IsAdjustedForReturn ? 1 : 0; // RTS/RTL adjustment
|
|
|
|
// Walk through the file data, generating addresses as we go.
|
|
byte[] fileData = mProject.FileData;
|
|
for (int i = 0; i < span; i++) {
|
|
byte low, high, bank;
|
|
|
|
low = fileData[offsets[lowOff + i * stride]];
|
|
if (highOff >= 0) {
|
|
high = fileData[offsets[highOff + i * stride]];
|
|
} else {
|
|
high = (byte) highConst;
|
|
}
|
|
if (bankOff >= 0) {
|
|
bank = fileData[offsets[bankOff + i * stride]];
|
|
} else {
|
|
bank = (byte) bankConst;
|
|
}
|
|
|
|
// This is the target address.
|
|
int addr = ((bank << 16) | (high << 8) | low) + adj;
|
|
|
|
// Map it to a target label. Start by looking for an offset inside file bounds.
|
|
int targetOffset = mProject.AddrMap.AddressToOffset(offsets[0], addr);
|
|
string targetLabel = null;
|
|
if (targetOffset < 0) {
|
|
// Address not within file bounds; check for a matching project/platform symbol.
|
|
// The method we're calling will potentially return globals and locals, but
|
|
// those should have been found by AddressToOffset.
|
|
Symbol ppsym = mProject.SymbolTable.FindNonVariableByAddress(addr,
|
|
OpDef.MemoryEffect.ReadModifyWrite);
|
|
if (ppsym != null) {
|
|
targetLabel = ppsym.Label;
|
|
AddPreviewItem(addr, -1, targetLabel);
|
|
}
|
|
} else {
|
|
// Note the same target offset may appear more than once.
|
|
targetOffsets.Add(targetOffset);
|
|
|
|
// If there's a user-defined label there already, use it. Otherwise, we'll
|
|
// need to generate one.
|
|
if (mProject.UserLabels.TryGetValue(targetOffset, out Symbol sym)) {
|
|
targetLabel = sym.Label;
|
|
AddPreviewItem(addr, targetOffset, targetLabel);
|
|
} else {
|
|
// Generate a symbol that's unique vs. the symbol table. We don't need
|
|
// it to be unique vs. the labels we're generating here, because we
|
|
// won't generate identical labels for different addresses, and we do
|
|
// want to generate a single label if more than one table entry refers
|
|
// to the same target.
|
|
Symbol tmpSym = AutoLabel.GenerateUniqueForAddress(addr,
|
|
mProject.SymbolTable, "T");
|
|
// tmpSym was returned as an auto-label, make it a user label instead
|
|
// (with global scope).
|
|
tmpSym = new Symbol(tmpSym.Label, tmpSym.Value, Symbol.Source.User,
|
|
Symbol.Type.GlobalAddr, Symbol.LabelAnnotation.Generated);
|
|
newLabels[targetOffset] = tmpSym; // overwrites previous
|
|
targetLabel = tmpSym.Label;
|
|
AddPreviewItem(addr, targetOffset, "(+) " + targetLabel);
|
|
}
|
|
}
|
|
|
|
if (targetLabel != null) {
|
|
if (IsSplitTable) {
|
|
// Now we need to create format descriptors for the addresses where we
|
|
// extracted the low, high, and bank values.
|
|
newDfds.Add(offsets[lowOff + i * stride], FormatDescriptor.Create(1,
|
|
new WeakSymbolRef(targetLabel, WeakSymbolRef.Part.Low), false));
|
|
if (highOff >= 0) {
|
|
newDfds.Add(offsets[highOff + i * stride], FormatDescriptor.Create(1,
|
|
new WeakSymbolRef(targetLabel, WeakSymbolRef.Part.High), false));
|
|
}
|
|
if (bankOff >= 0) {
|
|
newDfds.Add(offsets[bankOff + i * stride], FormatDescriptor.Create(1,
|
|
new WeakSymbolRef(targetLabel, WeakSymbolRef.Part.Bank), false));
|
|
}
|
|
} else {
|
|
// Create a single format descriptor that spans all bytes. Note we
|
|
// don't want to use lowOff here -- we want to put the format on
|
|
// whichever byte came first.
|
|
// TODO(maybe): we don't correctly deal with a "scrambled" non-split
|
|
// 24-bit table, i.e. low then bank then high. This is not really
|
|
// a thing, but we should either prevent it or punt to single-byte
|
|
// like we do for split tables.
|
|
Debug.Assert(stride >= 1 && stride <= 3);
|
|
newDfds.Add(offsets[0 + i * stride], FormatDescriptor.Create(stride,
|
|
new WeakSymbolRef(targetLabel, WeakSymbolRef.Part.Low), isBigEndian));
|
|
}
|
|
} else {
|
|
AddPreviewItem(addr, -1, Res.Strings.INVALID_ADDRESS);
|
|
}
|
|
}
|
|
|
|
NewFormatDescriptors = newDfds;
|
|
NewUserLabels = newLabels;
|
|
AllTargetOffsets = targetOffsets;
|
|
|
|
// Don't show ready if all entries are invalid. It's okay if some work and
|
|
// some don't.
|
|
mOutputReady = (NewFormatDescriptors.Count > 0);
|
|
}
|
|
|
|
private void AddPreviewItem(int addr, int offset, string label) {
|
|
OutputPreviewItem newItem = new OutputPreviewItem(
|
|
mFormatter.FormatAddress(addr, !mProject.CpuDef.HasAddr16),
|
|
(offset >= 0 ? mFormatter.FormatOffset24(offset) : "---"),
|
|
label);
|
|
OutputPreviewList.Add(newItem);
|
|
}
|
|
}
|
|
}
|