diff --git a/CMakeLists.txt b/CMakeLists.txt index 38b52b0..fde72e4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,6 +49,7 @@ libhal_unit_test(SOURCES tests/main.test.cpp tests/elf_parser.test.cpp tests/gcc_callgraph.test.cpp + tests/abi_parser.test.cpp tests/validator.test.cpp src/elf_parser.cpp diff --git a/include/abi_parse.hpp b/include/abi_parse.hpp index 0a5d4f8..7a8f777 100644 --- a/include/abi_parse.hpp +++ b/include/abi_parse.hpp @@ -54,11 +54,11 @@ struct Scope std::vector handlers; }; -class GccParser +class LsdaParser { public: - explicit GccParser(const std::vector& lsda_data); - explicit GccParser(const std::vector& lsda_data); + explicit LsdaParser(const std::vector& lsda_data); + explicit LsdaParser(const std::vector& lsda_data); std::optional resolve_type(int64_t type_index) const; void print_call_sites(const std::string& filename) const; diff --git a/include/validator.hpp b/include/validator.hpp index b5d6365..3f037df 100644 --- a/include/validator.hpp +++ b/include/validator.hpp @@ -2,8 +2,13 @@ #include #include +#include #include +#include +#include +#include #include +#include #include #include #include @@ -11,13 +16,27 @@ #include #include #include -#include -#include +#include "abi_parse.hpp" #include "elf_parser.hpp" #include "gelf.h" namespace safe { +struct CatchRecord +{ + std::string scope_id; // example: "scope[0]" + HandlerType kind; // Catch / Cleanup / Filter + std::uint64_t range_begin; // scope.start + std::uint64_t range_end; // scope.end + std::uint64_t landing_pad; // handler.landing_pad + std::int64_t type_index; // handler.type_index +}; + +struct ThrowCatchMatch +{ + symbol_s thrown; // RTTI symbol for the thrown type + std::vector handlers; // matching catch handlers +}; class Validator { @@ -32,16 +51,34 @@ class Validator collect_rtti_sym(); } ~Validator() = default; - std::optional> find_typeinfo( - std::string_view func_name); + std::optional> find_typeinfo(std::string_view func_name); std::optional demangle(const char* mangled); std::optional get_symbol(std::string_view name); + enum class CorrelateError + { + NoTypeinfoForFunction, // Validator has no typeinfo for this function + NoThrownTypes, // Function exists, but no throws recorded + NoCatchRecords, // No LSDA catch records matched any thrown type + TypeResolveFailed, // LSDA::resolve_type() failed for some index + NoLsdaLoaded, // load_lsda() never called + }; + + using Result = std::expected, CorrelateError>; + + void load_lsda(const LsdaParser& lsda); + Result analyze_exceptions(std::string_view func_name) const; + + const std::vector& records() const noexcept { return m_records; } + private: std::span m_sym; section_s m_text; - std::unordered_map rtti_sym; + std::unordered_map rtti_sym; + const LsdaParser* m_lsda = nullptr; + std::vector m_records; // flattened handler table void collect_rtti_sym(); }; + } // namespace safe diff --git a/src/abi_parse.cpp b/src/abi_parse.cpp index 263bb9d..50ab1e0 100644 --- a/src/abi_parse.cpp +++ b/src/abi_parse.cpp @@ -14,7 +14,7 @@ #include #include -void GccParser::check(size_t n) const +void LsdaParser::check(size_t n) const { if (index + n > data.size()) { throw std::runtime_error("LSDA read out of bounds"); @@ -22,7 +22,7 @@ void GccParser::check(size_t n) const } // reads 1 byte and then it goes forward with cursor -uint8_t GccParser::read8() +uint8_t LsdaParser::read8() { check(1); return data[index++]; @@ -30,7 +30,7 @@ uint8_t GccParser::read8() // reads 2 bytes and makes a 16 bit integer -uint16_t GccParser::read16() +uint16_t LsdaParser::read16() { check(2); uint16_t v = 0; @@ -40,7 +40,7 @@ uint16_t GccParser::read16() return v; } // reads 4 bytes and makes a 32 bit integer -uint32_t GccParser::read32() +uint32_t LsdaParser::read32() { check(4); uint32_t v = 0; @@ -50,7 +50,7 @@ uint32_t GccParser::read32() return v; } // reads 8 bytes and makes a 64 bit integer -uint64_t GccParser::read64() +uint64_t LsdaParser::read64() { check(8); uint64_t value = 0; @@ -60,7 +60,7 @@ uint64_t GccParser::read64() return value; } -void GccParser::build_scopes() +void LsdaParser::build_scopes() { scopes.clear(); @@ -74,12 +74,18 @@ void GccParser::build_scopes() s.end = cs.start + cs.length; // LSDA: action is a byte offset into the action table (0 = none) - if (cs.action == 0) { + if (cs.action <= 0) { + // landing_pad != 0 means there is a cleanup landing pad + ScopeHandler h{}; + h.type = HandlerType::Cleanup; + h.type_index = 0; + h.landing_pad = cs.landing_pad; + s.handlers.push_back(h); + scopes.push_back(std::move(s)); continue; } - - const int64_t action_offset = cs.action; + const int64_t action_offset = cs.action - 1; // Map offset -> first action index int64_t action_index = -1; @@ -91,13 +97,16 @@ void GccParser::build_scopes() } if (action_index < 0) { - std::cerr - << "[AbiParser] warning: call-site action offset " - << action_offset - << " not found in action table – treating call-site as having " - "no handlers\n"; + std::cerr << "... not found ... adding cleanup handler\n"; + + ScopeHandler h{}; + h.type = HandlerType::Cleanup; + h.type_index = 0; + h.landing_pad = cs.landing_pad; + s.handlers.push_back(h); + scopes.push_back(std::move(s)); - continue; // go on to the next call-site + continue; } // Follow action chain using resolved next_index @@ -125,7 +134,7 @@ void GccParser::build_scopes() } } -GccParser::GccParser(const std::vector& lsda_data) +LsdaParser::LsdaParser(const std::vector& lsda_data) { data.reserve(lsda_data.size()); for (std::byte b : lsda_data) { @@ -134,13 +143,13 @@ GccParser::GccParser(const std::vector& lsda_data) parse(); } -GccParser::GccParser(const std::vector& lsda_data) +LsdaParser::LsdaParser(const std::vector& lsda_data) { data = lsda_data; // copy into owned storage parse(); } -uint64_t GccParser::read_uleb128(const std::vector& buf, size_t& i) +uint64_t LsdaParser::read_uleb128(const std::vector& buf, size_t& i) { uint64_t result = 0; int shift = 0; @@ -158,7 +167,7 @@ uint64_t GccParser::read_uleb128(const std::vector& buf, size_t& i) return result; } -int64_t GccParser::read_sleb128(const std::vector& buf, size_t& i) +int64_t LsdaParser::read_sleb128(const std::vector& buf, size_t& i) { int64_t result = 0; int shift = 0; @@ -181,7 +190,7 @@ int64_t GccParser::read_sleb128(const std::vector& buf, size_t& i) } // read encode value -uint64_t GccParser::r_encode(uint8_t encoding, uint64_t pcrel) +uint64_t LsdaParser::r_encode(uint8_t encoding, uint64_t pcrel) { if (encoding == 0xFF) { return 0; // omitted @@ -234,7 +243,7 @@ uint64_t GccParser::r_encode(uint8_t encoding, uint64_t pcrel) } // parse -void GccParser::parse() +void LsdaParser::parse() { call_sites.clear(); actions.clear(); @@ -306,7 +315,7 @@ void GccParser::parse() build_scopes(); } -std::optional GccParser::resolve_type(int64_t type_index) const +std::optional LsdaParser::resolve_type(int64_t type_index) const { // Cleanup and filter entries have no concrete type address if (type_index <= 0) { @@ -325,7 +334,7 @@ std::optional GccParser::resolve_type(int64_t type_index) const } // parse LSDA header -void GccParser::parse_header(uint8_t& start_enc, +void LsdaParser::parse_header(uint8_t& start_enc, uint8_t& tt_enc, uint64_t& tt_off) { @@ -344,7 +353,7 @@ void GccParser::parse_header(uint8_t& start_enc, } // parse callsite table -void GccParser::parse_call_sites(uint8_t call_enc, uint64_t table_len) +void LsdaParser::parse_call_sites(uint8_t call_enc, uint64_t table_len) { size_t end = index + static_cast(table_len); while (index < end) { @@ -361,7 +370,7 @@ void GccParser::parse_call_sites(uint8_t call_enc, uint64_t table_len) } // parse action table end -void GccParser::parse_actions_tail(size_t table_start, size_t limit_end) +void LsdaParser::parse_actions_tail(size_t table_start, size_t limit_end) { while (index < limit_end) { Action a{}; @@ -371,8 +380,11 @@ void GccParser::parse_actions_tail(size_t table_start, size_t limit_end) throw std::runtime_error("action parsing went over limit"); } if (index == limit_end) { - throw std::runtime_error( - "malformed action table odd sleb128 count"); + a.next_field_offset = -1; + a.next_offset = 0; + a.next_index = -1; + actions.push_back(a); + break; } // location of the 'next' field @@ -399,7 +411,7 @@ void GccParser::parse_actions_tail(size_t table_start, size_t limit_end) continue; } - const int64_t target_offset = a.next_field_offset + a.next_offset; + const int64_t target_offset = a.entry_offset + a.next_offset; int64_t found = -1; for (size_t j = 0; j < actions.size(); ++j) { @@ -427,7 +439,7 @@ void GccParser::parse_actions_tail(size_t table_start, size_t limit_end) // printers // NOTE: TEMPORARILY ADDED FILENAME PARAMETER FOR DEBUGGING -void GccParser::print_call_sites(const std::string& filename) const +void LsdaParser::print_call_sites(const std::string& filename) const { std::ofstream out(filename); if (!out) { @@ -441,7 +453,7 @@ void GccParser::print_call_sites(const std::string& filename) const } } -void GccParser::print_actions(const std::string& filename) const +void LsdaParser::print_actions(const std::string& filename) const { std::ofstream out(filename, std::ios::app); if (!out) { diff --git a/src/main.cpp b/src/main.cpp index 5ad3df2..b45daec 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -17,9 +17,12 @@ #include #include #include +#include +#include #include "abi_parse.hpp" #include "elf_parser.hpp" +#include "validator.hpp" /** * @enum main_error @@ -83,10 +86,24 @@ int main(int argc, char* argv[]) { auto args = validate_args(argc, argv); if (!args.has_value()) { - exit(EXIT_FAILURE); + return EXIT_FAILURE; + } + + ElfParser elf(args->file_name); + + auto sym = elf.get_symbol_table(); + if (!sym.has_value()) { + std::print("Failed to get symbol table\n"); + return EXIT_FAILURE; + } + + auto text = elf.get_section(".text"); + if (!text.has_value()) { + std::print("Failed to get .text section\n"); + return EXIT_FAILURE; } - ElfParser elf(args.value().file_name); + safe::Validator val(sym.value(), text.value()); auto gcc_except_table = elf.get_section(".gcc_except_table"); if (!gcc_except_table.has_value()) { @@ -97,10 +114,28 @@ int main(int argc, char* argv[]) if (gcc_except_table.error() == elf_parser_error::SECTION_NOT_FOUND) { std::print("Section was not found.\n"); } - exit(EXIT_FAILURE); + return EXIT_FAILURE; } - GccParser abi(gcc_except_table.value().data); + LsdaParser lsda(gcc_except_table->data); + + // Load LSDA catch table into Validator + val.load_lsda(lsda); + + auto res = val.analyze_exceptions("_Z3fooi"); + if (!res.has_value()) { + std::print("analyze_exceptions failed\n"); + return EXIT_FAILURE; + } + + // we print it but we canremove this + for (const auto& m : res.value()) { + auto dn = val.demangle(m.thrown.name.c_str()).value_or(m.thrown.name); + std::print("Thrown: {}\n", dn); + for (auto* h : m.handlers) { + std::print(" caught by {} type_index={}\n", h->scope_id, h->type_index); + } + } return 0; } \ No newline at end of file diff --git a/src/validator.cpp b/src/validator.cpp index a8e6d28..5fbe381 100644 --- a/src/validator.cpp +++ b/src/validator.cpp @@ -5,7 +5,12 @@ namespace safe { std::optional> Validator::find_typeinfo( std::string_view func_name) { - symbol_s func_sym = get_symbol(func_name).value(); + auto func_sym_opt = get_symbol(func_name); + if (!func_sym_opt.has_value()) { + return std::nullopt; + } + + symbol_s func_sym = *func_sym_opt; uint64_t func_addr = func_sym.value; GElf_Shdr text_hdr = m_text.header; uint64_t text_addr = text_hdr.sh_addr; @@ -27,10 +32,11 @@ std::optional> Validator::find_typeinfo( demangle(func_name.data()).value_or(func_name.data())); out << std::format("===========================\n"); - for (size_t i = 0; i < func_size - 8; ++i) { + // safer than (func_size - 8) + for (size_t i = 0; i + 4 <= func_size; ++i) { uint64_t current_addr = func_addr + i; int32_t rel_offset = *reinterpret_cast(func_start + i); - uint64_t target_addr = current_addr + 4 + rel_offset; + uint64_t target_addr = current_addr + 4 + static_cast(rel_offset); out << std::format("Offset: {:4} | Bytes: {:02x} {:02x} {:02x} {:02x} " "| Target: 0x{:x}\n", @@ -68,8 +74,7 @@ void Validator::collect_rtti_sym() continue; } if (demangle_sym->starts_with("typeinfo")) { - out << std::format( - " {} | {}\n", sym.value, demangle_sym.value()); + out << std::format(" {} | {}\n", sym.value, demangle_sym.value()); rtti_sym.emplace(sym.value, sym); } } @@ -88,10 +93,10 @@ std::optional Validator::get_symbol(std::string_view name) std::optional Validator::demangle(const char* mangled) { - int status; + int status = 0; char* demangled = abi::__cxa_demangle(mangled, nullptr, nullptr, &status); - if (status == 0) { + if (status == 0 && demangled) { std::string result(demangled); std::free(demangled); return result; @@ -99,4 +104,98 @@ std::optional Validator::demangle(const char* mangled) return std::nullopt; } +void Validator::load_lsda(const LsdaParser& lsda) +{ + std::println("[Validator] load_lsda: begin"); + m_lsda = &lsda; + m_records.clear(); + + const auto& scopes = lsda.get_scopes(); + std::println("[Validator] load_lsda: scopes = {}", scopes.size()); + std::size_t idx = 0; + + for (const auto& scope : scopes) { + std::string scope_label = "scope[" + std::to_string(idx) + ']'; + for (const auto& h : scope.handlers) { + CatchRecord rec{}; + rec.scope_id = scope_label; + rec.kind = h.type; + rec.range_begin = scope.start; + rec.range_end = scope.end; + rec.landing_pad = h.landing_pad; + rec.type_index = h.type_index; + m_records.push_back(std::move(rec)); + } + ++idx; + } + std::println("[Validator] load_lsda: records = {}", m_records.size()); +} + +Validator::Result Validator::analyze_exceptions(std::string_view func_name) const +{ + if (m_lsda == nullptr) { + return std::unexpected(CorrelateError::NoLsdaLoaded); + } + + // Use teammate's throw info as the source of truth + auto thrown_opt = const_cast(this)->find_typeinfo(func_name); + if (!thrown_opt.has_value()) { + return std::unexpected(CorrelateError::NoTypeinfoForFunction); + } + + const auto& thrown_vec = *thrown_opt; + if (thrown_vec.empty()) { + return std::unexpected(CorrelateError::NoThrownTypes); + } + + if (m_records.empty()) { + return std::unexpected(CorrelateError::NoCatchRecords); + } + + std::vector result; + result.reserve(thrown_vec.size()); + + for (const auto& t : thrown_vec) { + ThrowCatchMatch rel{ t, {} }; + const std::uint64_t thrown_addr = t.value; + + for (const auto& rec : m_records) { + if (rec.type_index == 0) { + rel.handlers.push_back(&rec); + continue; + } + if (rec.type_index < 0) { + continue; + } + if (rec.kind != HandlerType::Catch) { + continue; + } + + auto handler_addr_opt = m_lsda->resolve_type(rec.type_index); + if (!handler_addr_opt.has_value()) { + continue; + } + + if (*handler_addr_opt == thrown_addr) { + rel.handlers.push_back(&rec); + } + } + + result.push_back(std::move(rel)); + } + + bool any_handlers = false; + for (const auto& m : result) { + if (!m.handlers.empty()) { + any_handlers = true; + break; + } + } + if (!any_handlers) { + return std::unexpected(CorrelateError::NoCatchRecords); + } + + return result; +} + } // namespace safe diff --git a/tests/abi_parser.test.cpp b/tests/abi_parser.test.cpp index d15d55c..1890eb1 100644 --- a/tests/abi_parser.test.cpp +++ b/tests/abi_parser.test.cpp @@ -1,43 +1,58 @@ #include "abi_parse.hpp" +#include "elf_parser.hpp" + +#include + +#include #include #include #include #include -// keep in mind of pathing!!! -int main(int argc, char** argv) -{ - // temporarily reading LSDA file before merging with main - const char* path = (argc >= 2 ? argv[1] : "LSDA/lsda"); - std::ifstream file(path, std::ios::binary); - if (!file) { - path = "LSDA/lsda"; - file.open(path, std::ios::binary); - } - if (!file) { - std::cerr << "cannot open LSDA file\n"; - return 1; - } - - std::vector lsda_data((std::istreambuf_iterator(file)), - std::istreambuf_iterator()); - GccParser parser(lsda_data); - - try { - const auto& call_sites = parser.get_call_sites(); - const auto& actions = parser.get_actions(); - const auto& scopes = parser.get_scopes(); - - std::cout << "[Call Sites] count=" << call_sites.size() << "\n"; - std::cout << "[Actions] count=" << actions.size() << "\n"; - std::cout << "[Scopes] count=" << scopes.size() << "\n"; - // parser.print_call_sites("LSDA/lsda_output.txt"); // temporary file - // output parser.print_actions("LSDA/lsda_output.txt"); // temporary - // file output - - } catch (const std::exception& e) { - std::cerr << "parsing error: " << e.what() << "\n"; - return 1; - } - return 0; -} \ No newline at end of file +boost::ut::suite<"Abi_Parser_Test"> abi_parser_test = [] { + using namespace boost::ut; +#if defined(__unix__) || defined(__APPLE__) + // ensure the test binary exists (same pattern as other tests) + std::system("cd ../../testing_programs/ && ./generate_and_build.sh"); +#elif defined(_WIN32) + std::system("cd ../../testing_programs/ && ./generate_and_build.ps1"); +#endif + + "basic_lsda_parse"_test = [] { + // keep in mind of pathing!!! + const char* bin_path = "../../testing_programs/build/simple"; + + + ElfParser elf(bin_path); + + auto gcc_except_table = elf.get_section(".gcc_except_table"); + if (!gcc_except_table.has_value()) { + std::cerr << "Failed to get .gcc_except_table section\nReason: "; + if (gcc_except_table.error() == elf_parser_error::EMPTY_SECTION) { + std::cerr << "Elf parser does not contain sections.\n"; + } + if (gcc_except_table.error() + == elf_parser_error::SECTION_NOT_FOUND) { + std::cerr << "Section was not found.\n"; + } + expect(false) << "missing .gcc_except_table"; + return; + } + + LsdaParser parser(gcc_except_table.value().data); + + try { + const auto& call_sites = parser.get_call_sites(); + const auto& actions = parser.get_actions(); + const auto& scopes = parser.get_scopes(); + + expect(call_sites.size() > 0_u) << "no call sites parsed"; + expect(actions.size() > 0_u) << "no actions parsed"; + expect(scopes.size() > 0_u) << "no scopes parsed"; + + } catch (const std::exception& e) { + std::cerr << "parsing error: " << e.what() << "\n"; + expect(false) << "exception thrown while parsing LSDA"; + } + }; +}; diff --git a/tests/validator.test.cpp b/tests/validator.test.cpp index 7e84b73..e0f2c7d 100644 --- a/tests/validator.test.cpp +++ b/tests/validator.test.cpp @@ -12,6 +12,7 @@ #include #include #include +#include boost::ut::suite<"Validator_Test"> validator_test = [] { using namespace boost::ut; @@ -69,5 +70,37 @@ boost::ut::suite<"Validator_Test"> validator_test = [] { "Got: " << type_name << "\n"; } }; + + "Exception correlation"_test = [test_file] { + ElfParser elf(test_file); + + auto sym = elf.get_symbol_table(); + expect(sym.has_value()) << "sym table fail\n"; + + auto text = elf.get_section(".text"); + expect(text.has_value()) << "text fail\n"; + + auto gcc_except = elf.get_section(".gcc_except_table"); + expect(gcc_except.has_value()) << ".gcc_except_table fail\n"; + + safe::Validator val(sym.value(), text.value()); + LsdaParser lsda(gcc_except->data); + + val.load_lsda(lsda); + + auto res = val.analyze_exceptions("_Z3fooi"); + expect(res.has_value()) << "analyze_exceptions failed\n"; + + if (!res.has_value()) { + std::println("analyze_exceptions error code = {}", static_cast(res.error())); + return; + } + + bool any_caught = false; + for (const auto& m : res.value()) { + if (!m.handlers.empty()) { any_caught = true; break; } + } + expect(any_caught) << "No thrown types matched any catch handlers\n"; + }; }; }; diff --git a/tests/validator_catch.test.cpp b/tests/validator_catch.test.cpp new file mode 100644 index 0000000..0714f12 --- /dev/null +++ b/tests/validator_catch.test.cpp @@ -0,0 +1,106 @@ +#include "validator_catch.hpp" +#include "abi_parse.hpp" +#include "elf_parser.hpp" +#include "validator.hpp" + +#include + +#include +#include +#include + +using namespace std::string_view_literals; + +boost::ut::suite validator_catch_test = [] { + using namespace boost::ut; + +#if defined(__unix__) || defined(__APPLE__) + // small test program we parse LSDA from. + std::system("cd ../../testing_programs/ && ./generate_and_build.sh"); +#elif defined(_WIN32) + std::system("cd ../../testing_programs/ && ./generate_and_build.ps1"); +#endif + + "catch handler printer"_test = [] { + std::string_view test_file = "../../testing_programs/build/simple"; + + ElfParser elf(test_file); + + auto gcc_except_table = elf.get_section(".gcc_except_table"); + if (!gcc_except_table.has_value()) { + std::cerr << "Failed to get .gcc_except_table section\nReason: "; + if (gcc_except_table.error() == elf_parser_error::EMPTY_SECTION) { + std::cerr << "Elf parser does not contain sections.\n"; + } + if (gcc_except_table.error() + == elf_parser_error::SECTION_NOT_FOUND) { + std::cerr << "Section was not found.\n"; + } + expect(false) << "missing .gcc_except_table"; + return; + } + + // section_s has std::vector data, feed that directly into + // the LSDA parser. + LsdaParser lsda(gcc_except_table.value().data); + + safe::CatchValidator validator{ lsda }; + + const auto& records = validator.records(); + expect(records.size() > 0_u) << "no catch records parsed"; + + for (const auto& rec : records) { + expect(rec.range_begin <= rec.range_end) << "invalid handler range"; + } + + validator.print_records(); + }; + "throw-catch correlation"_test = [] { + std::string_view test_file = "../../testing_programs/build/simple"; + + ElfParser elf(test_file); + + // Symbol table and .text are for the throw-side Validator + auto symtab = elf.get_symbol_table(); + expect(symtab.has_value()) << "no symbol table"; + + auto text = elf.get_section(".text"); + expect(text.has_value()) << "missing .text section"; + + // LSDA section for CatchValidator + auto gcc_except_table = elf.get_section(".gcc_except_table"); + expect(gcc_except_table.has_value()) << "missing .gcc_except_table"; + + // Build throw-side validator (your teammate's) + safe::Validator throw_validator{ symtab.value(), text.value() }; + + // Build LSDA-side validator (yours) + LsdaParser lsda(gcc_except_table.value().data); + safe::CatchValidator catch_validator{ lsda }; + + // Function to inspect (mangled name); adjust if your test program + // uses a different function name. + constexpr auto func_name = "_Z3foov"sv; + + auto result + = catch_validator.correlate_with_throws(throw_validator, func_name); + + expect(result.has_value()) << "correlation failed with error code"; + + const auto& matches = result.value(); + expect(matches.size() > 0_u) << "no thrown types recorded for function"; + + // At least one thrown type should have at least one matching handler. + bool any_matched = false; + for (const auto& m : matches) { + if (!m.handlers.empty()) { + any_matched = true; + break; + } + } + expect(any_matched) << "no LSDA catch handlers matched any thrown type"; + + // Optional: print report for manual inspection + catch_validator.print_throw_catch_report(throw_validator, func_name); + }; +};