diff --git a/Base/home/anon/little/Makefile b/Base/home/anon/little/Makefile index 137ef0065e..7c29668846 100644 --- a/Base/home/anon/little/Makefile +++ b/Base/home/anon/little/Makefile @@ -1,5 +1,6 @@ PROGRAM = little OBJS = main.o +CXXFLAGS = -g all: $(PROGRAM) diff --git a/Base/res/icons/16x16/play-debug.png b/Base/res/icons/16x16/play-debug.png new file mode 100644 index 0000000000..3de78d4c35 Binary files /dev/null and b/Base/res/icons/16x16/play-debug.png differ diff --git a/Base/res/icons/16x16/single-step.png b/Base/res/icons/16x16/single-step.png new file mode 100644 index 0000000000..3de1fc9689 Binary files /dev/null and b/Base/res/icons/16x16/single-step.png differ diff --git a/DevTools/HackStudio/Debugger.cpp b/DevTools/HackStudio/Debugger.cpp new file mode 100644 index 0000000000..2db430f016 --- /dev/null +++ b/DevTools/HackStudio/Debugger.cpp @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2020, Itamar S. + * 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 "Debugger.h" + +static Debugger* s_the; + +Debugger& Debugger::the() +{ + ASSERT(s_the); + return *s_the; +} + +void Debugger::initialize( + Function on_stop_callback, + Function on_continue_callback, + Function on_exit_callback) +{ + s_the = new Debugger(move(on_stop_callback), move(on_continue_callback), move(on_exit_callback)); +} + +bool Debugger::is_initialized() +{ + return s_the; +} + +Debugger::Debugger( + Function on_stop_callback, + Function on_continue_callback, + Function on_exit_callback) + : m_on_stopped_callback(move(on_stop_callback)) + , m_on_continue_callback(move(on_continue_callback)) + , m_on_exit_callback(move(on_exit_callback)) +{ + pthread_mutex_init(&m_continue_mutex, nullptr); + pthread_cond_init(&m_continue_cond, nullptr); +} + +void Debugger::on_breakpoint_change(const String& file, size_t line, BreakpointChange change_type) +{ + auto position = create_source_position(file, line); + + if (change_type == BreakpointChange::Added) { + Debugger::the().m_breakpoints.append(position); + } else { + Debugger::the().m_breakpoints.remove_all_matching([&](DebugInfo::SourcePosition val) { return val == position; }); + } + + auto session = Debugger::the().session(); + if (!session) + return; + + auto address = session->debug_info().get_instruction_from_source(position.file_path, position.line_number); + if (!address.has_value()) + return; + + if (change_type == BreakpointChange::Added) { + bool success = session->insert_breakpoint(reinterpret_cast(address.value())); + ASSERT(success); + } else { + bool success = session->remove_breakpoint(reinterpret_cast(address.value())); + ASSERT(success); + } +} + +DebugInfo::SourcePosition Debugger::create_source_position(const String& file, size_t line) +{ + return { String::format("./%s", file.characters()), line + 1 }; +} + +int Debugger::start_static() +{ + Debugger::the().start(); + return 0; +} + +void Debugger::start() +{ + m_debug_session = DebugSession::exec_and_attach(m_executable_path); + ASSERT(!!m_debug_session); + + for (const auto& breakpoint : m_breakpoints) { + dbg() << "insertig breakpoint at: " << breakpoint.file_path << ":" << breakpoint.line_number; + auto address = m_debug_session->debug_info().get_instruction_from_source(breakpoint.file_path, breakpoint.line_number); + if (address.has_value()) { + bool success = m_debug_session->insert_breakpoint(reinterpret_cast(address.value())); + ASSERT(success); + } else { + dbg() << "couldn't insert breakpoint"; + } + } + + debugger_loop(); +} + +int Debugger::debugger_loop() +{ + bool in_single_step_mode = false; + Vector temporary_breakpoints; + + m_debug_session->run([&](DebugSession::DebugBreakReason reason, Optional optional_regs) { + if (reason == DebugSession::DebugBreakReason::Exited) { + dbg() << "Program exited"; + m_on_exit_callback(); + return DebugSession::DebugDecision::Detach; + } + ASSERT(optional_regs.has_value()); + const PtraceRegisters& regs = optional_regs.value(); + auto source_position = m_debug_session->debug_info().get_source_position(regs.eip); + if (!source_position.has_value()) { + return DebugSession::DebugDecision::Continue; + } + + if (in_single_step_mode) { + for (auto address : temporary_breakpoints) { + m_debug_session->remove_breakpoint(address); + } + temporary_breakpoints.clear(); + in_single_step_mode = false; + } + + dbg() << "Debugee stopped @ " << source_position.value().file_path << ":" << source_position.value().line_number; + m_on_stopped_callback(source_position.value()); + + pthread_mutex_lock(&m_continue_mutex); + pthread_cond_wait(&m_continue_cond, &m_continue_mutex); + pthread_mutex_unlock(&m_continue_mutex); + + m_on_continue_callback(); + + if (m_continue_type == ContinueType::Continue) { + return DebugSession::DebugDecision::Continue; + } + + if (m_continue_type == ContinueType::SourceSingleStep) { + // A possible method for source level single stepping is to single step + // in assembly level, until the current instruction's source position has changed. + // However, since we do not currently generate debug symbols for library code, + // we may have to single-step over lots of library code instructions until we get back to our code, + // which is very slow. + // So the current method is to insert a temporary breakpoint at every known statement in our source code, + // continue execution, and remove the temporary breakpoints once we hit the first breakpoint. + m_debug_session->debug_info().for_each_source_position([&](DebugInfo::SourcePosition position) { + auto address = (void*)position.address_of_first_statement; + if ((u32)address != regs.eip && !m_debug_session->breakpoint_exists(address)) { + m_debug_session->insert_breakpoint(address); + temporary_breakpoints.append(address); + } + }); + in_single_step_mode = true; + return DebugSession::DebugDecision::Continue; + } + ASSERT_NOT_REACHED(); + }); + m_debug_session.clear(); + return 0; +} diff --git a/DevTools/HackStudio/Debugger.h b/DevTools/HackStudio/Debugger.h new file mode 100644 index 0000000000..8af2b38460 --- /dev/null +++ b/DevTools/HackStudio/Debugger.h @@ -0,0 +1,90 @@ + +/* + * Copyright (c) 2020, Itamar S. + * 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 "BreakpointCallback.h" +#include +#include +#include +#include +#include + +class Debugger { +public: + static Debugger& the(); + static void initialize( + Function on_stop_callback, + Function on_continue_callback, + Function on_exit_callback); + + static bool is_initialized(); + + static void on_breakpoint_change(const String& file, size_t line, BreakpointChange change_type); + + void set_executable_path(const String& path) { m_executable_path = path; } + + DebugSession* session() { return m_debug_session.ptr(); } + + // Thread entry point + static int start_static(); + + pthread_mutex_t* continue_mutex() { return &m_continue_mutex; } + pthread_cond_t* continue_cond() { return &m_continue_cond; } + + enum class ContinueType { + Continue, + SourceSingleStep, + }; + + void set_continue_type(ContinueType type) { m_continue_type = type; } + void reset_breakpoints() { m_breakpoints.clear(); } + +private: + explicit Debugger( + Function on_stop_callback, + Function on_continue_callback, + Function on_exit_callback); + + static DebugInfo::SourcePosition create_source_position(const String& file, size_t line); + + void start(); + int debugger_loop(); + + OwnPtr m_debug_session; + + pthread_mutex_t m_continue_mutex {}; + pthread_cond_t m_continue_cond {}; + + Vector m_breakpoints; + String m_executable_path; + + Function m_on_stopped_callback; + Function m_on_continue_callback; + Function m_on_exit_callback; + + ContinueType m_continue_type { ContinueType::Continue }; +}; diff --git a/DevTools/HackStudio/Makefile b/DevTools/HackStudio/Makefile index 50443ca585..468a60ef85 100644 --- a/DevTools/HackStudio/Makefile +++ b/DevTools/HackStudio/Makefile @@ -13,10 +13,11 @@ OBJS = \ CursorTool.o \ WidgetTool.o \ WidgetTreeModel.o \ + Debugger.o \ main.o PROGRAM = HackStudio -LIB_DEPS = GUI Web VT Protocol Markdown Gfx IPC Thread Pthread Core JS +LIB_DEPS = GUI Web VT Protocol Markdown Gfx IPC Thread Pthread Core JS Debug include ../../Makefile.common diff --git a/DevTools/HackStudio/Project.h b/DevTools/HackStudio/Project.h index 9551ad2ffd..a9e029644c 100644 --- a/DevTools/HackStudio/Project.h +++ b/DevTools/HackStudio/Project.h @@ -56,6 +56,8 @@ public: ProjectType type() const { return m_type; } GUI::Model& model() { return *m_model; } String default_file() const; + String name() const { return m_name; } + String path() const { return m_path; } template void for_each_text_file(Callback callback) const diff --git a/DevTools/HackStudio/main.cpp b/DevTools/HackStudio/main.cpp index 048db6cd26..0f64113890 100644 --- a/DevTools/HackStudio/main.cpp +++ b/DevTools/HackStudio/main.cpp @@ -25,6 +25,7 @@ */ #include "CursorTool.h" +#include "Debugger.h" #include "Editor.h" #include "EditorWrapper.h" #include "FindInFilesWidget.h" @@ -36,7 +37,10 @@ #include "WidgetTool.h" #include "WidgetTreeModel.h" #include +#include +#include #include +#include #include #include #include @@ -62,6 +66,8 @@ #include #include #include +#include +#include #include #include #include @@ -84,7 +90,7 @@ static RefPtr s_action_tab_widget; void add_new_editor(GUI::Widget& parent) { - auto wrapper = EditorWrapper::construct(); + auto wrapper = EditorWrapper::construct(Debugger::on_breakpoint_change); if (s_action_tab_widget) { parent.insert_child_before(wrapper, *s_action_tab_widget); } else { @@ -120,6 +126,24 @@ Editor& current_editor() return current_editor_wrapper().editor(); } +NonnullRefPtr get_editor_of_file(const String& file) +{ + for (auto& wrapper : g_all_editor_wrappers) { + String wrapper_file = wrapper.filename_label().text(); + if (wrapper_file == file || String::format("./%s", wrapper_file.characters()) == file) { + return wrapper; + } + } + ASSERT_NOT_REACHED(); +} + +String get_project_executable_path() +{ + // e.g /my/project.files => /my/project + // TODO: Perhaps a Makefile rule for getting the value of $(PROGRAM) would be better? + return g_project->path().substring(0, g_project->path().index_of(".").value()); +} + static void build(TerminalWrapper&); static void run(TerminalWrapper&); void open_project(String); @@ -531,13 +555,78 @@ int main(int argc, char** argv) run(terminal_wrapper); stop_action->set_enabled(true); }); + + RefPtr debugger_thread; + auto debug_action = GUI::Action::create("Debug", Gfx::Bitmap::load_from_file("/res/icons/16x16/play-debug.png"), [&](auto&) { + if (g_project->type() != ProjectType::Cpp) { + GUI::MessageBox::show(String::format("Cannot debug current project type", get_project_executable_path().characters()), "Error", GUI::MessageBox::Type::Error, GUI::MessageBox::InputType::OK, g_window); + return; + } + if (!GUI::FilePicker::file_exists(get_project_executable_path())) { + GUI::MessageBox::show(String::format("Could not find file: %s. (did you build the project?)", get_project_executable_path().characters()), "Error", GUI::MessageBox::Type::Error, GUI::MessageBox::InputType::OK, g_window); + return; + } + if (Debugger::the().session()) { + GUI::MessageBox::show("Debugger is already running", "Error", GUI::MessageBox::Type::Error, GUI::MessageBox::InputType::OK, g_window); + return; + } + Debugger::the().set_executable_path(get_project_executable_path()); + debugger_thread = adopt(*new LibThread::Thread(Debugger::start_static)); + debugger_thread->start(); + }); + + auto continue_action = GUI::Action::create("Continue", Gfx::Bitmap::load_from_file("/res/icons/16x16/go-last.png"), [&](auto&) { + pthread_mutex_lock(Debugger::the().continue_mutex()); + Debugger::the().set_continue_type(Debugger::ContinueType::Continue); + pthread_cond_signal(Debugger::the().continue_cond()); + pthread_mutex_unlock(Debugger::the().continue_mutex()); + }); + + auto single_step_action = GUI::Action::create("Single Step", Gfx::Bitmap::load_from_file("/res/icons/16x16/single-step.png"), [&](auto&) { + pthread_mutex_lock(Debugger::the().continue_mutex()); + Debugger::the().set_continue_type(Debugger::ContinueType::SourceSingleStep); + pthread_cond_signal(Debugger::the().continue_cond()); + pthread_mutex_unlock(Debugger::the().continue_mutex()); + }); + continue_action->set_enabled(false); + single_step_action->set_enabled(false); + toolbar.add_action(run_action); toolbar.add_action(stop_action); + toolbar.add_action(debug_action); + toolbar.add_action(continue_action); + toolbar.add_action(single_step_action); + + RefPtr current_editor_in_execution; + Debugger::initialize( + [&](DebugInfo::SourcePosition source_position) { + dbg() << "Program stopped"; + current_editor_in_execution = get_editor_of_file(source_position.file_path); + current_editor_in_execution->editor().set_execution_position(source_position.line_number - 1); + continue_action->set_enabled(true); + single_step_action->set_enabled(true); + }, + [&]() { + dbg() << "Program continued"; + continue_action->set_enabled(false); + single_step_action->set_enabled(false); + if (current_editor_in_execution) { + current_editor_in_execution->editor().clear_execution_position(); + } + }, + [&]() { + dbg() << "Program exited"; + Core::EventLoop::main().post_event(*g_window, make([=](auto&) { + GUI::MessageBox::show("Program Exited", "Debugger", GUI::MessageBox::Type::Information, GUI::MessageBox::InputType::OK, g_window); + })); + Core::EventLoop::wake(); + }); auto& build_menu = menubar->add_menu("Build"); build_menu.add_action(build_action); build_menu.add_action(run_action); build_menu.add_action(stop_action); + build_menu.add_action(debug_action); auto& view_menu = menubar->add_menu("View"); view_menu.add_action(hide_action_tabs_action); @@ -613,6 +702,9 @@ void open_project(String filename) g_project_tree_view->toggle_index(g_project_tree_view->model()->index(0, 0)); g_project_tree_view->update(); } + if (Debugger::is_initialized()) { + Debugger::the().reset_breakpoints(); + } } void open_file(const String& filename)