Skip to content
Snippets Groups Projects
Commit bff60aea authored by Martin Unzner's avatar Martin Unzner
Browse files

Additionally passing the current Bochs CPU context and instruction cache entry...

Additionally passing the current Bochs CPU context and instruction cache entry to BochsController (enables detailed instruction analysis and modification)


git-svn-id: https://www4.informatik.uni-erlangen.de/i4svn/danceos/trunk/devel/fail@1361 8c4709b5-6ec9-48aa-a5cd-a96041d1645a
parent 33772f75
No related branches found
No related tags found
No related merge requests found
......@@ -1841,19 +1841,19 @@
</dia:object>
<dia:object type="UML - Class" version="0" id="O1">
<dia:attribute name="obj_pos">
<dia:point val="20.3,55.05"/>
<dia:point val="13.75,58.55"/>
</dia:attribute>
<dia:attribute name="obj_bb">
<dia:rectangle val="20.25,55;43.565,73.7"/>
<dia:rectangle val="13.7,58.5;44.33,83.6"/>
</dia:attribute>
<dia:attribute name="elem_corner">
<dia:point val="20.3,55.05"/>
<dia:point val="13.75,58.55"/>
</dia:attribute>
<dia:attribute name="elem_width">
<dia:real val="23.215"/>
<dia:real val="30.530000000000001"/>
</dia:attribute>
<dia:attribute name="elem_height">
<dia:real val="18.600000000000005"/>
<dia:real val="25.000000000000007"/>
</dia:attribute>
<dia:attribute name="name">
<dia:string>#BochsController#</dia:string>
......@@ -2035,6 +2035,52 @@
<dia:boolean val="false"/>
</dia:attribute>
</dia:composite>
<dia:composite type="umlattribute">
<dia:attribute name="name">
<dia:string>#m_CPUContext#</dia:string>
</dia:attribute>
<dia:attribute name="type">
<dia:string>#BX_CPU_C*#</dia:string>
</dia:attribute>
<dia:attribute name="value">
<dia:string>##</dia:string>
</dia:attribute>
<dia:attribute name="comment">
<dia:string>##</dia:string>
</dia:attribute>
<dia:attribute name="visibility">
<dia:enum val="1"/>
</dia:attribute>
<dia:attribute name="abstract">
<dia:boolean val="false"/>
</dia:attribute>
<dia:attribute name="class_scope">
<dia:boolean val="false"/>
</dia:attribute>
</dia:composite>
<dia:composite type="umlattribute">
<dia:attribute name="name">
<dia:string>#m_CacheEntry#</dia:string>
</dia:attribute>
<dia:attribute name="type">
<dia:string>#bxICacheEntry_c*#</dia:string>
</dia:attribute>
<dia:attribute name="value">
<dia:string>##</dia:string>
</dia:attribute>
<dia:attribute name="comment">
<dia:string>##</dia:string>
</dia:attribute>
<dia:attribute name="visibility">
<dia:enum val="1"/>
</dia:attribute>
<dia:attribute name="abstract">
<dia:boolean val="false"/>
</dia:attribute>
<dia:attribute name="class_scope">
<dia:boolean val="false"/>
</dia:attribute>
</dia:composite>
</dia:attribute>
<dia:attribute name="operations">
<dia:composite type="umloperation">
......@@ -2239,6 +2285,57 @@
<dia:enum val="1"/>
</dia:attribute>
</dia:composite>
<dia:composite type="umlparameter">
<dia:attribute name="name">
<dia:string>#address_space#</dia:string>
</dia:attribute>
<dia:attribute name="type">
<dia:string>#address_t#</dia:string>
</dia:attribute>
<dia:attribute name="value">
<dia:string>##</dia:string>
</dia:attribute>
<dia:attribute name="comment">
<dia:string>##</dia:string>
</dia:attribute>
<dia:attribute name="kind">
<dia:enum val="1"/>
</dia:attribute>
</dia:composite>
<dia:composite type="umlparameter">
<dia:attribute name="name">
<dia:string>#context#</dia:string>
</dia:attribute>
<dia:attribute name="type">
<dia:string>#BX_CPU_C*#</dia:string>
</dia:attribute>
<dia:attribute name="value">
<dia:string>##</dia:string>
</dia:attribute>
<dia:attribute name="comment">
<dia:string>##</dia:string>
</dia:attribute>
<dia:attribute name="kind">
<dia:enum val="1"/>
</dia:attribute>
</dia:composite>
<dia:composite type="umlparameter">
<dia:attribute name="name">
<dia:string>#cache_entry#</dia:string>
</dia:attribute>
<dia:attribute name="type">
<dia:string>#bxICacheEntry_c*#</dia:string>
</dia:attribute>
<dia:attribute name="value">
<dia:string>##</dia:string>
</dia:attribute>
<dia:attribute name="comment">
<dia:string>##</dia:string>
</dia:attribute>
<dia:attribute name="kind">
<dia:enum val="1"/>
</dia:attribute>
</dia:composite>
</dia:attribute>
</dia:composite>
<dia:composite type="umloperation">
......@@ -2732,6 +2829,148 @@
</dia:attribute>
<dia:attribute name="parameters"/>
</dia:composite>
<dia:composite type="umloperation">
<dia:attribute name="name">
<dia:string>#onIOPortEvent#</dia:string>
</dia:attribute>
<dia:attribute name="stereotype">
<dia:string>##</dia:string>
</dia:attribute>
<dia:attribute name="type">
<dia:string>#void#</dia:string>
</dia:attribute>
<dia:attribute name="visibility">
<dia:enum val="0"/>
</dia:attribute>
<dia:attribute name="comment">
<dia:string>##</dia:string>
</dia:attribute>
<dia:attribute name="abstract">
<dia:boolean val="false"/>
</dia:attribute>
<dia:attribute name="inheritance_type">
<dia:enum val="2"/>
</dia:attribute>
<dia:attribute name="query">
<dia:boolean val="false"/>
</dia:attribute>
<dia:attribute name="class_scope">
<dia:boolean val="false"/>
</dia:attribute>
<dia:attribute name="parameters">
<dia:composite type="umlparameter">
<dia:attribute name="name">
<dia:string>#data#</dia:string>
</dia:attribute>
<dia:attribute name="type">
<dia:string>#unsigned char#</dia:string>
</dia:attribute>
<dia:attribute name="value">
<dia:string>##</dia:string>
</dia:attribute>
<dia:attribute name="comment">
<dia:string>##</dia:string>
</dia:attribute>
<dia:attribute name="kind">
<dia:enum val="1"/>
</dia:attribute>
</dia:composite>
<dia:composite type="umlparameter">
<dia:attribute name="name">
<dia:string>#port#</dia:string>
</dia:attribute>
<dia:attribute name="type">
<dia:string>#unsigned#</dia:string>
</dia:attribute>
<dia:attribute name="value">
<dia:string>##</dia:string>
</dia:attribute>
<dia:attribute name="comment">
<dia:string>##</dia:string>
</dia:attribute>
<dia:attribute name="kind">
<dia:enum val="1"/>
</dia:attribute>
</dia:composite>
<dia:composite type="umlparameter">
<dia:attribute name="name">
<dia:string>#out#</dia:string>
</dia:attribute>
<dia:attribute name="type">
<dia:string>#bool#</dia:string>
</dia:attribute>
<dia:attribute name="value">
<dia:string>##</dia:string>
</dia:attribute>
<dia:attribute name="comment">
<dia:string>##</dia:string>
</dia:attribute>
<dia:attribute name="kind">
<dia:enum val="1"/>
</dia:attribute>
</dia:composite>
</dia:attribute>
</dia:composite>
<dia:composite type="umloperation">
<dia:attribute name="name">
<dia:string>#getICacheEntry#</dia:string>
</dia:attribute>
<dia:attribute name="stereotype">
<dia:string>##</dia:string>
</dia:attribute>
<dia:attribute name="type">
<dia:string>#bxICacheEntry_c*#</dia:string>
</dia:attribute>
<dia:attribute name="visibility">
<dia:enum val="0"/>
</dia:attribute>
<dia:attribute name="comment">
<dia:string>##</dia:string>
</dia:attribute>
<dia:attribute name="abstract">
<dia:boolean val="false"/>
</dia:attribute>
<dia:attribute name="inheritance_type">
<dia:enum val="2"/>
</dia:attribute>
<dia:attribute name="query">
<dia:boolean val="true"/>
</dia:attribute>
<dia:attribute name="class_scope">
<dia:boolean val="false"/>
</dia:attribute>
<dia:attribute name="parameters"/>
</dia:composite>
<dia:composite type="umloperation">
<dia:attribute name="name">
<dia:string>#getCPUContext#</dia:string>
</dia:attribute>
<dia:attribute name="stereotype">
<dia:string>##</dia:string>
</dia:attribute>
<dia:attribute name="type">
<dia:string>#BX_CPU_C*#</dia:string>
</dia:attribute>
<dia:attribute name="visibility">
<dia:enum val="0"/>
</dia:attribute>
<dia:attribute name="comment">
<dia:string>##</dia:string>
</dia:attribute>
<dia:attribute name="abstract">
<dia:boolean val="false"/>
</dia:attribute>
<dia:attribute name="inheritance_type">
<dia:enum val="2"/>
</dia:attribute>
<dia:attribute name="query">
<dia:boolean val="true"/>
</dia:attribute>
<dia:attribute name="class_scope">
<dia:boolean val="false"/>
</dia:attribute>
<dia:attribute name="parameters"/>
</dia:composite>
</dia:attribute>
<dia:attribute name="template">
<dia:boolean val="false"/>
......@@ -2743,16 +2982,16 @@
<dia:point val="32.55,48.5503"/>
</dia:attribute>
<dia:attribute name="obj_bb">
<dia:rectangle val="31.7,48.5003;33.4,55.0497"/>
<dia:rectangle val="28.965,48.5003;33.4,58.5496"/>
</dia:attribute>
<dia:attribute name="meta">
<dia:composite type="dict"/>
</dia:attribute>
<dia:attribute name="orth_points">
<dia:point val="32.55,48.5503"/>
<dia:point val="32.55,52.175"/>
<dia:point val="31.9075,52.175"/>
<dia:point val="31.9075,54.9997"/>
<dia:point val="32.55,53.9249"/>
<dia:point val="29.015,53.9249"/>
<dia:point val="29.015,58.4996"/>
</dia:attribute>
<dia:attribute name="orth_orient">
<dia:enum val="1"/>
......@@ -2776,7 +3015,7 @@
</dia:attribute>
<dia:connections>
<dia:connection handle="0" to="O0" connection="88"/>
<dia:connection handle="1" to="O1" connection="48"/>
<dia:connection handle="1" to="O1" connection="58"/>
</dia:connections>
</dia:object>
<dia:object type="UML - Class" version="0" id="O3">
......
doc/class-diagram.png

