mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-04 02:09:44 +02:00
GP-973 Added ApplyClassFunctionSignatureUpdatesScript and ApplyClassFunctionDefinitionUpdatesScript fix-up scripts for when users update RecoveredClass virtual function signatures or definitions.
This commit is contained in:
parent
8c488aaacf
commit
b6a5ce659b
7 changed files with 1306 additions and 299 deletions
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
//Script to graph class hierarchies given metadata found in class structure description that
|
||||
// was applied using the ExtractClassInfoFromRTTIScript.
|
||||
// was applied using the RecoverClassesFromRTTIScript.
|
||||
//@category C++
|
||||
import java.util.*;
|
||||
|
||||
|
|
|
@ -1,191 +0,0 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
//Script to update the given class's virtual functions' function signature data types and
|
||||
// the given class's vfunction structure field name for any differing functions in
|
||||
// the class virtual function table(s). To run, put the cursor on any of the desired class's
|
||||
// virtual functions or at the top a class vftable. The script will not work if the <class>_vftable
|
||||
// structure is not applied to the vftable using the ExtractClassInfoFromRTTIScript.
|
||||
//@category C++
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import ghidra.app.script.GhidraScript;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.data.*;
|
||||
import ghidra.program.model.listing.*;
|
||||
import ghidra.program.model.symbol.*;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.exception.DuplicateNameException;
|
||||
|
||||
public class UpdateClassFunctionDataScript extends GhidraScript {
|
||||
@Override
|
||||
public void run() throws Exception {
|
||||
|
||||
if (currentProgram == null) {
|
||||
println("There is no open program");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Function function = getFunctionContaining(currentAddress);
|
||||
if (function != null) {
|
||||
|
||||
Namespace parentNamespace = function.getParentNamespace();
|
||||
|
||||
Parameter thisParam = function.getParameter(0);
|
||||
if (thisParam.getName().equals("this")) {
|
||||
DataType dataType = thisParam.getDataType();
|
||||
if (dataType instanceof Pointer) {
|
||||
Pointer pointer = (Pointer) dataType;
|
||||
DataType baseDataType = pointer.getDataType();
|
||||
if (baseDataType.getName().equals(parentNamespace.getName())) {
|
||||
// call update
|
||||
println("updating class " + parentNamespace.getName());
|
||||
updateClassFunctionDataTypes(parentNamespace);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Symbol primarySymbol = currentProgram.getSymbolTable().getPrimarySymbol(currentAddress);
|
||||
if (primarySymbol.getName().equals("vftable") ||
|
||||
primarySymbol.getName().substring(1).startsWith("vftable")) {
|
||||
updateClassFunctionDataTypes(primarySymbol.getParentNamespace());
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void updateClassFunctionDataTypes(Namespace classNamespace)
|
||||
throws CancelledException, DuplicateNameException, DataTypeDependencyException {
|
||||
|
||||
List<Symbol> classVftableSymbols = getClassVftableSymbols(classNamespace);
|
||||
|
||||
Iterator<Symbol> vftableIterator = classVftableSymbols.iterator();
|
||||
while (vftableIterator.hasNext()) {
|
||||
monitor.checkCanceled();
|
||||
Symbol vftableSymbol = vftableIterator.next();
|
||||
Address vftableAddress = vftableSymbol.getAddress();
|
||||
Data data = getDataAt(vftableAddress);
|
||||
if (data == null) {
|
||||
continue;
|
||||
}
|
||||
DataType baseDataType = data.getBaseDataType();
|
||||
if (!(baseDataType instanceof Structure)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Structure vfunctionStructure = (Structure) baseDataType;
|
||||
|
||||
Category category = getDataTypeCategory(vfunctionStructure);
|
||||
|
||||
if (category == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// check that the structure name starts with <classname>_vtable and that it is in
|
||||
// the dt folder with name <classname>
|
||||
if (category.getName().equals(classNamespace.getName()) &&
|
||||
vfunctionStructure.getName().startsWith(classNamespace.getName() + "_vftable")) {
|
||||
println(
|
||||
"Updating vfunction signature data types and (if necessary) vtable structure for vftable at address " +
|
||||
vftableAddress.toString());
|
||||
updateVfunctionDataTypes(data, vfunctionStructure, vftableAddress);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to find any function signatures in the given vfunction structure that have changed
|
||||
* and update the function signature data types
|
||||
* @throws DuplicateNameException
|
||||
* @throws DataTypeDependencyException
|
||||
*/
|
||||
private void updateVfunctionDataTypes(Data structureAtAddress, Structure vfunctionStructure,
|
||||
Address vftableAddress) throws DuplicateNameException, DataTypeDependencyException {
|
||||
|
||||
DataTypeManager dtMan = currentProgram.getDataTypeManager();
|
||||
|
||||
int numVfunctions = structureAtAddress.getNumComponents();
|
||||
|
||||
for (int i = 0; i < numVfunctions; i++) {
|
||||
Data dataComponent = structureAtAddress.getComponent(i);
|
||||
|
||||
Reference[] referencesFrom = dataComponent.getReferencesFrom();
|
||||
if (referencesFrom.length != 1) {
|
||||
continue;
|
||||
}
|
||||
Address functionAddress = referencesFrom[0].getToAddress();
|
||||
Function vfunction = getFunctionAt(functionAddress);
|
||||
if (vfunction == null) {
|
||||
continue;
|
||||
}
|
||||
FunctionDefinitionDataType functionSignatureDataType =
|
||||
(FunctionDefinitionDataType) vfunction.getSignature();
|
||||
|
||||
DataTypeComponent structureComponent = vfunctionStructure.getComponent(i);
|
||||
DataType componentDataType = structureComponent.getDataType();
|
||||
if (!(componentDataType instanceof Pointer)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Pointer pointer = (Pointer) componentDataType;
|
||||
DataType pointedToDataType = pointer.getDataType();
|
||||
if (functionSignatureDataType.equals(pointedToDataType)) {
|
||||
continue;
|
||||
}
|
||||
// update data type with new new signature
|
||||
dtMan.replaceDataType(pointedToDataType, functionSignatureDataType, true);
|
||||
if (!structureComponent.getFieldName().equals(vfunction.getName())) {
|
||||
structureComponent.setFieldName(vfunction.getName());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private Category getDataTypeCategory(DataType dataType) {
|
||||
|
||||
DataTypeManager dataTypeManager = currentProgram.getDataTypeManager();
|
||||
CategoryPath originalPath = dataType.getCategoryPath();
|
||||
Category category = dataTypeManager.getCategory(originalPath);
|
||||
|
||||
return category;
|
||||
}
|
||||
|
||||
private List<Symbol> getClassVftableSymbols(Namespace classNamespace)
|
||||
throws CancelledException {
|
||||
|
||||
SymbolTable symbolTable = currentProgram.getSymbolTable();
|
||||
List<Symbol> vftableSymbols = new ArrayList<Symbol>();
|
||||
|
||||
SymbolIterator symbols = symbolTable.getSymbols(classNamespace);
|
||||
while (symbols.hasNext()) {
|
||||
|
||||
monitor.checkCanceled();
|
||||
Symbol symbol = symbols.next();
|
||||
if (symbol.getName().equals("vftable") ||
|
||||
symbol.getName().substring(1).startsWith("vftable")) {
|
||||
vftableSymbols.add(symbol);
|
||||
}
|
||||
|
||||
}
|
||||
return vftableSymbols;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
/* ###
|
||||
* 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.
|
||||
*/
|
||||
// Script to apply any changes the user has made to class virtual function definitions, ie ones they
|
||||
// have edited in the data type manager. To run the script, put the cursor on any member of the
|
||||
// desired class in the listing then run the script. For each function definition in the given class
|
||||
// that differs from the associated function signature in the listing, the script will update the
|
||||
// listing function signatures of any related virtual vunctions (ie parents and children) and update
|
||||
// related data types such as function definitions of the given class and related classes and also
|
||||
// field names in related vftable structures.
|
||||
// Note: The script will not work if the vftable structures were not originally applied to
|
||||
// the vftables using the RecoverClassesFromRTTIScript.
|
||||
// At some point, the Ghidra API will be updated to do this automatically instead of needing the script to do so.
|
||||
//@category C++
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import classrecovery.RecoveredClassUtils;
|
||||
import ghidra.app.script.GhidraScript;
|
||||
import ghidra.program.model.data.FunctionDefinition;
|
||||
import ghidra.program.model.data.Structure;
|
||||
import ghidra.program.model.listing.Function;
|
||||
import ghidra.program.model.symbol.Namespace;
|
||||
import ghidra.program.model.symbol.Symbol;
|
||||
|
||||
public class ApplyClassFunctionDefinitionUpdatesScript extends GhidraScript {
|
||||
@Override
|
||||
public void run() throws Exception {
|
||||
|
||||
if (currentProgram == null) {
|
||||
println("There is no open program");
|
||||
return;
|
||||
}
|
||||
|
||||
RecoveredClassUtils classUtils = new RecoveredClassUtils(currentProgram, currentLocation,
|
||||
state.getTool(), this, false, false, false, monitor);
|
||||
|
||||
Namespace classNamespace = classUtils.getClassNamespace(currentAddress);
|
||||
if (classNamespace == null) {
|
||||
println(
|
||||
"Either cannot retrieve class namespace or cursor is not in a member of a class namepace");
|
||||
return;
|
||||
}
|
||||
|
||||
List<Symbol> classVftableSymbols = classUtils.getClassVftableSymbols(classNamespace);
|
||||
if (classVftableSymbols.isEmpty()) {
|
||||
println("There are no vftables in this class");
|
||||
return;
|
||||
}
|
||||
|
||||
println(
|
||||
"Applying differing function definitions for class " + classNamespace.getName(true));
|
||||
|
||||
List<Object> changedItems =
|
||||
classUtils.applyNewFunctionDefinitions(classNamespace, classVftableSymbols);
|
||||
|
||||
if (changedItems.isEmpty()) {
|
||||
println("No differences found for class " + classNamespace.getName(true) +
|
||||
" between the vftable listing function signatures and their associated data type manager function definition data types");
|
||||
return;
|
||||
}
|
||||
|
||||
List<Structure> structuresOnList = classUtils.getStructuresOnList(changedItems);
|
||||
List<FunctionDefinition> functionDefinitionsOnList =
|
||||
classUtils.getFunctionDefinitionsOnList(changedItems);
|
||||
List<Function> functionsOnList = classUtils.getFunctionsOnList(changedItems);
|
||||
|
||||
println();
|
||||
println("Updated structures:");
|
||||
for (Structure structure : structuresOnList) {
|
||||
monitor.checkCanceled();
|
||||
println(structure.getPathName());
|
||||
}
|
||||
|
||||
println();
|
||||
println("Updated function definitions:");
|
||||
for (FunctionDefinition functionDef : functionDefinitionsOnList) {
|
||||
monitor.checkCanceled();
|
||||
println(functionDef.getPathName());
|
||||
}
|
||||
|
||||
println();
|
||||
println("Updated functions:");
|
||||
for (Function function : functionsOnList) {
|
||||
monitor.checkCanceled();
|
||||
println(function.getEntryPoint().toString());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
/* ###
|
||||
* 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.
|
||||
*/
|
||||
// Script to apply any changes the user has made to class virtual function signatures, ie ones they
|
||||
// have edited in the listing. To run the script, put the cursor on any member of the desired class in
|
||||
// the listing then run the script. For each function signature in the given class that differs from
|
||||
// the associated function definition in the data type manager, the script will update the listing
|
||||
// function signatures of any related virtual vunctions (ie parents and children) and update related
|
||||
// data types such as function definitions of the given class and related classes and also field names
|
||||
// in related vftable structures.
|
||||
// Note: The script will not work if the vftable structures were not originally applied to
|
||||
// the vftables using the RecoverClassesFromRTTIScript.
|
||||
// At some point, the Ghidra API will be updated to do this automatically instead of needing the script to do so.
|
||||
//@category C++
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import classrecovery.RecoveredClassUtils;
|
||||
import ghidra.app.script.GhidraScript;
|
||||
import ghidra.program.model.data.FunctionDefinition;
|
||||
import ghidra.program.model.data.Structure;
|
||||
import ghidra.program.model.listing.Function;
|
||||
import ghidra.program.model.symbol.Namespace;
|
||||
import ghidra.program.model.symbol.Symbol;
|
||||
|
||||
public class ApplyClassFunctionSignatureUpdatesScript extends GhidraScript {
|
||||
@Override
|
||||
public void run() throws Exception {
|
||||
|
||||
if (currentProgram == null) {
|
||||
println("There is no open program");
|
||||
return;
|
||||
}
|
||||
|
||||
RecoveredClassUtils classUtils = new RecoveredClassUtils(currentProgram, currentLocation,
|
||||
state.getTool(), this, false, false, false, monitor);
|
||||
|
||||
Namespace classNamespace = classUtils.getClassNamespace(currentAddress);
|
||||
if (classNamespace == null) {
|
||||
println(
|
||||
"Either cannot retrieve class namespace or cursor is not in a member of a class namepace");
|
||||
return;
|
||||
}
|
||||
|
||||
List<Symbol> classVftableSymbols = classUtils.getClassVftableSymbols(classNamespace);
|
||||
if (classVftableSymbols.isEmpty()) {
|
||||
println("There are no vftables in this class");
|
||||
return;
|
||||
}
|
||||
|
||||
println("Applying differing virtual function signatures for class " +
|
||||
classNamespace.getName(true));
|
||||
|
||||
List<Object> changedItems =
|
||||
classUtils.applyNewFunctionSignatures(classNamespace, classVftableSymbols);
|
||||
|
||||
if (changedItems.isEmpty()) {
|
||||
println("No differences found for class " + classNamespace.getName(true) +
|
||||
" between the listing virtual function signatures and their associated data type manager function definition data types.");
|
||||
return;
|
||||
}
|
||||
|
||||
List<Structure> structuresOnList = classUtils.getStructuresOnList(changedItems);
|
||||
List<FunctionDefinition> functionDefinitionsOnList =
|
||||
classUtils.getFunctionDefinitionsOnList(changedItems);
|
||||
List<Function> functionsOnList = classUtils.getFunctionsOnList(changedItems);
|
||||
|
||||
println();
|
||||
println("Updated structures:");
|
||||
for (Structure structure : structuresOnList) {
|
||||
monitor.checkCanceled();
|
||||
println(structure.getPathName());
|
||||
}
|
||||
|
||||
println();
|
||||
println("Updated function definitions:");
|
||||
for (FunctionDefinition functionDef : functionDefinitionsOnList) {
|
||||
monitor.checkCanceled();
|
||||
println(functionDef.getPathName());
|
||||
}
|
||||
|
||||
println();
|
||||
println("Updated functions:");
|
||||
for (Function function : functionsOnList) {
|
||||
monitor.checkCanceled();
|
||||
println(function.getEntryPoint().toString());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -37,6 +37,17 @@
|
|||
// NOTE: Windows class recovery is more complete and tested than gcc class recovery, which is still
|
||||
// in early stages of development. Gcc class data types have not been recovered yet but if the program
|
||||
// has DWARF, there will be some amount of data recovered by the DWARF analyzer in the DWARF data folder.
|
||||
// NOTE: For likely the best results, run this script on freshly analyzed programs. No testing has been
|
||||
// done on user marked-up programs.
|
||||
// NOTE: After running this script if you edit function signatures in the listing for a particular
|
||||
// class and wish to update the corresponding class data (function definition data types, vftable
|
||||
// structure field names, ...) then you can run the ApplyClassFunctionSignatureUpdatesScript.java
|
||||
// to have it do so for you.
|
||||
// Conversely, if you update a particular class's function definitions in the data type manager and
|
||||
// wish to have related function signatures in the listing updated, as well as other data types that
|
||||
// are related, then run the ApplyClassFunctionDefinitionsUpdatesScript.java to do so. At some point,
|
||||
// the Ghidra API will be updated to do this all automatically instead of needing the scripts to do so.
|
||||
|
||||
//@category C++
|
||||
|
||||
import java.io.File;
|
||||
|
@ -124,8 +135,6 @@ public class RecoverClassesFromRTTIScript extends GhidraScript {
|
|||
|
||||
int defaultPointerSize;
|
||||
|
||||
RecoveredClassUtils classUtils;
|
||||
|
||||
RTTIClassRecoverer recoverClassesFromRTTI;
|
||||
|
||||
ExtraScriptUtils extraUtils;
|
||||
|
@ -143,25 +152,18 @@ public class RecoverClassesFromRTTIScript extends GhidraScript {
|
|||
|
||||
|
||||
if (isWindows()) {
|
||||
|
||||
// TODO: check for typeinfo using the other way i had then pull the hasRTTI in and if first
|
||||
// is true and second isn't then run the analyzer - move all this into a method
|
||||
isPDBLoaded = isPDBLoadedInProgram();
|
||||
nameVfunctions = !isPDBLoaded;
|
||||
recoverClassesFromRTTI = new RTTIWindowsClassRecoverer(currentProgram,
|
||||
currentLocation, state.getTool(), this, BOOKMARK_FOUND_FUNCTIONS,
|
||||
USE_SHORT_TEMPLATE_NAMES_IN_STRUCTURE_FIELDS, nameVfunctions, isPDBLoaded, monitor);
|
||||
|
||||
}
|
||||
else if (isGcc()) {
|
||||
|
||||
// for now assume gcc has named vfunctions
|
||||
// for now assume gcc has named vfunctions until a way to check is developed
|
||||
nameVfunctions = true;
|
||||
recoverClassesFromRTTI = new RTTIGccClassRecoverer(currentProgram, currentLocation,
|
||||
state.getTool(), this, BOOKMARK_FOUND_FUNCTIONS,
|
||||
USE_SHORT_TEMPLATE_NAMES_IN_STRUCTURE_FIELDS, nameVfunctions, monitor);
|
||||
|
||||
|
||||
}
|
||||
else {
|
||||
println("This script will not work on this program type");
|
||||
|
@ -185,9 +187,6 @@ public class RecoverClassesFromRTTIScript extends GhidraScript {
|
|||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
decompilerUtils = recoverClassesFromRTTI.getDecompilerUtils();
|
||||
DecompInterface decompInterface = decompilerUtils.getDecompilerInterface();
|
||||
|
||||
|
@ -962,12 +961,12 @@ public class RecoverClassesFromRTTIScript extends GhidraScript {
|
|||
println("Total number of indetermined constructor/destructors: " +
|
||||
remainingIndeterminates.size());
|
||||
|
||||
//TODO: need to get from the new class
|
||||
// println("Total fixed incorrect FID functions: " + badFIDFunctions.size());
|
||||
// println("Total resolved functions that had multiple FID possiblities: " +
|
||||
// resolvedFIDFunctions.size());
|
||||
// println("Total fixed functions that had incorrect data types due to incorrect FID: " +
|
||||
// fixedFIDFunctions.size());
|
||||
println("Total fixed incorrect FID functions: " +
|
||||
recoverClassesFromRTTI.getBadFIDFunctions().size());
|
||||
println("Total resolved functions that had multiple FID possiblities: " +
|
||||
recoverClassesFromRTTI.getResolvedFIDFunctions().size());
|
||||
println("Total fixed functions that had incorrect data types due to incorrect FID: " +
|
||||
recoverClassesFromRTTI.getFixedFIDFunctions().size());
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -506,20 +506,19 @@ public class ExtraScriptUtils extends FlatProgramAPI {
|
|||
* Method to create a function in the given program at the given address
|
||||
* @param prog the given program
|
||||
* @param addr the given address
|
||||
* @param tMonitor a cancellable task monitor
|
||||
* @return true if the function was created, false otherwise
|
||||
*/
|
||||
public boolean createFunction(Program prog, Address addr, TaskMonitor tMonitor) {
|
||||
public boolean createFunction(Program prog, Address addr) {
|
||||
|
||||
try {
|
||||
AddressSet subroutineAddresses = getSubroutineAddresses(prog, addr, tMonitor);
|
||||
AddressSet subroutineAddresses = getSubroutineAddresses(prog, addr);
|
||||
if (subroutineAddresses.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CreateFunctionCmd cmd = new CreateFunctionCmd(null, subroutineAddresses.getMinAddress(),
|
||||
null, SourceType.DEFAULT);
|
||||
if (cmd.applyTo(prog, tMonitor)) {
|
||||
if (cmd.applyTo(prog, monitor)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -535,12 +534,11 @@ public class ExtraScriptUtils extends FlatProgramAPI {
|
|||
* Method to figure out a subroutine address set given an address contained in it
|
||||
* @param program the given program
|
||||
* @param address address in the potential subroutine
|
||||
* @param monitor allows canceling
|
||||
* @return address set of the subroutine to be created
|
||||
* @throws CancelledException if cancelled
|
||||
*/
|
||||
public static AddressSet getSubroutineAddresses(Program program, Address address,
|
||||
TaskMonitor monitor) throws CancelledException {
|
||||
public AddressSet getSubroutineAddresses(Program program, Address address)
|
||||
throws CancelledException {
|
||||
|
||||
// Create a new address set to hold the entire selection.
|
||||
AddressSet subroutineAddresses = new AddressSet();
|
||||
|
|
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue