diff options
| author | Mitja Felicijan <mitja.felicijan@gmail.com> | 2026-01-16 10:13:49 +0100 |
|---|---|---|
| committer | Mitja Felicijan <mitja.felicijan@gmail.com> | 2026-01-16 10:13:49 +0100 |
| commit | c79ff421298851d2b7bffa52db500e0ab9f5f12d (patch) | |
| tree | d766f6438e3423dbd8e2afcdea5e762106100c9b | |
| download | toy-debugger-c79ff421298851d2b7bffa52db500e0ab9f5f12d.tar.gz | |
Engage!
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | LICENSE | 24 | ||||
| -rw-r--r-- | Makefile | 7 | ||||
| -rw-r--r-- | README.md | 24 | ||||
| -rw-r--r-- | compile_flags.txt | 1 | ||||
| -rw-r--r-- | example.c | 27 | ||||
| -rw-r--r-- | main.cpp | 148 |
7 files changed, 233 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cde9fe7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +example +tdbg @@ -0,0 +1,24 @@ +BSD 2-Clause License + +Copyright (c) 2026, Mitja Felicijan <mitja.felicijan@gmail.com> + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ff1b104 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +all: tdbg example + +tdbg: main.cpp + clang++ main.cpp -o tdbg -I/usr/lib/llvm/21/include -L/usr/lib/llvm/21/lib -Wl,-rpath,/usr/lib/llvm/21/lib -llldb -std=c++17 + +example: example.c + clang -g -o example example.c diff --git a/README.md b/README.md new file mode 100644 index 0000000..3f73f2c --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# Toy Debugger + +This project demonstrates how to use the LLDB C++ API to build a very basic +debugger tool. + +**Requirements** + +```sh +sudo xbps-install -S lldb21-devel clang llvm llvm-devel +``` + +After you clone the repository build the debugger and a sample program with +`make`. + +Then run the debugger with example program with `./tdbg example`. + +**Available commands:** + +- `c` - Continue execution until the next breakpoint or stop +- `s` - Step into the next instruction/function +- `n` - Step over the next instruction/function +- `bt` - Print a backtrace (call stack) of the current thread +- `v` - Print local variables in the current stack frame +- `q` - Kill the debugged process and exit diff --git a/compile_flags.txt b/compile_flags.txt new file mode 100644 index 0000000..3848ca8 --- /dev/null +++ b/compile_flags.txt @@ -0,0 +1 @@ +-I/usr/lib/llvm/21/include diff --git a/example.c b/example.c new file mode 100644 index 0000000..6a5119e --- /dev/null +++ b/example.c @@ -0,0 +1,27 @@ +#include <stdio.h> +#include <stdlib.h> + +typedef struct { + int q; + int w; +} Bar; + +int main(void) { + const char *myenv = getenv("MYENV"); + + int a = 100; + int b = 123; + int c = a + b; + + Bar bar = { .q = 565, .w = 949 }; + + printf("> MYENV: %s\n", myenv); + printf("> c: %d\n", c); + printf("> bar.q: %d\n", bar.q); + + for (int i=0; i<10; i++) { + printf("> loop %d\n", i); + } + + return 0; +} diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..b1c8078 --- /dev/null +++ b/main.cpp @@ -0,0 +1,148 @@ +#include <lldb/API/LLDB.h> + +#include <iostream> +#include <string> +#include <thread> +#include <chrono> + +using namespace lldb; + +void print_variables(SBFrame &frame) { + SBValueList vars = frame.GetVariables(true, true, false, true); + + for (uint32_t i = 0; i < vars.GetSize(); ++i) { + SBValue var = vars.GetValueAtIndex(i); + std::cout << var.GetName() << " = "; + + if (!var.IsValid()) { + std::cout << "(invalid)\n"; + } + else if (var.GetType().IsPointerType()) { + uint64_t addr = var.GetValueAsUnsigned(); + if (addr == 0) { + std::cout << "(null pointer)\n"; + } else { + std::cout << "(pointer at 0x" << std::hex << addr << std::dec << ")\n"; + } + } + else if (var.GetNumChildren() > 0) { + std::cout << "{ "; + uint32_t n = var.GetNumChildren(); + for (uint32_t j = 0; j < n; ++j) { + SBValue child = var.GetChildAtIndex(j); + std::cout << child.GetName() << ": " << (child.IsValid() ? child.GetValue() : "(invalid)"); + if (j + 1 < n) std::cout << ", "; + } + std::cout << " }\n"; + } else { + std::cout << var.GetValue() << "\n"; + } + } +} + + +void print_backtrace(SBThread &thread) { + int num_frames = thread.GetNumFrames(); + for (int i = 0; i < num_frames; ++i) { + SBFrame frame = thread.GetFrameAtIndex(i); + std::cout << "#" << i << " " << frame.GetFunctionName() + << " at line " << frame.GetLineEntry().GetLine() << "\n"; + } +} + +int main(int argc, char** argv) { + if (argc < 2) { + std::cerr << "Usage: " << argv[0] << " <target_executable>\n"; + return 1; + } + + const char* target_path = argv[1]; + + // Initialize LLDB. + SBDebugger::Initialize(); + SBDebugger debugger = SBDebugger::Create(); + debugger.SetAsync(true); + + SBTarget target = debugger.CreateTarget(target_path); + if (!target.IsValid()) { + std::cerr << "Failed to create target for " << target_path << "\n"; + return 1; + } + + SBBreakpoint bp = target.BreakpointCreateByName("main"); + std::cout << "Breakpoint set at main\n"; + + SBProcess process = target.LaunchSimple(nullptr, nullptr, "."); + if (!process.IsValid()) { + std::cerr << "Failed to launch process\n"; + return 1; + } + + std::cout << "Process launched\n"; + + SBListener listener = debugger.GetListener(); + SBEvent event; + + while (true) { + // Wait for events from LLDB. + if (listener.WaitForEvent(1, event)) { + StateType state = SBProcess::GetStateFromEvent(event); + + switch (state) { + case eStateStopped: + { + std::cout << "\nProcess stopped!\n"; + SBThread thread = process.GetSelectedThread(); + SBFrame frame = thread.GetFrameAtIndex(0); + std::cout << "Stopped at function: " << frame.GetFunctionName() + << ", line: " << frame.GetLineEntry().GetLine() << "\n"; + + // REPL loop for user commands. + std::string cmd; + while (true) { + std::cout << "(tdbg) "; + std::getline(std::cin, cmd); + if (cmd == "c") { + process.Continue(); + break; // exit REPL, wait for next stop + } else if (cmd == "s") { + thread.StepInto(); + break; + } else if (cmd == "n") { + thread.StepOver(); + break; + } else if (cmd == "bt") { + print_backtrace(thread); + } else if (cmd == "v") { + print_variables(frame); + } else if (cmd == "q") { + process.Kill(); + goto cleanup; + } else { + std::cout << "Commands: c=continue, s=step in, n=step over, bt=backtrace, v=variables, q=quit\n"; + } + } + break; + } + + case eStateExited: + { + std::cout << "Process exited\n"; + goto cleanup; + } + + case eStateRunning: + break; + + default: + break; + } + } else { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + } + +cleanup: + SBDebugger::Terminate(); + return 0; +} |