761 KiB | W: | H:

doc/class-diagram.png

783 KiB | W: | H:

doc/class-diagram.png
doc/class-diagram.png
doc/class-diagram.png
doc/class-diagram.png
  • 2-up
  • Swipe
  • Onion skin
......@@ -33,7 +33,7 @@
// Just a dummy function to define a join-point. This function is
// *just* called once within bx_cpu_c::cpu_loop(...).
static inline void defineCPULoopJoinPoint(BX_CPU_C* pThis)
static inline void defineCPULoopJoinPoint(BX_CPU_C* pThis, bxICacheEntry_c *pEntry)
{
/* nothing to do here */
}
......@@ -160,7 +160,7 @@ void BX_CPU_C::cpu_loop(Bit32u max_instr_count)
*
*/
defineCPULoopJoinPoint(BX_CPU_THIS);
defineCPULoopJoinPoint(BX_CPU_THIS, entry);
/****************************************************************/
// instruction decoding completed -> continue with execution
......
......@@ -19,7 +19,8 @@ bx_bool interrupt_injection_request = false;
int interrupt_to_fire = -1;
BochsController::BochsController()
: SimulatorController(new BochsRegisterManager(), new BochsMemoryManager())
: SimulatorController(new BochsRegisterManager(), new BochsMemoryManager()),
m_CPUContext(NULL), m_CacheEntry(NULL)
{
// -------------------------------------
// Add the general purpose register:
......@@ -89,12 +90,15 @@ void BochsController::dbgEnableInstrPtrOutput(unsigned regularity, std::ostream*
}
#endif // DEBUG
void BochsController::onInstrPtrChanged(address_t instrPtr, address_t address_space)
void BochsController::onInstrPtrChanged(address_t instrPtr, address_t address_space,
BX_CPU_C *context, bxICacheEntry_c *cache_entry)
{
#ifdef DEBUG
if(m_Regularity != 0 && ++m_Counter % m_Regularity == 0)
(*m_pDest) << "0x" << std::hex << instrPtr;
#endif
m_CPUContext = context;
m_CacheEntry = cache_entry;
bool do_fire = false;
// Check for active breakpoint-events:
bp_cache_t &buffer_cache = m_EvList.getBPBuffer();
......@@ -117,31 +121,6 @@ void BochsController::onInstrPtrChanged(address_t instrPtr, address_t address_sp
m_EvList.fireActiveEvents();
// Note: SimulatorController::onBreakpointEvent will not be invoked in this
// implementation.
#if 0
//deprecated - this code is ugly
bool do_fire = false;
int i = 0;
BufferCache<BPEvent*> *buffer_cache = m_EvList.getBPBuffer();
while(i < buffer_cache->getCount()) {
BPEvent *pEvBreakpt = buffer_cache->get(i);
if(pEvBreakpt->isMatching(instrPtr, address_space)) {
pEvBreakpt->setTriggerInstructionPointer(instrPtr);
i = buffer_cache->makeActive(m_EvList, i);
assert(i >= 0 &&
"FATAL ERROR: Could not erase BPEvent from cache");
// we now know we need to fire the active events - usually we do not have to
do_fire = true;
// "i" has already been set to the next element (by calling
// makeActive()):
continue; // -> skip loop increment
}
i++;
}
if(do_fire)
m_EvList.fireActiveEvents();
#endif
}
void BochsController::onIOPortEvent(unsigned char data, unsigned port, bool out) {
......@@ -300,11 +279,13 @@ void BochsController::onEventTrigger(BaseEvent* pev)
const std::string& BochsController::getMnemonic() const
{
static std::string str;
#if 0
bxICacheEntry_c* pEntry = BX_CPU(0)->getICacheEntry();
assert(pEntry != NULL && "FATAL ERROR: Bochs internal function returned NULL (not expected)!");
bxInstruction_c* pInstr = pEntry->i;
assert(pInstr != NULL && "FATAL ERROR: Bochs internal member was NULL (not expected)!");
const char* pszName = get_bx_opcode_name(pInstr->getIaOpcode());
#endif
const char* pszName = get_bx_opcode_name(getICacheEntry()->i->getIaOpcode());
if (pszName != NULL)
str = pszName;
else
......
......@@ -81,7 +81,7 @@ public:
* @param instrPtr the new instruction pointer
* @param address_space the address space the CPU is currently in
*/
void onInstrPtrChanged(address_t instrPtr, address_t address_space);
void onInstrPtrChanged(address_t instrPtr, address_t address_space, BX_CPU_C *context, bxICacheEntry_c *cache_entry);
/**
* I/O port communication handler. This method is called (from
* the IOPortCom aspect) every time when Bochs performs a port I/O operation.
......@@ -174,6 +174,19 @@ public:
* the returned string is empty
*/
const std::string& getMnemonic() const;
/**
* Retrieves the current Bochs instruction cache entry
* @returns a pointer to a bxICacheEntry_c object
*/
inline bxICacheEntry_c *getICacheEntry() const { return m_CacheEntry; }
/**
* Retrieves the current CPU context
* @return a pointer to a BX_CPU_C object
*/
inline BX_CPU_C *getCPUContext() const { return m_CPUContext; }
private:
BX_CPU_C *m_CPUContext;
bxICacheEntry_c *m_CacheEntry;
};
} // end-of-namespace: fail
......
......@@ -19,10 +19,10 @@ aspect Breakpoints {
// BX_CPU(0) otherwise
BX_CPU_C* pThis = *(tjp->arg<0>());
// Points to the *current* bxInstruction-object
//bxInstruction_c* pInstr = *(tjp->arg<1>());
bxICacheEntry_c* pEntry = *(tjp->arg<1>());
// report this event to the Bochs controller:
fail::simulator.onInstrPtrChanged(pThis->get_instruction_pointer(), pThis->cr3);
fail::simulator.onInstrPtrChanged(pThis->get_instruction_pointer(), pThis->cr3, pThis, pEntry);
// Note: get_bx_opcode_name(pInstr->getIaOpcode()) retrieves the mnemonics.
}
};
......
......@@ -11,9 +11,24 @@ using namespace std;
using namespace fail;
char const * const results_csv = "l4sys.csv";
const char *l4sys_output_strings[] = { "Unknown", "Done", "Timeout", "Trap", "Interrupt", "Wrong output", "Error" };
bool L4SysCampaign::run()
{
std::string L4SysCampaign::output_result(L4SysProtoMsg_ResultType res) {
#define OUTPUT_CASE(OUTPUT) case L4SysProtoMsg::OUTPUT: return l4sys_output_strings[L4SysProtoMsg::OUTPUT];
switch (res) {
OUTPUT_CASE(DONE);
OUTPUT_CASE(TIMEOUT);
OUTPUT_CASE(TRAP);
OUTPUT_CASE(INTR);
OUTPUT_CASE(WRONG);
OUTPUT_CASE(UNKNOWN);
default:
return l4sys_output_strings[0];
}
#undef OUTPUT_CASE
}
bool L4SysCampaign::run() {
Logger log("L4SysCampaign");
#if 0
......@@ -32,17 +47,17 @@ bool L4SysCampaign::run()
log << "startup" << endl;
int count = 0;
//iterate over one register
for (int bit_offset = 0; bit_offset < 1; ++bit_offset) {
for (int instr_offset = 0; instr_offset < L4SYS_NUMINSTR; ++instr_offset) {
L4SysExperimentData *d = new L4SysExperimentData;
d->msg.set_instr_offset(instr_offset);
d->msg.set_bit_offset(bit_offset);
d->msg.set_bit_offset(0);
campaignmanager.addParam(d);
++count;
}
srand(time(NULL));
for (int i = 0; i < 3000; ++i) {
L4SysExperimentData *d = new L4SysExperimentData;
d->msg.set_exp_type(d->msg.IDCFLIP);
d->msg.set_instr_offset(rand() % L4SYS_NUMINSTR);
// 15 bytes (120 bits) are the longest instruction Bochs still executes
int bit_offset = rand() % 120;
d->msg.set_bit_offset(bit_offset);
campaignmanager.addParam(d);
++count;
}
campaignmanager.noMoreParameters();
log << "done enqueueing parameter sets (" << count << ")." << endl;
......@@ -50,19 +65,19 @@ bool L4SysCampaign::run()
// collect results
L4SysExperimentData *res;
int rescount = 0;
results << "injection_ip,instr_offset,injection_bit,resulttype,resultdata,output,details" << endl;
results
<< "injection_ip,instr_offset,injection_bit,resulttype,resultdata,output,details"
<< endl;
while ((res = static_cast<L4SysExperimentData *>(campaignmanager.getDone()))) {
rescount++;
results << hex
<< res->msg.injection_ip() << ","
<< dec << res->msg.instr_offset() << ","
<< res->msg.bit_offset() << ","
<< res->msg.resulttype() << ","
<< res->msg.resultdata();
if(res->msg.has_output())
results << hex << res->msg.injection_ip() << "," << dec
<< res->msg.instr_offset() << "," << res->msg.bit_offset()
<< "," << output_result(res->msg.resulttype()) << ","
<< res->msg.resultdata();
if (res->msg.has_output())
results << "," << res->msg.output();
if(res->msg.has_details())
if (res->msg.has_details())
results << "," << res->msg.details();
results << endl;
delete res;
......
......@@ -14,6 +14,8 @@ public:
class L4SysCampaign : public fail::Campaign {
public:
virtual bool run();
private:
std::string output_result(L4SysProtoMsg_ResultType res);
};
#endif // __L4SYS_CAMPAIGN_HPP__
......@@ -3,13 +3,11 @@
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include "util/Logger.hpp"
#include "experiment.hpp"
#include "experimentInfo.hpp"
#include "campaign.hpp"
#include "sal/SALConfig.hpp"
#include "sal/SALInst.hpp"
......@@ -33,43 +31,31 @@ using namespace fail;
#endif
typedef struct __trace_instr_type {
address_t trigger_addr;
fail::address_t trigger_addr;
unsigned bp_counter;
} trace_instr;
ostream& operator<<(ostream& out, const trace_instr &val) {
out << val.trigger_addr << "," << val.bp_counter;
return out;
}
istream& operator>>(istream& in, trace_instr &val) {
in >> val.trigger_addr;
//skip the comma
in.ignore(1);
in >> val.bp_counter;
return in;
}
char const * const state_folder = "l4sys.state";
char const * const instr_list_fn = "ip.list";
char const * const golden_run_fn = "golden.out";
address_t const aspace = 0x01e00000;
string output;
#if 0
//disabled (see "STEP 2" below)
vector<trace_instr> instr_list;
vector<trace_instr> alu_instr_list;
#endif
string golden_run;
//the program needs to run 5 times without a fault
const unsigned times_run = 5;
string L4SysExperiment::sanitised(string in_str) {
string L4SysExperiment::sanitised(const string &in_str) {
string result;
result.reserve(in_str.size());
for (string::iterator it = in_str.begin(); it != in_str.end(); it++) {
unsigned char_value = static_cast<unsigned>(*it);
if (char_value < 0x20 || char_value > 0x7E) {
int in_str_size = in_str.size();
result.reserve(in_str_size);
for (int idx = 0; idx < in_str_size; idx++) {
char cur_char = in_str[idx];
unsigned cur_char_value = static_cast<unsigned>(cur_char);
if (cur_char_value < 0x20 || cur_char_value > 0x7E) {
char str_nr[5];
sprintf(str_nr, "\\%03o", char_value);
sprintf(str_nr, "\\%03o", cur_char_value);
result += str_nr;
} else {
result += *it;
result += cur_char;
}
}
return result;
......@@ -93,15 +79,119 @@ BaseEvent* L4SysExperiment::waitIOOrOther(bool clear_output) {
return ev;
}
Bit32u L4SysExperiment::eipBiased() {
BX_CPU_C *cpu_context = simulator.getCPUContext();
Bit32u EIP = cpu_context->gen_reg[BX_32BIT_REG_EIP].dword.erx;
return EIP + cpu_context->eipPageBias;
}
const Bit8u *L4SysExperiment::calculateInstructionAddress() {
// pasted in from various nested Bochs functions and macros - I hope
// they will not change too soon (as do the Bochs developers, probably)
BX_CPU_C *cpu_context = simulator.getCPUContext();
const Bit8u *result = cpu_context->eipFetchPtr + eipBiased();
return result;
}
bx_bool L4SysExperiment::fetchInstruction(BX_CPU_C *instance, const Bit8u *instr, bxInstruction_c *iStorage)
{
unsigned remainingInPage = instance->eipPageWindowSize - eipBiased();
int ret;
#if BX_SUPPORT_X86_64
if (BX_CPU_THIS_PTR cpu_mode == BX_MODE_LONG_64)
ret = instance->fetchDecode64(instr, iStorage, remainingInPage);
else
#endif
ret = instance->fetchDecode32(instr, iStorage, remainingInPage);
if (ret < 0) {
// handle instrumentation callback inside boundaryFetch
instance->boundaryFetch(instr, remainingInPage, iStorage);
return 0;
}
return 1;
}
void L4SysExperiment::logInjection(Logger &log, const L4SysExperimentData &param) {
// explicit type assignment necessary before sending over output stream
int id = param.getWorkloadID();
int instr_offset = param.msg.instr_offset();
int bit_offset = param.msg.bit_offset();
int exp_type = param.msg.exp_type();
address_t injection_ip = param.msg.injection_ip();
log << "job " << id << " exp_type " << exp_type << endl;
log << "inject @ ip " << injection_ip << " (offset " << dec
<< instr_offset << ")" << " bit " << bit_offset << endl;
}
bool L4SysExperiment::isALUInstruction(unsigned opcode) {
switch(opcode) {
case BX_IA_INC_Eb: case BX_IA_INC_Ew: case BX_IA_INC_Ed: case BX_IA_INC_RX: case BX_IA_INC_ERX:
case BX_IA_DEC_Eb: case BX_IA_DEC_Ew: case BX_IA_DEC_Ed: case BX_IA_DEC_RX: case BX_IA_DEC_ERX:
case BX_IA_ADC_EbGb: case BX_IA_ADC_EdGd: case BX_IA_ADC_EwGw: case BX_IA_ADD_EbGb: case BX_IA_ADD_EdGd:
case BX_IA_ADD_EwGw: case BX_IA_AND_EbGb: case BX_IA_AND_EdGd: case BX_IA_AND_EwGw: case BX_IA_CMP_EbGb:
case BX_IA_CMP_EdGd: case BX_IA_CMP_EwGw: case BX_IA_OR_EbGb: case BX_IA_OR_EdGd: case BX_IA_OR_EwGw:
case BX_IA_SBB_EbGb: case BX_IA_SBB_EdGd: case BX_IA_SBB_EwGw: case BX_IA_SUB_EbGb: case BX_IA_SUB_EdGd:
case BX_IA_SUB_EwGw: case BX_IA_XOR_EbGb: case BX_IA_XOR_EdGd: case BX_IA_XOR_EwGw: case BX_IA_ADC_ALIb:
case BX_IA_ADC_AXIw: case BX_IA_ADC_EAXId: case BX_IA_ADD_EbIb: case BX_IA_OR_EbIb: case BX_IA_ADC_EbIb:
case BX_IA_SBB_EbIb: case BX_IA_AND_EbIb: case BX_IA_SUB_EbIb: case BX_IA_XOR_EbIb: case BX_IA_CMP_EbIb:
case BX_IA_ADD_EwIw: case BX_IA_OR_EwIw: case BX_IA_ADC_EwIw: case BX_IA_SBB_EwIw: case BX_IA_AND_EwIw:
case BX_IA_SUB_EwIw: case BX_IA_XOR_EwIw: case BX_IA_CMP_EwIw: case BX_IA_ADD_EdId: case BX_IA_OR_EdId:
case BX_IA_ADC_EdId: case BX_IA_SBB_EdId: case BX_IA_AND_EdId: case BX_IA_SUB_EdId: case BX_IA_XOR_EdId:
case BX_IA_CMP_EdId: case BX_IA_ADC_GbEb: case BX_IA_ADC_GwEw: case BX_IA_ADC_GdEd: case BX_IA_ADD_ALIb:
case BX_IA_ADD_AXIw: case BX_IA_ADD_EAXId: case BX_IA_ADD_GbEb: case BX_IA_ADD_GwEw: case BX_IA_ADD_GdEd:
case BX_IA_AND_ALIb: case BX_IA_AND_AXIw: case BX_IA_AND_EAXId: case BX_IA_AND_GbEb: case BX_IA_AND_GwEw:
case BX_IA_AND_GdEd: case BX_IA_ROL_Eb: case BX_IA_ROR_Eb: case BX_IA_RCL_Eb: case BX_IA_RCR_Eb:
case BX_IA_SHL_Eb: case BX_IA_SHR_Eb: case BX_IA_SAR_Eb: case BX_IA_ROL_Ew: case BX_IA_ROR_Ew:
case BX_IA_RCL_Ew: case BX_IA_RCR_Ew: case BX_IA_SHL_Ew: case BX_IA_SHR_Ew: case BX_IA_SAR_Ew:
case BX_IA_ROL_Ed: case BX_IA_ROR_Ed: case BX_IA_RCL_Ed: case BX_IA_RCR_Ed: case BX_IA_SHL_Ed:
case BX_IA_SHR_Ed: case BX_IA_SAR_Ed: case BX_IA_NOT_Eb: case BX_IA_NEG_Eb: case BX_IA_NOT_Ew:
case BX_IA_NEG_Ew: case BX_IA_NOT_Ed: case BX_IA_NEG_Ed:
return true;
default:
return false;
}
}
void L4SysExperiment::readFromFileToVector(std::ifstream &file, std::vector<trace_instr> &instr_list)
{
file >> hex;
while (!file.eof()) {
trace_instr curr_instr;
file.read(reinterpret_cast<char*>(&curr_instr), sizeof(trace_instr));
instr_list.push_back(curr_instr);
}
file.close();
}
void L4SysExperiment::changeBochsInstruction(bxInstruction_c *dest, bxInstruction_c *src) {
// backup the current and insert the faulty instruction
bxInstruction_c old_instr;
memcpy(&old_instr, dest, sizeof(bxInstruction_c));
memcpy(dest, src, sizeof(bxInstruction_c));
// execute the faulty instruction, then return
BPSingleEvent singlestepping_event(ANY_ADDR, L4SYS_ADDRESS_SPACE);
simulator.addEvent(&singlestepping_event);
waitIOOrOther(false);
simulator.removeEvent(&singlestepping_event);
//restore the old instruction
memcpy(dest, &old_instr, sizeof(bxInstruction_c));
}
bool L4SysExperiment::run() {
Logger log("L4Sys", false);
BPSingleEvent bp(0, aspace);
BPSingleEvent bp(0, L4SYS_ADDRESS_SPACE);
log << "startup" << endl;
struct stat teststruct;
// STEP 1: run until interesting function starts, and save state
if (stat(state_folder, &teststruct) == -1) {
if (stat(L4SYS_STATE_FOLDER, &teststruct) == -1) {
bp.setWatchInstructionPointer(L4SYS_FUNC_ENTRY);
simulator.addEventAndWait(&bp);
......@@ -109,13 +199,19 @@ bool L4SysExperiment::run() {
log << "EIP = " << hex << bp.getTriggerInstructionPointer() << " or "
<< simulator.getRegisterManager().getInstructionPointer()
<< endl;
simulator.save(state_folder);
simulator.save(L4SYS_STATE_FOLDER);
}
// STEP 2: determine instructions executed
if (stat(instr_list_fn, &teststruct) == -1) {
#if 0
// the files currently get too big.
/* I do not really have a clever idea to solve this.
* You would probably need some kind of loop detection,
* but for the moment, I have to focus on different issues.
*/
if (stat(L4SYS_INSTRUCTION_LIST, &teststruct) == -1 || stat(L4SYS_ALU_INSTRUCTIONS, &teststruct) == -1) {
log << "restoring state" << endl;
simulator.restore(state_folder);
simulator.restore(L4SYS_STATE_FOLDER);
log << "EIP = " << hex
<< simulator.getRegisterManager().getInstructionPointer()
<< endl;
......@@ -123,8 +219,8 @@ bool L4SysExperiment::run() {
// make sure the timer interrupt doesn't disturb us
simulator.addSuppressedInterrupt(32);
ofstream instr_list_file(instr_list_fn);
instr_list_file << hex;
ofstream instr_list_file(L4SYS_INSTRUCTION_LIST, ios::out | ios::binary);
ofstream alu_instr_file(L4SYS_ALU_INSTRUCTIONS, ios::out | ios::binary);
bp.setWatchInstructionPointer(ANY_ADDR);
map<address_t, unsigned> times_called_map;
......@@ -144,25 +240,34 @@ bool L4SysExperiment::run() {
new_instr.bp_counter = times_called;
instr_list.push_back(new_instr);
instr_list_file << new_instr << endl;
instr_list_file.write(reinterpret_cast<char*>(&new_instr), sizeof(trace_instr));
// ALU instructions
// decode the instruction
bxInstruction_c instr;
fetchInstruction(simulator.getCPUContext(), calculateInstructionAddress(), &instr);
// add it to a second list if it is an ALU instruction
if(isALUInstruction(instr.getIaOpcode())) {
alu_instr_list.push_back(new_instr);
alu_instr_file.write(reinterpret_cast<char*>(&new_instr), sizeof(trace_instr));
}
}
log << "saving instructions triggered during normal execution" << endl;
alu_instr_file.close();
instr_list_file.close();
} else {
ifstream instr_list_file(instr_list_fn);
instr_list_file >> hex;
while (!instr_list_file.eof()) {
trace_instr curr_instr;
instr_list_file >> curr_instr;
instr_list.push_back(curr_instr);
}
instr_list_file.close();
ifstream instr_list_file(L4SYS_INSTRUCTION_LIST, ios::in | ios::binary);
ifstream alu_instr_file(L4SYS_ALU_INSTRUCTIONS, ios::in | ios::binary);
readFromFileToVector(instr_list_file, instr_list);
readFromFileToVector(alu_instr_file, alu_instr_list);
}
#endif
// STEP 3: determine the output of a "golden run"
if (stat(golden_run_fn, &teststruct) == -1) {
if (stat(L4SYS_CORRECT_OUTPUT, &teststruct) == -1) {
log << "restoring state" << endl;
simulator.restore(state_folder);
simulator.restore(L4SYS_STATE_FOLDER);
log << "EIP = " << hex
<< simulator.getRegisterManager().getInstructionPointer()
<< endl;
......@@ -170,9 +275,9 @@ bool L4SysExperiment::run() {
// make sure the timer interrupt doesn't disturb us
simulator.addSuppressedInterrupt(32);
ofstream golden_run_file(golden_run_fn);
ofstream golden_run_file(L4SYS_CORRECT_OUTPUT);
bp.setWatchInstructionPointer(L4SYS_FUNC_EXIT);
bp.setCounter(times_run);
bp.setCounter(L4SYS_ITERATION_COUNT);
simulator.addEvent(&bp);
BaseEvent* ev = waitIOOrOther(true);
if (ev == &bp) {
......@@ -191,13 +296,9 @@ bool L4SysExperiment::run() {
log << "saving output generated during normal execution" << endl;
golden_run_file.close();
} else {
ifstream golden_run_file(golden_run_fn);
ifstream golden_run_file(L4SYS_CORRECT_OUTPUT);
//shamelessly copied from http://stackoverflow.com/questions/2602013/:
golden_run_file.seekg(0, ios::end);
size_t flen = golden_run_file.tellg();
golden_run.reserve(flen);
golden_run_file.seekg(0, ios::beg);
golden_run.reserve(teststruct.st_size);
golden_run.assign((istreambuf_iterator<char>(golden_run_file)),
istreambuf_iterator<char>());
......@@ -205,131 +306,178 @@ bool L4SysExperiment::run() {
golden_run_file.close();
//the generated output probably has a similar length
output.reserve(flen);
output.reserve(teststruct.st_size);
}
// STEP 4: The actual experiment.
while (1) {
log << "restoring state" << endl;
simulator.restore(state_folder);
log << "asking job server for experiment parameters" << endl;
L4SysExperimentData param;
if (!m_jc.getParam(param)) {
log << "Dying." << endl;
// communicate that we were told to die
simulator.terminate(1);
}
int id = param.getWorkloadID();
int instr_offset = param.msg.instr_offset();
int bit_offset = param.msg.bit_offset();
log << "job " << id << " instr " << instr_offset << " bit "
<< bit_offset << endl;
bp.setWatchInstructionPointer(instr_list[instr_offset].trigger_addr);
bp.setCounter(instr_list[instr_offset].bp_counter);
simulator.addEvent(&bp);
//and log the output
waitIOOrOther(true);
log << "restoring state" << endl;
simulator.restore(L4SYS_STATE_FOLDER);
log << "asking job server for experiment parameters" << endl;
L4SysExperimentData param;
if (!m_jc.getParam(param)) {
log << "Dying." << endl;
// communicate that we were told to die
simulator.terminate(1);
}
int instr_offset = param.msg.instr_offset();
int bit_offset = param.msg.bit_offset();
int exp_type = param.msg.exp_type();
bp.setWatchInstructionPointer(ANY_ADDR);
bp.setCounter(instr_offset);
simulator.addEvent(&bp);
//and log the output
waitIOOrOther(true);
// note at what IP we will do the injection
address_t injection_ip =
simulator.getRegisterManager().getInstructionPointer();
param.msg.set_injection_ip(injection_ip);
#if 0
// temporarily out of order (see above)
// sanity check (only works if we're working with an instruction trace)
if (injection_ip != instr_list[instr_offset].trigger_addr) {
stringstream ss;
ss << "SANITY CHECK FAILED: " << injection_ip << " != "
<< instr_list[instr_offset].trigger_addr << endl;
log << ss.str();
param.msg.set_resulttype(param.msg.UNKNOWN);
param.msg.set_resultdata(injection_ip);
param.msg.set_details(ss.str());
// inject
simulator.clearEvents();
m_jc.sendResult(param);
simulator.terminate(20);
}
#endif
// inject
if (exp_type == param.msg.GPRFLIP) {
RegisterManager& rm = simulator.getRegisterManager();
Register *ebx = rm.getRegister(RID_CBX);
Register *ebx = rm.getRegister(RID_EBX);
regdata_t data = ebx->getData();
regdata_t newdata = data ^ (1 << bit_offset);
ebx->setData(newdata);
// note at what IP we did it
address_t injection_ip =
simulator.getRegisterManager().getInstructionPointer();
param.msg.set_injection_ip(injection_ip);
log << "inject @ ip " << injection_ip << " (offset " << dec
<< instr_offset << ")" << " bit " << bit_offset << ": 0x" << hex
<< ((int) data) << " -> 0x" << ((int) newdata) << endl;
// sanity check (only works if we're working with an instruction trace)
if (injection_ip != instr_list[instr_offset].trigger_addr) {
stringstream ss;
ss << "SANITY CHECK FAILED: " << injection_ip << " != "
<< instr_list[instr_offset].trigger_addr << endl;
log << ss.str();
param.msg.set_resulttype(param.msg.UNKNOWN);
param.msg.set_resultdata(injection_ip);
param.msg.set_details(ss.str());
simulator.clearEvents();
m_jc.sendResult(param);
simulator.terminate(20);
}
// aftermath
BPSingleEvent ev_done(L4SYS_FUNC_EXIT, aspace);
ev_done.setCounter(times_run);
simulator.addEvent(&ev_done);
const unsigned instr_run = times_run * L4SYS_NUMINSTR;
BPSingleEvent ev_timeout(ANY_ADDR, aspace);
ev_timeout.setCounter(instr_run + 3000);
simulator.addEvent(&ev_timeout);
TrapEvent ev_trap(ANY_TRAP);
simulator.addEvent(&ev_trap);
InterruptEvent ev_intr(ANY_INTERRUPT);
//ten times as many interrupts as instructions justify an exception
ev_intr.setCounter(instr_run * 10);
simulator.addEvent(&ev_intr);
//do not discard output recorded so far
BaseEvent *ev = waitIOOrOther(false);
/* copying a string object that contains control sequences
* unfortunately does not work with the library I am using,
* which is why output is passed on as C string and
* the string compare is done on C strings
*/
if (ev == &ev_done) {
if (strcmp(output.c_str(), golden_run.c_str()) == 0) {
log << dec << "Result DONE" << endl;
param.msg.set_resulttype(param.msg.DONE);
} else {
log << dec << "Result WRONG" << endl;
param.msg.set_resulttype(param.msg.WRONG);
param.msg.set_output(sanitised(output.c_str()));
}
} else if (ev == &ev_timeout) {
log << dec << "Result TIMEOUT" << endl;
param.msg.set_resulttype(param.msg.TIMEOUT);
param.msg.set_resultdata(
simulator.getRegisterManager().getInstructionPointer());
param.msg.set_output(sanitised(output.c_str()));
} else if (ev == &ev_trap) {
log << dec << "Result TRAP #" << ev_trap.getTriggerNumber() << endl;
param.msg.set_resulttype(param.msg.TRAP);
param.msg.set_resultdata(
simulator.getRegisterManager().getInstructionPointer());
param.msg.set_output(sanitised(output.c_str()));
} else if (ev == &ev_intr) {
log << hex << "Result INT FLOOD; Last INT #:"
<< ev_intr.getTriggerNumber() << endl;
param.msg.set_resulttype(param.msg.INTR);
param.msg.set_resultdata(
simulator.getRegisterManager().getInstructionPointer());
param.msg.set_output(sanitised(output.c_str()));
// do the logging in case everything worked out
logInjection(log, param);
log << "register data: 0x" << hex
<< ((int) data) << " -> 0x" << ((int) newdata) << endl;
} else if(exp_type == param.msg.IDCFLIP) {
// this is a twisted one
// initial definitions
bxICacheEntry_c *cache_entry = simulator.getICacheEntry();
unsigned length_in_bits = cache_entry->i->ilen() << 3;
// get the instruction in plain text in inject the error there
// Note: we need to fetch some extra bytes into the array
// in case the faulty instruction is interpreted to be longer
// than the original one
Bit8u curr_instr_plain[MAX_INSTR_BYTES];
const Bit8u *addr = calculateInstructionAddress();
memcpy(curr_instr_plain, addr, MAX_INSTR_BYTES);
// CampaignManager has no idea of the instruction length
// (neither do we), therefore this small adaption
bit_offset %= length_in_bits;
param.msg.set_bit_offset(bit_offset);
// do some access calculation
int byte_index = bit_offset >> 3;
Bit8u bit_index = bit_offset & 7;
// apply the fault
curr_instr_plain[byte_index] ^= 1 << bit_index;
// decode the instruction
bxInstruction_c bochs_instr;
memset(&bochs_instr, 0, sizeof(bxInstruction_c));
fetchInstruction(simulator.getCPUContext(), curr_instr_plain, &bochs_instr);
// inject it
changeBochsInstruction(cache_entry->i, &bochs_instr);
// do the logging
logInjection(log, param);
} else if(exp_type == param.msg.RATFLIP) {
bxICacheEntry_c *cache_entry = simulator.getICacheEntry();
}
// aftermath
BPSingleEvent ev_done(L4SYS_FUNC_EXIT, L4SYS_ADDRESS_SPACE);
ev_done.setCounter(L4SYS_ITERATION_COUNT);
simulator.addEvent(&ev_done);
const unsigned instr_run = L4SYS_ITERATION_COUNT * L4SYS_NUMINSTR;
BPSingleEvent ev_timeout(ANY_ADDR, L4SYS_ADDRESS_SPACE);
ev_timeout.setCounter(instr_run + 3000);
simulator.addEvent(&ev_timeout);
TrapEvent ev_trap(ANY_TRAP);
//one trap for each 150 instructions justifies an exception
ev_trap.setCounter(instr_run / 150);
simulator.addEvent(&ev_trap);
InterruptEvent ev_intr(ANY_INTERRUPT);
//one interrupt for each 100 instructions justifies an exception (timeout mostly)
ev_intr.setCounter(instr_run / 100);
simulator.addEvent(&ev_intr);
//do not discard output recorded so far
BaseEvent *ev = waitIOOrOther(false);
/* copying a string object that contains control sequences
* unfortunately does not work with the library I am using,
* which is why output is passed on as C string and
* the string compare is done on C strings
*/
if (ev == &ev_done) {
if (strcmp(output.c_str(), golden_run.c_str()) == 0) {
log << dec << "Result DONE" << endl;
param.msg.set_resulttype(param.msg.DONE);
} else {
log << dec << "Result WTF?" << endl;
param.msg.set_resulttype(param.msg.UNKNOWN);
param.msg.set_resultdata(
simulator.getRegisterManager().getInstructionPointer());
log << dec << "Result WRONG" << endl;
param.msg.set_resulttype(param.msg.WRONG);
param.msg.set_output(sanitised(output.c_str()));
stringstream ss;
ss << "eventid " << ev << " EIP "
<< simulator.getRegisterManager().getInstructionPointer()
<< endl;
param.msg.set_details(ss.str());
}
simulator.clearEvents();
m_jc.sendResult(param);
} else if (ev == &ev_timeout) {
log << dec << "Result TIMEOUT" << endl;
param.msg.set_resulttype(param.msg.TIMEOUT);
param.msg.set_resultdata(
simulator.getRegisterManager().getInstructionPointer());
param.msg.set_output(sanitised(output.c_str()));
} else if (ev == &ev_trap) {
log << dec << "Result TRAP #" << ev_trap.getTriggerNumber() << endl;
param.msg.set_resulttype(param.msg.TRAP);
param.msg.set_resultdata(
simulator.getRegisterManager().getInstructionPointer());
param.msg.set_output(sanitised(output.c_str()));
} else if (ev == &ev_intr) {
log << hex << "Result INT FLOOD; Last INT #:"
<< ev_intr.getTriggerNumber() << endl;
param.msg.set_resulttype(param.msg.INTR);
param.msg.set_resultdata(
simulator.getRegisterManager().getInstructionPointer());
param.msg.set_output(sanitised(output.c_str()));
} else {
log << dec << "Result WTF?" << endl;
param.msg.set_resulttype(param.msg.UNKNOWN);
param.msg.set_resultdata(
simulator.getRegisterManager().getInstructionPointer());
param.msg.set_output(sanitised(output.c_str()));
stringstream ss;
ss << "eventid " << ev << " EIP "
<< simulator.getRegisterManager().getInstructionPointer()
<< endl;
param.msg.set_details(ss.str());
}
simulator.clearEvents();
m_jc.sendResult(param);
#ifdef HEADLESS_EXPERIMENT
simulator.terminate(0);
#endif
......
......@@ -5,6 +5,8 @@
#include "efw/ExperimentFlow.hpp"
#include "efw/JobClient.hpp"
#include "campaign.hpp"
#include "util/Logger.hpp"
class L4SysExperiment : public fail::ExperimentFlow {
fail::JobClient m_jc;
......@@ -12,12 +14,45 @@ public:
L4SysExperiment() : m_jc("localhost") {}
bool run();
private:
// NOTE: It's good practise to use "const std::string&" as parameter type.
// Additionaly, if you don't need the return value to be copied,
// return a (const) reference to a class member or a static string-
// object.
std::string sanitised(std::string in_str);
/**
* Sanitises the output string of the serial device monitored.
* @param a string containing special ASCII characters
* @returns a byte-stuffed version of the given string
*/
std::string sanitised(const std::string &in_str);
/**
* Waits for events and simultaneously logs output from the serial console
* @param clear_output if true, the output logged so far is deleted, thus the buffer is reset (cleared)
* @returns the event returned by waitAny, as long as it did not log output
*/
fail::BaseEvent* waitIOOrOther(bool clear_output);
/**
* Calculates the address where Bochs will read the current instruction from.
* This code is copied from various Bochs methods and should be reviewed as
* soon as a new Bochs version is introduced.
* @returns a pointer to the memory region containing the current Bochs instruction
*/
const Bit8u *calculateInstructionAddress();
/**
* A function necessary for Bochs internal address translation
* @returns a value for Bochs' eipBiased variable
*/
Bit32u eipBiased();
/**
* Parses a raw instruction into a bxInstruction_c structure.
* This simple version of the function is taken from Bochs
* where it is currently disabled due to the TRACE_CACHE option,
* and has been modified to fit the needs of instruction modification.
* @param instance a pointer to the current Bochs CPU
* @param instr a pointer to the address the instruction is fetched from
* @param iStorage an outgoing value which contains the parsed instruction
* @returns \a false if the instruction continued on the following page in memory
*/
bx_bool fetchInstruction(BX_CPU_C *instance, const Bit8u *instr, bxInstruction_c *iStorage);
void logInjection(fail::Logger &log, const L4SysExperimentData &param);
bool isALUInstruction(unsigned opcode);
void readFromFileToVector(std::ifstream &file, std::vector<struct __trace_instr_type> &instr_list);
void changeBochsInstruction(bxInstruction_c *dest, bxInstruction_c *src);
};
#endif // __L4SYS_EXPERIMENT_HPP__
#ifndef __EXPERIMENT_INFO_HPP__
#define __EXPERIMENT_INFO_HPP__
//experiment types:
#define GPRFLIP 10
#define IDCFLIP 20
// the maximum number of bytes in a Bochs instruction
#define MAX_INSTR_BYTES 15
#define L4SYS_FUNC_ENTRY 0x1007cd0
#define L4SYS_FUNC_EXIT 0x1007d3a
#define L4SYS_NUMINSTR 3184
// the bounds of the program
#define L4SYS_ADDRESS_SPACE 0x203d000
#define L4SYS_FUNC_ENTRY 0x1000400
#define L4SYS_FUNC_EXIT 0x10005b0
#define L4SYS_NUMINSTR 56052772
#define L4SYS_ITERATION_COUNT 1
// several file names used
#define L4SYS_STATE_FOLDER "l4sys.state"
#define L4SYS_INSTRUCTION_LIST "ip.list"
#define L4SYS_ALU_INSTRUCTIONS "alu.list"
#define L4SYS_CORRECT_OUTPUT "golden.out"
// flags
#define HEADLESS_EXPERIMENT
#define EXPERIMENT_TYPE IDCFLIP
//#define PREPARE_EXPERIMENT
#endif // __EXPERIMENT_INFO_HPP__
message L4SysProtoMsg {
// experiment types
enum ExperimentType {
GPRFLIP = 10;
RATFLIP = 15;
IDCFLIP = 20;
ALUINSTR = 30;
}
// parameters
required int32 instr_offset = 1;
required int32 bit_offset = 2;
required ExperimentType exp_type = 10;
required int32 instr_offset = 20;
required int32 bit_offset = 30;
// results
// make these optional to reduce overhead for server->client communication
......@@ -14,13 +22,13 @@ message L4SysProtoMsg {
UNKNOWN = 6;
}
// instruction pointer where injection was done
optional uint32 injection_ip = 3;
optional uint32 injection_ip = 40;
// result type, see above
optional ResultType resulttype = 4;
optional ResultType resulttype = 50;
// result data, depending on resulttype (see source code)
optional uint32 resultdata = 5;
optional uint32 resultdata = 60;
// generated output
optional string output = 6;
optional string output = 70;
// optional textual description of what happened
optional string details = 7;
optional string details = 80;
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment