diff --git a/.circleci/config.yml b/.circleci/config.yml index 0a75d995..c98ecdbd 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -119,6 +119,7 @@ jobs: libmsgpack-dev \ libzstd-dev \ llvm-15-dev \ + liblldb-15-dev \ ninja-build \ pkg-config \ python3-setuptools @@ -129,7 +130,7 @@ jobs: - run: name: Build command: | - cmake -G Ninja -B build/ -DWITH_FLAKY_TESTS=Off -DCODE_COVERAGE=On -DWARNINGS_AS_ERRORS=<< parameters.warnings_as_errors >> + cmake -G Ninja -B build/ -DWITH_FLAKY_TESTS=Off -DCODE_COVERAGE=On -DWARNINGS_AS_ERRORS=<< parameters.warnings_as_errors >> -DLLDB_ROOT=/usr/lib/llvm-15 cmake --build build/ # Testing rubbish: cp test/ci.oid.toml build/testing.oid.toml @@ -166,6 +167,7 @@ jobs: libboost-all-dev \ libgflags-dev \ llvm-15-dev \ + liblldb-15-dev \ libfmt-dev \ libjemalloc-dev environment: diff --git a/CMakeLists.txt b/CMakeLists.txt index d7f9ee7b..c7c712fc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -185,6 +185,8 @@ find_package(Clang REQUIRED CONFIG) message(STATUS "Found Clang ${LLVM_PACKAGE_VERSION}") message(STATUS "Using ClangConfig.cmake in: ${Clang_DIR}") +find_package(LLDB REQUIRED) + ### msgpack # msgpack v3.0.0 doesn't define the msgpackc-cxx target, but since the library is header only, # we can locate the header dir and add it to our include directories. diff --git a/cmake/FindLLDB.cmake b/cmake/FindLLDB.cmake new file mode 100644 index 00000000..871eb434 --- /dev/null +++ b/cmake/FindLLDB.cmake @@ -0,0 +1,29 @@ +# - Find LLDB +# Find the LLDB debugger library and includes +# +# LLDB_INCLUDE_DIRS - where to find LLDB.h, etc. +# LLDB_LIBRARIES - List of libraries when using LLDB. +# LLDB_FOUND - True if LLDB found. + +find_path(LLDB_INCLUDE_DIRS LLDB.h + HINTS ${LLDB_ROOT_DIR}/include + PATH_SUFFIXES lldb/API/) +# Remove the path suffixes +cmake_path(APPEND LLDB_INCLUDE_DIRS .. ..) + +find_library(LLDB_LIBRARIES lldb lldb-15 HINTS ${LLDB_ROOT_DIR}/lib) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(LLDB DEFAULT_MSG LLDB_LIBRARIES LLDB_INCLUDE_DIRS) + +mark_as_advanced( + LLDB_LIBRARIES + LLDB_INCLUDE_DIRS) + +if(LLDB_FOUND AND NOT (TARGET LLDB)) + add_library(LLDB UNKNOWN IMPORTED) + set_target_properties(LLDB + PROPERTIES + IMPORTED_LOCATION ${LLDB_LIBRARIES} + INTERFACE_INCLUDE_DIRECTORIES ${LLDB_INCLUDE_DIRS}) +endif() diff --git a/oi/CMakeLists.txt b/oi/CMakeLists.txt index 3754f413..c6112001 100644 --- a/oi/CMakeLists.txt +++ b/oi/CMakeLists.txt @@ -17,6 +17,7 @@ add_library(symbol_service SymbolService.cpp ) target_link_libraries(symbol_service + LLDB drgn_utils Boost::headers diff --git a/oi/Features.cpp b/oi/Features.cpp index 9d82b17c..9e04ac50 100644 --- a/oi/Features.cpp +++ b/oi/Features.cpp @@ -36,6 +36,8 @@ std::optional featureHelp(Feature f) { return "Generate statistics on padding of structures."; case Feature::CaptureThriftIsset: return "Capture isset data for Thrift object."; + case Feature::LLDB: + return "Use LLDB (instead of drgn) to parse the debug info."; case Feature::TypeGraph: return "Use Type Graph for code generation (CodeGen v2)."; case Feature::PruneTypeGraph: @@ -60,6 +62,9 @@ std::optional featureHelp(Feature f) { std::span requirements(Feature f) { switch (f) { + case Feature::LLDB: + static constexpr std::array lldb = {Feature::TypeGraph}; + return lldb; case Feature::TreeBuilderV2: static constexpr std::array tb2 = {Feature::TypeGraph}; return tb2; diff --git a/oi/Features.h b/oi/Features.h index 959a5761..01300cc5 100644 --- a/oi/Features.h +++ b/oi/Features.h @@ -27,6 +27,7 @@ X(PackStructs, "pack-structs") \ X(GenPaddingStats, "gen-padding-stats") \ X(CaptureThriftIsset, "capture-thrift-isset") \ + X(LLDB, "lldb") \ X(TypeGraph, "type-graph") \ X(PruneTypeGraph, "prune-type-graph") \ X(Library, "library") \ diff --git a/oi/OIDebugger.cpp b/oi/OIDebugger.cpp index ee9f040c..2d4fa1ee 100644 --- a/oi/OIDebugger.cpp +++ b/oi/OIDebugger.cpp @@ -2962,17 +2962,24 @@ bool OIDebugger::processTargetData() { } std::optional OIDebugger::generateCode(const irequest& req) { - auto root = symbols->getRootType(req); - if (!root.has_value()) { - return std::nullopt; - } std::string code(headers::oi_OITraceCode_cpp); if (generatorConfig.features[Feature::TypeGraph]) { // CodeGen v2 + std::string rootVariableName; CodeGen codegen2{generatorConfig, *symbols}; - codegen2.codegenFromDrgn(root->type.type, code); + if (generatorConfig.features[Feature::LLDB]) { + throw std::runtime_error{"LLDB is not implemented yet"}; + } else { + auto root = symbols->getDrgnRootType(req); + if (!root.has_value()) { + return std::nullopt; + } + + rootVariableName = std::move(root->varName); + codegen2.codegenFromDrgn(root->type.type, code); + } TypeHierarchy th; // Make this static as a big hack to extend the fake drgn_types' lifetimes @@ -2981,10 +2988,15 @@ std::optional OIDebugger::generateCode(const irequest& req) { drgn_type* rootType; codegen2.exportDrgnTypes(th, drgnTypes, &rootType); - typeInfos[req] = {RootInfo{root->varName, {rootType, drgn_qualifiers{}}}, + typeInfos[req] = {RootInfo{rootVariableName, {rootType, drgn_qualifiers{}}}, th, std::map{}}; } else { + auto root = symbols->getDrgnRootType(req); + if (!root.has_value()) { + return std::nullopt; + } + // OICodeGen (v1) auto codegen = OICodeGen::buildFromConfig(generatorConfig, *symbols); if (!codegen) { diff --git a/oi/SymbolService.cpp b/oi/SymbolService.cpp index 15d19d2e..f637c907 100644 --- a/oi/SymbolService.cpp +++ b/oi/SymbolService.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include "oi/DrgnUtils.h" #include "oi/OIParser.h" @@ -106,7 +107,8 @@ static bool isExecutableAddr( return it != end(exeAddrs) && addr >= it->first; } -SymbolService::SymbolService(pid_t pid) : target{pid} { +SymbolService::SymbolService(pid_t pid, Backend back) + : target{pid}, backend{back} { // Update target processes memory map LoadExecutableAddressRange(pid, executableAddrs); if (!loadModules()) { @@ -115,8 +117,8 @@ SymbolService::SymbolService(pid_t pid) : target{pid} { } } -SymbolService::SymbolService(fs::path executablePath) - : target{std::move(executablePath)} { +SymbolService::SymbolService(fs::path executablePath, Backend back) + : target{std::move(executablePath)}, backend{back} { if (!loadModules()) { throw std::runtime_error("Failed to load modules for executable " + executablePath.string()); @@ -131,6 +133,15 @@ SymbolService::~SymbolService() { if (prog != nullptr) { drgn_program_destroy(prog); } + + if (lldbTarget) { + lldbDebugger.DeleteTarget(lldbTarget); + } + + if (lldbDebugger) { + lldb::SBDebugger::Destroy(lldbDebugger); + lldb::SBDebugger::Terminate(); + } } struct ModParams { @@ -432,7 +443,12 @@ std::optional SymbolService::locateBuildID() { struct drgn_program* SymbolService::getDrgnProgram() { if (hardDisableDrgn) { - LOG(ERROR) << "drgn is disabled, refusing to initialize"; + LOG(ERROR) << "drgn/LLDB is disabled, refusing to initialize"; + return nullptr; + } + + if (backend != Backend::DRGN) { + LOG(ERROR) << "drgn is not the selected backend, refusing to initialize"; return nullptr; } @@ -484,6 +500,53 @@ struct drgn_program* SymbolService::getDrgnProgram() { return prog; } +lldb::SBTarget SymbolService::getLLDBTarget() { + if (hardDisableDrgn) { + LOG(ERROR) << "drgn/LLDB is disabled, refusing to initialize"; + return lldb::SBTarget(); + } + + if (backend != Backend::LLDB) { + LOG(ERROR) << "LLDB is not the selected backend, refusing to initialize"; + return lldb::SBTarget(); + } + + bool success = false; + + lldb::SBDebugger::Initialize(); + lldbDebugger = lldb::SBDebugger::Create(false); + BOOST_SCOPE_EXIT_ALL(&) { + if (!success) { + lldb::SBDebugger::Destroy(lldbDebugger); + lldb::SBDebugger::Terminate(); + } + }; + + switch (target.index()) { + case 0: { + auto pid = std::get(target); + lldbTarget = lldbDebugger.FindTargetWithProcessID(pid); + if (!lldbTarget) { + LOG(ERROR) << "Failed to find target with PID " << pid; + return lldb::SBTarget(); + } + break; + } + case 1: { + auto path = std::get(target); + lldbTarget = lldbDebugger.CreateTarget(path.c_str()); + if (!lldbTarget) { + LOG(ERROR) << "Failed to create target from " << path; + return lldb::SBTarget(); + } + break; + } + } + + success = true; + return lldbTarget; +} + /* * Although 'parseFormalParam' has an all-encompassing sounding name, its sole * task is to extract the location information for this parameter if any exist. @@ -804,7 +867,7 @@ std::string SymbolService::getTypeName(struct drgn_type* type) { return drgn_utils::typeToName(type); } -std::optional SymbolService::getRootType(const irequest& req) { +std::optional SymbolService::getDrgnRootType(const irequest& req) { if (req.type == "global") { /* * This is super simple as all we have to do is locate assign the @@ -912,4 +975,81 @@ std::optional SymbolService::getRootType(const irequest& req) { return RootInfo{paramName, paramType}; } +std::optional SymbolService::getLLDBRootType(const irequest& req) { + auto lldbTarget = getLLDBTarget(); + + if (req.type == "global") { + /* + * This is super simple as all we have to do is locate assign the + * type of the provided global variable. + */ + VLOG(1) << "Processing global: " << req.func; + + auto globalDesc = findGlobalDesc(req.func); + if (!globalDesc) { + return std::nullopt; + } + + auto globalVariable = lldbTarget.FindFirstGlobalVariable(req.func.c_str()); + if (!globalVariable.IsValid()) { + LOG(ERROR) << "Failed to lookup global variable '" << req.func << "'"; + return std::nullopt; + } + + return globalVariable.GetType(); + } + + VLOG(1) << "Passing : " << req.func; + auto fd = findFuncDesc(req); + if (!fd) { + VLOG(1) << "Failed to lookup function " << req.func; + return std::nullopt; + } + + auto functions = lldbTarget.FindFunctions(req.func.c_str()); + if (functions.GetSize() != 1) { + LOG(ERROR) << "Failed to lookup function '" << req.func << "'"; + return std::nullopt; + } + + auto function = functions.GetContextAtIndex(0).GetFunction(); + if (!function.IsValid()) { + LOG(ERROR) << "Failed to lookup function '" << req.func << "'"; + return std::nullopt; + } + + if (req.isReturnRetVal()) { + VLOG(1) << "Processing return retval"; + return function.GetType().GetFunctionReturnType(); + } + + if (req.arg == "this") { + VLOG(1) << "Processing this pointer"; + auto vars = function.GetBlock().GetVariables(lldbTarget, true, true, true); + for (uint32_t i = 0; i < vars.GetSize(); ++i) { + auto var = vars.GetValueAtIndex(i); + if (strcmp(var.GetName(), "this")) + return var.GetType(); + } + + LOG(ERROR) << "This pointer not found in function '" << req.func << "'"; + return std::nullopt; + } + + auto argIdx = fd->getArgumentIndex(req.arg); + if (!argIdx.has_value()) { + LOG(ERROR) << "Failed to lookup argument " << req.arg << " in function '" + << req.func << "'"; + return std::nullopt; + } + + auto args = function.GetBlock().GetVariables(lldbTarget, true, false, false); + if (!args.IsValid() || args.GetSize() <= argIdx.value()) { + LOG(ERROR) << "Failed to lookup arguments in function '" << req.func << "'"; + return std::nullopt; + } + + return args.GetValueAtIndex(*argIdx).GetType(); +} + } // namespace oi::detail diff --git a/oi/SymbolService.h b/oi/SymbolService.h index c95f125a..c4f7f654 100644 --- a/oi/SymbolService.h +++ b/oi/SymbolService.h @@ -16,6 +16,9 @@ #pragma once #include +#include +#include +#include #include #include #include @@ -38,14 +41,16 @@ struct SymbolInfo { }; class SymbolService { + enum class Backend { DRGN, LLDB }; public: - SymbolService(pid_t); - SymbolService(std::filesystem::path); + SymbolService(pid_t, Backend = Backend::DRGN); + SymbolService(std::filesystem::path, Backend = Backend::DRGN); SymbolService(const SymbolService&) = delete; SymbolService& operator=(const SymbolService&) = delete; ~SymbolService(); struct drgn_program* getDrgnProgram(); + lldb::SBTarget getLLDBTarget(); std::optional locateBuildID(); std::optional locateSymbol(const std::string&, @@ -54,7 +59,8 @@ class SymbolService { std::shared_ptr findFuncDesc(const irequest&); std::shared_ptr findGlobalDesc(const std::string&); static std::string getTypeName(struct drgn_type*); - std::optional getRootType(const irequest&); + std::optional getDrgnRootType(const irequest&); + std::optional getLLDBRootType(const irequest&); static std::optional findTypeOfSymbol( drgn_program*, const std::string& symbolName); @@ -70,8 +76,11 @@ class SymbolService { private: std::variant target; + Backend backend; struct Dwfl* dwfl{nullptr}; struct drgn_program* prog{nullptr}; + lldb::SBDebugger lldbDebugger{}; + lldb::SBTarget lldbTarget{}; bool loadModules(); bool loadModulesFromPid(pid_t); diff --git a/oi/type_graph/CMakeLists.txt b/oi/type_graph/CMakeLists.txt index 190bdfcd..2ddf726d 100644 --- a/oi/type_graph/CMakeLists.txt +++ b/oi/type_graph/CMakeLists.txt @@ -8,6 +8,7 @@ add_library(type_graph Flattener.cpp IdentifyContainers.cpp KeyCapture.cpp + LLDBParser.cpp NameGen.cpp PassManager.cpp Printer.cpp @@ -23,7 +24,9 @@ add_dependencies(type_graph libdrgn) target_link_libraries(type_graph container_info symbol_service + Boost::headers + LLDB "-L${DRGN_PATH}/.libs" drgn ) diff --git a/oi/type_graph/LLDBParser.cpp b/oi/type_graph/LLDBParser.cpp new file mode 100644 index 00000000..4d40f768 --- /dev/null +++ b/oi/type_graph/LLDBParser.cpp @@ -0,0 +1,387 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "LLDBParser.h" + +#include +#include +#include + +#include + +namespace std { +/* We use the address of the name string for hashing and equality. + * This relies on string de-duplication, two objects with the same name + * might have different addresses, but their de-duplicated name will + * have the same address. + * + * TODO: Is this correct? How does -fsimple-template-names impact this? + * Is there a better way to do this? + */ +size_t hash::operator()(const lldb::SBType& key) const { + auto* name = const_cast(key).GetName(); + auto Hasher = std::hash(); + { /* Debugging + auto& keyAsSP = reinterpret_cast(key); + fprintf(stderr, "LLDBType::hash(key@0x%08zx = (0x%08zx, %s)) = 0x%08zx\n", + (uintptr_t)keyAsSP.get(), (uintptr_t)name, name, Hasher(name)); + // Debugging */ + } + return Hasher(name); +} + +bool equal_to::operator()(const lldb::SBType& lhs, + const lldb::SBType& rhs) const { + auto* lhsName = const_cast(lhs).GetName(); + auto* rhsName = const_cast(rhs).GetName(); + auto EqualTo = std::equal_to(); + { /* Debugging + auto& lhsAsSP = reinterpret_cast(lhs); + auto& rhsAsSP = reinterpret_cast(rhs); + fprintf(stderr, "LLDBType::equal_to(" + "lhs@0x%08zx = (0x%08zx, %s), " + "rhs@0x%08zx = (0x%08zx, %s)) = %d\n", + (uintptr_t)lhsAsSP.get(), (uintptr_t)lhsName, lhsName, + (uintptr_t)rhsAsSP.get(), (uintptr_t)rhsName, rhsName, + EqualTo(lhsName, rhsName)); + // Debugging */ + } + return EqualTo(lhsName, rhsName); +} +} // namespace std + +namespace oi::detail::type_graph { + +Type& LLDBParser::parse(lldb::SBType& root) { + depth_ = 0; + return enumerateType(root); +} + +Type& LLDBParser::enumerateType(lldb::SBType& type) { + // Avoid re-enumerating an already-processsed type + if (auto it = lldb_types_.find(type); it != lldb_types_.end()) + return it->second; + + ++depth_; + BOOST_SCOPE_EXIT_ALL(&) { --depth_; }; + + std::optional> t; + try { + switch (auto kind = type.GetTypeClass()) { + case lldb::eTypeClassClass: + case lldb::eTypeClassStruct: + case lldb::eTypeClassUnion: + t = enumerateClass(type); + break; + case lldb::eTypeClassEnumeration: + t = enumerateEnum(type); + break; + case lldb::eTypeClassTypedef: + t = enumerateTypedef(type); + break; + case lldb::eTypeClassPointer: + case lldb::eTypeClassReference: + t = enumeratePointer(type); + break; + case lldb::eTypeClassBuiltin: + t = enumeratePrimitive(type); + break; + case lldb::eTypeClassArray: + t = enumerateArray(type); + break; + case lldb::eTypeClassInvalid: + case lldb::eTypeClassBlockPointer: + case lldb::eTypeClassComplexFloat: + case lldb::eTypeClassComplexInteger: + case lldb::eTypeClassFunction: + case lldb::eTypeClassMemberPointer: + case lldb::eTypeClassObjCObject: + case lldb::eTypeClassObjCInterface: + case lldb::eTypeClassObjCObjectPointer: + case lldb::eTypeClassVector: + case lldb::eTypeClassOther: + case lldb::eTypeClassAny: + throw LLDBParserError{"Unhandled type class: " + std::to_string(kind)}; + } + } catch (const LLDBParserError& e) { + if (type.IsTypeComplete()) + throw; + } + + if (!type.IsTypeComplete()) { + return makeType(type, *t); + } + + return t.value(); +} + +Class& LLDBParser::enumerateClass(lldb::SBType& type) { + auto name = type.GetName(); + auto displayName = type.GetUnqualifiedType().GetDisplayTypeName(); + auto size = type.GetByteSize(); + auto virtuality = type.IsPolymorphicClass(); + + Class::Kind kind; + switch (auto typeClass = type.GetTypeClass()) { + case lldb::eTypeClassClass: + kind = Class::Kind::Class; + break; + case lldb::eTypeClassStruct: + kind = Class::Kind::Struct; + break; + case lldb::eTypeClassUnion: + kind = Class::Kind::Union; + break; + default: + throw LLDBParserError{"Invalid type class for compound type: " + + std::to_string(typeClass)}; + } + + auto& c = makeType(type, kind, displayName, name, size, virtuality); + + enumerateClassTemplateParams(type, c.templateParams); + enumerateClassParents(type, c.parents); + enumerateClassMembers(type, c.members); + enumerateClassFunctions(type, c.functions); + + return c; +} + +void LLDBParser::enumerateClassTemplateParams( + lldb::SBType& type, std::vector& params) { + assert(params.empty()); + params.reserve(type.GetNumberOfTemplateArguments()); + + for (uint32_t i = 0; i < type.GetNumberOfTemplateArguments(); i++) { + auto param = type.GetTemplateArgumentType(i); + enumerateTemplateParam(type, param, i, params); + } +} + +void LLDBParser::enumerateTemplateParam(lldb::SBType& /*type*/, + lldb::SBType& param, + uint32_t /*i*/, + std::vector& params) { + QualifierSet qualifiers; + qualifiers[Qualifier::Const] = // TODO: Wonky as hell :/ + param.GetName() != param.GetUnqualifiedType().GetName(); + auto& paramType = enumerateType(param); + + params.emplace_back(paramType, qualifiers); +} + +void LLDBParser::enumerateClassParents(lldb::SBType& type, + std::vector& parents) { + assert(parents.empty()); + parents.reserve(type.GetNumberOfDirectBaseClasses()); + + for (uint32_t i = 0; i < type.GetNumberOfDirectBaseClasses(); i++) { + auto base = type.GetDirectBaseClassAtIndex(i); + auto baseType = base.GetType(); + + parents.emplace_back(enumerateType(baseType), base.GetOffsetInBits()); + } +} + +void LLDBParser::enumerateClassMembers(lldb::SBType& type, + std::vector& members) { + assert(members.empty()); + members.reserve(type.GetNumberOfFields()); + + /* TODO: We are missing the _vptr */ + for (uint32_t i = 0; i < type.GetNumberOfFields(); i++) { + auto field = type.GetFieldAtIndex(i); + if (field.GetName() == nullptr) + continue; // Skip anonymous fields (TODO: Why?) + auto fieldName = field.GetName(); + auto fieldType = field.GetType(); + auto bitFieldSize = field.GetBitfieldSizeInBits(); + auto bitOffset = field.GetOffsetInBits(); + + auto& enumeratedField = enumerateType(fieldType); + members.emplace_back(enumeratedField, fieldName, bitOffset, bitFieldSize); + } + + std::sort(members.begin(), members.end(), [](const auto& a, const auto& b) { + return a.bitOffset < b.bitOffset; + }); +} + +void LLDBParser::enumerateClassFunctions(lldb::SBType& type, + std::vector& functions) { + assert(functions.empty()); + functions.reserve(type.GetNumberOfMemberFunctions()); + + /* TODO: We are missing the default constructors */ + for (uint32_t i = 0; i < type.GetNumberOfMemberFunctions(); i++) { + auto function = type.GetMemberFunctionAtIndex(i); + + /* TODO: We don't know if the function is virtual */ + functions.emplace_back(function.GetName(), false); + } +} + +Enum& LLDBParser::enumerateEnum(lldb::SBType& type) { + const char* typeName = type.GetName(); + std::string name = typeName ? typeName : ""; + uint64_t size = type.GetByteSize(); + + std::map enumeratorMap; + if (options_.readEnumValues) { + auto members = type.GetEnumMembers(); + for (uint32_t i = 0; i < members.GetSize(); i++) { + auto member = members.GetTypeEnumMemberAtIndex(i); + enumeratorMap[member.GetValueAsSigned()] = member.GetName(); + } + } + + return makeType(type, name, size, std::move(enumeratorMap)); +} + +Typedef& LLDBParser::enumerateTypedef(lldb::SBType& type) { + std::string name = type.GetName(); + + lldb::SBType underlyingType = type.GetTypedefedType(); + auto& t = enumerateType(underlyingType); + return makeType(type, name, t); +} + +Type& LLDBParser::enumeratePointer(lldb::SBType& type) { + if (!chasePointer()) { + return makeType(type, Primitive::Kind::StubbedPointer); + } + + lldb::SBType pointeeType; + switch (auto kind = type.GetTypeClass()) { + case lldb::eTypeClassPointer: + pointeeType = type.GetPointeeType(); + break; + case lldb::eTypeClassReference: + pointeeType = type.GetDereferencedType(); + break; + default: + throw LLDBParserError{"Invalid type class for pointer type: " + + std::to_string(kind)}; + } + + if (pointeeType.IsFunctionType()) { + return makeType(type, Primitive::Kind::StubbedPointer); + } + + Type& t = enumerateType(pointeeType); + return makeType(type, t); +} + +bool LLDBParser::chasePointer() const { + // Always chase top-level pointers. + if (depth_ == 1) + return true; + return options_.chaseRawPointers; +} + +Primitive::Kind LLDBParser::primitiveIntKind(lldb::SBType& type, + bool is_signed) { + switch (auto size = type.GetByteSize()) { + case 1: + return is_signed ? Primitive::Kind::Int8 : Primitive::Kind::UInt8; + case 2: + return is_signed ? Primitive::Kind::Int16 : Primitive::Kind::UInt16; + case 4: + return is_signed ? Primitive::Kind::Int32 : Primitive::Kind::UInt32; + case 8: + return is_signed ? Primitive::Kind::Int64 : Primitive::Kind::UInt64; + default: + throw LLDBParserError{"Invalid integer size: " + std::to_string(size)}; + } +} +Primitive::Kind LLDBParser::primitiveFloatKind(lldb::SBType& type) { + switch (auto size = type.GetByteSize()) { + case 4: + return Primitive::Kind::Float32; + case 8: + return Primitive::Kind::Float64; + case 16: + return Primitive::Kind::Float128; + default: + throw LLDBParserError{"Invalid float size: " + std::to_string(size)}; + } +} + +Array& LLDBParser::enumerateArray(lldb::SBType& type) { + auto elementType = type.GetArrayElementType(); + /* TODO: Is there a better way to get the array size? */ + auto len = type.GetByteSize() / elementType.GetByteSize(); + auto& t = enumerateType(elementType); + return makeType(type, t, len); +} + +Primitive& LLDBParser::enumeratePrimitive(lldb::SBType& type) { + Primitive::Kind primitiveKind = Primitive::Kind::Void; + + switch (auto kind = type.GetBasicType()) { + /* TODO: Do we need to handle char differently from int? */ + case lldb::eBasicTypeChar: + case lldb::eBasicTypeSignedChar: + case lldb::eBasicTypeWChar: + case lldb::eBasicTypeSignedWChar: + case lldb::eBasicTypeChar16: + case lldb::eBasicTypeChar32: + case lldb::eBasicTypeChar8: + case lldb::eBasicTypeShort: + case lldb::eBasicTypeInt: + case lldb::eBasicTypeLong: + case lldb::eBasicTypeLongLong: + case lldb::eBasicTypeInt128: + primitiveKind = primitiveIntKind(type, true); + break; + case lldb::eBasicTypeUnsignedChar: + case lldb::eBasicTypeUnsignedWChar: + case lldb::eBasicTypeUnsignedShort: + case lldb::eBasicTypeUnsignedInt: + case lldb::eBasicTypeUnsignedLong: + case lldb::eBasicTypeUnsignedLongLong: + case lldb::eBasicTypeUnsignedInt128: + primitiveKind = primitiveIntKind(type, false); + break; + case lldb::eBasicTypeHalf: + case lldb::eBasicTypeFloat: + case lldb::eBasicTypeDouble: + case lldb::eBasicTypeLongDouble: + primitiveKind = primitiveFloatKind(type); + break; + case lldb::eBasicTypeBool: + primitiveKind = Primitive::Kind::Bool; + break; + case lldb::eBasicTypeVoid: + case lldb::eBasicTypeNullPtr: + primitiveKind = Primitive::Kind::Void; + break; + case lldb::eBasicTypeInvalid: + case lldb::eBasicTypeFloatComplex: + case lldb::eBasicTypeDoubleComplex: + case lldb::eBasicTypeLongDoubleComplex: + case lldb::eBasicTypeObjCID: + case lldb::eBasicTypeObjCClass: + case lldb::eBasicTypeObjCSel: + case lldb::eBasicTypeOther: + default: + throw LLDBParserError{"Unhandled primitive type: " + + std::to_string(kind)}; + } + + return makeType(type, primitiveKind); +} + +} // namespace oi::detail::type_graph diff --git a/oi/type_graph/LLDBParser.h b/oi/type_graph/LLDBParser.h new file mode 100644 index 00000000..be9e158b --- /dev/null +++ b/oi/type_graph/LLDBParser.h @@ -0,0 +1,97 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include + +#include "TypeGraph.h" +#include "Types.h" + +namespace std { +/* lldb::SBType doesn't provide a hash and equality operators. + * So we define our own specialization of them here. + */ +template <> +struct hash { + size_t operator()(const lldb::SBType& type) const; +}; + +template <> +struct equal_to { + bool operator()(const lldb::SBType& lhs, const lldb::SBType& rhs) const; +}; +} // namespace std + +namespace oi::detail::type_graph { + +struct LLDBParserOptions { + bool chaseRawPointers = false; + bool readEnumValues = false; +}; + +class LLDBParser { + public: + LLDBParser(TypeGraph& typeGraph, LLDBParserOptions options) + : typeGraph_(typeGraph), options_(options) { + } + Type& parse(lldb::SBType& root); + + private: + Type& enumerateType(lldb::SBType& type); + Class& enumerateClass(lldb::SBType& type); + Enum& enumerateEnum(lldb::SBType& type); + Typedef& enumerateTypedef(lldb::SBType& type); + Type& enumeratePointer(lldb::SBType& type); + Array& enumerateArray(lldb::SBType& type); + Primitive& enumeratePrimitive(lldb::SBType& type); + + Primitive::Kind primitiveIntKind(lldb::SBType& type, bool is_signed); + Primitive::Kind primitiveFloatKind(lldb::SBType& type); + + void enumerateClassTemplateParams(lldb::SBType& type, + std::vector& params); + void enumerateTemplateParam(lldb::SBType& type, + lldb::SBType& param, + uint32_t i, + std::vector& params); + void enumerateClassParents(lldb::SBType& type, std::vector& parents); + void enumerateClassMembers(lldb::SBType& type, std::vector& members); + void enumerateClassFunctions(lldb::SBType& type, + std::vector& functions); + + bool chasePointer() const; + + template + T& makeType(lldb::SBType& lldbType, Args&&... args) { + auto& newType = typeGraph_.makeType(std::forward(args)...); + lldb_types_.emplace(lldbType, newType); + return newType; + } + + std::unordered_map> lldb_types_; + + TypeGraph& typeGraph_; + int depth_; + LLDBParserOptions options_; +}; + +class LLDBParserError : public std::runtime_error { + public: + LLDBParserError(const std::string& msg) : std::runtime_error{msg} { + } +}; + +} // namespace oi::detail::type_graph diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 287ebcda..d24b3c58 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -45,6 +45,7 @@ add_executable(test_type_graph test_flattener.cpp test_identify_containers.cpp test_key_capture.cpp + test_lldb_parser.cpp test_name_gen.cpp test_node_tracker.cpp test_prune.cpp diff --git a/test/integration/alignment.toml b/test/integration/alignment.toml index b87e1620..7baeea16 100644 --- a/test/integration/alignment.toml +++ b/test/integration/alignment.toml @@ -41,6 +41,21 @@ definitions = ''' }; ''' [cases] + [cases.struct] + param_types = ["const Align16&"] + setup = "return {};" + expect_json = '''[ + {"staticSize": 16, "exclusiveSize": 15, "members": [ + {"typeName": "int8_t", "staticSize": 1, "exclusiveSize": 1} + ]}]''' + [cases.member_alignment] + param_types = ["const MemberAlignment&"] + setup = "return {};" + expect_json = '''[ + {"staticSize": 64, "exclusiveSize": 32, "members": [ + {"typeName": "int8_t", "staticSize": 1, "exclusiveSize": 1}, + {"typeName": "int8_t", "staticSize": 1, "exclusiveSize": 1} + ]}]''' [cases.wrapper_struct] param_types = ["const Wrapper&"] setup = "return {};" diff --git a/test/test_drgn_parser.cpp b/test/test_drgn_parser.cpp index e8d03e3d..87796ac6 100644 --- a/test/test_drgn_parser.cpp +++ b/test/test_drgn_parser.cpp @@ -10,6 +10,7 @@ #include "oi/type_graph/Printer.h" #include "oi/type_graph/TypeGraph.h" #include "oi/type_graph/Types.h" +#include "type_graph_utils.h" using namespace type_graph; @@ -34,7 +35,7 @@ DrgnParser DrgnParserTest::getDrgnParser(TypeGraph& typeGraph, drgn_type* DrgnParserTest::getDrgnRoot(std::string_view function) { irequest req{"entry", std::string{function}, "arg0"}; - auto* drgnRoot = symbols_->getRootType(req)->type.type; + auto* drgnRoot = symbols_->getDrgnRootType(req)->type.type; return drgnRoot; } @@ -73,64 +74,6 @@ void DrgnParserTest::test(std::string_view function, test(function, expected, options); } -std::pair globMatch(std::string_view pattern, - std::string_view str) { - size_t i = 0; - size_t j = 0; - size_t prevWildcardIdx = -1; - size_t backtrackIdx = -1; - size_t patternLineStart = 0; - size_t strLineStart = 0; - - while (i < str.size()) { - if (i + 1 < str.size() && str[i] == '\n') - strLineStart = i + 1; - if (j + 1 < pattern.size() && pattern[j] == '\n') - patternLineStart = j + 1; - - if (j < pattern.size() && str[i] == pattern[j]) { - // Exact character match - i++; - j++; - } else if (j < pattern.size() && pattern[j] == '*') { - // Wildcard - backtrackIdx = i + 1; - prevWildcardIdx = j++; - } else if (prevWildcardIdx != static_cast(-1)) { - // No match, backtrack to previous wildcard - i = ++backtrackIdx; - j = prevWildcardIdx + 1; - } else { - // No match - return {patternLineStart, strLineStart}; - } - } - - while (j < pattern.size() && pattern[j] == '*') { - j++; - } - - // If the pattern has been fully consumed then it's a match - return {j, i}; -} - -std::string prefixLines(std::string_view str, - std::string_view prefix, - size_t maxLen) { - std::string res; - res += prefix; - for (size_t i = 0; i < maxLen && i < str.size(); i++) { - char c = str[i]; - res += c; - if (c == '\n') { - res += prefix; - } - } - if (str.size() > maxLen) - res += "..."; - return res; -} - void DrgnParserTest::testGlob(std::string_view function, std::string_view expected, DrgnParserOptions options) { diff --git a/test/test_lldb_parser.cpp b/test/test_lldb_parser.cpp new file mode 100644 index 00000000..1089d6ce --- /dev/null +++ b/test/test_lldb_parser.cpp @@ -0,0 +1,661 @@ +#include "test_lldb_parser.h" + +#include +#include +#include + +#include + +#include "oi/OIParser.h" +#include "oi/SymbolService.h" +#include "oi/type_graph/NodeTracker.h" +#include "oi/type_graph/Printer.h" +#include "oi/type_graph/TypeGraph.h" +#include "oi/type_graph/Types.h" +#include "type_graph_utils.h" + +using namespace type_graph; + +// TODO setup google logging for tests so it doesn't appear on terminal by +// default + +lldb::SBDebugger LLDBParserTest::debugger_; +lldb::SBTarget LLDBParserTest::target_; + +void LLDBParserTest::SetUpTestSuite() { + lldb::SBDebugger::Initialize(); + debugger_ = lldb::SBDebugger::Create(false); + target_ = debugger_.CreateTarget(TARGET_EXE_PATH); +} + +void LLDBParserTest::TearDownTestSuite() { + debugger_.DeleteTarget(target_); + lldb::SBDebugger::Destroy(debugger_); + lldb::SBDebugger::Terminate(); +} + +LLDBParser LLDBParserTest::getLLDBParser(TypeGraph& typeGraph, + LLDBParserOptions options) { + return LLDBParser{typeGraph, options}; +} + +lldb::SBType LLDBParserTest::getLLDBRoot(std::string_view function) { + /* This method makes a lot of assumptions about the structure of the + * integration_test_target. Each test case has a single function with a + * single argument. + */ + auto fns = target_.FindFunctions(function.data()); + EXPECT_EQ(fns.GetSize(), 1); + auto fn = fns.GetContextAtIndex(0).GetFunction(); + auto args = fn.GetBlock().GetVariables(target_, true, false, false); + EXPECT_EQ(args.GetSize(), 1); + auto root = args.GetValueAtIndex(0).GetType(); + EXPECT_TRUE(root.IsValid()); + return root; +} + +std::string LLDBParserTest::run(std::string_view function, + LLDBParserOptions options) { + TypeGraph typeGraph; + auto lldbParser = getLLDBParser(typeGraph, options); + auto lldbRoot = getLLDBRoot(function); + + Type& type = lldbParser.parse(lldbRoot); + + std::stringstream out; + NodeTracker tracker; + Printer printer{out, tracker, typeGraph.size()}; + printer.print(type); + + return out.str(); +} + +void LLDBParserTest::test(std::string_view function, + std::string_view expected, + LLDBParserOptions options) { + auto actual = run(function, options); + + expected.remove_prefix(1); // Remove initial '\n' + EXPECT_EQ(expected, actual); +} + +void LLDBParserTest::test(std::string_view function, + std::string_view expected) { + // Enable options in unit tests so we get more coverage + LLDBParserOptions options = { + .chaseRawPointers = true, + .readEnumValues = true, + }; + test(function, expected, options); +} + +void LLDBParserTest::testGlob(std::string_view function, + std::string_view expected, + LLDBParserOptions options) { + auto actual = run(function, options); + + expected.remove_prefix(1); // Remove initial '\n' + auto [expectedIdx, actualIdx] = globMatch(expected, actual); + if (expected.size() != expectedIdx) { + ADD_FAILURE() << prefixLines(expected.substr(expectedIdx), "-", 10000) + << "\n" + << prefixLines(actual.substr(actualIdx), "+", 10000); + } +} + +void LLDBParserTest::testMultiCompiler( + std::string_view function, + [[maybe_unused]] std::string_view expectedClang, + [[maybe_unused]] std::string_view expectedGcc, + LLDBParserOptions options) { +#if defined(__clang__) + test(function, expectedClang, options); +#else + test(function, expectedGcc, options); +#endif +} + +void LLDBParserTest::testMultiCompilerGlob( + std::string_view function, + [[maybe_unused]] std::string_view expectedClang, + [[maybe_unused]] std::string_view expectedGcc, + LLDBParserOptions options) { +#if defined(__clang__) + testGlob(function, expectedClang, options); +#else + testGlob(function, expectedGcc, options); +#endif +} + +TEST_F(LLDBParserTest, SimpleStruct) { + test("oid_test_case_simple_struct", R"( +[1] Pointer +[0] Struct: ns_simple::SimpleStruct (size: 16) + Member: a (offset: 0) + Primitive: int32_t + Member: b (offset: 4) + Primitive: int8_t + Member: c (offset: 8) + Primitive: int64_t +)"); +} + +TEST_F(LLDBParserTest, SimpleClass) { + test("oid_test_case_simple_class", R"( +[1] Pointer +[0] Class: ns_simple::SimpleClass (size: 16) + Member: a (offset: 0) + Primitive: int32_t + Member: b (offset: 4) + Primitive: int8_t + Member: c (offset: 8) + Primitive: int64_t +)"); +} + +TEST_F(LLDBParserTest, SimpleUnion) { + test("oid_test_case_simple_union", R"( +[1] Pointer +[0] Union: ns_simple::SimpleUnion (size: 8) + Member: a (offset: 0) + Primitive: int32_t + Member: b (offset: 0) + Primitive: int8_t + Member: c (offset: 0) + Primitive: int64_t +)"); +} + +TEST_F(LLDBParserTest, Inheritance) { + test("oid_test_case_inheritance_access_public", R"( +[4] Pointer +[0] Class: ns_inheritance_access::Public (size: 8) + Parent (offset: 0) +[1] Class: ns_inheritance_access::Base (size: 4) + Member: base_int (offset: 0) +[3] Typedef: int32_t +[2] Typedef: __int32_t + Primitive: int32_t + Member: public_int (offset: 4) + [3] +)"); +} + +TEST_F(LLDBParserTest, InheritanceMultiple) { + test("oid_test_case_inheritance_multiple_a", R"( +[6] Pointer +[0] Struct: ns_inheritance_multiple::Derived_2 (size: 24) + Parent (offset: 0) +[1] Struct: ns_inheritance_multiple::Base_1 (size: 4) + Member: a (offset: 0) + Primitive: int32_t + Parent (offset: 4) +[2] Struct: ns_inheritance_multiple::Derived_1 (size: 12) + Parent (offset: 0) +[3] Struct: ns_inheritance_multiple::Base_2 (size: 4) + Member: b (offset: 0) + Primitive: int32_t + Parent (offset: 4) +[4] Struct: ns_inheritance_multiple::Base_3 (size: 4) + Member: c (offset: 0) + Primitive: int32_t + Member: d (offset: 8) + Primitive: int32_t + Parent (offset: 16) +[5] Struct: ns_inheritance_multiple::Base_4 (size: 4) + Member: e (offset: 0) + Primitive: int32_t + Member: f (offset: 20) + Primitive: int32_t +)"); +} + +TEST_F(LLDBParserTest, Container) { + testMultiCompilerGlob("oid_test_case_std_vector_int_empty", + R"( +[13] Pointer +[0] Class: std::vector > (size: 24) + Param + Primitive: int32_t + Param +[1] Class: std::allocator (size: 1) + Param + Primitive: int32_t + Parent (offset: 0) +[3] Typedef: std::__allocator_base +[2] Class: __gnu_cxx::new_allocator (size: 1) + Param + Primitive: int32_t + Function: new_allocator + Function: new_allocator + Function: allocate + Function: deallocate + Function: _M_max_size + Function: allocator + Function: allocator + Function: operator= + Function: ~allocator + Function: allocate + Function: deallocate + Parent (offset: 0) +* +)", + R"( +[9] Pointer +[0] Class: std::vector > (size: 24) + Param + Primitive: int32_t + Param +[1] Class: std::allocator (size: 1) + Parent (offset: 0) +[2] Class: __gnu_cxx::new_allocator (size: 1) + Param + Primitive: int32_t + Function: new_allocator + Function: new_allocator + Function: allocate + Function: deallocate + Function: _M_max_size + Function: allocator + Function: allocator + Function: operator= + Function: ~allocator + Function: allocate + Function: deallocate + Parent (offset: 0) +* +)"); +} +// TODO test vector with custom allocator + +TEST_F(LLDBParserTest, Enum) { + test("oid_test_case_enums_scoped", R"( + Enum: ns_enums::ScopedEnum (size: 4) + Enumerator: 0:CaseA + Enumerator: 1:CaseB + Enumerator: 2:CaseC +)"); +} + +TEST_F(LLDBParserTest, EnumUint8) { + test("oid_test_case_enums_scoped_uint8", R"( + Enum: ns_enums::ScopedEnumUint8 (size: 1) + Enumerator: 2:CaseA + Enumerator: 3:CaseB + Enumerator: 4:CaseC +)"); +} + +TEST_F(LLDBParserTest, UnscopedEnum) { + test("oid_test_case_enums_unscoped", R"( + Enum: ns_enums::UNSCOPED_ENUM (size: 4) + Enumerator: -2:CASE_B + Enumerator: 5:CASE_A + Enumerator: 20:CASE_C +)"); +} + +TEST_F(LLDBParserTest, EnumNoValues) { + LLDBParserOptions options{}; + test("oid_test_case_enums_scoped", + R"( + Enum: ns_enums::ScopedEnum (size: 4) +)", + options); +} + +TEST_F(LLDBParserTest, Typedef) { + test("oid_test_case_typedefs_c_style", R"( +[2] Typedef: ns_typedefs::TdUInt64 +[1] Typedef: uint64_t +[0] Typedef: __uint64_t + Primitive: uint64_t +)"); +} + +TEST_F(LLDBParserTest, Using) { + test("oid_test_case_typedefs_using", R"( +[2] Typedef: ns_typedefs::UsingUInt64 +[1] Typedef: uint64_t +[0] Typedef: __uint64_t + Primitive: uint64_t +)"); +} + +TEST_F(LLDBParserTest, ArrayMember) { + test("oid_test_case_arrays_member_int10", R"( +[2] Pointer +[0] Struct: ns_arrays::Foo10 (size: 40) + Member: arr (offset: 0) +[1] Array: (length: 10) + Primitive: int32_t +)"); +} + +TEST_F(LLDBParserTest, ArrayRef) { + test("oid_test_case_arrays_ref_int10", R"( +[1] Pointer +[0] Array: (length: 10) + Primitive: int32_t +)"); +} + +TEST_F(LLDBParserTest, ArrayDirect) { + test("oid_test_case_arrays_direct_int10", R"( +[0] Pointer + Primitive: int32_t +)"); +} + +TEST_F(LLDBParserTest, Pointer) { + test("oid_test_case_pointers_struct_primitive_ptrs", R"( +[3] Pointer +[0] Struct: ns_pointers::PrimitivePtrs (size: 24) + Member: a (offset: 0) + Primitive: int32_t + Member: b (offset: 8) +[1] Pointer + Primitive: int32_t + Member: c (offset: 16) +[2] Pointer + Primitive: void +)"); +} + +TEST_F(LLDBParserTest, PointerNoFollow) { + LLDBParserOptions options{}; + test("oid_test_case_pointers_struct_primitive_ptrs", + R"( +[1] Pointer +[0] Struct: ns_pointers::PrimitivePtrs (size: 24) + Member: a (offset: 0) + Primitive: int32_t + Member: b (offset: 8) + Primitive: StubbedPointer + Member: c (offset: 16) + Primitive: StubbedPointer +)", + options); +} + +TEST_F(LLDBParserTest, PointerIncomplete) { + test("oid_test_case_pointers_incomplete_raw", R"( +[1] Pointer + Incomplete +[0] Struct: ns_pointers_incomplete::IncompleteType (size: 0) +)"); +} + +TEST_F(LLDBParserTest, Cycle) { + test("oid_test_case_cycles_raw_ptr", R"( +[2] Pointer +[0] Struct: ns_cycles::RawNode (size: 16) + Member: value (offset: 0) + Primitive: int32_t + Member: next (offset: 8) +[1] Pointer + [0] +)"); +} + +TEST_F(LLDBParserTest, ClassTemplateInt) { + test("oid_test_case_templates_int", R"( +[1] Pointer +[0] Class: ns_templates::TemplatedClass1 (size: 4) + Param + Primitive: int32_t + Member: val (offset: 0) + Primitive: int32_t +)"); +} + +TEST_F(LLDBParserTest, ClassTemplateTwo) { + test("oid_test_case_templates_two", R"( +[3] Pointer +[0] Class: ns_templates::TemplatedClass2 (size: 12) + Param +[1] Struct: ns_templates::Foo (size: 8) + Member: a (offset: 0) + Primitive: int32_t + Member: b (offset: 4) + Primitive: int32_t + Param + Primitive: int32_t + Member: tc1 (offset: 0) +[2] Class: ns_templates::TemplatedClass1 (size: 8) + Param + [1] + Member: val (offset: 0) + [1] + Member: val2 (offset: 8) + Primitive: int32_t +)"); +} + +TEST_F(LLDBParserTest, ClassTemplateValue) { + test("oid_test_case_templates_value", R"( +[2] Pointer +[0] Struct: ns_templates::TemplatedClassVal<3> (size: 12) + Param + Value: 3 + Primitive: int32_t + Member: arr (offset: 0) +[1] Array: (length: 3) + Primitive: int32_t +)"); +} + +TEST_F(LLDBParserTest, TemplateEnumValue) { + testMultiCompilerGlob("oid_test_case_enums_params_scoped_enum_val", + R"( +[1] Pointer +[0] Class: ns_enums_params::MyClass (size: 4) + Param + Value: ns_enums_params::MyNS::ScopedEnum::One + Enum: ns_enums_params::MyNS::ScopedEnum (size: 4) +* +)", + R"( +[1] Pointer +[0] Class: ns_enums_params::MyClass<(ns_enums_params::MyNS::ScopedEnum)1> (size: 4) + Param + Value: ns_enums_params::MyNS::ScopedEnum::One + Enum: ns_enums_params::MyNS::ScopedEnum (size: 4) +* +)"); +} + +TEST_F(LLDBParserTest, TemplateEnumValueGaps) { + testMultiCompilerGlob("oid_test_case_enums_params_scoped_enum_val_gaps", + R"( +[1] Pointer +[0] Class: ns_enums_params::ClassGaps (size: 4) + Param + Value: ns_enums_params::MyNS::EnumWithGaps::Twenty + Enum: ns_enums_params::MyNS::EnumWithGaps (size: 4) +* +)", + R"( +[1] Pointer +[0] Class: ClassGaps<(ns_enums_params::MyNS::EnumWithGaps)20> (size: 4) + Param + Value: ns_enums_params::MyNS::EnumWithGaps::Twenty + Enum: ns_enums_params::MyNS::EnumWithGaps (size: 4) +* +)"); +} + +TEST_F(LLDBParserTest, TemplateEnumValueNegative) { + testMultiCompilerGlob("oid_test_case_enums_params_scoped_enum_val_negative", + R"( +[1] Pointer +[0] Class: ns_enums_params::ClassGaps (size: 4) + Param + Value: ns_enums_params::MyNS::EnumWithGaps::MinusTwo + Enum: ns_enums_params::MyNS::EnumWithGaps (size: 4) +* +)", + R"( +[1] Pointer +[0] Class: ns_enums_params::ClassGaps<(ns_enums_params::MyNS::EnumWithGaps)-2> (size: 4) + Param + Value: ns_enums_params::MyNS::EnumWithGaps::MinusTwo + Enum: ns_enums_params::MyNS::EnumWithGaps (size: 4) +* +)"); +} + +// TODO +// TEST_F(LLDBParserTest, ClassFunctions) { +// test("TestClassFunctions", R"( +//[0] Pointer +//[1] Class: ClassFunctions (size: 4) +// Member: memberA (offset: 0) +// Primitive: int32_t +// Function: foo (virtuality: 0) +// Function: bar (virtuality: 0) +//)"); +//} + +TEST_F(LLDBParserTest, StructAlignment) { + GTEST_SKIP() << "Alignment not reported by LLDB yet"; + test("oid_test_case_alignment_struct", R"( +[1] Pointer +[0] Struct: ns_alignment::Align16 (size: 16, align: 16) + Member: c (offset: 0) + Primitive: int8_t +)"); +} + +TEST_F(LLDBParserTest, MemberAlignment) { + GTEST_SKIP() << "Alignment not reported by LLDB yet"; + test("oid_test_case_alignment_member_alignment", R"( +[1] Pointer +[0] Struct: ns_alignment::MemberAlignment (size: 64) + Member: c (offset: 0) + Primitive: int8_t + Member: c32 (offset: 32, align: 32) + Primitive: int8_t +)"); +} + +TEST_F(LLDBParserTest, VirtualFunctions) { + testMultiCompiler("oid_test_case_inheritance_polymorphic_a_as_a", + R"( +[1] Pointer +[0] Class: ns_inheritance_polymorphic::A (size: 16) + Member: _vptr$A (offset: 0) + Primitive: StubbedPointer + Member: int_a (offset: 8) + Primitive: int32_t + Function: ~A (virtual) + Function: myfunc (virtual) + Function: A + Function: A +)", + R"( +[1] Pointer +[0] Class: A (size: 16) + Member: _vptr.A (offset: 0) + Primitive: StubbedPointer + Member: int_a (offset: 8) + Primitive: int32_t + Function: operator= + Function: A + Function: A + Function: ~A (virtual) + Function: myfunc (virtual) +)"); +} + +TEST_F(LLDBParserTest, BitfieldsSingle) { + test("oid_test_case_bitfields_single", R"( +[1] Pointer +[0] Struct: ns_bitfields::Single (size: 4) + Member: bitfield (offset: 0, bitsize: 3) + Primitive: int32_t +)"); +} + +TEST_F(LLDBParserTest, BitfieldsWithinBytes) { + test("oid_test_case_bitfields_within_bytes", R"( +[1] Pointer +[0] Struct: ns_bitfields::WithinBytes (size: 2) + Member: a (offset: 0, bitsize: 3) + Primitive: int8_t + Member: b (offset: 0.375, bitsize: 5) + Primitive: int8_t + Member: c (offset: 1, bitsize: 7) + Primitive: int8_t +)"); +} + +TEST_F(LLDBParserTest, BitfieldsStraddleBytes) { + test("oid_test_case_bitfields_straddle_bytes", R"( +[1] Pointer +[0] Struct: ns_bitfields::StraddleBytes (size: 3) + Member: a (offset: 0, bitsize: 7) + Primitive: int8_t + Member: b (offset: 1, bitsize: 7) + Primitive: int8_t + Member: c (offset: 2, bitsize: 2) + Primitive: int8_t +)"); +} + +TEST_F(LLDBParserTest, BitfieldsMixed) { + test("oid_test_case_bitfields_mixed", R"( +[1] Pointer +[0] Struct: ns_bitfields::Mixed (size: 12) + Member: a (offset: 0) + Primitive: int32_t + Member: b (offset: 4, bitsize: 4) + Primitive: int8_t + Member: c (offset: 4.5, bitsize: 12) + Primitive: int16_t + Member: d (offset: 6) + Primitive: int8_t + Member: e (offset: 8, bitsize: 22) + Primitive: int32_t +)"); +} + +TEST_F(LLDBParserTest, BitfieldsMoreBitsThanType) { + // TODO: Validate with integration tests that 29 bitsize doesn't break CodeGen + test("oid_test_case_bitfields_more_bits_than_type", R"( +[1] Pointer +[0] Struct: ns_bitfields::MoreBitsThanType (size: 4) + Member: a (offset: 0, bitsize: 29) + Primitive: int8_t +)"); +} + +TEST_F(LLDBParserTest, BitfieldsZeroBits) { + test("oid_test_case_bitfields_zero_bits", R"( +[1] Pointer +[0] Struct: ns_bitfields::ZeroBits (size: 2) + Member: b1 (offset: 0, bitsize: 3) + Primitive: int8_t + Member: b2 (offset: 1, bitsize: 2) + Primitive: int8_t +)"); +} + +TEST_F(LLDBParserTest, BitfieldsEnum) { + test("oid_test_case_bitfields_enum", R"( +[1] Pointer +[0] Struct: ns_bitfields::Enum (size: 4) + Member: e (offset: 0, bitsize: 2) + Enum: ns_bitfields::MyEnum (size: 4) + Enumerator: 0:One + Enumerator: 1:Two + Enumerator: 2:Three + Member: f (offset: 0.25, bitsize: 4) + Enum: ns_bitfields::MyEnum (size: 4) + Enumerator: 0:One + Enumerator: 1:Two + Enumerator: 2:Three +)"); +} + +// TODO test virtual classes diff --git a/test/test_lldb_parser.h b/test/test_lldb_parser.h new file mode 100644 index 00000000..fd25d27e --- /dev/null +++ b/test/test_lldb_parser.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include + +#include "oi/type_graph/LLDBParser.h" + +namespace oi::detail { +class SymbolService; +} +namespace oi::detail::type_graph { +class TypeGraph; +} + +using namespace oi::detail; + +class LLDBParserTest : public ::testing::Test { + protected: + static void SetUpTestSuite(); + static void TearDownTestSuite(); + + static type_graph::LLDBParser getLLDBParser( + type_graph::TypeGraph& typeGraph, type_graph::LLDBParserOptions options); + lldb::SBType getLLDBRoot(std::string_view function); + + virtual std::string run(std::string_view function, + type_graph::LLDBParserOptions options); + void test(std::string_view function, + std::string_view expected, + type_graph::LLDBParserOptions options); + void test(std::string_view function, std::string_view expected); + void testGlob(std::string_view function, + std::string_view expected, + type_graph::LLDBParserOptions options = {}); + void testMultiCompiler(std::string_view function, + std::string_view expectedClang, + std::string_view expectedGcc, + type_graph::LLDBParserOptions options = {}); + void testMultiCompilerGlob(std::string_view function, + std::string_view expectedClang, + std::string_view expectedGcc, + type_graph::LLDBParserOptions options = {}); + + static lldb::SBDebugger debugger_; + static lldb::SBTarget target_; +}; diff --git a/test/type_graph_utils.cpp b/test/type_graph_utils.cpp index 2d5709f3..3c81e96a 100644 --- a/test/type_graph_utils.cpp +++ b/test/type_graph_utils.cpp @@ -106,3 +106,61 @@ Container getPair(NodeId id) { return Container{ id, info, 8, nullptr}; // Nonsense size, shouldn't matter for tests } + +std::pair globMatch(std::string_view pattern, + std::string_view str) { + size_t i = 0; + size_t j = 0; + size_t prevWildcardIdx = -1; + size_t backtrackIdx = -1; + size_t patternLineStart = 0; + size_t strLineStart = 0; + + while (i < str.size()) { + if (i + 1 < str.size() && str[i] == '\n') + strLineStart = i + 1; + if (j + 1 < pattern.size() && pattern[j] == '\n') + patternLineStart = j + 1; + + if (j < pattern.size() && str[i] == pattern[j]) { + // Exact character match + i++; + j++; + } else if (j < pattern.size() && pattern[j] == '*') { + // Wildcard + backtrackIdx = i + 1; + prevWildcardIdx = j++; + } else if (prevWildcardIdx != static_cast(-1)) { + // No match, backtrack to previous wildcard + i = ++backtrackIdx; + j = prevWildcardIdx + 1; + } else { + // No match + return {patternLineStart, strLineStart}; + } + } + + while (j < pattern.size() && pattern[j] == '*') { + j++; + } + + // If the pattern has been fully consumed then it's a match + return {j, i}; +} + +std::string prefixLines(std::string_view str, + std::string_view prefix, + size_t maxLen) { + std::string res; + res += prefix; + for (size_t i = 0; i < maxLen && i < str.size(); i++) { + char c = str[i]; + res += c; + if (c == '\n') { + res += prefix; + } + } + if (str.size() > maxLen) + res += "..."; + return res; +} diff --git a/test/type_graph_utils.h b/test/type_graph_utils.h index 216b6b32..bba9fd55 100644 --- a/test/type_graph_utils.h +++ b/test/type_graph_utils.h @@ -28,3 +28,9 @@ type_graph::Container getVector(type_graph::NodeId id = 0); type_graph::Container getMap(type_graph::NodeId id = 0); type_graph::Container getList(type_graph::NodeId id = 0); type_graph::Container getPair(type_graph::NodeId id = 0); + +std::pair globMatch(std::string_view pattern, + std::string_view str); +std::string prefixLines(std::string_view str, + std::string_view prefix, + size_t maxLen);