mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-04 18:29:37 +02:00
Refactor block position <-> address index map
This commit is contained in:
parent
f779dab216
commit
ad005086ab
3 changed files with 93 additions and 67 deletions
|
@ -610,7 +610,7 @@ void Funcdata::installSwitchDefaults(void)
|
||||||
PcodeOp *indop = jt->getIndirectOp();
|
PcodeOp *indop = jt->getIndirectOp();
|
||||||
BlockBasic *ind = indop->getParent();
|
BlockBasic *ind = indop->getParent();
|
||||||
// Mark any switch blocks default edge
|
// Mark any switch blocks default edge
|
||||||
if (jt->getMostCommon() != ~((uint4)0)) // If a mostcommon was found
|
if (jt->getMostCommon() != -1) // If a mostcommon was found
|
||||||
ind->setDefaultSwitch(jt->getMostCommon());
|
ind->setDefaultSwitch(jt->getMostCommon());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1205,7 +1205,7 @@ bool JumpBasic::foldInOneGuard(Funcdata *fd,GuardRecord &guard,JumpTable *jump)
|
||||||
// Adjust tables and control flow graph
|
// Adjust tables and control flow graph
|
||||||
// for new jumptable destination
|
// for new jumptable destination
|
||||||
jump->addBlockToSwitch(guardtarget,0xBAD1ABE1);
|
jump->addBlockToSwitch(guardtarget,0xBAD1ABE1);
|
||||||
jump->setMostCommonIndex(jump->numEntries()-1);
|
jump->setLastAsMostCommon();
|
||||||
fd->pushBranch(cbranchblock,1-indpath,switchbl);
|
fd->pushBranch(cbranchblock,1-indpath,switchbl);
|
||||||
guard.clear();
|
guard.clear();
|
||||||
change = true;
|
change = true;
|
||||||
|
@ -1452,7 +1452,7 @@ bool JumpBasic2::foldInOneGuard(Funcdata *fd,GuardRecord &guard,JumpTable *jump)
|
||||||
// So we don't make any special mods, in case there are extra statements in these blocks
|
// So we don't make any special mods, in case there are extra statements in these blocks
|
||||||
|
|
||||||
// The final block in the table is the single value produced by the model2 guard
|
// The final block in the table is the single value produced by the model2 guard
|
||||||
jump->setMostCommonIndex(jump->numEntries()-1); // It should be the default block
|
jump->setLastAsMostCommon(); // It should be the default block
|
||||||
guard.clear(); // Mark that we are folded
|
guard.clear(); // Mark that we are folded
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -2004,7 +2004,7 @@ bool JumpAssisted::foldInGuards(Funcdata *fd,JumpTable *jump)
|
||||||
|
|
||||||
{
|
{
|
||||||
int4 origVal = jump->getMostCommon();
|
int4 origVal = jump->getMostCommon();
|
||||||
jump->setMostCommonIndex(jump->numEntries()-1); // Default case is always the last block
|
jump->setLastAsMostCommon(); // Default case is always the last block
|
||||||
return (origVal != jump->getMostCommon());
|
return (origVal != jump->getMostCommon());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2097,21 +2097,18 @@ void JumpTable::sanityCheck(Funcdata *fd)
|
||||||
/// If no edge hits it, throw an exception.
|
/// If no edge hits it, throw an exception.
|
||||||
/// \param bl is the specific basic-block
|
/// \param bl is the specific basic-block
|
||||||
/// \return the position of the basic-block
|
/// \return the position of the basic-block
|
||||||
uint4 JumpTable::block2Position(const FlowBlock *bl) const
|
int4 JumpTable::block2Position(const FlowBlock *bl) const
|
||||||
|
|
||||||
{
|
{
|
||||||
FlowBlock *parent;
|
FlowBlock *parent;
|
||||||
uint4 position;
|
int4 position;
|
||||||
|
|
||||||
if (!isSwitchedOver())
|
|
||||||
throw LowlevelError("Jumptable switchover has not happened yet");
|
|
||||||
|
|
||||||
parent = indirect->getParent();
|
parent = indirect->getParent();
|
||||||
for(position=0;position<parent->sizeOut();++position)
|
for(position=0;position<bl->sizeIn();++position)
|
||||||
if (parent->getOut(position) == bl) break;
|
if (bl->getIn(position) == parent) break;
|
||||||
if (position==parent->sizeOut())
|
if (position==bl->sizeIn())
|
||||||
throw LowlevelError("Requested block, not in jumptable");
|
throw LowlevelError("Requested block, not in jumptable");
|
||||||
return position;
|
return bl->getInRevIndex(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// We are not doing a complete check, we are looking for a guard that has collapsed to "if (false)"
|
/// We are not doing a complete check, we are looking for a guard that has collapsed to "if (false)"
|
||||||
|
@ -2151,7 +2148,8 @@ JumpTable::JumpTable(Architecture *g,Address ad)
|
||||||
origmodel = (JumpModel *)0;
|
origmodel = (JumpModel *)0;
|
||||||
indirect = (PcodeOp *)0;
|
indirect = (PcodeOp *)0;
|
||||||
switchVarConsume = ~((uintb)0);
|
switchVarConsume = ~((uintb)0);
|
||||||
mostcommon = ~((uint4)0);
|
mostcommon = -1;
|
||||||
|
lastBlock = -1;
|
||||||
maxtablesize = 1024;
|
maxtablesize = 1024;
|
||||||
maxaddsub = 1;
|
maxaddsub = 1;
|
||||||
maxleftright = 1;
|
maxleftright = 1;
|
||||||
|
@ -2171,7 +2169,8 @@ JumpTable::JumpTable(const JumpTable *op2)
|
||||||
origmodel = (JumpModel *)0;
|
origmodel = (JumpModel *)0;
|
||||||
indirect = (PcodeOp *)0;
|
indirect = (PcodeOp *)0;
|
||||||
switchVarConsume = ~((uintb)0);
|
switchVarConsume = ~((uintb)0);
|
||||||
mostcommon = ~((uint4)0);
|
mostcommon = -1;
|
||||||
|
lastBlock = op2->lastBlock;
|
||||||
maxtablesize = op2->maxtablesize;
|
maxtablesize = op2->maxtablesize;
|
||||||
maxaddsub = op2->maxaddsub;
|
maxaddsub = op2->maxaddsub;
|
||||||
maxleftright = op2->maxleftright;
|
maxleftright = op2->maxleftright;
|
||||||
|
@ -2202,15 +2201,10 @@ JumpTable::~JumpTable(void)
|
||||||
int4 JumpTable::numIndicesByBlock(const FlowBlock *bl) const
|
int4 JumpTable::numIndicesByBlock(const FlowBlock *bl) const
|
||||||
|
|
||||||
{
|
{
|
||||||
uint4 position,count;
|
IndexPair val(block2Position(bl),0);
|
||||||
int4 i;
|
pair<vector<IndexPair>::const_iterator,vector<IndexPair>::const_iterator> range;
|
||||||
|
range = equal_range(block2addr.begin(),block2addr.end(),val,IndexPair::compareByPosition);
|
||||||
position = block2Position(bl);
|
return range.second - range.first;
|
||||||
count = 0;
|
|
||||||
for(i=0;i<blocktable.size();++i)
|
|
||||||
if (blocktable[i] == position)
|
|
||||||
count += 1;
|
|
||||||
return count;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool JumpTable::isOverride(void) const
|
bool JumpTable::isOverride(void) const
|
||||||
|
@ -2254,26 +2248,24 @@ void JumpTable::setOverride(const vector<Address> &addrtable,const Address &nadd
|
||||||
int4 JumpTable::getIndexByBlock(const FlowBlock *bl,int4 i) const
|
int4 JumpTable::getIndexByBlock(const FlowBlock *bl,int4 i) const
|
||||||
|
|
||||||
{
|
{
|
||||||
uint4 position,count;
|
IndexPair val(block2Position(bl),0);
|
||||||
int4 j;
|
int4 count = 0;
|
||||||
|
vector<IndexPair>::const_iterator iter = lower_bound(block2addr.begin(),block2addr.end(),val,IndexPair::compareByPosition);
|
||||||
position = block2Position(bl);
|
while(iter != block2addr.end()) {
|
||||||
count = 0;
|
if ((*iter).blockPosition == val.blockPosition) {
|
||||||
for(j=0;j<blocktable.size();++j) {
|
if (count == i)
|
||||||
if (blocktable[j] == position) {
|
return (*iter).addressIndex;
|
||||||
if (i==count) return j;
|
|
||||||
count += 1;
|
count += 1;
|
||||||
}
|
}
|
||||||
|
++iter;
|
||||||
}
|
}
|
||||||
throw LowlevelError("Could not get jumptable index for block");
|
throw LowlevelError("Could not get jumptable index for block");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the most common address destination by supplying an index into the address table
|
void JumpTable::setLastAsMostCommon(void)
|
||||||
/// \param tableind is the supplied address table index
|
|
||||||
void JumpTable::setMostCommonIndex(uint4 tableind)
|
|
||||||
|
|
||||||
{
|
{
|
||||||
mostcommon = blocktable[tableind]; // Translate addresstable index to switch block out index
|
mostcommon = lastBlock;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This is used to add address targets from guard branches if they are
|
/// This is used to add address targets from guard branches if they are
|
||||||
|
@ -2285,50 +2277,56 @@ void JumpTable::addBlockToSwitch(BlockBasic *bl,uintb lab)
|
||||||
|
|
||||||
{
|
{
|
||||||
addresstable.push_back(bl->getStart());
|
addresstable.push_back(bl->getStart());
|
||||||
uint4 pos = indirect->getParent()->sizeOut();
|
lastBlock = indirect->getParent()->sizeOut(); // The block WILL be added to the end of the out-edges
|
||||||
blocktable.push_back(pos);
|
block2addr.push_back(IndexPair(lastBlock,addresstable.size()-1));
|
||||||
label.push_back(lab);
|
label.push_back(lab);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert addresses in \b this table to actual targeted basic-blocks.
|
/// Convert addresses in \b this table to actual targeted basic-blocks.
|
||||||
///
|
///
|
||||||
/// This constructs a map from each address table entry to the corresponding
|
/// This constructs a map from each out-edge from the basic-block containing the BRANCHIND
|
||||||
/// out-edge from the the basic-block containing the BRANCHIND. The most common
|
/// to addresses in the table targetting that out-block. The most common
|
||||||
/// address table entry is also calculated here.
|
/// address table entry is also calculated here.
|
||||||
/// \param flow is used to resolve address targets
|
/// \param flow is used to resolve address targets
|
||||||
void JumpTable::switchOver(const FlowInfo &flow)
|
void JumpTable::switchOver(const FlowInfo &flow)
|
||||||
|
|
||||||
{
|
{
|
||||||
FlowBlock *parent,*tmpbl;
|
FlowBlock *parent,*tmpbl;
|
||||||
uint4 pos;
|
int4 pos;
|
||||||
int4 i,j,count,maxcount;
|
|
||||||
PcodeOp *op;
|
PcodeOp *op;
|
||||||
|
|
||||||
blocktable.clear();
|
block2addr.clear();
|
||||||
blocktable.resize(addresstable.size(),~((uint4)0));
|
block2addr.reserve(addresstable.size());
|
||||||
mostcommon = ~((uint4)0); // There is no "mostcommon"
|
|
||||||
maxcount = 1; // If the maxcount is less than 2
|
|
||||||
parent = indirect->getParent();
|
parent = indirect->getParent();
|
||||||
|
|
||||||
for(i=0;i<addresstable.size();++i) {
|
for(int4 i=0;i<addresstable.size();++i) {
|
||||||
Address addr = addresstable[i];
|
Address addr = addresstable[i];
|
||||||
if (blocktable[i] != ~((uint4)0)) continue;
|
|
||||||
op = flow.target(addr);
|
op = flow.target(addr);
|
||||||
tmpbl = op->getParent();
|
tmpbl = op->getParent();
|
||||||
for(pos=0;pos<parent->sizeOut();++pos)
|
for(pos=0;pos<parent->sizeOut();++pos)
|
||||||
if (parent->getOut(pos) == tmpbl) break;
|
if (parent->getOut(pos) == tmpbl) break;
|
||||||
if (pos==parent->sizeOut())
|
if (pos==parent->sizeOut())
|
||||||
throw LowlevelError("Jumptable destination not linked");
|
throw LowlevelError("Jumptable destination not linked");
|
||||||
count = 0;
|
block2addr.push_back(IndexPair(pos,i));
|
||||||
for(j=i;j<addresstable.size();++j) {
|
}
|
||||||
if (addr == addresstable[j]) {
|
lastBlock = block2addr.back().blockPosition; // Out-edge of last address in table
|
||||||
|
sort(block2addr.begin(),block2addr.end());
|
||||||
|
|
||||||
|
mostcommon = -1; // There is no "mostcommon"
|
||||||
|
int4 maxcount = 1; // If the maxcount is less than 2
|
||||||
|
vector<IndexPair>::const_iterator iter = block2addr.begin();
|
||||||
|
while(iter != block2addr.end()) {
|
||||||
|
int4 curPos = (*iter).blockPosition;
|
||||||
|
vector<IndexPair>::const_iterator nextiter = iter;
|
||||||
|
int4 count = 0;
|
||||||
|
while(nextiter != block2addr.end() && (*nextiter).blockPosition == curPos) {
|
||||||
count += 1;
|
count += 1;
|
||||||
blocktable[j] = pos;
|
++nextiter;
|
||||||
}
|
}
|
||||||
}
|
iter = nextiter;
|
||||||
if (count>maxcount) {
|
if (count > maxcount) {
|
||||||
maxcount = count;
|
maxcount = count;
|
||||||
mostcommon = pos;
|
mostcommon = curPos;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2361,15 +2359,16 @@ void JumpTable::trivialSwitchOver(void)
|
||||||
{
|
{
|
||||||
FlowBlock *parent;
|
FlowBlock *parent;
|
||||||
|
|
||||||
blocktable.clear();
|
block2addr.clear();
|
||||||
blocktable.resize(addresstable.size(),~((uint4)0));
|
block2addr.reserve(addresstable.size());
|
||||||
parent = indirect->getParent();
|
parent = indirect->getParent();
|
||||||
|
|
||||||
if (parent->sizeOut() != addresstable.size())
|
if (parent->sizeOut() != addresstable.size())
|
||||||
throw LowlevelError("Trivial addresstable and switch block size do not match");
|
throw LowlevelError("Trivial addresstable and switch block size do not match");
|
||||||
for(uint4 i=0;i<parent->sizeOut();++i)
|
for(uint4 i=0;i<parent->sizeOut();++i)
|
||||||
blocktable[i] = i; // blocktable corresponds exactly to outlist of switch block
|
block2addr.push_back(IndexPair(i,i)); // Addresses corresponds exactly to out-edges of switch block
|
||||||
mostcommon = ~((uint4)0); // There is no "mostcommon"
|
lastBlock = parent->sizeOut()-1;
|
||||||
|
mostcommon = -1; // There is no "mostcommon"
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The addresses that the raw BRANCHIND op might branch to itself are recovered,
|
/// The addresses that the raw BRANCHIND op might branch to itself are recovered,
|
||||||
|
@ -2518,7 +2517,8 @@ void JumpTable::clear(void)
|
||||||
delete jmodel;
|
delete jmodel;
|
||||||
jmodel = (JumpModel *)0;
|
jmodel = (JumpModel *)0;
|
||||||
}
|
}
|
||||||
blocktable.clear();
|
block2addr.clear();
|
||||||
|
lastBlock = -1;
|
||||||
label.clear();
|
label.clear();
|
||||||
loadpoints.clear();
|
loadpoints.clear();
|
||||||
indirect = (PcodeOp *)0;
|
indirect = (PcodeOp *)0;
|
||||||
|
|
|
@ -492,17 +492,26 @@ public:
|
||||||
/// It knows how to map from specific switch variable values to the destination
|
/// It knows how to map from specific switch variable values to the destination
|
||||||
/// \e case block and how to label the value.
|
/// \e case block and how to label the value.
|
||||||
class JumpTable {
|
class JumpTable {
|
||||||
|
/// \brief An address table index and its corresponding out-edge
|
||||||
|
struct IndexPair {
|
||||||
|
int4 blockPosition; ///< Out-edge index for the basic-block
|
||||||
|
int4 addressIndex; /// Index of address targetting the basic-block
|
||||||
|
IndexPair(int4 pos,int4 index) { blockPosition = pos; addressIndex = index; } ///< Constructor
|
||||||
|
bool operator<(const IndexPair &op2) const; ///< Compare by position then by index
|
||||||
|
static bool compareByPosition(const IndexPair &op1,const IndexPair &op2); ///< Compare just by position
|
||||||
|
};
|
||||||
Architecture *glb; ///< Architecture under which this jump-table operates
|
Architecture *glb; ///< Architecture under which this jump-table operates
|
||||||
JumpModel *jmodel; ///< Current model of how the jump table is implemented in code
|
JumpModel *jmodel; ///< Current model of how the jump table is implemented in code
|
||||||
JumpModel *origmodel; ///< Initial jump table model, which may be incomplete
|
JumpModel *origmodel; ///< Initial jump table model, which may be incomplete
|
||||||
vector<Address> addresstable; ///< Raw addresses in the jump-table
|
vector<Address> addresstable; ///< Raw addresses in the jump-table
|
||||||
vector<uint4> blocktable; ///< Addresses converted to basic blocks
|
vector<IndexPair> block2addr; ///< Map from basic-blocks to address table index
|
||||||
vector<uintb> label; ///< The case label for each explicit target
|
vector<uintb> label; ///< The case label for each explicit target
|
||||||
vector<LoadTable> loadpoints; ///< Any recovered in-memory data for the jump-table
|
vector<LoadTable> loadpoints; ///< Any recovered in-memory data for the jump-table
|
||||||
Address opaddress; ///< Absolute address of the BRANCHIND jump
|
Address opaddress; ///< Absolute address of the BRANCHIND jump
|
||||||
PcodeOp *indirect; ///< CPUI_BRANCHIND linked to \b this jump-table
|
PcodeOp *indirect; ///< CPUI_BRANCHIND linked to \b this jump-table
|
||||||
uintb switchVarConsume; ///< Bits of the switch variable being consumed
|
uintb switchVarConsume; ///< Bits of the switch variable being consumed
|
||||||
uint4 mostcommon; ///< The out-edge corresponding to the most common address in the address table
|
int4 mostcommon; ///< The out-edge corresponding to the most common address in the address table
|
||||||
|
int4 lastBlock; ///< Block out-edge corresponding to last entry in the address table
|
||||||
uint4 maxtablesize; ///< Maximum table size we allow to be built (sanity check)
|
uint4 maxtablesize; ///< Maximum table size we allow to be built (sanity check)
|
||||||
uint4 maxaddsub; ///< Maximum ADDs or SUBs to normalize
|
uint4 maxaddsub; ///< Maximum ADDs or SUBs to normalize
|
||||||
uint4 maxleftright; ///< Maximum shifts to normalize
|
uint4 maxleftright; ///< Maximum shifts to normalize
|
||||||
|
@ -512,13 +521,12 @@ class JumpTable {
|
||||||
void recoverModel(Funcdata *fd); ///< Attempt recovery of the jump-table model
|
void recoverModel(Funcdata *fd); ///< Attempt recovery of the jump-table model
|
||||||
void trivialSwitchOver(void); ///< Switch \b this table over to a trivial model
|
void trivialSwitchOver(void); ///< Switch \b this table over to a trivial model
|
||||||
void sanityCheck(Funcdata *fd); ///< Perform sanity check on recovered address targets
|
void sanityCheck(Funcdata *fd); ///< Perform sanity check on recovered address targets
|
||||||
uint4 block2Position(const FlowBlock *bl) const; ///< Convert a basic-block to an out-edge index from the switch.
|
int4 block2Position(const FlowBlock *bl) const; ///< Convert a basic-block to an out-edge index from the switch.
|
||||||
static bool isReachable(PcodeOp *op); ///< Check if the given PcodeOp still seems reachable in its function
|
static bool isReachable(PcodeOp *op); ///< Check if the given PcodeOp still seems reachable in its function
|
||||||
public:
|
public:
|
||||||
JumpTable(Architecture *g,Address ad=Address()); ///< Constructor
|
JumpTable(Architecture *g,Address ad=Address()); ///< Constructor
|
||||||
JumpTable(const JumpTable *op2); ///< Copy constructor
|
JumpTable(const JumpTable *op2); ///< Copy constructor
|
||||||
~JumpTable(void); ///< Destructor
|
~JumpTable(void); ///< Destructor
|
||||||
bool isSwitchedOver(void) const { return !blocktable.empty(); } ///< Return \b true if addresses converted to basic-blocks
|
|
||||||
bool isRecovered(void) const { return !addresstable.empty(); } ///< Return \b true if a model has been recovered
|
bool isRecovered(void) const { return !addresstable.empty(); } ///< Return \b true if a model has been recovered
|
||||||
bool isLabelled(void) const { return !label.empty(); } ///< Return \b true if \e case labels are computed
|
bool isLabelled(void) const { return !label.empty(); } ///< Return \b true if \e case labels are computed
|
||||||
bool isOverride(void) const; ///< Return \b true if \b this table was manually overridden
|
bool isOverride(void) const; ///< Return \b true if \b this table was manually overridden
|
||||||
|
@ -537,7 +545,7 @@ public:
|
||||||
int4 numIndicesByBlock(const FlowBlock *bl) const;
|
int4 numIndicesByBlock(const FlowBlock *bl) const;
|
||||||
int4 getIndexByBlock(const FlowBlock *bl,int4 i) const;
|
int4 getIndexByBlock(const FlowBlock *bl,int4 i) const;
|
||||||
Address getAddressByIndex(int4 i) const { return addresstable[i]; } ///< Get the i-th address table entry
|
Address getAddressByIndex(int4 i) const { return addresstable[i]; } ///< Get the i-th address table entry
|
||||||
void setMostCommonIndex(uint4 tableind); ///< Set the most common jump-table target by index
|
void setLastAsMostCommon(void); ///< Set the most common jump-table target to be the last address in the table
|
||||||
void setMostCommonBlock(uint4 bl) { mostcommon = bl; } ///< Set the most common jump-table target by out-edge
|
void setMostCommonBlock(uint4 bl) { mostcommon = bl; } ///< Set the most common jump-table target by out-edge
|
||||||
void setLoadCollect(bool val) { collectloads = val; } ///< Set whether LOAD records should be collected
|
void setLoadCollect(bool val) { collectloads = val; } ///< Set whether LOAD records should be collected
|
||||||
void addBlockToSwitch(BlockBasic *bl,uintb lab); ///< Force a given basic-block to be a switch destination
|
void addBlockToSwitch(BlockBasic *bl,uintb lab); ///< Force a given basic-block to be a switch destination
|
||||||
|
@ -554,4 +562,22 @@ public:
|
||||||
void restoreXml(const Element *el); ///< Recover \b this jump-table from a \<jumptable> XML tag
|
void restoreXml(const Element *el); ///< Recover \b this jump-table from a \<jumptable> XML tag
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// \param op2 is the other IndexPair to compare with \b this
|
||||||
|
/// \return \b true if \b this is ordered before the other IndexPair
|
||||||
|
inline bool JumpTable::IndexPair::operator<(const IndexPair &op2) const
|
||||||
|
|
||||||
|
{
|
||||||
|
if (blockPosition != op2.blockPosition) return (blockPosition < op2.blockPosition);
|
||||||
|
return (addressIndex < op2.addressIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// \param op1 is the first IndexPair to compare
|
||||||
|
/// \param op2 is the second IndexPair to compare
|
||||||
|
/// \return \b true if op1 is ordered before op2
|
||||||
|
inline bool JumpTable::IndexPair::compareByPosition(const IndexPair &op1,const IndexPair &op2)
|
||||||
|
|
||||||
|
{
|
||||||
|
return (op1.blockPosition < op2.blockPosition);
|
||||||
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue