mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-04 18:29:37 +02:00
TransformVar split arrays
This commit is contained in:
parent
0af94c23c1
commit
16864aa187
2 changed files with 68 additions and 65 deletions
|
@ -24,6 +24,8 @@
|
||||||
void TransformVar::createReplacement(Funcdata *fd)
|
void TransformVar::createReplacement(Funcdata *fd)
|
||||||
|
|
||||||
{
|
{
|
||||||
|
if (replacement != (Varnode *)0)
|
||||||
|
return; // Replacement already created
|
||||||
switch(type) {
|
switch(type) {
|
||||||
case TransformVar::preexisting:
|
case TransformVar::preexisting:
|
||||||
replacement = vn;
|
replacement = vn;
|
||||||
|
@ -32,6 +34,7 @@ void TransformVar::createReplacement(Funcdata *fd)
|
||||||
replacement = fd->newConstant(byteSize,val);
|
replacement = fd->newConstant(byteSize,val);
|
||||||
break;
|
break;
|
||||||
case TransformVar::normal_temp:
|
case TransformVar::normal_temp:
|
||||||
|
case TransformVar::piece_temp:
|
||||||
if (def == (TransformOp *)0)
|
if (def == (TransformOp *)0)
|
||||||
replacement = fd->newUnique(byteSize);
|
replacement = fd->newUnique(byteSize);
|
||||||
else
|
else
|
||||||
|
@ -104,6 +107,15 @@ bool TransformOp::attemptInsertion(Funcdata *fd)
|
||||||
return true; // Already inserted
|
return true; // Already inserted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TransformManager::~TransformManager(void)
|
||||||
|
|
||||||
|
{
|
||||||
|
map<int4,TransformVar *>::iterator iter;
|
||||||
|
for(iter=pieceMap.begin();iter!=pieceMap.end();++iter) {
|
||||||
|
delete [] (*iter).second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// \brief Should the address of the given Varnode be preserved when constructing a piece
|
/// \brief Should the address of the given Varnode be preserved when constructing a piece
|
||||||
///
|
///
|
||||||
/// A new Varnode will be created that represents a logical piece of the given Varnode.
|
/// A new Varnode will be created that represents a logical piece of the given Varnode.
|
||||||
|
@ -127,16 +139,15 @@ bool TransformManager::preserveAddress(Varnode *vn,int4 bitSize,int4 lsbOffset)
|
||||||
TransformVar *TransformManager::newPreexistingVarnode(Varnode *vn)
|
TransformVar *TransformManager::newPreexistingVarnode(Varnode *vn)
|
||||||
|
|
||||||
{
|
{
|
||||||
newVarnodes.push_back(TransformVar());
|
TransformVar *res = new TransformVar[1];
|
||||||
TransformVar *res = &newVarnodes.back();
|
pieceMap[vn->getCreateIndex()] = res; // Enter preexisting Varnode into map, so we don't make another placeholder
|
||||||
res->vn = vn;
|
res->vn = vn;
|
||||||
res->replacement = (Varnode *)0;
|
res->replacement = (Varnode *)0;
|
||||||
res->byteSize = vn->getSize();
|
res->byteSize = vn->getSize();
|
||||||
res->bitSize = res->byteSize * 8;
|
res->bitSize = res->byteSize * 8;
|
||||||
res->def = (TransformOp *)0;
|
res->def = (TransformOp *)0;
|
||||||
res->type = TransformVar::preexisting;
|
res->type = TransformVar::preexisting;
|
||||||
MapKey key(vn->getCreateIndex(),0);
|
res->flags = TransformVar::split_terminator;
|
||||||
pieceMap[key] = res; // Enter preexisting Varnode into map, so we don't make another placeholder
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,6 +163,7 @@ TransformVar *TransformManager::newUnique(int4 size)
|
||||||
res->bitSize = size * 8;
|
res->bitSize = size * 8;
|
||||||
res->def = (TransformOp *)0;
|
res->def = (TransformOp *)0;
|
||||||
res->type = TransformVar::normal_temp;
|
res->type = TransformVar::normal_temp;
|
||||||
|
res->flags = 0;
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,6 +180,7 @@ TransformVar *TransformManager::newConstant(int4 size,uintb val)
|
||||||
res->val = val;
|
res->val = val;
|
||||||
res->def = (TransformOp *)0;
|
res->def = (TransformOp *)0;
|
||||||
res->type = TransformVar::constant;
|
res->type = TransformVar::constant;
|
||||||
|
res->flags = 0;
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,6 +196,7 @@ TransformVar *TransformManager::newIop(Varnode *vn)
|
||||||
res->val = vn->getOffset(); // The encoded iop
|
res->val = vn->getOffset(); // The encoded iop
|
||||||
res->def = (TransformOp *)0;
|
res->def = (TransformOp *)0;
|
||||||
res->type = TransformVar::constant_iop;
|
res->type = TransformVar::constant_iop;
|
||||||
|
res->flags = 0;
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,8 +209,8 @@ TransformVar *TransformManager::newIop(Varnode *vn)
|
||||||
TransformVar *TransformManager::newPiece(Varnode *vn,int4 bitSize,int4 lsbOffset)
|
TransformVar *TransformManager::newPiece(Varnode *vn,int4 bitSize,int4 lsbOffset)
|
||||||
|
|
||||||
{
|
{
|
||||||
newVarnodes.push_back(TransformVar());
|
TransformVar *res = new TransformVar[1];
|
||||||
TransformVar *res = &newVarnodes.back();
|
pieceMap[vn->getCreateIndex()] = res;
|
||||||
res->vn = vn;
|
res->vn = vn;
|
||||||
res->replacement = (Varnode *)0;
|
res->replacement = (Varnode *)0;
|
||||||
res->bitSize = bitSize;
|
res->bitSize = bitSize;
|
||||||
|
@ -205,10 +219,9 @@ TransformVar *TransformManager::newPiece(Varnode *vn,int4 bitSize,int4 lsbOffset
|
||||||
if (preserveAddress(vn, bitSize, lsbOffset))
|
if (preserveAddress(vn, bitSize, lsbOffset))
|
||||||
res->type = TransformVar::piece;
|
res->type = TransformVar::piece;
|
||||||
else
|
else
|
||||||
res->type = TransformVar::normal_temp;
|
res->type = TransformVar::piece_temp;
|
||||||
|
res->flags = TransformVar::split_terminator;
|
||||||
res->val = lsbOffset;
|
res->val = lsbOffset;
|
||||||
MapKey key(vn->getCreateIndex(),lsbOffset);
|
|
||||||
pieceMap[key] = res;
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,29 +229,29 @@ TransformVar *TransformManager::newPiece(Varnode *vn,int4 bitSize,int4 lsbOffset
|
||||||
///
|
///
|
||||||
/// Given a big Varnode and a lane description, create placeholders for all the explicit pieces
|
/// Given a big Varnode and a lane description, create placeholders for all the explicit pieces
|
||||||
/// that the big Varnode will be split into.
|
/// that the big Varnode will be split into.
|
||||||
/// \param res will hold references to the new placeholders in significance order
|
|
||||||
/// \param vn is the big Varnode to split
|
/// \param vn is the big Varnode to split
|
||||||
/// \param description shows how the big Varnode will be split
|
/// \param description shows how the big Varnode will be split
|
||||||
void TransformManager::newSplit(vector<TransformVar *> &res,Varnode *vn,const LaneDescription &description)
|
/// \return an array of the new TransformVar placeholders from least to most significant
|
||||||
|
TransformVar *TransformManager::newSplit(Varnode *vn,const LaneDescription &description)
|
||||||
|
|
||||||
{
|
{
|
||||||
int4 num = description.getNumLanes();
|
int4 num = description.getNumLanes();
|
||||||
res.resize(num,(TransformVar *)0);
|
TransformVar *res = new TransformVar[num];
|
||||||
|
pieceMap[vn->getCreateIndex()] = res;
|
||||||
for(int4 i=0;i<num;++i) {
|
for(int4 i=0;i<num;++i) {
|
||||||
int4 bitpos = description.getPosition(i) * 8;
|
int4 bitpos = description.getPosition(i) * 8;
|
||||||
newVarnodes.push_back(TransformVar());
|
TransformVar *newVar = &res[i];
|
||||||
TransformVar *newVar = &newVarnodes.back();
|
|
||||||
newVar->vn = vn;
|
newVar->vn = vn;
|
||||||
newVar->replacement = (Varnode *)0;
|
newVar->replacement = (Varnode *)0;
|
||||||
newVar->byteSize = description.getSize(i);
|
newVar->byteSize = description.getSize(i);
|
||||||
newVar->bitSize = newVar->byteSize * 8;
|
newVar->bitSize = newVar->byteSize * 8;
|
||||||
newVar->def = (TransformOp *)0;
|
newVar->def = (TransformOp *)0;
|
||||||
newVar->type = TransformVar::piece;
|
newVar->type = TransformVar::piece;
|
||||||
|
newVar->flags = 0;
|
||||||
newVar->val = bitpos;
|
newVar->val = bitpos;
|
||||||
MapKey key(vn->getCreateIndex(),bitpos);
|
|
||||||
pieceMap[key] = newVar;
|
|
||||||
res[i] = newVar;
|
|
||||||
}
|
}
|
||||||
|
res[num-1].flags = TransformVar::split_terminator;
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// \brief Create a new placeholder op intended to replace an existing op
|
/// \brief Create a new placeholder op intended to replace an existing op
|
||||||
|
@ -317,9 +330,8 @@ TransformOp *TransformManager::newPreexistingOp(int4 numParams,OpCode opc,PcodeO
|
||||||
TransformVar *TransformManager::getPreexistingVarnode(Varnode *vn)
|
TransformVar *TransformManager::getPreexistingVarnode(Varnode *vn)
|
||||||
|
|
||||||
{
|
{
|
||||||
map<MapKey,TransformVar *>::const_iterator iter;
|
map<int4,TransformVar *>::const_iterator iter;
|
||||||
MapKey key(vn->getCreateIndex(),0);
|
iter = pieceMap.find(vn->getCreateIndex());
|
||||||
iter = pieceMap.find(key);
|
|
||||||
if (iter != pieceMap.end())
|
if (iter != pieceMap.end())
|
||||||
return (*iter).second;
|
return (*iter).second;
|
||||||
return newPreexistingVarnode(vn);
|
return newPreexistingVarnode(vn);
|
||||||
|
@ -334,11 +346,13 @@ TransformVar *TransformManager::getPreexistingVarnode(Varnode *vn)
|
||||||
TransformVar *TransformManager::getPiece(Varnode *vn,int4 bitSize,int4 lsbOffset)
|
TransformVar *TransformManager::getPiece(Varnode *vn,int4 bitSize,int4 lsbOffset)
|
||||||
|
|
||||||
{
|
{
|
||||||
map<MapKey,TransformVar *>::const_iterator iter;
|
map<int4,TransformVar *>::const_iterator iter;
|
||||||
MapKey key(vn->getCreateIndex(),lsbOffset);
|
iter = pieceMap.find(vn->getCreateIndex());
|
||||||
iter = pieceMap.find(key);
|
|
||||||
if (iter != pieceMap.end()) {
|
if (iter != pieceMap.end()) {
|
||||||
return (*iter).second;
|
TransformVar *res = (*iter).second;
|
||||||
|
if (res->bitSize != bitSize || res->val != lsbOffset)
|
||||||
|
throw LowlevelError("Cannot create multiple pieces for one Varnode through getPiece");
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
return newPiece(vn,bitSize,lsbOffset);
|
return newPiece(vn,bitSize,lsbOffset);
|
||||||
}
|
}
|
||||||
|
@ -347,25 +361,18 @@ TransformVar *TransformManager::getPiece(Varnode *vn,int4 bitSize,int4 lsbOffset
|
||||||
///
|
///
|
||||||
/// Given a big Varnode and a lane description, look up placeholders for all its
|
/// Given a big Varnode and a lane description, look up placeholders for all its
|
||||||
/// explicit pieces. If they don't exist, create them.
|
/// explicit pieces. If they don't exist, create them.
|
||||||
/// \param res will hold the array of recovered placeholders in significance order
|
|
||||||
/// \param vn is the big Varnode to split
|
/// \param vn is the big Varnode to split
|
||||||
/// \param description shows how the big Varnode will be split
|
/// \param description shows how the big Varnode will be split
|
||||||
void TransformManager::getSplit(vector<TransformVar *> &res,Varnode *vn,const LaneDescription &description)
|
/// \return an array of the TransformVar placeholders from least to most significant
|
||||||
|
TransformVar *TransformManager::getSplit(Varnode *vn,const LaneDescription &description)
|
||||||
|
|
||||||
{
|
{
|
||||||
map<MapKey,TransformVar *>::const_iterator iter;
|
map<int4,TransformVar *>::const_iterator iter;
|
||||||
MapKey key(vn->getCreateIndex(),0);
|
iter = pieceMap.find(vn->getCreateIndex());
|
||||||
iter = pieceMap.lower_bound(key);
|
if (iter != pieceMap.end()) {
|
||||||
if (iter != pieceMap.end() && (*iter).first.getCreateIndex() == vn->getCreateIndex()) {
|
return (*iter).second;
|
||||||
int4 num = description.getNumLanes();
|
|
||||||
res.resize(num,(TransformVar *)0);
|
|
||||||
for(int4 i=0;i<num;++i) {
|
|
||||||
res[i] = (*iter).second;
|
|
||||||
++iter;
|
|
||||||
}
|
}
|
||||||
return;
|
return newSplit(vn,description);
|
||||||
}
|
|
||||||
newSplit(res,vn,description);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TransformManager::opSetInput(TransformOp *rop,TransformVar *rvn,int4 slot)
|
void TransformManager::opSetInput(TransformOp *rop,TransformVar *rvn,int4 slot)
|
||||||
|
@ -408,6 +415,15 @@ void TransformManager::createOps(void)
|
||||||
void TransformManager::createVarnodes(void)
|
void TransformManager::createVarnodes(void)
|
||||||
|
|
||||||
{
|
{
|
||||||
|
map<int4,TransformVar *>::iterator piter;
|
||||||
|
for(piter=pieceMap.begin();piter!=pieceMap.end();++piter) {
|
||||||
|
TransformVar *vArray = (*piter).second;
|
||||||
|
for(int4 i=0;;++i) {
|
||||||
|
vArray[i].createReplacement(fd);
|
||||||
|
if ((vArray[i].flags & TransformVar::split_terminator)!=0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
list<TransformVar>::iterator iter;
|
list<TransformVar>::iterator iter;
|
||||||
for(iter=newVarnodes.begin();iter!=newVarnodes.end();++iter) {
|
for(iter=newVarnodes.begin();iter!=newVarnodes.end();++iter) {
|
||||||
(*iter).createReplacement(fd);
|
(*iter).createReplacement(fd);
|
||||||
|
|
|
@ -24,18 +24,25 @@ class TransformOp;
|
||||||
class TransformVar {
|
class TransformVar {
|
||||||
friend class TransformManager;
|
friend class TransformManager;
|
||||||
public:
|
public:
|
||||||
/// Types of replacement Varnodes
|
/// \brief Types of replacement Varnodes
|
||||||
enum {
|
enum {
|
||||||
piece = 1, ///< New Varnode is a piece of an original Varnode
|
piece = 1, ///< New Varnode is a piece of an original Varnode
|
||||||
preexisting = 2, ///< Varnode preexisted in the original data-flow
|
preexisting = 2, ///< Varnode preexisted in the original data-flow
|
||||||
normal_temp = 3, ///< A new temporary (unique space) Varnode
|
normal_temp = 3, ///< A new temporary (unique space) Varnode
|
||||||
constant = 4, ///< A new constant Varnode
|
piece_temp = 4, ///< A temporary representing a piece of an original Varnode
|
||||||
constant_iop = 5, ///< Special iop constant encoding a PcodeOp reference
|
constant = 5, ///< A new constant Varnode
|
||||||
|
constant_iop = 6, ///< Special iop constant encoding a PcodeOp reference
|
||||||
|
};
|
||||||
|
/// \brief Flags for a TransformVar
|
||||||
|
enum {
|
||||||
|
split_terminator = 1, ///< The last (most significant piece) of a split array
|
||||||
|
def_traverse = 2 ///< The op defining this Varnode has already been traversed
|
||||||
};
|
};
|
||||||
private:
|
private:
|
||||||
Varnode *vn; ///< Original \b big Varnode of which \b this is a component
|
Varnode *vn; ///< Original \b big Varnode of which \b this is a component
|
||||||
Varnode *replacement; ///< The new explicit lane Varnode
|
Varnode *replacement; ///< The new explicit lane Varnode
|
||||||
uint4 type; ///< Type of new Varnode
|
uint4 type; ///< Type of new Varnode
|
||||||
|
uint4 flags; ///< Boolean properties of the placeholder
|
||||||
int4 byteSize; ///< Size of the lane Varnode in bytes
|
int4 byteSize; ///< Size of the lane Varnode in bytes
|
||||||
int4 bitSize; ///< Size of the logical value in bits
|
int4 bitSize; ///< Size of the logical value in bits
|
||||||
uintb val; ///< Value of constant or (bit) position within the original big Varnode
|
uintb val; ///< Value of constant or (bit) position within the original big Varnode
|
||||||
|
@ -88,18 +95,8 @@ public:
|
||||||
/// If the interpretation is consistent for data-flow involving the Varnode, split
|
/// If the interpretation is consistent for data-flow involving the Varnode, split
|
||||||
/// Varnode and data-flow into explicit operations on the lanes.
|
/// Varnode and data-flow into explicit operations on the lanes.
|
||||||
class TransformManager {
|
class TransformManager {
|
||||||
/// \brief Key for mapping from a \b big Varnode to its pieces
|
|
||||||
class MapKey {
|
|
||||||
uint4 create_index; ///< Creation index of Varnode being split
|
|
||||||
int4 position; ///< Position within the Varnode
|
|
||||||
public:
|
|
||||||
MapKey(uint4 index,int4 pos) { create_index = index; position = pos; } ///< Constructor
|
|
||||||
uint4 getCreateIndex(void) const { return create_index; } ///< Return the creation index part of \b this key
|
|
||||||
bool operator<(const MapKey &op2) const; ///< Comparator function for \b this key
|
|
||||||
};
|
|
||||||
|
|
||||||
Funcdata *fd; ///< Function being operated on
|
Funcdata *fd; ///< Function being operated on
|
||||||
map<MapKey,TransformVar *> pieceMap; ///< Map from large Varnodes to their new pieces
|
map<int4,TransformVar *> pieceMap; ///< Map from large Varnodes to their new pieces
|
||||||
list<TransformVar> newVarnodes; ///< Storage for Varnode placeholder nodes
|
list<TransformVar> newVarnodes; ///< Storage for Varnode placeholder nodes
|
||||||
list<TransformOp> newOps; ///< Storage for PcodeOp placeholder nodes
|
list<TransformOp> newOps; ///< Storage for PcodeOp placeholder nodes
|
||||||
|
|
||||||
|
@ -111,37 +108,27 @@ class TransformManager {
|
||||||
void placeInputs(void); ///< Set input Varnodes for all new ops
|
void placeInputs(void); ///< Set input Varnodes for all new ops
|
||||||
public:
|
public:
|
||||||
TransformManager(Funcdata *f) { fd = f; } ///< Constructor
|
TransformManager(Funcdata *f) { fd = f; } ///< Constructor
|
||||||
virtual ~TransformManager(void) {}
|
virtual ~TransformManager(void); ///< Destructor
|
||||||
virtual bool preserveAddress(Varnode *vn,int4 bitSize,int4 lsbOffset) const;
|
virtual bool preserveAddress(Varnode *vn,int4 bitSize,int4 lsbOffset) const;
|
||||||
TransformVar *newPreexistingVarnode(Varnode *vn); ///< Make placeholder for preexisting Varnode
|
TransformVar *newPreexistingVarnode(Varnode *vn); ///< Make placeholder for preexisting Varnode
|
||||||
TransformVar *newUnique(int4 size); ///< Make placeholder for new unique space Varnode
|
TransformVar *newUnique(int4 size); ///< Make placeholder for new unique space Varnode
|
||||||
TransformVar *newConstant(int4 size,uintb val); ///< Make placeholder for constant Varnode
|
TransformVar *newConstant(int4 size,uintb val); ///< Make placeholder for constant Varnode
|
||||||
TransformVar *newIop(Varnode *vn); ///< Make placeholder for special iop constant
|
TransformVar *newIop(Varnode *vn); ///< Make placeholder for special iop constant
|
||||||
TransformVar *newPiece(Varnode *vn,int4 bitSize,int4 lsbOffset); ///< Make placeholder for piece of a Varnode
|
TransformVar *newPiece(Varnode *vn,int4 bitSize,int4 lsbOffset); ///< Make placeholder for piece of a Varnode
|
||||||
void newSplit(vector<TransformVar *> &res,Varnode *vn,const LaneDescription &description);
|
TransformVar *newSplit(Varnode *vn,const LaneDescription &description);
|
||||||
TransformOp *newOpReplace(int4 numParams,OpCode opc,PcodeOp *replace);
|
TransformOp *newOpReplace(int4 numParams,OpCode opc,PcodeOp *replace);
|
||||||
TransformOp *newOp(int4 numParams,OpCode opc,TransformOp *follow);
|
TransformOp *newOp(int4 numParams,OpCode opc,TransformOp *follow);
|
||||||
TransformOp *newPreexistingOp(int4 numParams,OpCode opc,PcodeOp *originalOp);
|
TransformOp *newPreexistingOp(int4 numParams,OpCode opc,PcodeOp *originalOp);
|
||||||
|
|
||||||
TransformVar *getPreexistingVarnode(Varnode *vn); ///< Get (or create) placeholder for preexisting Varnode
|
TransformVar *getPreexistingVarnode(Varnode *vn); ///< Get (or create) placeholder for preexisting Varnode
|
||||||
TransformVar *getPiece(Varnode *vn,int4 bitSize,int4 lsbOffset); ///< Get (or create) placeholder piece
|
TransformVar *getPiece(Varnode *vn,int4 bitSize,int4 lsbOffset); ///< Get (or create) placeholder piece
|
||||||
void getSplit(vector<TransformVar *> &res,Varnode *vn,const LaneDescription &description);
|
TransformVar *getSplit(Varnode *vn,const LaneDescription &description);
|
||||||
void opSetInput(TransformOp *rop,TransformVar *rvn,int4 slot); ///< Mark given variable as input to given op
|
void opSetInput(TransformOp *rop,TransformVar *rvn,int4 slot); ///< Mark given variable as input to given op
|
||||||
void opSetOutput(TransformOp *rop,TransformVar *rvn); ///< Mark given variable as output of given op
|
void opSetOutput(TransformOp *rop,TransformVar *rvn); ///< Mark given variable as output of given op
|
||||||
|
|
||||||
void apply(void); ///< Apply the full transform to the function
|
void apply(void); ///< Apply the full transform to the function
|
||||||
};
|
};
|
||||||
|
|
||||||
/// \param op2 is the other key to compare with \b this
|
|
||||||
/// \return \b true if \b this should come before the other key
|
|
||||||
inline bool TransformManager::MapKey::operator<(const TransformManager::MapKey &op2) const
|
|
||||||
|
|
||||||
{
|
|
||||||
if (create_index != op2.create_index)
|
|
||||||
return (create_index < op2.create_index);
|
|
||||||
return (position < op2.position);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// \param rop is the given placeholder op whose input is set
|
/// \param rop is the given placeholder op whose input is set
|
||||||
/// \param rvn is the placeholder variable to set
|
/// \param rvn is the placeholder variable to set
|
||||||
/// \param slot is the input position to set
|
/// \param slot is the input position to set
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue