From 4999e24bb7e58cc51fbf93c8a151bb389d22807c Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Wed, 29 Jul 2026 10:48:40 +1000 Subject: [PATCH] [common] debug: switch to libunwind instead of bfd --- .github/workflows/build.yml | 8 +- client/CMakeLists.txt | 33 ++ common/cmake/FindBFD.cmake | 40 -- common/src/platform/linux/CMakeLists.txt | 9 +- common/src/platform/linux/bfd.inc.h | 39 -- common/src/platform/linux/crash.c | 470 +++++++++++++++-------- doc/build.rst | 15 +- 7 files changed, 363 insertions(+), 251 deletions(-) delete mode 100644 common/cmake/FindBFD.cmake delete mode 100644 common/src/platform/linux/bfd.inc.h diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b0b0fec9..624fadce 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,7 +21,7 @@ jobs: - name: Install client dependencies run: | sudo apt-get install \ - binutils-dev \ + libdw-dev libunwind-dev \ libspice-protocol-dev nettle-dev \ libgl-dev libgles-dev \ libx11-dev libxss-dev libxi-dev libxinerama-dev libxcursor-dev libxpresent-dev \ @@ -77,8 +77,8 @@ jobs: sudo apt-get update - name: Install Linux host dependencies run: | - sudo apt-get install binutils-dev libglib2.0-dev libxcb-xfixes0-dev \ - libpipewire-0.3-dev libxcb-shm0-dev + sudo apt-get install libdw-dev libunwind-dev libglib2.0-dev \ + libxcb-xfixes0-dev libpipewire-0.3-dev libxcb-shm0-dev - name: Configure Linux host run: | mkdir host/build @@ -206,7 +206,7 @@ jobs: sudo apt-get update - name: Install obs plugin dependencies run: | - sudo apt-get install binutils-dev libobs-dev libgl1-mesa-dev + sudo apt-get install libdw-dev libunwind-dev libobs-dev libgl1-mesa-dev - name: Configure obs plugin run: | mkdir obs/build diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 5ff2c9eb..85b9c559 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -57,12 +57,20 @@ add_compile_options( "$<$:-Wimplicit-fallthrough=2>" "-Werror" "-Wfatal-errors" + "-g" "-ffast-math" "-fdata-sections" "-ffunction-sections" "$<$:-O0;-g3;-ggdb>" ) +if(ENABLE_BACKTRACE) + add_compile_options( + "-fno-omit-frame-pointer" + "-fno-optimize-sibling-calls" + ) +endif() + set(EXE_FLAGS "-Wl,--gc-sections -z noexecstack") set(CMAKE_C_STANDARD 11) @@ -156,6 +164,27 @@ add_executable(looking-glass-client ${SOURCES}) target_compile_definitions(looking-glass-client PRIVATE CIMGUI_DEFINE_ENUMS_AND_STRUCTS=1) +if(NOT CMAKE_OBJCOPY) + message(FATAL_ERROR "objcopy is required to generate the client debug file") +endif() + +if(NOT CMAKE_STRIP) + message(FATAL_ERROR "strip is required to generate the client debug file") +endif() + +set(CLIENT_DEBUG_FILE + "${CMAKE_CURRENT_BINARY_DIR}/looking-glass-client.debug") + +add_custom_command(TARGET looking-glass-client POST_BUILD + COMMAND "${CMAKE_OBJCOPY}" --only-keep-debug + "$" "${CLIENT_DEBUG_FILE}" + COMMAND "${CMAKE_STRIP}" --strip-debug "$" + COMMAND "${CMAKE_OBJCOPY}" "--add-gnu-debuglink=${CLIENT_DEBUG_FILE}" + "$" + COMMENT "Extracting Looking Glass client debug information" + VERBATIM +) + target_link_libraries(looking-glass-client ${EXE_FLAGS} PkgConfig::FONTCONFIG @@ -182,6 +211,10 @@ install(TARGETS looking-glass-client RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT binary) +install(FILES "${CLIENT_DEBUG_FILE}" + DESTINATION "${CMAKE_INSTALL_BINDIR}/.debug" + COMPONENT binary) + install(FILES "${CMAKE_BINARY_DIR}/resources/looking-glass-client.desktop" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications") install(FILES "${PROJECT_TOP}/resources/lg-logo.svg" diff --git a/common/cmake/FindBFD.cmake b/common/cmake/FindBFD.cmake deleted file mode 100644 index e16d1d5b..00000000 --- a/common/cmake/FindBFD.cmake +++ /dev/null @@ -1,40 +0,0 @@ -# Try to find the BFD librairies -# BFD_FOUND - system has BFD lib -# BFD_INCLUDE_DIR - the BFD include directory -# BFD_LIBRARIES - Libraries needed to use BFD - -if (BFD_INCLUDE_DIR AND BFD_LIBRARIES) - # Already in cache, be silent - set(BFD_FIND_QUIETLY TRUE) -endif (BFD_INCLUDE_DIR AND BFD_LIBRARIES) - -find_path(BFD_INCLUDE_DIR NAMES bfd.h) -find_library(BFD_LIBRARIES NAMES bfd) - -include(FindPackageHandleStandardArgs) - -if (";${BFD_LIBRARIES};" MATCHES "bfd.a;") - MESSAGE(STATUS "Linking against static bfd") - - find_library(BFD_LIBIBERTY_LIBRARIES NAMES libiberty.a) - find_package_handle_standard_args(BFD_LIBIBERTY DEFAULT_MSG BFD_LIBIBERTY_LIBRARIES) - - find_library(BFD_LIBZ_LIBRARIES NAMES libz.a) - find_package_handle_standard_args(BFD_LIBZ DEFAULT_MSG BFD_LIBZ_LIBRARIES) - - if (NOT ${BFD_LIBIBERTY_FOUND}) - message(FATAL_ERROR "Using static libbfd.a, but libiberty.a not available") - elseif (NOT ${BFD_LIBZ_FOUND}) - message(FATAL_ERROR "Using static libbfd.a, but libz.a not available") - else() - list(APPEND BFD_LIBRARIES ${BFD_LIBIBERTY_LIBRARIES} ${BFD_LIBZ_LIBRARIES}) - endif() -endif() - -MESSAGE(STATUS "BFD libs: " "${BFD_LIBRARIES}") - -find_package_handle_standard_args(BFD DEFAULT_MSG BFD_LIBRARIES BFD_INCLUDE_DIR) - -MESSAGE(STATUS "BFD libs: " "${BFD_LIBRARIES}") - -mark_as_advanced(BFD_INCLUDE_DIR BFD_LIBRARIES) diff --git a/common/src/platform/linux/CMakeLists.txt b/common/src/platform/linux/CMakeLists.txt index 4b7515cb..e723ea58 100644 --- a/common/src/platform/linux/CMakeLists.txt +++ b/common/src/platform/linux/CMakeLists.txt @@ -20,8 +20,13 @@ add_library(lg_common_platform_code STATIC ) if(ENABLE_BACKTRACE) - find_package(BFD) - target_link_libraries(lg_common_platform_code ${BFD_LIBRARIES}) + find_package(PkgConfig REQUIRED) + pkg_check_modules(LIBUNWIND REQUIRED IMPORTED_TARGET libunwind) + pkg_check_modules(LIBDW REQUIRED IMPORTED_TARGET libdw) + target_link_libraries(lg_common_platform_code + PkgConfig::LIBUNWIND + PkgConfig::LIBDW + ) endif() target_link_libraries(lg_common_platform_code diff --git a/common/src/platform/linux/bfd.inc.h b/common/src/platform/linux/bfd.inc.h deleted file mode 100644 index c49d3649..00000000 --- a/common/src/platform/linux/bfd.inc.h +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Looking Glass - * Copyright © 2017-2026 The Looking Glass Authors - * https://looking-glass.io - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 of the License, or (at your option) - * any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., 59 - * Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -#ifndef PACKAGE - #define PACKAGE - #ifndef PACKAGE_VERSION - #define PACKAGE_VERSION - #include - #undef PACKAGE_VERSION - #else - #include - #endif - #undef PACKAGE -#else - #ifndef PACKAGE_VERSION - #define PACKAGE_VERSION - #include - #undef PACKAGE_VERSION - #else - #include - #endif -#endif \ No newline at end of file diff --git a/common/src/platform/linux/crash.c b/common/src/platform/linux/crash.c index a2f255d4..5768825a 100644 --- a/common/src/platform/linux/crash.c +++ b/common/src/platform/linux/crash.c @@ -24,226 +24,374 @@ #if defined(ENABLE_BACKTRACE) -#include +#define UNW_LOCAL_ONLY +#include +#include + +#include +#include #include -#include -#include +#include #include +#include +#include +#include #include #include #include -#include +#define CRASH_MAX_FRAMES 64 -#include -#include "bfd.inc.h" - -struct range +struct CrashRequest { - intptr_t start, end; + int signal; + uintptr_t faultAddress; + unsigned frameCount; + uintptr_t frames[CRASH_MAX_FRAMES]; }; -struct crash +struct CrashState { - char * exe; - struct range * ranges; - int rangeCount; - bfd * fd; - asection * section; - asymbol ** syms; - long symCount; - bool loaded; + pid_t reporter; + int requestFd; + int responseFd; }; -static struct crash crash = {0}; - -static bool load_symbols(void) +static struct CrashState crash = { - bfd_init(); - crash.fd = bfd_openr(crash.exe, NULL); - if (!crash.fd) - { - DEBUG_ERROR("failed to open '%s'", crash.exe); - return false; - } + .reporter = -1, + .requestFd = -1, + .responseFd = -1 +}; - crash.fd->flags |= BFD_DECOMPRESS; - - char **matching; - if (!bfd_check_format_matches(crash.fd, bfd_object, &matching)) +static bool writeAll(int fd, const void * buffer, size_t size) +{ + const uint8_t * pos = buffer; + while (size) { - DEBUG_ERROR("executable is not a bfd_object"); - return false; - } + ssize_t written = write(fd, pos, size); + if (written < 0) + { + if (errno == EINTR) + continue; + return false; + } - crash.section = bfd_get_section_by_name(crash.fd, ".text"); - if (!crash.section) - { - DEBUG_ERROR("failed to find .text section"); - return false; - } - - if ((bfd_get_file_flags(crash.fd) & HAS_SYMS) == 0) - { - DEBUG_ERROR("executable '%s' has no symbols", crash.exe); - return false; - } - - long storage = bfd_get_symtab_upper_bound(crash.fd); - crash.syms = malloc(storage); - crash.symCount = bfd_canonicalize_symtab(crash.fd, crash.syms); - if (crash.symCount < 0) - { - DEBUG_ERROR("failed to get the symbol count"); - return false; + pos += written; + size -= written; } return true; } -static bool lookup_address(bfd_vma pc, const char ** filename, const char ** function, unsigned int * line, unsigned int * discriminator) +static bool readAll(int fd, void * buffer, size_t size) { -#ifdef bfd_get_section_flags - if ((bfd_get_section_flags(crash.fd, crash.section) & SEC_ALLOC) == 0) -#else - if ((bfd_section_flags(crash.section) & SEC_ALLOC) == 0) -#endif - return false; + uint8_t * pos = buffer; + while (size) + { + ssize_t received = read(fd, pos, size); + if (received == 0) + return false; -#ifdef bfd_get_section_size - bfd_size_type size = bfd_get_section_size(crash.section); -#else - bfd_size_type size = bfd_section_size(crash.section); -#endif - if (pc >= size) - return false; + if (received < 0) + { + if (errno == EINTR) + continue; + return false; + } - if (!bfd_find_nearest_line_discriminator( - crash.fd, - crash.section, - crash.syms, - pc, - filename, - function, - line, - discriminator - )) - return false; + pos += received; + size -= received; + } - if (!*filename) + return true; +} + +static void reportFrame(Dwfl * dwfl, unsigned index, uintptr_t pc) +{ + // Except for the faulting instruction, an IP is a return address. Looking + // up the preceding byte attributes it to the call instruction. + Dwarf_Addr address = pc - (index != 0 && pc != 0); + Dwfl_Module * module = dwfl_addrmodule(dwfl, address); + if (!module) + { + DEBUG_ERROR("[trace]: (%u) 0x%" PRIxPTR, index, pc); + return; + } + + GElf_Off functionOffset = 0; + GElf_Sym symbol; + const char * function = dwfl_module_addrinfo(module, address, + &functionOffset, &symbol, NULL, NULL, NULL); + Dwfl_Line * source = dwfl_module_getsrc(module, address); + if (source) + { + int line = 0; + int column = 0; + const char * filename = + dwfl_lineinfo(source, NULL, &line, &column, NULL, NULL); + if (filename) + { + DEBUG_ERROR("[trace]: (%u) %s:%d:%d (%s)", index, filename, line, + column, function ? function : "unknown"); + return; + } + } + + Dwarf_Addr moduleStart = 0; + const char * moduleName = dwfl_module_info(module, NULL, &moduleStart, + NULL, NULL, NULL, NULL, NULL); + if (!moduleName) + moduleName = "unknown"; + + if (function) + DEBUG_ERROR("[trace]: (%u) %s+0x%" PRIx64 " (%s)", index, function, + (uint64_t)functionOffset, moduleName); + else + DEBUG_ERROR("[trace]: (%u) %s+0x%" PRIx64 " [0x%" PRIxPTR "]", index, + moduleName, (uint64_t)(address - moduleStart), pc); +} + +static void reportCrash(pid_t parent, const struct CrashRequest * request) +{ + if (request->signal) + { + DEBUG_ERROR("==== FATAL CRASH (%s) ====", BUILD_VERSION); + DEBUG_ERROR("signal %d (%s), address is 0x%" PRIxPTR, request->signal, + strsignal(request->signal), request->faultAddress); + } + + static const Dwfl_Callbacks callbacks = + { + .find_elf = dwfl_linux_proc_find_elf, + .find_debuginfo = dwfl_standard_find_debuginfo + }; + + Dwfl * dwfl = dwfl_begin(&callbacks); + if (!dwfl) + { + DEBUG_ERROR("Failed to initialize stack trace symbolizer: %s", + dwfl_errmsg(-1)); + goto raw; + } + + if (dwfl_linux_proc_report(dwfl, parent) != 0 || + dwfl_report_end(dwfl, NULL, NULL) != 0) + { + DEBUG_ERROR("Failed to inspect the crashed process: %s", + dwfl_errmsg(-1)); + dwfl_end(dwfl); + goto raw; + } + + for (unsigned i = 0; i < request->frameCount; ++i) + reportFrame(dwfl, i, request->frames[i]); + + dwfl_end(dwfl); + return; + +raw: + for (unsigned i = 0; i < request->frameCount; ++i) + DEBUG_ERROR("[trace]: (%u) 0x%" PRIxPTR, i, request->frames[i]); +} + +static void reporterLoop(pid_t parent, int requestFd, int responseFd) +{ + // Do not leave an orphaned reporter if the client exits unexpectedly before + // it closes the request pipe. + prctl(PR_SET_PDEATHSIG, SIGKILL); + if (getppid() != parent) + return; + + const uint8_t ready = 1; + if (!writeAll(responseFd, &ready, sizeof(ready))) + return; + + struct CrashRequest request; + while (readAll(requestFd, &request, sizeof(request))) + { + reportCrash(parent, &request); + + const uint8_t complete = 1; + if (!writeAll(responseFd, &complete, sizeof(complete))) + return; + } +} + +static bool startReporter(void) +{ + int requestPipe[2]; + int responsePipe[2]; + if (pipe(requestPipe) != 0) + { + DEBUG_ERROR("Failed to create crash reporter request pipe"); return false; + } + + if (pipe(responsePipe) != 0) + { + DEBUG_ERROR("Failed to create crash reporter response pipe"); + close(requestPipe[0]); + close(requestPipe[1]); + return false; + } + + for (unsigned i = 0; i < 2; ++i) + { + fcntl(requestPipe[i], F_SETFD, FD_CLOEXEC); + fcntl(responsePipe[i], F_SETFD, FD_CLOEXEC); + } + + const pid_t parent = getpid(); + const pid_t child = fork(); + if (child < 0) + { + DEBUG_ERROR("Failed to start the crash reporter"); + close(requestPipe [0]); + close(requestPipe [1]); + close(responsePipe[0]); + close(responsePipe[1]); + return false; + } + + if (child == 0) + { + close(requestPipe [1]); + close(responsePipe[0]); + reporterLoop(parent, requestPipe[0], responsePipe[1]); + close(requestPipe [0]); + close(responsePipe[1]); + _exit(0); + } + + close(requestPipe [0]); + close(responsePipe[1]); + + crash.reporter = child; + crash.requestFd = requestPipe [1]; + crash.responseFd = responsePipe[0]; + + uint8_t ready; + if (!readAll(crash.responseFd, &ready, sizeof(ready))) + { + DEBUG_ERROR("Crash reporter failed to initialize"); + cleanupCrashHandler(); + return false; + } return true; } void cleanupCrashHandler(void) { - if (crash.syms) - free(crash.syms); - - if (crash.fd) - bfd_close(crash.fd); - - if (crash.ranges) - free(crash.ranges); - - if (crash.exe) - free(crash.exe); -} - -static int dl_iterate_phdr_callback(struct dl_phdr_info * info, size_t size, void * data) -{ - // we are not a module, and as such we don't have a name - if (strlen(info->dlpi_name) != 0) - return 0; - - size_t ttl = 0; - for(int i = 0; i < info->dlpi_phnum; ++i) + if (crash.requestFd >= 0) { - const ElfW(Phdr) hdr = info->dlpi_phdr[i]; - if (hdr.p_type == PT_LOAD && (hdr.p_flags & PF_X) == PF_X) - ttl += hdr.p_memsz; + close(crash.requestFd); + crash.requestFd = -1; } - void * tmp = realloc(crash.ranges, - sizeof(*crash.ranges) * (crash.rangeCount + 1)); - if (!tmp) + if (crash.responseFd >= 0) { - DEBUG_ERROR("out of memory"); - return 1; + close(crash.responseFd); + crash.responseFd = -1; } - crash.ranges = tmp; - crash.ranges[crash.rangeCount].start = info->dlpi_addr; - crash.ranges[crash.rangeCount].end = info->dlpi_addr + ttl; - ++crash.rangeCount; - return 0; + if (crash.reporter > 0) + { + while (waitpid(crash.reporter, NULL, 0) < 0 && errno == EINTR) + continue; + crash.reporter = -1; + } } void printBacktrace(void) { - void * array[50]; - char ** messages; - int size, i; - - size = backtrace(array, 50); - messages = backtrace_symbols(array, size); - - for (i = 2; i < size && messages != NULL; ++i) + struct CrashRequest request = { - intptr_t base = -1; - for(int c = 0; c < crash.rangeCount; ++c) - { - if ((intptr_t)array[i] >= crash.ranges[c].start && (intptr_t)array[i] < crash.ranges[c].end) - { - base = crash.ranges[c].start + crash.section->vma; - break; - } - } + .signal = 0 + }; - if (base != -1) - { - const char * filename, * function; - unsigned int line, discriminator; - if (lookup_address((intptr_t)array[i] - base, &filename, &function, &line, &discriminator)) - { - DEBUG_ERROR("[trace]: (%d) %s:%u (%s)", i - 2, filename, line, function); - continue; - } - } + unw_context_t context; + unw_cursor_t cursor; + if (unw_getcontext(&context) < 0 || + unw_init_local(&cursor, &context) < 0) + return; - DEBUG_ERROR("[trace]: (%d) %s", i - 2, messages[i]); + // Omit unw_getcontext and printBacktrace itself. + if (unw_step(&cursor) <= 0 || unw_step(&cursor) <= 0) + return; + + do + { + unw_word_t pc; + if (unw_get_reg(&cursor, UNW_REG_IP, &pc) < 0 || !pc) + break; + + request.frames[request.frameCount++] = (uintptr_t)pc; } + while (request.frameCount < CRASH_MAX_FRAMES && unw_step(&cursor) > 0); - free(messages); + if (crash.requestFd >= 0 && crash.responseFd >= 0 && + writeAll(crash.requestFd, &request, sizeof(request))) + { + uint8_t complete; + readAll(crash.responseFd, &complete, sizeof(complete)); + } } static void crit_err_hdlr(int sig_num, siginfo_t * info, void * ucontext) { - DEBUG_ERROR("==== FATAL CRASH (%s) ====", BUILD_VERSION); - DEBUG_ERROR("signal %d (%s), address is %p", sig_num, strsignal(sig_num), info->si_addr); - printBacktrace(); - cleanupCrashHandler(); - abort(); + struct CrashRequest request; + request.signal = sig_num; + request.faultAddress = (uintptr_t)info->si_addr; + request.frameCount = 0; + + unw_cursor_t cursor; + if (unw_init_local2(&cursor, (unw_context_t *)ucontext, + UNW_INIT_SIGNAL_FRAME) >= 0) + { + do + { + unw_word_t pc; + if (unw_get_reg(&cursor, UNW_REG_IP, &pc) < 0 || !pc) + break; + + request.frames[request.frameCount++] = (uintptr_t)pc; + } + while (request.frameCount < CRASH_MAX_FRAMES && + unw_step(&cursor) > 0); + } + + if (crash.requestFd >= 0 && crash.responseFd >= 0 && + writeAll(crash.requestFd, &request, sizeof(request))) + { + uint8_t complete; + readAll(crash.responseFd, &complete, sizeof(complete)); + } + + // SA_RESETHAND has restored the default disposition and SA_NODEFER leaves + // this signal unblocked, so this preserves the original fatal signal and + // still permits the operating system to produce a core dump. + kill(getpid(), sig_num); + _exit(128 + sig_num); } bool installCrashHandler(const char * exe) { + (void)exe; + + if (!startReporter()) + return false; + struct sigaction sigact = { 0 }; - - crash.exe = realpath(exe, NULL); - if (!load_symbols()) - { - DEBUG_WARN("Unable to load the binary symbols, not installing crash handler"); - return true; - } - dl_iterate_phdr(dl_iterate_phdr_callback, NULL); - sigact.sa_sigaction = crit_err_hdlr; - sigact.sa_flags = SA_RESTART | SA_SIGINFO; + sigact.sa_flags = + SA_RESTART | SA_SIGINFO | SA_NODEFER | SA_RESETHAND; + sigemptyset(&sigact.sa_mask); - if (sigaction(SIGSEGV, &sigact, (struct sigaction *)NULL) != 0) + if (sigaction(SIGSEGV, &sigact, NULL) != 0) { - DEBUG_ERROR("Error setting signal handler for %d (%s)", SIGSEGV, strsignal(SIGSEGV)); + DEBUG_ERROR("Error setting SIGSEGV handler"); + cleanupCrashHandler(); return false; } diff --git a/doc/build.rst b/doc/build.rst index 48ebed7d..e2f33314 100644 --- a/doc/build.rst +++ b/doc/build.rst @@ -67,6 +67,7 @@ Required dependencies All required packages must be listed. - ``cmake`` +- ``binutils`` - ``gcc``, ``g++`` \| ``clang`` - ``libegl-dev`` - ``libgl-dev`` @@ -88,7 +89,8 @@ feature is disabled when running :ref:`cmake `. - Disable with ``cmake -DENABLE_BACKTRACE=no ..`` - - ``binutils-dev`` + - ``libdw-dev`` + - ``libunwind-dev`` - Disable with ``cmake -DENABLE_X11=no ..`` @@ -142,9 +144,9 @@ You can fetch these dependencies with the following command: .. code:: bash - apt-get install binutils-dev cmake fonts-dejavu-core libfontconfig-dev \ - gcc g++ pkg-config libegl-dev libgl-dev libgles-dev libspice-protocol-dev \ - nettle-dev libx11-dev libxcursor-dev libxi-dev libxinerama-dev \ + apt-get install binutils cmake fonts-dejavu-core libdw-dev libfontconfig-dev \ + libunwind-dev gcc g++ pkg-config libegl-dev libgl-dev libgles-dev \ + libspice-protocol-dev nettle-dev libx11-dev libxcursor-dev libxi-dev libxinerama-dev \ libxpresent-dev libxss-dev libxkbcommon-dev libwayland-dev \ libpipewire-0.3-dev libpulse-dev libsamplerate0-dev @@ -169,7 +171,10 @@ into the *LookingGlass* directory. make This will build the ``looking-glass-client`` binary, which is used to display -frames from the guest. +frames from the guest. It also produces ``looking-glass-client.debug``. Keep +this file with the matching binary to obtain source locations in crash stack +traces. The install target places it in the standard ``bin/.debug`` location +automatically. You can then :ref:`continue installing Looking Glass `, or run it directly from the build directory: