diff --git a/lldb/cmake/modules/FindDebuginfod.cmake b/lldb/cmake/modules/FindDebuginfod.cmake new file mode 100644 --- /dev/null +++ b/lldb/cmake/modules/FindDebuginfod.cmake @@ -0,0 +1,58 @@ +#.rst: +# FindDebuginfod +# ----------- +# +# Find debuginfod library and headers +# +# The module defines the following variables: +# +# :: +# +# Debuginfod_FOUND - true if debuginfod was found +# Debuginfod_INCLUDE_DIRS - include search path +# Debuginfod_LIBRARIES - libraries to link +# Debuginfod_VERSION_STRING - version number +# +# TODO(kwk): Debuginfod_VERSION_STRING is only set if pkg-config file is +# available. Trying to see if we can get a MAJOR, MINOR, PATCH define in the +# debuginfod.h file. + +if(Debuginfod_INCLUDE_DIRS AND Debuginfod_LIBRARIES) + set(Debuginfod_FOUND TRUE) +else() + # Utilize package config (e.g. /usr/lib64/pkgconfig/libdebuginfod.pc) to fetch + # version information. + find_package(PkgConfig QUIET) + pkg_check_modules(PC_Debuginfod QUIET libdebuginfod) + + find_path(Debuginfod_INCLUDE_DIRS + NAMES + elfutils/debuginfod.h + HINTS + /usr/include + ${PC_Debuginfod_INCLUDEDIR} + ${PC_Debuginfod_INCLUDE_DIRS} + ${CMAKE_INSTALL_FULL_INCLUDEDIR}) + find_library(Debuginfod_LIBRARIES + NAMES + debuginfod + HINTS + ${PC_Debuginfod_LIBDIR} + ${PC_Debuginfod_LIBRARY_DIRS} + ${CMAKE_INSTALL_FULL_LIBDIR}) + + if(Debuginfod_INCLUDE_DIRS AND EXISTS "${Debuginfod_INCLUDE_DIRS}/debuginfod.h") + set(Debuginfod_VERSION_STRING "${PC_Debuginfod_VERSION}") + endif() + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(Debuginfod + FOUND_VAR + Debuginfod_FOUND + REQUIRED_VARS + Debuginfod_INCLUDE_DIRS + Debuginfod_LIBRARIES + VERSION_VAR + Debuginfod_VERSION_STRING) + mark_as_advanced(Debuginfod_INCLUDE_DIRS Debuginfod_LIBRARIES) +endif() \ No newline at end of file diff --git a/lldb/cmake/modules/LLDBConfig.cmake b/lldb/cmake/modules/LLDBConfig.cmake --- a/lldb/cmake/modules/LLDBConfig.cmake +++ b/lldb/cmake/modules/LLDBConfig.cmake @@ -58,6 +58,7 @@ add_optional_dependency(LLDB_ENABLE_LUA "Enable Lua scripting support in LLDB" LuaAndSwig LUAANDSWIG_FOUND) add_optional_dependency(LLDB_ENABLE_PYTHON "Enable Python scripting support in LLDB" PythonInterpAndLibs PYTHONINTERPANDLIBS_FOUND) add_optional_dependency(LLDB_ENABLE_LIBXML2 "Enable Libxml 2 support in LLDB" LibXml2 LIBXML2_FOUND VERSION 2.8) +add_optional_dependency(LLDB_ENABLE_DEBUGINFOD "Enable Debuginfod support in LLDB" Debuginfod Debuginfod_FOUND) option(LLDB_USE_SYSTEM_SIX "Use six.py shipped with system and do not install a copy of it" OFF) option(LLDB_USE_ENTITLEMENTS "When codesigning, use entitlements if available" ON) @@ -233,6 +234,10 @@ include_directories(${LIBLZMA_INCLUDE_DIRS}) endif() +if (LLDB_ENABLE_DEBUGINFOD) + include_directories(${Debuginfod_INCLUDE_DIRS}) +endif() + if (LLDB_ENABLE_LIBXML2) list(APPEND system_libs ${LIBXML2_LIBRARIES}) include_directories(${LIBXML2_INCLUDE_DIR}) diff --git a/lldb/include/lldb/Host/Config.h.cmake b/lldb/include/lldb/Host/Config.h.cmake --- a/lldb/include/lldb/Host/Config.h.cmake +++ b/lldb/include/lldb/Host/Config.h.cmake @@ -36,6 +36,8 @@ #cmakedefine01 LLDB_ENABLE_LZMA +#cmakedefine01 LLDB_ENABLE_DEBUGINFOD + #cmakedefine01 LLDB_ENABLE_CURSES #cmakedefine01 LLDB_ENABLE_LIBEDIT diff --git a/lldb/include/lldb/Host/DebugInfoD.h b/lldb/include/lldb/Host/DebugInfoD.h new file mode 100644 --- /dev/null +++ b/lldb/include/lldb/Host/DebugInfoD.h @@ -0,0 +1,33 @@ +//===-- DebugInfoD.h --------------------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLDB_HOST_DEBUGINFOD_H +#define LLDB_HOST_DEBUGINFOD_H + +#include "lldb/Utility/UUID.h" +#include "llvm/ADT/Optional.h" +#include "llvm/Support/Error.h" + +namespace lldb_private { + +namespace debuginfod { + +// Returns \c true if debuginfod support was compiled-in; otherwise \c false is +// returned. +bool isAvailable(); + +// Asks all servers in environment variable \c DEBUGINFOD_URLS for the \a path +// of an artifact with a given \a buildID and returns the path to a locally +// cached version of the file. If there was an error, we return that instead. +llvm::Expected findSource(UUID buildID, const std::string &path); + +} // End of namespace debuginfod + +} // End of namespace lldb_private + +#endif // LLDB_HOST_DEBUGINFOD_H diff --git a/lldb/packages/Python/lldbsuite/test/httpserver.py b/lldb/packages/Python/lldbsuite/test/httpserver.py new file mode 100644 --- /dev/null +++ b/lldb/packages/Python/lldbsuite/test/httpserver.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +""" +The httpserver module contains an easy and ready-to-use HTTP file server. + +o ServeDirectoryWithHTTP: Spawns an HTTP file server in a separate thread. +""" + +import http.server +from threading import Thread, current_thread +from sys import stderr +from functools import partial +from os.path import abspath + + +def ServeDirectoryWithHTTP(directory="."): + """Spawns an http.server.HTTPServer in a separate thread on the given port. + + The server serves files from the given *directory*. The port listening on + will automatically be picked by the operating system to avoid race + conditions when trying to bind to an open port that turns out not to be + free afterall. The hostname is always "localhost". + + Parameters + ---------- + directory : str, optional + The directory to server files from. Defaults to the current directory. + + Returns + ------- + http.server.HTTPServer + The HTTP server which is serving files from a separate thread. + + It is not super necessary but you might want to call shutdown() on the + returned HTTP server object. This will stop the inifinite request loop + running in the thread which in turn will then exit. The reason why this + is only optional is that the thread in which the server runs is a daemon + thread which will be terminated when the main thread ends. + + By calling shutdown() you'll get a cleaner shutdown because the socket + is properly closed. + + str + The address of the server as a string, e.g. "http://localhost:1234". + + Examples + -------- + >>> from httpserver import ServeDirectoryWithHTTP + >>> from urllib.request import urlopen + >>> httpd, address = ServeDirectoryWithHTTP() + >>> print("Address:", address) # doctest:+ELLIPSIS + ... + Address: http://localhost...:... + >>> try: + ... url = address + "/httpserver.py" + ... print("Getting URL:", url) # doctest:+ELLIPSIS + ... with urlopen(url) as f: + ... print("Code:", f.getcode()) + ... finally: + ... httpd.shutdown() + ... + Getting URL: http://localhost...:.../httpserver.py + Code: 200 + + In the example above, you can call f.read() to read the content of the file + you've asked for. + + """ + + hostname = "localhost" + port = 0 + directory = abspath(directory) + handler = partial(_SimpleRequestHandler, directory=directory) + httpd = http.server.HTTPServer((hostname, 0), handler, False) + # Block only for 0.5 seconds max + httpd.timeout = 0.5 + # Allow for reusing the address + # HTTPServer sets this as well but I wanted to make this more obvious. + httpd.allow_reuse_address = True + + _xprint("server about to bind to port %d on hostname '%s'" % (port, hostname)) + httpd.server_bind() + + address = "http://%s:%d" % (httpd.server_name, httpd.server_port) + + _xprint("server about to listen on:", address) + httpd.server_activate() + + def serve_forever(httpd): + with httpd: # to make sure httpd.server_close is called + _xprint("server about to serve files from directory (infinite request loop):", directory) + httpd.serve_forever() + _xprint("server left infinite request loop") + + thread = Thread(target=serve_forever, args=(httpd, )) + thread.setDaemon(True) + thread.start() + + return httpd, address + + +def _xprint(*args, **kwargs): + """Wrapper function around print() that prepends the current thread name""" + print("[", current_thread().name, "]", + " ".join(map(str, args)), **kwargs, file=stderr) + + +class _SimpleRequestHandler(http.server.SimpleHTTPRequestHandler): + """Same as SimpleHTTPRequestHandler with adjusted logging.""" + + def log_message(self, format, *args): + """Log an arbitrary message and prepend the given thread name.""" + stderr.write("[ " + current_thread().name + " ] ") + http.server.SimpleHTTPRequestHandler.log_message(self, format, *args) + + +if __name__ == "__main__": + from doctest import testmod + testmod(verbose=True) diff --git a/lldb/source/Core/SourceManager.cpp b/lldb/source/Core/SourceManager.cpp --- a/lldb/source/Core/SourceManager.cpp +++ b/lldb/source/Core/SourceManager.cpp @@ -15,6 +15,7 @@ #include "lldb/Core/Highlighter.h" #include "lldb/Core/Module.h" #include "lldb/Core/ModuleList.h" +#include "lldb/Host/DebugInfoD.h" #include "lldb/Host/FileSystem.h" #include "lldb/Symbol/CompileUnit.h" #include "lldb/Symbol/Function.h" @@ -402,7 +403,9 @@ if (target) { m_source_map_mod_id = target->GetSourcePathMap().GetModificationID(); - if (!file_spec.GetDirectory() && file_spec.GetFilename()) { + SymbolContext sc; + if ((!file_spec.GetDirectory() && file_spec.GetFilename()) || + !FileSystem::Instance().Exists(m_file_spec)) { // If this is just a file name, lets see if we can find it in the // target: bool check_inlines = false; @@ -416,7 +419,7 @@ bool got_multiple = false; if (num_matches != 0) { if (num_matches > 1) { - SymbolContext sc; + // SymbolContext sc; CompileUnit *test_cu = nullptr; for (unsigned i = 0; i < num_matches; i++) { @@ -432,11 +435,12 @@ } } if (!got_multiple) { - SymbolContext sc; + // SymbolContext sc; sc_list.GetContextAtIndex(0, sc); if (sc.comp_unit) m_file_spec = sc.comp_unit->GetPrimaryFile(); - m_mod_time = FileSystem::Instance().GetModificationTime(m_file_spec); + m_mod_time = + FileSystem::Instance().GetModificationTime(m_file_spec); } } } @@ -452,6 +456,24 @@ m_mod_time = FileSystem::Instance().GetModificationTime(m_file_spec); } } + + // Try finding the file using elfutils' debuginfod + if (!FileSystem::Instance().Exists(m_file_spec) && + debuginfod::isAvailable() && sc.module_sp) { + llvm::Expected cache_path = debuginfod::findSource( + sc.module_sp->GetUUID(), file_spec.GetCString()); + if (!cache_path) { + sc.module_sp->ReportWarning( + "An error occurred while finding the " + "source file %s using debuginfod for build ID %s: %s", + file_spec.GetCString(), + sc.module_sp->GetUUID().GetAsString("").c_str(), + llvm::toString(cache_path.takeError()).c_str()); + } else if (!cache_path->empty()) { + m_file_spec = FileSpec(*cache_path); + m_mod_time = FileSystem::Instance().GetModificationTime(*cache_path); + } + } } } diff --git a/lldb/source/Host/CMakeLists.txt b/lldb/source/Host/CMakeLists.txt --- a/lldb/source/Host/CMakeLists.txt +++ b/lldb/source/Host/CMakeLists.txt @@ -16,6 +16,7 @@ common/HostThread.cpp common/LockFileBase.cpp common/LZMA.cpp + common/DebugInfoD.cpp common/MainLoop.cpp common/MonitoringProcessLauncher.cpp common/NativeProcessProtocol.cpp @@ -147,6 +148,9 @@ if (LLDB_ENABLE_LZMA) list(APPEND EXTRA_LIBS ${LIBLZMA_LIBRARIES}) endif() +if (LLDB_ENABLE_DEBUGINFOD) + list(APPEND EXTRA_LIBS ${Debuginfod_LIBRARIES}) +endif() if (WIN32) list(APPEND LLDB_SYSTEM_LIBS psapi) endif () diff --git a/lldb/source/Host/common/DebugInfoD.cpp b/lldb/source/Host/common/DebugInfoD.cpp new file mode 100644 --- /dev/null +++ b/lldb/source/Host/common/DebugInfoD.cpp @@ -0,0 +1,97 @@ +//===-- DebugInfoD.cpp ----------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "lldb/Host/DebugInfoD.h" +#include "lldb/Core/Module.h" +#include "lldb/Host/Config.h" +#include "lldb/Symbol/ObjectFile.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Errno.h" + +#if LLDB_ENABLE_DEBUGINFOD +#include "elfutils/debuginfod.h" +#endif + +namespace lldb_private { + +namespace debuginfod { + +using namespace lldb; +using namespace lldb_private; + +#if !LLDB_ENABLE_DEBUGINFOD +bool isAvailable() { return false; } + +llvm::Error findSource(UUID buildID, const std::string &path, + std::string &cache_path) { + llvm_unreachable("debuginfod::findSource is unavailable"); +} + +#else // LLDB_ENABLE_DEBUGINFOD + +bool isAvailable() { return true; } + +llvm::Expected findSource(UUID buildID, const std::string &path) { + if (!buildID.IsValid()) + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "invalid build ID: %s", + buildID.GetAsString("").c_str()); + + debuginfod_client *client = debuginfod_begin(); + + if (!client) + return llvm::createStringError( + llvm::inconvertibleErrorCode(), + "failed to create debuginfod connection handle: %s", strerror(errno)); + + // debuginfod_set_progressfn(client, [](debuginfod_client *client, long a, + // long b) -> int { + // fprintf(stderr, "KWK === a: %ld b : %ld \n", a, b); + // return 0; // continue + // }); + + char *cache_path = nullptr; + int rc = debuginfod_find_source(client, buildID.GetBytes().data(), + buildID.GetBytes().size(), path.c_str(), + &cache_path); + + debuginfod_end(client); + + std::string result_path; + if (cache_path) { + result_path = std::string(cache_path); + free(cache_path); + } + + if (rc < 0) { + if (rc == -ENOSYS) // No DEBUGINFO_URLS were specified + return result_path; + else if (rc == -ENOENT) // No such file or directory, aka build-id not + // available on servers. + return result_path; + else + return llvm::createStringError( + llvm::inconvertibleErrorCode(), + "debuginfod_find_source query failed (CODE=%d): %s", -rc, + llvm::sys::StrError(-rc).c_str()); + } + + if (close(rc) < 0) { + return llvm::createStringError( + llvm::inconvertibleErrorCode(), + "failed to close result of call to debuginfo_find_source: %s", + llvm::sys::StrError(errno).c_str()); + } + + return result_path; +} + +#endif // LLDB_ENABLE_DEBUGINFOD + +} // end of namespace debuginfod +} // namespace lldb_private diff --git a/lldb/test/CMakeLists.txt b/lldb/test/CMakeLists.txt --- a/lldb/test/CMakeLists.txt +++ b/lldb/test/CMakeLists.txt @@ -151,6 +151,7 @@ LLDB_ENABLE_PYTHON LLDB_ENABLE_LUA LLDB_ENABLE_LZMA + LLDB_ENABLE_DEBUGINFOD LLVM_ENABLE_ZLIB LLVM_ENABLE_SHARED_LIBS LLDB_IS_64_BITS) diff --git a/lldb/test/Shell/SymbolFile/DWARF/source-list.cpp b/lldb/test/Shell/SymbolFile/DWARF/source-list.cpp new file mode 100644 --- /dev/null +++ b/lldb/test/Shell/SymbolFile/DWARF/source-list.cpp @@ -0,0 +1,128 @@ +// clang-format off +// REQUIRES: debuginfod +// UNSUPPORTED: darwin, windows + +// Test that we can display the source of functions using debuginfod when the +// original source file is no longer present. +// +// The debuginfod client requires a buildid in the binary, so we compile one in. +// We can create the directory structure on disc that the client expects on a +// webserver that serves source files. Then we fire up a python based http +// server in the root of that mock directory, set the DEBUGINFOD_URLS +// environment variable and let LLDB do the rest. +// +// Go here to find more about debuginfod: +// https://sourceware.org/elfutils/Debuginfod.html + + +// We copy this file because we want to compile and later move it away + +// RUN: mkdir -p %t.buildroot +// RUN: cp %s %t.buildroot/test.cpp + + +// We cd into the directory before compiling to get DW_AT_comp_dir pickup +// %t.buildroot as well so it will be replaced by /my/new/path. + +// RUN: cd %t.buildroot +// RUN: %clang_host \ +// RUN: -g \ +// RUN: -Wl,--build-id="0xaaaaaaaaaabbbbbbbbbbccccccccccdddddddddd" \ +// RUN: -fdebug-prefix-map=%t.buildroot=/my/new/path \ +// RUN: -o %t \ +// RUN: test.cpp + + +// We move the original source file to a directory that looks like a debuginfod +// URL part. + +// RUN: rm -rf %t.mock +// RUN: mkdir -p %t.mock/buildid/aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd/source/my/new/path +// RUN: mv test.cpp %t.mock/buildid/aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd/source/my/new/path + + +// Adjust where debuginfod stores cache files: + +// RUN: rm -rfv %t.debuginfod_cache_path +// RUN: mkdir -pv %t.debuginfod_cache_path +// RUN: export DEBUGINFOD_CACHE_PATH=%t.debuginfod_cache_path + + +// Start HTTP file server on port picked by OS and wait until it is ready +// The server will be closed on exit of the test. + +// RUN: rm -fv "%t.server.log" +// RUN: timeout 5 python3 -u -m http.server 0 --directory %t.mock --bind "localhost" &> %t.server.log & export PID=$! +// RUN: trap 'echo "SERVER LOG:"; cat %t.server.log; kill $PID;' EXIT INT + + +// Extract HTTP address from the first line of the server log +// (e.g. "Serving HTTP on 127.0.0.1 port 40587 (http://127.0.0.1:40587/) ..") + +// RUN: echo -n "Waiting for server to be ready" +// RUN: SERVER_ADDRESS="" +// RUN: while [ -z "$SERVER_ADDRESS" ]; do \ +// RUN: echo -n "."; \ +// RUN: sleep 0.01; \ +// RUN: SERVER_ADDRESS=$(head -n1 %t.server.log | grep "http://.\+/\+" -o); \ +// RUN: done +// RUN: echo "DONE" + + +//-- TEST 1 -- No debuginfod awareness ---------------------------------------- + + +// RUN: DEBUGINFOD_URLS="" \ +// RUN: %lldb -f %t -o 'source list -n main' | FileCheck --dump-input=fail %s --check-prefix=TEST-1 + +// TEST-1: (lldb) source list -n main +// TEST-1: File: /my/new/path/test.cpp +// TEST-1-EMPTY: + + +//-- TEST 2 -- debuginfod URL pointing in wrong place -------------------------- + + +// RUN: DEBUGINFOD_URLS="http://example.com/debuginfod" \ +// RUN: %lldb -f %t -o 'source list -n main' | FileCheck --dump-input=fail %s --check-prefix=TEST-2 + +// TEST-2: (lldb) source list -n main +// TEST-2: File: /my/new/path/test.cpp +// TEST-2-EMPTY: + + +//-- TEST 3 -- debuginfod URL pointing corectly -------------------------------- + + +// RUN: DEBUGINFOD_URLS="$SERVER_ADDRESS" \ +// RUN: %lldb -f %t -o 'source list -n main' | FileCheck --dump-input=fail %s --check-prefix=TEST-3 + +// TEST-3: (lldb) source list -n main +// TEST-3: File: /my/new/path/test.cpp +// TEST-3: 116 +// TEST-3-NEXT: {{[0-9]+}} // Some context lines before +// TEST-3-NEXT: {{[0-9]+}} // the function. +// TEST-3-NEXT: {{[0-9]+}} +// TEST-3-NEXT: {{[0-9]+}} +// TEST-3-NEXT: {{[0-9]+}} int main(int argc, char **argv) { +// TEST-3-NEXT: {{[0-9]+}} // Here are some comments. +// TEST-3-NEXT: {{[0-9]+}} // That we should print when listing source. +// TEST-3-NEXT: {{[0-9]+}} return 0; +// TEST-3-NEXT: {{[0-9]+}} } +// TEST-3-NEXT: {{[0-9]+}} +// TEST-3-NEXT: {{[0-9]+}} // Some context lines after +// TEST-3-NEXT: {{[0-9]+}} // the function. +// TEST-3-EMPTY: + +// Some context lines before +// the function. + + +int main(int argc, char **argv) { + // Here are some comments. + // That we should print when listing source. + return 0; +} + +// Some context lines after +// the function. \ No newline at end of file diff --git a/lldb/test/Shell/lit.cfg.py b/lldb/test/Shell/lit.cfg.py --- a/lldb/test/Shell/lit.cfg.py +++ b/lldb/test/Shell/lit.cfg.py @@ -117,6 +117,9 @@ if config.lldb_enable_lzma: config.available_features.add('lzma') +if config.lldb_enable_debuginfod: + config.available_features.add('debuginfod') + if find_executable('xz') != None: config.available_features.add('xz') diff --git a/lldb/test/Shell/lit.site.cfg.py.in b/lldb/test/Shell/lit.site.cfg.py.in --- a/lldb/test/Shell/lit.site.cfg.py.in +++ b/lldb/test/Shell/lit.site.cfg.py.in @@ -16,6 +16,7 @@ config.python_executable = "@PYTHON_EXECUTABLE@" config.have_zlib = @LLVM_ENABLE_ZLIB@ config.lldb_enable_lzma = @LLDB_ENABLE_LZMA@ +config.lldb_enable_debuginfod = @LLDB_ENABLE_DEBUGINFOD@ config.host_triple = "@LLVM_HOST_TRIPLE@" config.lldb_bitness = 64 if @LLDB_IS_64_BITS@ else 32 config.lldb_enable_python = @LLDB_ENABLE_PYTHON@