//===--- LLJITWithRemoteDebugging.cpp - LLJIT targeting a child process ---===// // // 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 // //===----------------------------------------------------------------------===// // // This example shows how to use LLJIT and JITLink for out-of-process execution // with debug support. A few notes beforehand: // // * Debuggers must implement the GDB JIT interface (gdb, udb, lldb 12+). // * Debug support is currently limited to ELF on x86-64 platforms that run // Unix-like systems. // * There is a test for this example and it ships an IR file that is prepared // for the instructions below. // // // The following command line session provides a complete walkthrough of the // feature using LLDB 12: // // [Terminal 1] Prepare a debuggable out-of-process JIT session: // // > cd llvm-project/build // > ninja LLJITWithRemoteDebugging llvm-jitlink-executor // > cp ../llvm/test/Examples/OrcV2Examples/Inputs/argc_sub1_elf.ll . // > bin/LLJITWithRemoteDebugging --wait-for-debugger argc_sub1_elf.ll // Found out-of-process executor: bin/llvm-jitlink-executor // Launched executor in subprocess: 65535 // Attach a debugger and press any key to continue. // // // [Terminal 2] Attach a debugger to the child process: // // (lldb) log enable lldb jit // (lldb) settings set plugin.jit-loader.gdb.enable on // (lldb) settings set target.source-map Inputs/ \ // /path/to/llvm-project/llvm/test/Examples/OrcV2Examples/Inputs/ // (lldb) attach -p 65535 // JITLoaderGDB::SetJITBreakpoint looking for JIT register hook // JITLoaderGDB::SetJITBreakpoint setting JIT breakpoint // Process 65535 stopped // (lldb) b sub1 // Breakpoint 1: no locations (pending). // WARNING: Unable to resolve breakpoint to any actual locations. // (lldb) c // Process 65535 resuming // // // [Terminal 1] Press a key to start code generation and execution: // // Parsed input IR code from: argc_sub1_elf.ll // Initialized LLJIT for remote executor // Running: argc_sub1_elf.ll // // // [Terminal 2] Breakpoint hits; we change the argc value from 1 to 42: // // (lldb) JITLoaderGDB::JITDebugBreakpointHit hit JIT breakpoint // JITLoaderGDB::ReadJITDescriptorImpl registering JIT entry at 0x106b34000 // 1 location added to breakpoint 1 // Process 65535 stopped // * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 // frame #0: JIT(0x106b34000)`sub1(x=1) at argc_sub1.c:1:28 // -> 1 int sub1(int x) { return x - 1; } // 2 int main(int argc, char **argv) { return sub1(argc); } // (lldb) p x // (int) $0 = 1 // (lldb) expr x = 42 // (int) $1 = 42 // (lldb) c // // // [Terminal 1] Example output reflects the modified value: // // Exit code: 41 // //===----------------------------------------------------------------------===// #include "llvm/ExecutionEngine/Orc/JITTargetMachineBuilder.h" #include "llvm/ExecutionEngine/Orc/LLJIT.h" #include "llvm/ExecutionEngine/Orc/ThreadSafeModule.h" #include "llvm/Support/CommandLine.h" #include "llvm/Support/Error.h" #include "llvm/Support/FormatVariadic.h" #include "llvm/Support/InitLLVM.h" #include "llvm/Support/TargetSelect.h" #include "llvm/Support/raw_ostream.h" #include "../ExampleModules.h" #include "RemoteJITUtils.h" #include #include using namespace llvm; using namespace llvm::orc; // The LLVM IR file to run. static cl::list InputFiles(cl::Positional, cl::OneOrMore, cl::desc("")); // Command line arguments to pass to the JITed main function. static cl::list InputArgv("args", cl::Positional, cl::desc("..."), cl::ZeroOrMore, cl::PositionalEatsArgs); // Given paths must exist on the remote target. static cl::list Dylibs("dlopen", cl::desc("Dynamic libraries to load before linking"), cl::value_desc("filename"), cl::ZeroOrMore); // File path of the executable to launch for execution in a child process. // Inter-process communication will go through stdin/stdout pipes. static cl::opt OOPExecutor("executor", cl::desc("Set the out-of-process executor"), cl::value_desc("filename")); // Network address of a running executor process that we can connected through a // TCP socket. It may run locally or on a remote machine. static cl::opt OOPExecutorConnect( "connect", cl::desc("Connect to an out-of-process executor through a TCP socket"), cl::value_desc(":")); // Give the user a chance to connect a debugger. Once we connected the executor // process, wait for the user to press a key (and print out its PID if it's a // child process). static cl::opt WaitForDebugger("wait-for-debugger", cl::desc("Wait for user input before entering JITed code"), cl::init(false)); ExitOnError ExitOnErr; static std::unique_ptr connectExecutor(const char *Argv0, ExecutionSession &ES) { // Connect to a running out-of-process executor through a TCP socket. if (!OOPExecutorConnect.empty()) { std::unique_ptr Exec = ExitOnErr(JITLinkExecutor::ConnectTCPSocket(OOPExecutorConnect, ES)); outs() << "Connected to executor at " << OOPExecutorConnect << "\n"; if (WaitForDebugger) { outs() << "Attach a debugger and press any key to continue.\n"; fflush(stdin); getchar(); } return std::move(Exec); } // Launch a out-of-process executor locally in a child process. std::unique_ptr Exec = ExitOnErr( OOPExecutor.empty() ? JITLinkExecutor::FindLocal(Argv0) : JITLinkExecutor::CreateLocal(OOPExecutor)); outs() << "Found out-of-process executor: " << Exec->getPath() << "\n"; ExitOnErr(Exec->launch(ES)); if (WaitForDebugger) { outs() << "Launched executor in subprocess: " << Exec->getPID() << "\n" << "Attach a debugger and press any key to continue.\n"; fflush(stdin); getchar(); } return std::move(Exec); } int main(int argc, char *argv[]) { InitLLVM X(argc, argv); InitializeNativeTarget(); InitializeNativeTargetAsmPrinter(); ExitOnErr.setBanner(std::string(argv[0]) + ": "); cl::ParseCommandLineOptions(argc, argv, "LLJITWithRemoteDebugging"); auto ES = std::make_unique(); ES->setErrorReporter([&](Error Err) { ExitOnErr(std::move(Err)); }); // Launch/connect the out-of-process executor. std::unique_ptr Executor = connectExecutor(argv[0], *ES); // Load the given IR files. std::vector TSMs; for (const std::string &Path : InputFiles) { outs() << "Parsing input IR code from: " << Path << "\n"; TSMs.push_back(ExitOnErr(parseExampleModuleFromFile(Path))); } StringRef TT; StringRef MainModuleName; TSMs.front().withModuleDo([&MainModuleName, &TT](Module &M) { MainModuleName = M.getName(); TT = M.getTargetTriple(); }); for (const ThreadSafeModule &TSM : TSMs) ExitOnErr(TSM.withModuleDo([TT, MainModuleName](Module &M) -> Error { if (M.getTargetTriple() != TT) return make_error( formatv("Different target triples in input files:\n" " '{0}' in '{1}'\n '{2}' in '{3}'", TT, MainModuleName, M.getTargetTriple(), M.getName()), inconvertibleErrorCode()); return Error::success(); })); // Create a target machine that matches the input triple. JITTargetMachineBuilder JTMB((Triple(TT))); JTMB.setCodeModel(CodeModel::Small); JTMB.setRelocationModel(Reloc::PIC_); // Create LLJIT and destroy it before disconnecting the target process. { outs() << "Initializing LLJIT for remote executor\n"; auto J = ExitOnErr(LLJITBuilder() .setExecutionSession(std::move(ES)) .setJITTargetMachineBuilder(std::move(JTMB)) .setObjectLinkingLayerCreator(std::ref(*Executor)) .create()); // Add plugin for debug support. ExitOnErr(Executor->addDebugSupport(J->getObjLinkingLayer())); // Load required shared libraries on the remote target and add a generator // for each of it, so the compiler can lookup their symbols. for (const std::string &Path : Dylibs) J->getMainJITDylib().addGenerator(ExitOnErr(Executor->loadDylib(Path))); // Add the loaded IR module to the JIT. This will set up symbol tables and // prepare for materialization. for (ThreadSafeModule &TSM : TSMs) ExitOnErr(J->addIRModule(std::move(TSM))); // The example uses a non-lazy JIT for simplicity. Thus, looking up the main // function will materialize all reachable code. It also triggers debug // registration in the remote target process. JITEvaluatedSymbol MainFn = ExitOnErr(J->lookup("main")); outs() << "Running: main("; int Pos = 0; for (const std::string &Arg : InputArgv) outs() << (Pos++ == 0 ? "" : ", ") << "\"" << Arg << "\""; outs() << ")\n"; // Execute the code in the remote target process and dump the result. With // the debugger attached to the target, it should be possible to inspect the // JITed code as if it was compiled statically. int Result = ExitOnErr(Executor->runAsMain(MainFn, InputArgv)); outs() << "Exit code: " << Result << "\n"; } ExitOnErr(Executor->disconnect()); return 0; }