diff options
author | Itamar <itamar8910@gmail.com> | 2020-04-10 17:35:49 +0300 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2020-04-13 00:53:22 +0200 |
commit | 3e9a7175d1137c481120e40100d8b98e1db9036b (patch) | |
tree | 7d0344228447d14f56e1113b77cdd66775416f3e /Applications/Debugger | |
parent | 50fd2cabff79e2faefda55030b44836288709075 (diff) | |
download | serenity-3e9a7175d1137c481120e40100d8b98e1db9036b.zip |
Debugger: Add DebugSession
The DebugSession class wraps the usage of Ptrace.
It is intended to be used by cli & gui debugger programs.
Also, call objdump for disassemly
Diffstat (limited to 'Applications/Debugger')
-rw-r--r-- | Applications/Debugger/DebugSession.cpp | 184 | ||||
-rw-r--r-- | Applications/Debugger/DebugSession.h | 138 | ||||
-rwxr-xr-x | Applications/Debugger/Makefile | 3 | ||||
-rw-r--r-- | Applications/Debugger/main.cpp | 267 |
4 files changed, 458 insertions, 134 deletions
diff --git a/Applications/Debugger/DebugSession.cpp b/Applications/Debugger/DebugSession.cpp new file mode 100644 index 0000000000..8766ad60b2 --- /dev/null +++ b/Applications/Debugger/DebugSession.cpp @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * All rights reserved. + * + * 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. + */ + +#include "DebugSession.h" +#include <AK/Optional.h> +#include <stdlib.h> + +DebugSession::DebugSession(int pid) + : m_debugee_pid(pid) + , m_executable(make<MappedFile>(String::format("/proc/%d/exe", pid))) + , m_elf_image(make<ELF::Image>(reinterpret_cast<u8*>(m_executable->data()), m_executable->size())) +{ +} + +DebugSession::~DebugSession() +{ + if (!m_is_debugee_dead) { + if (ptrace(PT_DETACH, m_debugee_pid, 0, 0) < 0) { + perror("PT_DETACH"); + } + } +} + +OwnPtr<DebugSession> DebugSession::exec_and_attach(const String& command) +{ + int pid = fork(); + + if (!pid) { + if (ptrace(PT_TRACE_ME, 0, 0, 0) < 0) { + perror("PT_TRACE_ME"); + exit(1); + } + + auto parts = command.split(' '); + ASSERT(!parts.is_empty()); + const char** args = (const char**)calloc(parts.size() + 1, sizeof(const char*)); + for (size_t i = 0; i < parts.size(); i++) { + args[i] = parts[i].characters(); + } + int rc = execvp(args[0], const_cast<char**>(args)); + if (rc < 0) { + perror("execvp"); + } + ASSERT_NOT_REACHED(); + } + + if (waitpid(pid, nullptr, WSTOPPED) != pid) { + perror("waitpid"); + return nullptr; + } + + if (ptrace(PT_ATTACH, pid, 0, 0) < 0) { + perror("PT_ATTACH"); + return nullptr; + } + + // TOOD: do we actually need this waitpid? + if (waitpid(pid, nullptr, WSTOPPED) != pid) { + perror("waitpid"); + return nullptr; + } + + if (ptrace(PT_CONTINUE, pid, 0, 0) < 0) { + perror("continue"); + return nullptr; + } + + // We want to continue until the exit from the 'execve' sycsall. + // This ensures that when we start debugging the process + // it executes the target image, and not the forked image of the tracing process. + // NOTE: we only need to do this when we are debugging a new process (i.e not attaching to a process that's already running!) + + if (waitpid(pid, nullptr, WSTOPPED) != pid) { + perror("wait_pid"); + return nullptr; + } + + return make<DebugSession>(pid); +} + +bool DebugSession::poke(u32* address, u32 data) +{ + if (ptrace(PT_POKE, m_debugee_pid, (void*)address, data) < 0) { + perror("PT_POKE"); + return false; + } + return true; +} + +Optional<u32> DebugSession::peek(u32* address) const +{ + Optional<u32> result; + int rc = ptrace(PT_PEEK, m_debugee_pid, (void*)address, 0); + if (errno == 0) + result = static_cast<u32>(rc); + return result; +} + +bool DebugSession::insert_breakpoint(void* address) +{ + // We insert a software breakpoint by + // patching the first byte of the instruction at 'address' + // with the breakpoint instruction (int3) + + if (m_breakpoints.contains(address)) + return false; + + auto original_bytes = peek(reinterpret_cast<u32*>(address)); + + if (!original_bytes.has_value()) + return false; + + if (!poke(reinterpret_cast<u32*>(address), (original_bytes.value() & ~(uint32_t)0xff) | BREAKPOINT_INSTRUCTION)) + return false; + + m_breakpoints.set(address, { address, original_bytes.value() }); + return true; +} + +void DebugSession::remove_breakpoint(const BreakPoint& breakpoint) +{ + ASSERT(m_breakpoints.contains(breakpoint.address)); + poke(reinterpret_cast<u32*>(reinterpret_cast<char*>(breakpoint.address)), breakpoint.original_first_word); + m_breakpoints.remove(breakpoint.address); +} + +PtraceRegisters DebugSession::get_registers() const +{ + PtraceRegisters regs; + if (ptrace(PT_GETREGS, m_debugee_pid, ®s, 0) < 0) { + perror("PT_GETREGS"); + ASSERT_NOT_REACHED(); + } + return regs; +} + +void DebugSession::set_registers(const PtraceRegisters& regs) +{ + if (ptrace(PT_SETREGS, m_debugee_pid, reinterpret_cast<void*>(&const_cast<PtraceRegisters&>(regs)), 0) < 0) { + perror("PT_SETREGS"); + ASSERT_NOT_REACHED(); + } +} + +Optional<DebugSession::BreakPoint> DebugSession::get_matching_breakpoint(const PtraceRegisters& regs) const +{ + return m_breakpoints.get(reinterpret_cast<void*>(regs.eip - 1)); +} + +void DebugSession::continue_debugee() +{ + if (ptrace(PT_CONTINUE, m_debugee_pid, 0, 0) < 0) { + perror("continue"); + ASSERT_NOT_REACHED(); + } +} + +VirtualAddress DebugSession::get_entry_point() const +{ + return m_elf_image->entry(); +} diff --git a/Applications/Debugger/DebugSession.h b/Applications/Debugger/DebugSession.h new file mode 100644 index 0000000000..9ac7774f20 --- /dev/null +++ b/Applications/Debugger/DebugSession.h @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * All rights reserved. + * + * 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. + */ + +#pragma once + +#include <AK/HashMap.h> +#include <AK/MappedFile.h> +#include <AK/Optional.h> +#include <AK/OwnPtr.h> +#include <AK/String.h> +#include <LibC/sys/arch/i386/regs.h> +#include <LibELF/Image.h> +#include <signal.h> +#include <stdio.h> +#include <sys/ptrace.h> +#include <sys/wait.h> +#include <unistd.h> + +class DebugSession { +public: + static OwnPtr<DebugSession> exec_and_attach(const String& command); + + // Has to be public for OwnPtr::make + DebugSession(int pid); + ~DebugSession(); + + int pid() const { return m_debugee_pid; } + + bool poke(u32* address, u32 data); + Optional<u32> peek(u32* address) const; + + struct BreakPoint { + void* address; + u32 original_first_word; + }; + + bool insert_breakpoint(void* address); + void remove_breakpoint(const BreakPoint&); + Optional<BreakPoint> get_matching_breakpoint(const PtraceRegisters&) const; + + PtraceRegisters get_registers() const; + void set_registers(const PtraceRegisters&); + + void continue_debugee(); + + template<typename Callback> + void run(Callback callback); + + VirtualAddress get_entry_point() const; + + enum DebugDecision { + Continue, + Detach, + Kill, + }; + + enum DebugBreakReason { + Breakpoint, + Exited, + }; + +private: + // x86 breakpoint instruction "int3" + static constexpr u8 BREAKPOINT_INSTRUCTION + = 0xcc; + + int m_debugee_pid { -1 }; + bool m_is_debugee_dead { false }; + + NonnullOwnPtr<MappedFile> m_executable; + NonnullOwnPtr<ELF::Image> m_elf_image; + + HashMap<void*, BreakPoint> m_breakpoints; +}; + +template<typename Callback> +void DebugSession::run(Callback callback) +{ + for (;;) { + continue_debugee(); + + int wstatus = 0; + if (waitpid(m_debugee_pid, &wstatus, WSTOPPED | WEXITED) != m_debugee_pid) { + perror("waitpid"); + ASSERT_NOT_REACHED(); + } + + // FIXME: This check actually only checks whether the debugee + // Is stopped because it hit a breakpoint or not + if (WSTOPSIG(wstatus) != SIGTRAP) { + callback(DebugBreakReason::Exited, Optional<PtraceRegisters>()); + m_is_debugee_dead = true; + break; + } + + auto regs = get_registers(); + + auto current_breakpoint = get_matching_breakpoint(regs); + if (current_breakpoint.has_value()) { + // FIXME: The current implementation removes a breakpoint + // after the first time it has been triggered. + remove_breakpoint(current_breakpoint.value()); + + // We need to re-execute the instruction we patched for the breakpoint, + // so we rollback the instruction pointer to the breakpoint's address + regs.eip = reinterpret_cast<u32>(current_breakpoint.value().address); + set_registers(regs); + DebugDecision decision = callback(DebugBreakReason::Breakpoint, regs); + if (decision != DebugDecision::Continue) { + // FIXME: implement detach & kill + ASSERT_NOT_REACHED(); + } + } + } +} diff --git a/Applications/Debugger/Makefile b/Applications/Debugger/Makefile index 2b9e6d7c50..32f18aa799 100755 --- a/Applications/Debugger/Makefile +++ b/Applications/Debugger/Makefile @@ -1,8 +1,9 @@ OBJS = \ + DebugSession.o \ main.o PROGRAM = Debugger -LIB_DEPS = Core +LIB_DEPS = Core X86 include ../../Makefile.common diff --git a/Applications/Debugger/main.cpp b/Applications/Debugger/main.cpp index 24d88dceaf..05fc498487 100644 --- a/Applications/Debugger/main.cpp +++ b/Applications/Debugger/main.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -24,17 +24,20 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +#include "DebugSession.h" #include <AK/Assertions.h> #include <AK/ByteBuffer.h> +#include <AK/LogStream.h> +#include <AK/StringBuilder.h> +#include <AK/kmalloc.h> #include <LibC/sys/arch/i386/regs.h> #include <LibCore/File.h> -#include <LibELF/ELFImage.h> +#include <LibX86/Disassembler.h> +#include <LibX86/Instruction.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> -#include <sys/ptrace.h> -#include <sys/wait.h> #include <unistd.h> static int usage() @@ -43,178 +46,176 @@ static int usage() return 1; } -static int g_pid = -1; +OwnPtr<DebugSession> g_debug_session; static void handle_sigint(int) { - if (g_pid == -1) - return; + printf("Debugger: SIGINT\n"); - if (ptrace(PT_DETACH, g_pid, 0, 0) == -1) { - perror("detach"); - } + // The destructor of DebugSession takes care of detaching + g_debug_session = nullptr; } -void run_child_and_attach(char** argv) +String get_command() { - int pid = fork(); - - if (!pid) { - if (ptrace(PT_TRACE_ME, 0, 0, 0) == -1) { - perror("traceme"); - return exit(1); - } - - int rc = execvp(argv[1], &argv[1]); - if (rc < 0) { - perror("execvp"); + printf("(sdb) "); + fflush(stdout); + char* line = nullptr; + size_t allocated_size = 0; + ssize_t nread = getline(&line, &allocated_size, stdin); + if (nread < 0) { + if (errno == 0) { + fprintf(stderr, "\n"); + } else { + perror("getline"); exit(1); } - ASSERT_NOT_REACHED(); } + String command(line); + free(line); + if (command.ends_with('\n')) + command = command.substring(0, command.length() - 1); + return command; +} - g_pid = pid; +void handle_print_registers(const PtraceRegisters& regs) +{ + printf("eax: 0x%x\n", regs.eax); + printf("ecx: 0x%x\n", regs.ecx); + printf("edx: 0x%x\n", regs.edx); + printf("ebx: 0x%x\n", regs.ebx); + printf("esp: 0x%x\n", regs.esp); + printf("ebp: 0x%x\n", regs.ebp); + printf("esi: 0x%x\n", regs.esi); + printf("edi: 0x%x\n", regs.edi); + printf("eip: 0x%x\n", regs.eip); + printf("eflags: 0x%x\n", regs.eflags); +} - if (waitpid(pid, nullptr, WSTOPPED) != pid) { - perror("waitpid"); - exit(1); +bool handle_disassemble_command(const String& command, void* first_instruction) +{ + auto parts = command.split(' '); + size_t number_of_instructions_to_disassemble = 5; + if (parts.size() == 2) { + bool ok; + number_of_instructions_to_disassemble = parts[1].to_uint(ok); + if (!ok) + return false; } - if (ptrace(PT_ATTACH, g_pid, 0, 0) == -1) { - perror("attach"); - exit(1); + // FIXME: Instead of using a fixed "dump_size", + // we can feed instructions to the disassembler one by one + constexpr size_t dump_size = 0x100; + ByteBuffer code; + for (size_t i = 0; i < dump_size / sizeof(u32); ++i) { + auto value = g_debug_session->peek(reinterpret_cast<u32*>(first_instruction) + i); + if (!value.has_value()) + break; + code.append(&value, sizeof(u32)); } - if (waitpid(g_pid, nullptr, WSTOPPED) != g_pid) { - perror("waitpid"); - exit(1); - } + X86::SimpleInstructionStream stream(code.data(), code.size()); + X86::Disassembler disassembler(stream); - dbg() << "debugee should continue until before execve exit"; - if (ptrace(PT_CONTINUE, g_pid, 0, 0) == -1) { - perror("continue"); - } + for (size_t i = 0; i < number_of_instructions_to_disassemble; ++i) { + auto offset = stream.offset(); + auto insn = disassembler.next(); + if (!insn.has_value()) + break; - // we want to continue until the exit from the 'execve' sycsall - // we do this to ensure that when we start debugging the process, - // it executes the target image, and not the forked image of the debugger - // NOTE: we only need to do this when we are debugging a new process (i.e not attaching to a process that's already running!) - // if (ptrace(PT_SYSCALL, g_pid, 0, 0) == -1) { - // perror("syscall"); - // exit(1); - // } - - if (waitpid(g_pid, nullptr, WSTOPPED) != g_pid) { - perror("wait_pid"); - exit(1); + printf(String::format(" %08x ", offset + reinterpret_cast<size_t>(first_instruction)).characters()); + printf("<+%lu>:\t", reinterpret_cast<size_t>(offset)); + printf("%s\n", insn.value().to_string(offset).characters()); } - // dbg() << "debugee should continue until after execve exit"; - // sleep(3); - // if (ptrace(PT_CONTINUE, g_pid, 0, 0) == -1) { - // perror("continue"); - // } - - // sleep(10); - - // if (waitpid(g_pid, nullptr, WSTOPPED) != g_pid) { - // perror("wait_pid"); - // exit(1); - // } + return true; +} - // dbg() << "debugee should already be running"; - // if (ptrace(PT_CONTINUE, g_pid, 0, 0) == -1) { - // perror("continue"); - // } +bool handle_breakpoint_command(const String& command) +{ + auto parts = command.split(' '); + if (parts.size() != 2) + return false; + + u32 breakpoint_address = strtoul(parts[1].characters(), nullptr, 16); + if (errno != 0) + return false; + bool success = g_debug_session->insert_breakpoint(reinterpret_cast<void*>(breakpoint_address)); + if (!success) { + fprintf(stderr, "coult not insert breakpoint at: 0x%x\n", breakpoint_address); + return false; + } + return true; } -VirtualAddress get_entry_point(int pid) +void print_help() { - auto path = String::format("/proc/%d/exe", pid); - dbg() << "path: " << path; - auto file = Core::File::construct(path); - if (!file->open(Core::File::ReadOnly)) { - fprintf(stderr, "Failed to open Debugged executable"); - exit(1); - } - auto data = file->read_all(); - dbg() << "data size:" << data.size(); - ELFImage elf(data.data(), data.size()); - return elf.entry(); + printf("Options:\n" + "cont - Continue execution\n" + "regs - Print registers\n" + "dis <number of instructions> - Print disassembly\n" + "bp <address> - Insert a breakpoint\n"); } int main(int argc, char** argv) { // TODO: pledge & unveil - // TOOD: check that we didn't somehow hurt performance. boot seems slower? (or it's just laptop battey) + // TODO: check that strace still works if (argc == 1) return usage(); - struct sigaction sa; - memset(&sa, 0, sizeof(struct sigaction)); - sa.sa_handler = handle_sigint; - sigaction(SIGINT, &sa, nullptr); - - run_child_and_attach(argv); - - dbg() << "pid:" << g_pid; - auto entry_point = get_entry_point(g_pid); - dbg() << "entry point:" << entry_point; - - const uint32_t original_instruction_data = ptrace(PT_PEEK, g_pid, (void*)entry_point.as_ptr(), 0); - - dbg() << "peeked data:" << (void*)original_instruction_data; - - if (ptrace(PT_POKE, g_pid, (void*)entry_point.as_ptr(), (original_instruction_data & ~(uint32_t)0xff) | 0xcc) < 0) { - perror("poke"); - return 1; + StringBuilder command; + command.append(argv[1]); + for (int i = 2; i < argc; ++i) { + command.appendf("%s ", argv[i]); } - dbg() << "continuting"; - - if (ptrace(PT_CONTINUE, g_pid, 0, 0) == -1) { - perror("continue"); + auto result = DebugSession::exec_and_attach(command.to_string()); + if (!result) { + fprintf(stderr, "Failed to start debugging session for: \"%s\"\n", command.to_string().characters()); + exit(1); } - dbg() << "continued"; + g_debug_session = result.release_nonnull(); - // wait for breakpoint - if (waitpid(g_pid, nullptr, WSTOPPED) != g_pid) { - perror("waitpid"); - return 1; - } + struct sigaction sa; + memset(&sa, 0, sizeof(struct sigaction)); + sa.sa_handler = handle_sigint; + sigaction(SIGINT, &sa, nullptr); - printf("hit breakpoint\n"); + bool rc = g_debug_session->insert_breakpoint(g_debug_session->get_entry_point().as_ptr()); + ASSERT(rc); - if (ptrace(PT_POKE, g_pid, (void*)entry_point.as_ptr(), original_instruction_data) < 0) { - perror("poke"); - return 1; - } + g_debug_session->run([&](DebugSession::DebugBreakReason reason, Optional<PtraceRegisters> optional_regs) { + if (reason == DebugSession::DebugBreakReason::Exited) { + printf("Program exited.\n"); + return DebugSession::DebugDecision::Detach; + } - PtraceRegisters regs; - if (ptrace(PT_GETREGS, g_pid, ®s, 0) < 0) { - perror("getregs"); - return 1; - } + ASSERT(optional_regs.has_value()); + const PtraceRegisters& regs = optional_regs.value(); - dbg() << "eip after breakpoint: " << (void*)regs.eip; + printf("Program is stopped at: 0x%x\n", regs.eip); + for (;;) { + auto command = get_command(); + bool success = false; - regs.eip = reinterpret_cast<u32>(entry_point.as_ptr()); - dbg() << "settings eip back to:" << (void*)regs.eip; - if (ptrace(PT_SETREGS, g_pid, ®s, 0) < 0) { - perror("setregs"); - return 1; - } + if (command == "cont") { + return DebugSession::DebugDecision::Continue; + } - dbg() << "continuig"; + if (command == "regs") { + handle_print_registers(regs); + success = true; - if (ptrace(PT_CONTINUE, g_pid, 0, 0) == -1) { - perror("continue"); - } + } else if (command.starts_with("dis")) { + success = handle_disassemble_command(command, reinterpret_cast<void*>(regs.eip)); - // wait for end + } else if (command.starts_with("bp")) { + success = handle_breakpoint_command(command); + } - if (waitpid(g_pid, nullptr, WSTOPPED) != g_pid) { - perror("waitpid"); - return 1; - } + if (!success) + print_help(); + } + }); } |