blob: e9949fc2eef9046373271f5aab3ac7244e272616 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "tools/fidlcat/lib/syscall_decoder.h"
#include <lib/syslog/cpp/macros.h>
#include <sys/time.h>
#include <zircon/types.h>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <vector>
#include "src/developer/debug/zxdb/client/breakpoint.h"
#include "src/developer/debug/zxdb/client/frame.h"
#include "src/developer/debug/zxdb/client/memory_dump.h"
#include "src/developer/debug/zxdb/client/process.h"
#include "src/developer/debug/zxdb/client/step_thread_controller.h"
#include "src/developer/debug/zxdb/client/thread.h"
#include "src/developer/debug/zxdb/symbols/symbol.h"
#include "tools/fidlcat/lib/interception_workflow.h"
#include "tools/fidlcat/lib/type_decoder.h"
namespace fidlcat {
constexpr int kBitsPerByte = 8;
// Helper function to convert a vector of bytes to a T.
template <typename T>
T GetValueFromBytes(const std::vector<uint8_t>& bytes, size_t offset) {
T ret = 0;
for (size_t i = 0; (i < sizeof(ret)) && (offset < bytes.size()); i++) {
ret |= static_cast<uint64_t>(bytes[offset++]) << (i * kBitsPerByte);
}
return ret;
}
uint64_t GetRegisterValue(const std::vector<debug_ipc::Register>& general_registers,
const debug_ipc::RegisterID register_id) {
for (const auto& reg : general_registers) {
if (reg.id == register_id) {
return GetValueFromBytes<uint64_t>(reg.data, 0);
}
}
return 0;
}
void MemoryDumpToVector(const zxdb::MemoryDump& dump, std::vector<uint8_t>* output_vector) {
output_vector->reserve(dump.size());
for (const debug_ipc::MemoryBlock& block : dump.blocks()) {
FX_DCHECK(block.valid);
for (size_t offset = 0; offset < block.size; ++offset) {
output_vector->push_back(block.data[offset]);
}
}
}
void SyscallUse::SyscallInputsDecoded(SyscallDecoder* decoder) {}
void SyscallUse::SyscallOutputsDecoded(SyscallDecoder* decoder) {}
void SyscallUse::SyscallDecodingError(const DecoderError& error, SyscallDecoder* decoder) {
FX_LOGS(ERROR) << error.message();
decoder->Destroy();
}
SyscallDecoder::SyscallDecoder(SyscallDecoderDispatcher* dispatcher,
InterceptingThreadObserver* thread_observer, zxdb::Thread* thread,
const Syscall* syscall, std::unique_ptr<SyscallUse> use,
int64_t timestamp)
: dispatcher_(dispatcher),
thread_observer_(thread_observer),
weak_thread_(thread->GetWeakPtr()),
arch_(thread->session()->arch()),
syscall_(syscall),
use_(std::move(use)),
timestamp_(timestamp) {
fidlcat_thread_ = dispatcher_->SearchThread(thread->GetKoid());
if (fidlcat_thread_ == nullptr) {
Process* fidlcat_process = dispatcher_->SearchProcess(thread->GetProcess()->GetKoid());
if (fidlcat_process == nullptr) {
fidlcat_process = dispatcher_->CreateProcess(thread->GetProcess()->GetName(),
thread->GetProcess()->GetKoid(),
thread->GetProcess()->GetWeakPtr());
}
fidlcat_thread_ = dispatcher_->CreateThread(thread->GetKoid(), fidlcat_process);
}
}
void SyscallDecoder::LoadMemory(uint64_t address, size_t size, std::vector<uint8_t>* destination) {
if (address == 0) {
// Null pointer => don't load anything.
return;
}
zxdb::Thread* thread = get_thread();
if (thread == nullptr) {
aborted_ = true;
Destroy();
}
++pending_request_count_;
thread->GetProcess()->ReadMemory(
address, size,
[this, address, size, destination](const zxdb::Err& err, zxdb::MemoryDump dump) {
--pending_request_count_;
if (aborted()) {
Destroy();
} else {
if (!err.ok()) {
Error(DecoderError::Type::kCantReadMemory)
<< "Can't load memory at " << address << ": " << err.msg();
} else if ((dump.size() != size) || !dump.AllValid()) {
Error(DecoderError::Type::kCantReadMemory)
<< "Can't load memory at " << address << ": not enough data";
} else {
MemoryDumpToVector(dump, destination);
}
if (input_arguments_loaded_) {
LoadOutputs();
} else {
LoadInputs();
}
}
});
}
void SyscallDecoder::LoadArgument(Stage stage, int argument_index, size_t size) {
if (decoded_arguments_[argument_index].loading(stage)) {
return;
}
decoded_arguments_[argument_index].set_loading(stage);
LoadMemory(ArgumentValue(argument_index), size,
&decoded_arguments_[argument_index].loaded_values(stage));
}
void SyscallDecoder::LoadBuffer(Stage stage, uint64_t address, size_t size) {
if (address == 0) {
return;
}
SyscallDecoderBuffer& buffer = buffers_[std::make_pair(stage, address)];
if (buffer.loading()) {
return;
}
buffer.set_loading();
LoadMemory(address, size, &buffer.loaded_values());
}
void SyscallDecoder::Decode() {
zxdb::Thread* thread = weak_thread_.get();
if (aborted_ || (thread == nullptr) || (thread->GetStack().size() == 0)) {
aborted_ = true;
Destroy();
return;
}
if (dispatcher_->decode_options().stack_level >= kFullStack) {
thread->GetStack().SyncFrames([this](const zxdb::Err& /*err*/) { DoDecode(); });
} else {
DoDecode();
}
}
void SyscallDecoder::DoDecode() {
zxdb::Thread* thread = weak_thread_.get();
if (aborted_ || (thread == nullptr) || (thread->GetStack().size() == 0)) {
aborted_ = true;
Destroy();
return;
}
const zxdb::Stack& stack = thread->GetStack();
// Don't keep the inner frame which is the syscall and is not useful.
for (size_t i = stack.size() - 1; i > 0; --i) {
const zxdb::Frame* caller = stack[i];
caller_locations_.push_back(caller->GetLocation());
}
const std::vector<debug_ipc::Register>* general_registers =
thread->GetStack()[0]->GetRegisterCategorySync(debug_ipc::RegisterCategory::kGeneral);
FX_DCHECK(general_registers); // General registers should always be available synchronously.
// The order of parameters in the System V AMD64 ABI we use, according to
// Wikipedia:
static std::vector<debug_ipc::RegisterID> amd64_abi = {
debug_ipc::RegisterID::kX64_rdi, debug_ipc::RegisterID::kX64_rsi,
debug_ipc::RegisterID::kX64_rdx, debug_ipc::RegisterID::kX64_rcx,
debug_ipc::RegisterID::kX64_r8, debug_ipc::RegisterID::kX64_r9};
// The order of parameters in the System V AArch64 ABI we use, according to
// Wikipedia:
static std::vector<debug_ipc::RegisterID> aarch64_abi = {
debug_ipc::RegisterID::kARMv8_x0, debug_ipc::RegisterID::kARMv8_x1,
debug_ipc::RegisterID::kARMv8_x2, debug_ipc::RegisterID::kARMv8_x3,
debug_ipc::RegisterID::kARMv8_x4, debug_ipc::RegisterID::kARMv8_x5,
debug_ipc::RegisterID::kARMv8_x6, debug_ipc::RegisterID::kARMv8_x7};
const std::vector<debug_ipc::RegisterID>* abi;
if (arch_ == debug_ipc::Arch::kX64) {
abi = &amd64_abi;
entry_sp_ = GetRegisterValue(*general_registers, debug_ipc::RegisterID::kX64_rsp);
} else if (arch_ == debug_ipc::Arch::kArm64) {
abi = &aarch64_abi;
entry_sp_ = GetRegisterValue(*general_registers, debug_ipc::RegisterID::kARMv8_sp);
return_address_ = GetRegisterValue(*general_registers, debug_ipc::RegisterID::kARMv8_lr);
} else {
Error(DecoderError::Type::kUnknownArchitecture) << "Unknown architecture";
if (pending_request_count_ == 0) {
use_->SyscallDecodingError(error_, this);
}
return;
}
size_t argument_count = syscall_->arguments().size();
decoded_arguments_.reserve(argument_count);
size_t register_count = std::min(argument_count, abi->size());
for (size_t i = 0; i < register_count; i++) {
decoded_arguments_.emplace_back(GetRegisterValue(*general_registers, (*abi)[i]));
}
LoadStack();
}
void SyscallDecoder::LoadStack() {
zxdb::Thread* thread = weak_thread_.get();
if (aborted_ || (thread == nullptr) || (thread->GetStack().size() == 0)) {
aborted_ = true;
Destroy();
return;
}
size_t stack_size = (syscall_->arguments().size() - decoded_arguments_.size()) * sizeof(uint64_t);
if (arch_ == debug_ipc::Arch::kX64) {
stack_size += sizeof(uint64_t);
}
if (stack_size == 0) {
LoadInputs();
return;
}
uint64_t address = entry_sp_;
++pending_request_count_;
thread->GetProcess()->ReadMemory(
address, stack_size,
[this, address, stack_size](const zxdb::Err& err, zxdb::MemoryDump dump) {
--pending_request_count_;
if (aborted()) {
Destroy();
} else {
if (!err.ok()) {
Error(DecoderError::Type::kCantReadMemory)
<< "Can't load stack at " << address << '/' << stack_size << ": " << err.msg();
} else if ((dump.size() != stack_size) || !dump.AllValid()) {
Error(DecoderError::Type::kCantReadMemory)
<< "Can't load stack at " << address << '/' << stack_size << ": not enough data";
} else {
std::vector<uint8_t> data;
MemoryDumpToVector(dump, &data);
size_t offset = 0;
if (arch_ == debug_ipc::Arch::kX64) {
return_address_ = GetValueFromBytes<uint64_t>(data, 0);
offset += sizeof(uint64_t);
}
while (offset < data.size()) {
decoded_arguments_.emplace_back(GetValueFromBytes<uint64_t>(data, offset));
offset += sizeof(uint64_t);
}
}
LoadInputs();
}
});
}
void SyscallDecoder::LoadInputs() {
if (error_.type() != DecoderError::Type::kNone) {
if (pending_request_count_ == 0) {
use_->SyscallDecodingError(error_, this);
}
return;
}
for (const auto& input : syscall_->inputs()) {
if (input->ConditionsAreTrue(this, Stage::kEntry)) {
input->Load(this, Stage::kEntry);
}
}
if (pending_request_count_ > 0) {
return;
}
input_arguments_loaded_ = true;
if (error_.type() != DecoderError::Type::kNone) {
use_->SyscallDecodingError(error_, this);
} else {
if (StepToReturnAddress()) {
DecodeInputs();
}
}
}
bool SyscallDecoder::StepToReturnAddress() {
zxdb::Thread* thread = weak_thread_.get();
if (aborted_ || (thread == nullptr) || (thread->GetStack().size() == 0)) {
aborted_ = true;
Destroy();
return false;
}
if (syscall_->return_type() != SyscallReturnType::kNoReturn) {
thread_observer_->Register(fidlcat_thread()->koid(), this);
thread_observer_->AddExitBreakpoint(thread, syscall_->name(), return_address_);
}
// Restarts the stopped thread. When the breakpoint will be reached (at the
// end of the syscall), LoadSyscallReturnValue will be called.
thread->Continue(false);
return true;
}
void SyscallDecoder::DecodeInputs() {
if (syscall_->fidl_codec_values_ready()) {
// We are able to create values from the syscall => create the values.
//
invoked_event_ = std::make_shared<InvokedEvent>(timestamp(), fidlcat_thread_, syscall_);
auto inline_member = syscall_->input_inline_members().begin();
auto outline_member = syscall_->input_outline_members().begin();
for (const auto& input : syscall_->inputs()) {
if (input->InlineValue()) {
if (input->ConditionsAreTrue(this, Stage::kEntry)) {
FX_DCHECK(inline_member != syscall_->input_inline_members().end());
std::unique_ptr<fidl_codec::Value> value = input->GenerateValue(this, Stage::kEntry);
FX_DCHECK(value != nullptr);
invoked_event_->AddInlineField(inline_member->get(), std::move(value));
}
++inline_member;
} else {
if (input->ConditionsAreTrue(this, Stage::kEntry)) {
FX_DCHECK(outline_member != syscall_->input_outline_members().end());
std::unique_ptr<fidl_codec::Value> value = input->GenerateValue(this, Stage::kEntry);
FX_DCHECK(value != nullptr);
invoked_event_->AddOutlineField(outline_member->get(), std::move(value));
}
++outline_member;
}
}
if (dispatcher_->needs_stack_frame()) {
CopyStackFrame(caller_locations(), &invoked_event_->stack_frame());
}
if (invoked_event_->NeedsToLoadHandleInfo(&dispatcher_->inference())) {
fidlcat_thread_->process()->LoadHandleInfo(&dispatcher_->inference());
}
}
// Eventually calls the code before displaying the input (which may invalidate
// the display).
if ((syscall_->inputs_decoded_action() == nullptr) ||
(dispatcher_->*(syscall_->inputs_decoded_action()))(timestamp(), this)) {
if (invoked_event_ != nullptr) {
// If we have been able to generate an invoked event, directly call the dispatcher.
dispatcher_->AddInvokedEvent(invoked_event_);
} else {
// Invoked event is not yet available for this syscall.
use_->SyscallInputsDecoded(this);
}
}
if (syscall_->return_type() == SyscallReturnType::kNoReturn) {
// We already called Continue in StepToReturnAddress. We don't want to call it twice. We set
// aborted_ to avoid that.
aborted_ = true;
// We don't expect the syscall to return and it doesn't have any output. We can now destroy
// the decoder.
Destroy();
}
}
void SyscallDecoder::LoadSyscallReturnValue() {
zxdb::Thread* thread = weak_thread_.get();
if (aborted_ || (thread == nullptr) || (thread->GetStack().size() == 0)) {
aborted_ = true;
Destroy();
return;
}
const std::vector<debug_ipc::Register>* general_registers =
thread->GetStack()[0]->GetRegisterCategorySync(debug_ipc::RegisterCategory::kGeneral);
FX_DCHECK(general_registers); // General registers should always be available synchronously.
debug_ipc::RegisterID result_register = (arch_ == debug_ipc::Arch::kX64)
? debug_ipc::RegisterID::kX64_rax
: debug_ipc::RegisterID::kARMv8_x0;
syscall_return_value_ = GetRegisterValue(*general_registers, result_register);
LoadOutputs();
}
void SyscallDecoder::LoadOutputs() {
if (error_.type() != DecoderError::Type::kNone) {
if (pending_request_count_ == 0) {
use_->SyscallDecodingError(error_, this);
}
return;
}
for (const auto& output : syscall_->outputs()) {
if ((output->error_code() == static_cast<zx_status_t>(syscall_return_value_)) &&
output->ConditionsAreTrue(this, Stage::kExit)) {
output->Load(this, Stage::kExit);
}
}
if (pending_request_count_ > 0) {
return;
}
if (error_.type() != DecoderError::Type::kNone) {
use_->SyscallDecodingError(error_, this);
} else {
DecodeOutputs();
}
}
void SyscallDecoder::DecodeOutputs() {
if (pending_request_count_ > 0) {
return;
}
if (syscall_->fidl_codec_values_ready()) {
output_event_ = std::make_shared<OutputEvent>(timestamp(), fidlcat_thread_, syscall_,
syscall_return_value_, invoked_event_);
auto inline_member = syscall_->output_inline_members().begin();
auto outline_member = syscall_->output_outline_members().begin();
for (const auto& output : syscall_->outputs()) {
if (output->InlineValue()) {
if ((output->error_code() == static_cast<zx_status_t>(syscall_return_value_)) &&
(output->ConditionsAreTrue(this, Stage::kExit))) {
FX_DCHECK(inline_member != syscall_->output_inline_members().end());
std::unique_ptr<fidl_codec::Value> value = output->GenerateValue(this, Stage::kExit);
FX_DCHECK(value != nullptr);
output_event_->AddInlineField(inline_member->get(), std::move(value));
}
++inline_member;
} else {
if ((output->error_code() == static_cast<zx_status_t>(syscall_return_value_)) &&
(output->ConditionsAreTrue(this, Stage::kExit))) {
FX_DCHECK(outline_member != syscall_->output_outline_members().end());
std::unique_ptr<fidl_codec::Value> value = output->GenerateValue(this, Stage::kExit);
FX_DCHECK(value != nullptr);
output_event_->AddOutlineField(outline_member->get(), std::move(value));
}
++outline_member;
}
}
if (output_event_->NeedsToLoadHandleInfo(&dispatcher_->inference())) {
fidlcat_thread_->process()->LoadHandleInfo(&dispatcher_->inference());
}
}
if (output_event_ != nullptr) {
if (syscall_->inference() != nullptr) {
// Executes the inference associated with the syscall.
// This is used to infer semantic about handles.
(dispatcher_->*(syscall_->inference()))(output_event_.get(), semantic());
}
// If we have been able to generate an invoked event, directly call the dispatcher.
dispatcher_->AddOutputEvent(output_event_);
} else {
// Output event is not yet available for this syscall.
use_->SyscallOutputsDecoded(this);
}
// Now our job is done, we can destroy the object.
Destroy();
}
void SyscallDecoder::Destroy() {
if (pending_request_count_ == 0) {
dispatcher_->DeleteDecoder(this);
}
}
void SyscallDisplay::SyscallInputsDecoded(SyscallDecoder* decoder) {
if (!dispatcher_->display_started()) {
// The display is not started. Only events can trigger it. We don't have an event so, we have
// nothing to display.
return;
}
displayed_ = true;
if (dispatcher_->decode_options().output_mode == OutputMode::kStandard) {
DisplayInputs(decoder);
}
}
void SyscallDisplay::DisplayInputs(SyscallDecoder* decoder) {
// This code will be deleted when we will be able to generate events for all the syscalls.
const fidl_codec::Colors& colors = dispatcher_->colors();
std::string line_header =
colors.green + std::to_string(dispatcher_->GetTime(decoder->timestamp())) + colors.reset +
' ' + decoder->fidlcat_thread()->process()->name() + ' ' + colors.red +
std::to_string(decoder->fidlcat_thread()->process()->koid()) + colors.reset + ':' +
colors.red + std::to_string(decoder->fidlcat_thread()->koid()) + colors.reset + ' ';
if (dispatcher_->with_process_info()) {
os_ << line_header;
}
os_ << '\n';
FidlcatPrinter printer(dispatcher_, decoder->fidlcat_thread()->process(), os_, line_header);
if (dispatcher_->decode_options().stack_level != kNoStack) {
// Display caller locations.
DisplayStackFrame(decoder->caller_locations(), printer);
}
// Displays the header and the inline input arguments.
printer << decoder->syscall()->name() << '(';
const char* separator = "";
for (const auto& input : decoder->syscall()->inputs()) {
if (input->ConditionsAreTrue(decoder, Stage::kEntry)) {
separator = input->DisplayInline(decoder, Stage::kEntry, separator, printer);
}
}
printer << ")\n";
{
// Displays the outline input arguments.
for (const auto& input : decoder->syscall()->inputs()) {
if (input->ConditionsAreTrue(decoder, Stage::kEntry)) {
input->DisplayOutline(decoder, Stage::kEntry, printer);
}
}
}
dispatcher_->set_last_displayed_syscall(this);
dispatcher_->clear_last_displayed_event();
}
void SyscallDisplay::SyscallOutputsDecoded(SyscallDecoder* decoder) {
// This code will be deleted when we will be able to generate events for all the syscalls.
if (!displayed_ || (dispatcher_->decode_options().output_mode != OutputMode::kStandard)) {
return;
}
if (decoder->syscall()->return_type() != SyscallReturnType::kNoReturn) {
if (dispatcher_->last_displayed_syscall() != this) {
// Add a blank line to tell the user that this display is not linked to the
// previous displayed lines.
os_ << "\n";
}
std::string line_header;
const fidl_codec::Colors& colors = dispatcher_->colors();
if (dispatcher_->with_process_info() || (dispatcher_->last_displayed_syscall() != this)) {
line_header = colors.green + std::to_string(dispatcher_->GetTime(decoder->timestamp())) +
colors.reset + ' ' + decoder->fidlcat_thread()->process()->name() + ' ' +
colors.red + std::to_string(decoder->fidlcat_thread()->process()->koid()) +
colors.reset + ':' + colors.red +
std::to_string(decoder->fidlcat_thread()->koid()) + colors.reset + ' ';
} else {
line_header = colors.green + std::to_string(dispatcher_->GetTime(decoder->timestamp())) +
colors.reset + ' ';
}
FidlcatPrinter printer(dispatcher_, decoder->fidlcat_thread()->process(), os_, line_header);
// Displays the returned value.
printer << " -> ";
switch (decoder->syscall()->return_type()) {
case SyscallReturnType::kNoReturn:
case SyscallReturnType::kVoid:
break;
case SyscallReturnType::kStatus:
printer.DisplayStatus(static_cast<zx_status_t>(decoder->syscall_return_value()));
break;
case SyscallReturnType::kTicks:
printer << fidl_codec::Green << "ticks" << fidl_codec::ResetColor << ": "
<< fidl_codec::Blue << static_cast<uint64_t>(decoder->syscall_return_value())
<< fidl_codec::ResetColor;
break;
case SyscallReturnType::kTime:
printer << fidl_codec::Green << "time" << fidl_codec::ResetColor << ": "
<< DisplayTime(static_cast<zx_time_t>(decoder->syscall_return_value()));
break;
case SyscallReturnType::kUint32:
printer << fidl_codec::Blue << static_cast<uint32_t>(decoder->syscall_return_value())
<< fidl_codec::ResetColor;
break;
case SyscallReturnType::kUint64:
printer << fidl_codec::Blue << static_cast<uint64_t>(decoder->syscall_return_value())
<< fidl_codec::ResetColor;
break;
}
// And the inline output arguments (if any).
const char* separator = " (";
for (const auto& output : decoder->syscall()->outputs()) {
if ((output->error_code() == static_cast<zx_status_t>(decoder->syscall_return_value())) &&
output->ConditionsAreTrue(decoder, Stage::kExit)) {
separator = output->DisplayInline(decoder, Stage::kExit, separator, printer);
}
}
if (std::string(" (") != separator) {
printer << ')';
}
printer << '\n';
{
fidl_codec::Indent indent(printer);
// Displays the outline output arguments.
for (const auto& output : decoder->syscall()->outputs()) {
if ((output->error_code() == static_cast<zx_status_t>(decoder->syscall_return_value())) &&
output->ConditionsAreTrue(decoder, Stage::kExit)) {
output->DisplayOutline(decoder, Stage::kExit, printer);
}
}
}
dispatcher_->set_last_displayed_syscall(this);
dispatcher_->clear_last_displayed_event();
}
}
void SyscallDisplay::SyscallDecodingError(const DecoderError& error, SyscallDecoder* decoder) {
std::string message = error.message();
size_t pos = 0;
for (;;) {
size_t end = message.find('\n', pos);
const fidl_codec::Colors& colors = dispatcher_->colors();
os_ << decoder->fidlcat_thread()->process()->name() << ' ' << colors.red
<< decoder->fidlcat_thread()->process()->koid() << colors.reset << ':' << colors.red
<< decoder->fidlcat_thread()->koid() << colors.reset << ' ' << decoder->syscall()->name()
<< ": " << colors.red << error.message().substr(pos, end) << colors.reset << '\n';
if (end == std::string::npos) {
break;
}
pos = end + 1;
}
os_ << '\n';
decoder->Destroy();
}
void SyscallCompare::SyscallInputsDecoded(SyscallDecoder* decoder) {
os_.clear();
os_.str("");
SyscallDisplay::SyscallInputsDecoded(decoder);
comparator_->CompareInput(os_.str(), decoder->fidlcat_thread()->process()->name(),
decoder->fidlcat_thread()->process()->koid(),
decoder->fidlcat_thread()->koid());
}
void SyscallCompare::SyscallOutputsDecoded(SyscallDecoder* decoder) {
os_.clear();
os_.str("");
SyscallDisplay::SyscallOutputsDecoded(decoder);
if (decoder->syscall()->return_type() != SyscallReturnType::kNoReturn) {
comparator_->CompareOutput(os_.str(), decoder->fidlcat_thread()->process()->name(),
decoder->fidlcat_thread()->process()->koid(),
decoder->fidlcat_thread()->koid());
}
}
void SyscallCompare::SyscallDecodingError(const DecoderError& error, SyscallDecoder* decoder) {
os_.clear();
os_.str("");
SyscallDisplay::SyscallDecodingError(error, decoder);
comparator_->DecodingError(os_.str());
}
} // namespace fidlcat