-
Notifications
You must be signed in to change notification settings - Fork 18
Using data in callbacks
Callbacks in a Pintool can receive data. This data is then used to by the callback to perform some kind of analysis. For example, the code below shows a snippet of a Pintool from Pin's manual examples that passes the number of instructions in a BBL to the callback. The callback then uses this number to increment the instruction count each time it is called.
// The running count of instructions is kept here
// make it static to help the compiler optimize docount
static UINT64 icount = 0;
// This function is called before every block
VOID docount(UINT32 c) { icount += c; }
// Pin calls this function every time a new basic block is encountered
// It inserts a call to docount
VOID Trace (TRACE trace, VOID *v)
{
// Visit every basic block in the trace
for (BBL bbl = TRACE_BblHead(trace); BBL_Valid(bbl); bbl = BBL_Next(bbl))
{
// Insert a call to docount before every bbl, passing the number of instructions
BBL_InsertCall(bbl, IPOINT_BEFORE, (AFUNPTR)docount, IARG_UINT32, BBL_NumIns(bbl), IARG_END);
}
}Although this implementation is more efficient than the Pintool we implemented that counts each instruction as its executes, there is unnecessary overhead associated with the code above. This is because Pin must pass the number of instructions to the callback each time. This extra parameter can actually increase the overhead of instrumentation. Moreover, it causes Pin to track more information for the callback that it really should not be concerned with.
The parameter type arguments that have this drawback are those in Pin the require additional arguments and the additional arguments is raw data passed through to the callback:
- IARG_ADDRINT
- IARG_PTR
- IARG_BOOL
- IARG_UINT32
In Pin++, we address this concern by allocating more callback objects for each unique piece of data that would otherwise be passed through Pin to the callback (as shown above). The following code illustrates the complete example above re-written as a Pintool using Pin++.
#include "pin++/Callback.h"
#include "pin++/Pintool.h"
#include "pin++/Buffer.h"
#include "pin++/Trace_Instrument.h"
#include <fstream>
#include <list>
class docount : public OASIS::Pin::Callback <docount (void)>
{
public:
docount (void)
: count_ (0),
ins_count_ (0) { }
void handle_analyze (void) { ++ this->count_; }
void ins_count (UINT64 count) { this->ins_count_ = count; }
UINT64 count (void) const { return this->ins_count_ * this->count_; }
private:
UINT64 count_;
UINT64 ins_count_;
};
class Trace : public OASIS::Pin::Trace_Instrument <Trace>
{
public:
void handle_instrument (const OASIS::Pin::Trace & trace)
{
// Allocate a callback for each BBL.
item_type item (trace.num_bbl ());
item_type::iterator callback = item.begin ();
for (const OASIS::Pin::Bbl & bbl : trace)
{
callback->ins_count (bbl.ins_count ());
callback->insert (IPOINT_BEFORE, bbl);
++ callback;
}
this->traces_.push_back (item);
}
UINT64 count (void) const
{
UINT64 count = 0;
for (auto trace : this->traces_)
for (auto item : trace)
count += item.count ();
return count;
}
private:
typedef OASIS::Pin::Buffer <docount> item_type;
typedef std::list <item_type> list_type;
list_type traces_;
};
class inscount : public OASIS::Pin::Tool <inscount>
{
public:
inscount (void) { this->enable_fini_callback (); }
void handle_fini (INT32)
{
std::ofstream fout (outfile_.Value ().c_str ());
fout << "Count " << this->trace_.count () << std::endl;
fout.close ();
}
private:
Trace trace_;
/// @{ KNOBS
static KNOB <string> outfile_;
/// @}
};
KNOB <string> inscount::outfile_ (KNOB_MODE_WRITEONCE, "pintool", "o", "inscount.out", "specify output file name");
DECLARE_PINTOOL (inscount);The first thing you will notice is that there is more code in this example than in the previous example! Yes, this is true... but there is a means to a end. ;-)
Let's first look at the callback object...
class docount : public OASIS::Pin::Callback <docount (void)>
{
public:
docount (void)
: count_ (0),
ins_count_ (0) { }
void handle_analyze (void) { ++ this->count_; }
void ins_count (UINT64 count) { this->ins_count_ = count; }
UINT64 count (void) const { return this->ins_count_ * this->count_; }
private:
UINT64 count_;
UINT64 ins_count_;
};The callback object keeps track of the number of instructions. Each time the callback is called, it will increment a counter. Then, to get the actual count, we take the product of the count and the number of instructions associated with this callback. This is a different approach than passing the instruction count to the callback, and incrementing a global counter.
Now, let's look at the instrument object...
class Trace : public OASIS::Pin::Trace_Instrument <Trace>
{
public:
void handle_instrument (const OASIS::Pin::Trace & trace)
{
// Allocate a callback for each BBL.
item_type item (trace.num_bbl ());
item_type::iterator callback = item.begin ();
for (const OASIS::Pin::Bbl & bbl : trace)
{
callback->ins_count (bbl.ins_count ());
callback->insert (IPOINT_BEFORE, bbl);
++ callback;
}
this->traces_.push_back (item);
}
UINT64 count (void) const
{
UINT64 count = 0;
for (auto trace : this->traces_)
for (auto item : trace)
count += item.count ();
return count;
}
private:
typedef OASIS::Pin::Buffer <docount> item_type;
typedef std::list <item_type> list_type;
list_type traces_;
};The instrument object has a list of Buffer objects (i.e., one for
each trace it instruments) and the Buffer object is a collection
of docount callback objects. Each time we instrument a trace, we
create a new Buffer that has a callback for each BBL in the trace.
We then set the ins_count() to the number of instructions in the
BBL. Lastly, instrument the BBL with the corresponding callback.
You will notice that we do not pass the number of instructions to the insert_call()
method.
To get the total instruction count for the instrument (see the count()
method in the code above), we iterate over each buffer in the list, and each
callback in the buffer, and add the callbacks count to the overall count
for the instrument.
Finally, let's look at the tool object...
class inscount : public OASIS::Pin::Tool <inscount>
{
public:
inscount (void)
{
this->enable_fini_callback ();
}
void handle_fini (INT32)
{
std::ofstream fout (outfile_.Value ().c_str ());
fout << "Count " << this->trace_.count () << std::endl;
fout.close ();
}
private:
Trace trace_;
/// @{ KNOBS
static KNOB <string> outfile_;
/// @}
};
KNOB <string> inscount::outfile_ (KNOB_MODE_WRITEONCE, "pintool", "o", "inscount.out", "specify output file name");
DECLARE_PINTOOL (inscount);In the tool object, we enable the fini callback, and request the instruction count from the contained instrument.
As stated before, there is a LOT more code in this example when compared to the previous example we implemented and Pin's manual example. But, the cyclomatic complexity of this example is more than 50% less than its counterparts with no added overhead! Depending on how long your program runs, you can experience reduced overheads when compared to the traditional Pin implementation.
Happy Coding!