Refactor GDB stub (#88)

The intention of this PR is not yet to extend/fix the GDB functionality,
but rather to get rid the `mini-gdbstub` dependency.

Its functionality should have been reimplemented as closely as possible.

The main reason for this is better portability and security fixes (there
are numerous OOB read/write vulnerabilities in `mini-gdbstub`)
This commit is contained in:
Maurice Heumann
2025-01-18 20:34:20 +01:00
committed by GitHub
31 changed files with 1164 additions and 272 deletions

1
deps/CMakeLists.txt vendored
View File

@@ -11,6 +11,5 @@ target_include_directories(reflect INTERFACE
##########################################
include(mini-gdbstub.cmake)
include(googletest.cmake)
include(zlib.cmake)

1
deps/mini-gdbstub vendored

Submodule deps/mini-gdbstub deleted from 632ebd3892

View File

@@ -1,11 +0,0 @@
file(GLOB_RECURSE SRC_FILES CONFIGURE_DEPENDS
mini-gdbstub/lib/*.c
)
list(SORT SRC_FILES)
add_library(mini-gdbstub ${SRC_FILES})
target_include_directories(mini-gdbstub PUBLIC
"${CMAKE_CURRENT_LIST_DIR}/mini-gdbstub/include"
)

View File

@@ -1,5 +1,6 @@
add_subdirectory(common)
add_subdirectory(emulator)
add_subdirectory(gdb-stub)
add_subdirectory(unicorn-emulator)
add_subdirectory(windows-emulator)
if (NOT MOMO_BUILD_AS_LIBRARY)

View File

@@ -15,6 +15,7 @@ target_precompile_headers(analyzer PRIVATE std_include.hpp)
target_link_libraries(analyzer PRIVATE
reflect
windows-emulator
gdb-stub
)
set_property(GLOBAL PROPERTY VS_STARTUP_PROJECT analyzer)

View File

@@ -52,7 +52,7 @@ namespace
win_emu.log.print(color::pink, "Waiting for GDB connection on %s...\n", address);
win_x64_gdb_stub_handler handler{win_emu};
run_gdb_stub(handler, "i386:x86-64", gdb_registers.size(), address);
gdb_stub::run_gdb_stub(network::address{"0.0.0.0:28960", AF_INET}, handler);
}
else
{
@@ -181,7 +181,7 @@ namespace
for (int i = 1; i < argc; ++i)
{
args.push_back(argv[i]);
args.emplace_back(argv[i]);
}
return args;

View File

@@ -48,6 +48,14 @@ namespace network
address(const sockaddr_in6& addr);
address(const sockaddr* addr, socklen_t length);
address(const address&) = default;
address(address&&) noexcept = default;
address& operator=(const address&) = default;
address& operator=(address&&) noexcept = default;
~address() = default;
void set_ipv4(uint32_t ip);
void set_ipv4(const in_addr& addr);
void set_ipv6(const in6_addr& addr);

View File

@@ -31,7 +31,7 @@ namespace network
socket::~socket()
{
this->release();
this->close();
}
socket::socket(socket&& obj) noexcept
@@ -43,7 +43,7 @@ namespace network
{
if (this != &obj)
{
this->release();
this->close();
this->socket_ = obj.socket_;
obj.socket_ = INVALID_SOCKET;
@@ -53,15 +53,20 @@ namespace network
}
socket::operator bool() const
{
return this->is_valid();
}
bool socket::is_valid() const
{
return this->socket_ != INVALID_SOCKET;
}
void socket::release()
void socket::close()
{
if (this->socket_ != INVALID_SOCKET)
{
closesocket(this->socket_);
::closesocket(this->socket_);
this->socket_ = INVALID_SOCKET;
}
}
@@ -90,7 +95,7 @@ namespace network
#endif
}
bool socket::sleep(const std::chrono::milliseconds timeout) const
bool socket::sleep(const std::chrono::milliseconds timeout, const bool in_poll) const
{
/*fd_set fdr;
FD_ZERO(&fdr);
@@ -119,13 +124,13 @@ namespace network
std::vector<const socket*> sockets{};
sockets.push_back(this);
return sleep_sockets(sockets, timeout);
return sleep_sockets(sockets, timeout, in_poll);
}
bool socket::sleep_until(const std::chrono::high_resolution_clock::time_point time_point) const
bool socket::sleep_until(const std::chrono::high_resolution_clock::time_point time_point, const bool in_poll) const
{
const auto duration = time_point - std::chrono::high_resolution_clock::now();
return this->sleep(std::chrono::duration_cast<std::chrono::milliseconds>(duration));
return this->sleep(std::chrono::duration_cast<std::chrono::milliseconds>(duration), in_poll);
}
SOCKET socket::get_socket() const
@@ -167,7 +172,13 @@ namespace network
return address->get_addr().sa_family;
}
bool socket::sleep_sockets(const std::span<const socket*>& sockets, const std::chrono::milliseconds timeout)
bool socket::is_ready(const bool in_poll) const
{
return this->is_valid() && is_socket_ready(this->socket_, in_poll);
}
bool socket::sleep_sockets(const std::span<const socket*>& sockets, const std::chrono::milliseconds timeout,
const bool in_poll)
{
std::vector<pollfd> pfds{};
pfds.resize(sockets.size());
@@ -178,7 +189,7 @@ namespace network
const auto& socket = sockets[i];
pfd.fd = socket->get_socket();
pfd.events = POLLIN;
pfd.events = in_poll ? POLLIN : POLLOUT;
pfd.revents = 0;
}
@@ -223,9 +234,10 @@ namespace network
}
bool socket::sleep_sockets_until(const std::span<const socket*>& sockets,
const std::chrono::high_resolution_clock::time_point time_point)
const std::chrono::high_resolution_clock::time_point time_point,
const bool in_poll)
{
const auto duration = time_point - std::chrono::high_resolution_clock::now();
return sleep_sockets(sockets, std::chrono::duration_cast<std::chrono::milliseconds>(duration));
return sleep_sockets(sockets, std::chrono::duration_cast<std::chrono::milliseconds>(duration), in_poll);
}
}

View File

@@ -10,7 +10,7 @@
using send_size = int;
#define GET_SOCKET_ERROR() (WSAGetLastError())
#define poll WSAPoll
#define SOCK_WOULDBLOCK WSAEWOULDBLOCK
#define SERR(x) (WSA##x)
#define SHUT_RDWR SD_BOTH
#else
using SOCKET = int;
@@ -19,7 +19,7 @@ using send_size = size_t;
#define SOCKET_ERROR (-1)
#define GET_SOCKET_ERROR() (errno)
#define closesocket close
#define SOCK_WOULDBLOCK EWOULDBLOCK
#define SERR(x) (x)
#endif
namespace network
@@ -42,14 +42,16 @@ namespace network
operator bool() const;
bool is_valid() const;
bool bind(const address& target);
bool set_blocking(bool blocking);
static bool set_blocking(SOCKET s, bool blocking);
static constexpr bool socket_is_ready = true;
bool sleep(std::chrono::milliseconds timeout) const;
bool sleep_until(std::chrono::high_resolution_clock::time_point time_point) const;
bool sleep(std::chrono::milliseconds timeout, bool in_poll = true) const;
bool sleep_until(std::chrono::high_resolution_clock::time_point time_point, bool in_poll = true) const;
SOCKET get_socket() const;
uint16_t get_port() const;
@@ -57,15 +59,18 @@ namespace network
int get_address_family() const;
static bool sleep_sockets(const std::span<const socket*>& sockets, std::chrono::milliseconds timeout);
bool is_ready(bool in_poll) const;
static bool sleep_sockets(const std::span<const socket*>& sockets, std::chrono::milliseconds timeout,
bool in_poll);
static bool sleep_sockets_until(const std::span<const socket*>& sockets,
std::chrono::high_resolution_clock::time_point time_point);
std::chrono::high_resolution_clock::time_point time_point, bool in_poll);
static bool is_socket_ready(SOCKET s, bool in_poll);
void close();
private:
SOCKET socket_ = INVALID_SOCKET;
void release();
};
}

View File

@@ -26,27 +26,53 @@ namespace network
bool tcp_client_socket::send(const void* data, const size_t size) const
{
const auto res = ::send(this->get_socket(), static_cast<const char*>(data), static_cast<send_size>(size), 0);
return static_cast<size_t>(res) == size;
return this->send(std::string_view(static_cast<const char*>(data), size));
}
bool tcp_client_socket::send(const std::string_view data) const
bool tcp_client_socket::send(std::string_view data) const
{
return this->send(data.data(), data.size());
}
bool tcp_client_socket::receive(std::string& data) const
{
char buffer[0x2000];
const auto result = recv(this->get_socket(), buffer, static_cast<int>(sizeof(buffer)), 0);
if (result == SOCKET_ERROR)
while (!data.empty())
{
return false;
const auto res = ::send(this->get_socket(), data.data(), static_cast<send_size>(data.size()), 0);
if (res < 0)
{
if (GET_SOCKET_ERROR() != SERR(EWOULDBLOCK))
{
break;
}
this->sleep(std::chrono::milliseconds(10), true);
continue;
}
if (static_cast<size_t>(res) > data.size())
{
break;
}
data = data.substr(res);
}
data.assign(buffer, buffer + result);
return true;
return data.empty();
}
std::optional<std::string> tcp_client_socket::receive(const std::optional<size_t> max_size)
{
char buffer[0x2000];
const auto size = std::min(sizeof(buffer), max_size.value_or(sizeof(buffer)));
const auto result = recv(this->get_socket(), buffer, static_cast<int>(size), 0);
if (result > 0)
{
return std::string(buffer, result);
}
if (result == 0 || (result < 0 && GET_SOCKET_ERROR() == SERR(ECONNRESET)))
{
this->close();
}
return std::nullopt;
}
std::optional<address> tcp_client_socket::get_target() const
@@ -69,6 +95,6 @@ namespace network
}
const auto error = GET_SOCKET_ERROR();
return error == SOCK_WOULDBLOCK;
return error == SERR(EWOULDBLOCK);
}
}

View File

@@ -21,7 +21,7 @@ namespace network
[[maybe_unused]] bool send(const void* data, size_t size) const;
[[maybe_unused]] bool send(std::string_view data) const;
bool receive(std::string& data) const;
std::optional<std::string> receive(std::optional<size_t> max_size = std::nullopt);
std::optional<address> get_target() const;

View File

@@ -9,29 +9,39 @@ namespace network
bool udp_socket::send(const address& target, const void* data, const size_t size) const
{
const auto res = sendto(this->get_socket(), static_cast<const char*>(data), static_cast<send_size>(size), 0,
&target.get_addr(), target.get_size());
return static_cast<size_t>(res) == size;
return this->send(target, std::string_view(static_cast<const char*>(data), size));
}
bool udp_socket::send(const address& target, const std::string_view data) const
{
return this->send(target, data.data(), data.size());
while (true)
{
const auto res = sendto(this->get_socket(), data.data(), static_cast<send_size>(data.size()), 0,
&target.get_addr(), target.get_size());
if (res < 0 && GET_SOCKET_ERROR() == SERR(EWOULDBLOCK))
{
this->sleep(std::chrono::milliseconds(10), true);
continue;
}
return static_cast<size_t>(res) == data.size();
}
}
bool udp_socket::receive(address& source, std::string& data) const
std::optional<std::pair<address, std::string>> udp_socket::receive() const
{
char buffer[0x2000];
address source{};
auto len = source.get_max_size();
const auto result =
recvfrom(this->get_socket(), buffer, static_cast<int>(sizeof(buffer)), 0, &source.get_addr(), &len);
if (result == SOCKET_ERROR)
{
return false;
return std::nullopt;
}
data.assign(buffer, buffer + result);
return true;
return {{source, std::string(buffer, result)}};
}
}

View File

@@ -17,6 +17,6 @@ namespace network
[[maybe_unused]] bool send(const address& target, const void* data, size_t size) const;
[[maybe_unused]] bool send(const address& target, std::string_view data) const;
bool receive(address& source, std::string& data) const;
std::optional<std::pair<address, std::string>> receive() const;
};
}

View File

@@ -28,6 +28,7 @@
#define DECLSPEC_ALIGN(n) alignas(n)
#define fopen_s fopen
#define sscanf_s sscanf
#define RESTRICTED_POINTER __restrict

View File

@@ -1,6 +1,7 @@
#pragma once
#include <span>
#include <string>
#include <ranges>
#include <cstddef>
#include <cwctype>
#include <algorithm>
@@ -44,4 +45,108 @@ namespace utils::string
{
return to_lower(std::move(str));
}
inline char to_nibble(std::byte value, const bool uppercase = false)
{
value = value & static_cast<std::byte>(0xF);
if (value <= static_cast<std::byte>(9))
{
return static_cast<char>('0' + static_cast<char>(value));
}
return static_cast<char>((uppercase ? 'A' : 'a') + (static_cast<char>(value) - 0xA));
}
inline std::pair<char, char> to_hex(const std::byte value, const bool uppercase = false)
{
return {to_nibble(value >> 4, uppercase), to_nibble(value, uppercase)};
}
inline std::string to_hex_string(const void* data, const size_t size, const bool uppercase = false)
{
std::string result{};
result.reserve(size * 2);
for (size_t i = 0; i < size; ++i)
{
const auto value = static_cast<const std::byte*>(data)[i];
const auto [high, low] = to_hex(value, uppercase);
result.push_back(high);
result.push_back(low);
}
return result;
}
template <typename Integer>
requires(std::is_integral_v<Integer>)
std::string to_hex_number(const Integer& i, const bool uppercase = false)
{
std::string res{};
res.reserve(sizeof(i) * 2);
const std::span data{reinterpret_cast<const std::byte*>(&i), sizeof(i)};
for (const auto value : data)
{
const auto [high, low] = to_hex(value, uppercase);
res.insert(res.begin(), {high, low});
}
while (res.size() > 1 && res.front() == '0')
{
res.erase(res.begin());
}
return res;
}
template <typename Integer>
requires(std::is_integral_v<Integer>)
std::string to_hex_string(const Integer& i, const bool uppercase = false)
{
return to_hex_string(&i, sizeof(Integer), uppercase);
}
inline std::string to_hex_string(const std::span<std::byte> data, const bool uppercase = false)
{
return to_hex_string(data.data(), data.size(), uppercase);
}
inline std::byte parse_nibble(const char nibble)
{
const auto lower = char_to_lower(nibble);
if (lower >= '0' && lower <= '9')
{
return static_cast<std::byte>(lower - '0');
}
if (lower >= 'a' && lower <= 'f')
{
return static_cast<std::byte>(lower - 'a');
}
return static_cast<std::byte>(0);
}
inline std::vector<std::byte> from_hex_string(const std::string_view str)
{
const auto size = str.size() / 2;
std::vector<std::byte> data{};
data.reserve(size);
for (size_t i = 0; i < size; ++i)
{
const auto high = parse_nibble(str[i * 2 + 0]);
const auto low = parse_nibble(str[i * 2 + 1]);
const auto value = (high << 4) | low;
data.push_back(value);
}
return data;
}
}

View File

@@ -0,0 +1,21 @@
file(GLOB_RECURSE SRC_FILES CONFIGURE_DEPENDS
*.cpp
*.hpp
*.rc
)
list(SORT SRC_FILES)
add_library(gdb-stub ${SRC_FILES})
momo_assign_source_group(${SRC_FILES})
target_link_libraries(gdb-stub PRIVATE
emulator-common
)
target_include_directories(gdb-stub INTERFACE
"${CMAKE_CURRENT_LIST_DIR}"
)
momo_strip_target(gdb-stub)

View File

@@ -0,0 +1,81 @@
#include "async_handler.hpp"
#include <utils/finally.hpp>
using namespace std::chrono_literals;
namespace gdb_stub
{
async_handler::async_handler(handler_function h)
: handler_(std::move(h))
{
this->stop_ = false;
this->runner_ = std::thread([this] {
this->work(); //
});
}
async_handler::~async_handler()
{
this->stop_ = true;
this->run_ = false;
if (this->runner_.joinable())
{
this->runner_.join();
}
}
void async_handler::pause()
{
this->run_ = false;
while (this->is_running_ && !this->stop_)
{
std::this_thread::sleep_for(1ms);
}
}
void async_handler::run()
{
if (this->stop_)
{
return;
}
this->run_ = true;
while (!this->is_running_ && !this->stop_)
{
std::this_thread::sleep_for(1ms);
}
}
bool async_handler::is_running() const
{
return this->is_running_;
}
void async_handler::work()
{
while (true)
{
while (!this->run_ && !this->stop_)
{
this->is_running_ = false;
std::this_thread::sleep_for(10ms);
}
if (this->stop_)
{
break;
}
const auto _ = utils::finally([this] {
this->is_running_ = false; //
});
this->is_running_ = true;
this->handler_(this->run_);
}
}
}

View File

@@ -0,0 +1,38 @@
#pragma once
#include <thread>
#include <atomic>
#include <functional>
#include <condition_variable>
namespace gdb_stub
{
class async_handler
{
public:
using handler = void(std::atomic_bool& should_run);
using handler_function = std::function<handler>;
async_handler(handler_function handler);
~async_handler();
async_handler(async_handler&&) = delete;
async_handler(const async_handler&) = delete;
async_handler& operator=(async_handler&&) = delete;
async_handler& operator=(const async_handler&) = delete;
void run();
void pause();
bool is_running() const;
private:
std::atomic_bool run_{false};
std::atomic_bool stop_{false};
std::atomic_bool is_running_{false};
handler_function handler_{};
std::thread runner_{};
void work();
};
}

19
src/gdb-stub/checksum.hpp Normal file
View File

@@ -0,0 +1,19 @@
#pragma once
#include <cstdint>
namespace gdb_stub
{
constexpr size_t CHECKSUM_SIZE = 2;
inline uint8_t compute_checksum(const std::string_view data)
{
uint8_t checksum = 0;
for (const auto c : data)
{
checksum += static_cast<uint8_t>(c);
}
return checksum;
}
}

View File

@@ -0,0 +1,65 @@
#include "connection_handler.hpp"
#include "checksum.hpp"
#include <utils/string.hpp>
#include <thread>
using namespace std::literals;
namespace gdb_stub
{
namespace
{
bool read_from_socket(stream_processor& processor, network::tcp_client_socket& client)
{
const auto data = client.receive();
if (!data)
{
return false;
}
processor.push_stream_data(*data);
return true;
}
}
connection_handler::connection_handler(network::tcp_client_socket& client)
: client_(client)
{
this->client_.set_blocking(false);
}
std::optional<std::string> connection_handler::get_packet()
{
while (this->client_.is_valid() && !this->processor_.has_packet())
{
if (!read_from_socket(this->processor_, this->client_))
{
std::this_thread::sleep_for(100ms);
}
}
if (this->processor_.has_packet())
{
return this->processor_.get_next_packet();
}
return std::nullopt;
}
void connection_handler::send_reply(const std::string_view data) const
{
const auto checksum = utils::string::to_hex_string(compute_checksum(data));
this->send_raw_data("$" + std::string(data) + "#" + checksum);
}
void connection_handler::send_raw_data(const std::string_view data) const
{
(void)this->client_.send(data);
}
void connection_handler::close() const
{
this->client_.close();
}
}

View File

@@ -0,0 +1,23 @@
#pragma once
#include "stream_processor.hpp"
#include <network/tcp_client_socket.hpp>
namespace gdb_stub
{
class connection_handler
{
public:
connection_handler(network::tcp_client_socket& client);
std::optional<std::string> get_packet();
void send_reply(std::string_view data) const;
void send_raw_data(std::string_view data) const;
void close() const;
private:
network::tcp_client_socket& client_;
stream_processor processor_{};
};
}

481
src/gdb-stub/gdb_stub.cpp Normal file
View File

@@ -0,0 +1,481 @@
#include "gdb_stub.hpp"
#include <cassert>
#include <cinttypes>
#include <utils/string.hpp>
#include <platform/compiler.hpp>
#include <network/tcp_server_socket.hpp>
#include "async_handler.hpp"
#include "connection_handler.hpp"
using namespace std::literals;
namespace gdb_stub
{
namespace
{
void rt_assert(const bool condition)
{
(void)condition;
assert(condition);
}
network::tcp_client_socket accept_client(const network::address& bind_address)
{
network::tcp_server_socket server{bind_address.get_family()};
if (!server.bind(bind_address))
{
return false;
}
return server.accept();
}
std::pair<std::string_view, std::string_view> split_string(const std::string_view payload, const char separator)
{
auto name = payload;
std::string_view args{};
const auto separator_pos = payload.find_first_of(separator);
if (separator_pos != std::string_view::npos)
{
name = payload.substr(0, separator_pos);
args = payload.substr(separator_pos + 1);
}
return {name, args};
}
void process_xfer(const connection_handler& connection, debugging_handler& handler,
const std::string_view payload)
{
auto [name, args] = split_string(payload, ':');
if (name == "features")
{
connection.send_reply("l<target version=\"1.0\"><architecture>" //
+ handler.get_target_description() //
+ "<architecture>%s</architecture></target>");
}
else
{
connection.send_reply({});
}
}
void process_query(const connection_handler& connection, debugging_handler& handler,
const std::string_view payload)
{
const auto [name, args] = split_string(payload, ':');
if (name == "Supported")
{
connection.send_reply("PacketSize=1024;qXfer:features:read+");
}
else if (name == "Attached")
{
connection.send_reply("1");
}
else if (name == "Xfer")
{
process_xfer(connection, handler, args);
}
else if (name == "Symbol")
{
connection.send_reply("OK");
}
else
{
connection.send_reply({});
}
}
void process_action(const connection_handler& connection, const action a)
{
if (a == action::shutdown)
{
connection.close();
}
}
breakpoint_type translate_breakpoint_type(const uint32_t type)
{
if (type >= static_cast<size_t>(breakpoint_type::END))
{
return breakpoint_type::software;
}
return static_cast<breakpoint_type>(type);
}
bool change_breakpoint(debugging_handler& handler, const bool set, const breakpoint_type type,
const uint64_t address, const size_t size)
{
if (set)
{
return handler.set_breakpoint(type, address, size);
}
return handler.delete_breakpoint(type, address, size);
}
void handle_breakpoint(const connection_handler& connection, debugging_handler& handler,
const std::string& data, const bool set)
{
uint32_t type{};
uint64_t addr{};
size_t kind{};
rt_assert(sscanf_s(data.c_str(), "%x,%" PRIX64 ",%zx", &type, &addr, &kind) == 3);
const auto res = change_breakpoint(handler, set, translate_breakpoint_type(type), addr, kind);
connection.send_reply(res ? "OK" : "E01");
}
void handle_v_packet(const connection_handler& connection, const std::string_view data)
{
const auto [name, args] = split_string(data, ':');
if (name == "Cont?")
{
// IDA pro gets confused if the reply arrives too early :(
std::this_thread::sleep_for(1s);
connection.send_reply("vCont;s;c;");
}
else
{
connection.send_reply({});
}
}
void read_registers(const connection_handler& connection, debugging_handler& handler)
{
std::string response{};
std::vector<std::byte> data{};
data.resize(handler.get_max_register_size());
const auto registers = handler.get_register_count();
for (size_t i = 0; i < registers; ++i)
{
memset(data.data(), 0, data.size());
const auto res = handler.read_register(i, data.data(), data.size());
if (!res)
{
connection.send_reply("E01");
return;
}
response.append(utils::string::to_hex_string(data));
}
connection.send_reply(response);
}
void write_registers(const connection_handler& connection, debugging_handler& handler,
const std::string_view payload)
{
const auto data = utils::string::from_hex_string(payload);
const auto registers = handler.get_register_count();
const auto register_size = handler.get_max_register_size();
for (size_t i = 0; i < registers; ++i)
{
const auto offset = i * register_size;
const auto end_offset = offset + register_size;
if (data.size() < end_offset)
{
connection.send_reply("E01");
return;
}
const auto res = handler.write_register(i, data.data() + offset, register_size);
if (!res)
{
connection.send_reply("E01");
return;
}
}
connection.send_reply("OK");
}
void read_single_register(const connection_handler& connection, debugging_handler& handler,
const std::string& payload)
{
size_t reg{};
rt_assert(sscanf_s(payload.c_str(), "%zx", &reg) == 1);
std::vector<std::byte> data{};
data.resize(handler.get_max_register_size());
const auto res = handler.read_register(reg, data.data(), data.size());
if (res)
{
connection.send_reply(utils::string::to_hex_string(data));
}
else
{
connection.send_reply("E01");
}
}
void write_single_register(const connection_handler& connection, debugging_handler& handler,
const std::string_view payload)
{
const auto [reg, hex_data] = split_string(payload, '=');
size_t register_index{};
rt_assert(sscanf_s(std::string(reg).c_str(), "%zx", &register_index) == 1);
const auto register_size = handler.get_max_register_size();
const auto data = utils::string::from_hex_string(hex_data);
const auto res = register_size <= data.size() && //
handler.write_register(register_index, data.data(), register_size);
connection.send_reply(res ? "OK" : "E01");
}
void read_memory(const connection_handler& connection, debugging_handler& handler, const std::string& payload)
{
uint64_t address{};
size_t size{};
rt_assert(sscanf_s(payload.c_str(), "%" PRIx64 ",%zx", &address, &size) == 2);
if (size > 0x1000)
{
connection.send_reply("E01");
return;
}
std::vector<std::byte> data{};
data.resize(size);
const auto res = handler.read_memory(address, data.data(), data.size());
if (!res)
{
connection.send_reply("E01");
return;
}
connection.send_reply(utils::string::to_hex_string(data));
}
void write_memory(const connection_handler& connection, debugging_handler& handler,
const std::string_view payload)
{
const auto [info, hex_data] = split_string(payload, ':');
size_t size{};
uint64_t address{};
rt_assert(sscanf_s(std::string(info).c_str(), "%" PRIx64 ",%zx", &address, &size) == 2);
if (size > 0x1000)
{
connection.send_reply("E01");
return;
}
auto data = utils::string::from_hex_string(hex_data);
data.resize(size);
const auto res = handler.write_memory(address, data.data(), data.size());
connection.send_reply(res ? "OK" : "E01");
}
std::string decode_x_memory(const std::string_view payload)
{
std::string result{};
result.reserve(payload.size());
bool xor_next = false;
for (auto value : payload)
{
if (xor_next)
{
value ^= 0x20;
xor_next = false;
}
else if (value == '}')
{
xor_next = true;
continue;
}
result.push_back(value);
}
return result;
}
void write_x_memory(const connection_handler& connection, debugging_handler& handler,
const std::string_view payload)
{
const auto [info, encoded_data] = split_string(payload, ':');
size_t size{};
uint64_t address{};
rt_assert(sscanf_s(std::string(info).c_str(), "%" PRIx64 ",%zx", &address, &size) == 2);
if (size > 0x1000)
{
connection.send_reply("E01");
return;
}
auto data = decode_x_memory(encoded_data);
data.resize(size);
const auto res = handler.write_memory(address, data.data(), data.size());
if (!res)
{
connection.send_reply("E01");
return;
}
connection.send_reply("OK");
}
void handle_command(const connection_handler& connection, async_handler& async, debugging_handler& handler,
const uint8_t command, const std::string_view data)
{
// printf("GDB command: %c -> %.*s\n", command, static_cast<int>(data.size()), data.data());
switch (command)
{
case 'c':
async.run();
process_action(connection, handler.run());
async.pause();
connection.send_reply("S05");
break;
case 's':
process_action(connection, handler.singlestep());
connection.send_reply("S05");
break;
case 'q':
process_query(connection, handler, data);
break;
case 'D':
connection.close();
break;
case 'z':
case 'Z':
handle_breakpoint(connection, handler, std::string(data), command == 'Z');
break;
case '?':
connection.send_reply("S05");
break;
case 'v':
handle_v_packet(connection, data);
break;
case 'g':
read_registers(connection, handler);
break;
case 'G':
write_registers(connection, handler, data);
break;
case 'p':
read_single_register(connection, handler, std::string(data));
break;
case 'P':
write_single_register(connection, handler, data);
break;
case 'm':
read_memory(connection, handler, std::string(data));
break;
case 'M':
write_memory(connection, handler, data);
break;
case 'X':
write_x_memory(connection, handler, data);
break;
default:
connection.send_reply({});
break;
}
}
void process_packet(const connection_handler& connection, async_handler& async, debugging_handler& handler,
const std::string_view packet)
{
connection.send_raw_data("+");
if (packet.empty())
{
return;
}
const auto command = packet.front();
handle_command(connection, async, handler, command, packet.substr(1));
}
bool is_interrupt_packet(const std::optional<std::string>& data)
{
return data && data->size() == 1 && data->front() == '\x03';
}
}
bool run_gdb_stub(const network::address& bind_address, debugging_handler& handler)
{
auto client = accept_client(bind_address);
if (!client)
{
return false;
}
async_handler async{[&](std::atomic_bool& can_run) {
while (can_run)
{
std::this_thread::sleep_for(10ms);
const auto data = client.receive(1);
if (is_interrupt_packet(data) || !client.is_valid())
{
handler.on_interrupt();
can_run = false;
}
}
}};
connection_handler connection{client};
while (true)
{
const auto packet = connection.get_packet();
if (!packet)
{
break;
}
process_packet(connection, async, handler, *packet);
}
return true;
}
}

49
src/gdb-stub/gdb_stub.hpp Normal file
View File

@@ -0,0 +1,49 @@
#pragma once
#include <cstdint>
#include <network/address.hpp>
namespace gdb_stub
{
enum class action : uint8_t
{
none,
resume,
shutdown,
};
enum class breakpoint_type : uint8_t
{
software = 0,
hardware_exec = 1,
hardware_write = 2,
hardware_read = 3,
hardware_read_write = 4,
END,
};
struct debugging_handler
{
virtual ~debugging_handler() = default;
virtual action run() = 0;
virtual action singlestep() = 0;
virtual size_t get_register_count() = 0;
virtual size_t get_max_register_size() = 0;
virtual bool read_register(size_t reg, void* data, size_t max_length) = 0;
virtual bool write_register(size_t reg, const void* data, size_t size) = 0;
virtual bool read_memory(uint64_t address, void* data, size_t length) = 0;
virtual bool write_memory(uint64_t address, const void* data, size_t length) = 0;
virtual bool set_breakpoint(breakpoint_type type, uint64_t address, size_t size) = 0;
virtual bool delete_breakpoint(breakpoint_type type, uint64_t address, size_t size) = 0;
virtual void on_interrupt() = 0;
virtual std::string get_target_description() = 0;
};
bool run_gdb_stub(const network::address& bind_address, debugging_handler& handler);
}

View File

@@ -0,0 +1,94 @@
#include "stream_processor.hpp"
#include "checksum.hpp"
#include <cassert>
#include <stdexcept>
namespace gdb_stub
{
namespace
{
void trim_data_stream_start(std::string& stream)
{
while (!stream.empty() && stream.front() != '$')
{
stream.erase(stream.begin());
}
}
}
bool stream_processor::has_packet() const
{
return !this->packets_.empty();
}
std::string stream_processor::get_next_packet()
{
if (this->packets_.empty())
{
throw std::runtime_error("No packet available");
}
auto packet = std::move(this->packets_.front());
this->packets_.pop();
return packet;
}
void stream_processor::push_stream_data(const std::string& data)
{
this->stream_.append(data);
this->process_data_stream();
}
void stream_processor::process_data_stream()
{
while (true)
{
trim_data_stream_start(this->stream_);
const auto end = this->stream_.find_first_of('#');
if (end == std::string::npos)
{
break;
}
const auto packet_size = end + CHECKSUM_SIZE + 1;
if (packet_size > this->stream_.size())
{
break;
}
auto packet = this->stream_.substr(0, packet_size);
this->stream_.erase(0, packet_size);
this->enqueue_packet(std::move(packet));
}
}
void stream_processor::enqueue_packet(std::string packet)
{
constexpr auto END_BYTES = CHECKSUM_SIZE + 1;
if (packet.size() < (END_BYTES + 1) //
|| packet.front() != '$' //
|| packet[packet.size() - END_BYTES] != '#')
{
return;
}
const auto checksum = strtoul(packet.c_str() + packet.size() - CHECKSUM_SIZE, nullptr, 16);
assert((checksum & 0xFF) == checksum);
packet.erase(packet.begin());
packet.erase(packet.size() - END_BYTES, END_BYTES);
const auto computed_checksum = compute_checksum(packet);
if (computed_checksum == checksum)
{
this->packets_.push(std::move(packet));
}
}
}

View File

@@ -0,0 +1,22 @@
#pragma once
#include <queue>
#include <string>
#include <optional>
namespace gdb_stub
{
class stream_processor
{
public:
bool has_packet() const;
std::string get_next_packet();
void push_stream_data(const std::string& data);
private:
std::string stream_{};
std::queue<std::string> packets_{};
void process_data_stream();
void enqueue_packet(std::string packet);
};
}

View File

@@ -14,7 +14,6 @@ target_precompile_headers(windows-emulator PRIVATE std_include.hpp)
target_link_libraries(windows-emulator PRIVATE
unicorn-emulator
mini-gdbstub
)
target_link_libraries(windows-emulator PUBLIC

View File

@@ -1,136 +0,0 @@
#include "../std_include.hpp"
#include "gdb_stub.hpp"
#include <utils/finally.hpp>
extern "C"
{
#include <gdbstub.h>
}
namespace
{
gdb_action_t map_gdb_action(const gdb_action action)
{
switch (action)
{
case gdb_action::none:
return ACT_NONE;
case gdb_action::resume:
return ACT_RESUME;
case gdb_action::shutdown:
return ACT_SHUTDOWN;
}
throw std::runtime_error("Bad action");
}
breakpoint_type map_breakpoint_type(const bp_type_t type)
{
switch (type)
{
case BP_SOFTWARE:
return breakpoint_type::software;
case BP_HARDWARE_EXEC:
return breakpoint_type::hardware_exec;
case BP_HARDWARE_WRITE:
return breakpoint_type::hardware_write;
case BP_HARDWARE_READ:
return breakpoint_type::hardware_read;
case BP_HARDWARE_READ_WRITE:
return breakpoint_type::hardware_read_write;
}
throw std::runtime_error("Bad breakpoint type");
}
gdb_stub_handler& get_handler(void* args)
{
return *static_cast<gdb_stub_handler*>(args);
}
gdb_action_t cont(void* args)
{
return map_gdb_action(get_handler(args).cont());
}
gdb_action_t stepi(void* args)
{
return map_gdb_action(get_handler(args).stepi());
}
int read_reg(void* args, const int regno, size_t* value)
{
return get_handler(args).read_reg(regno, value) ? 0 : 1;
}
int write_reg(void* args, const int regno, const size_t value)
{
return get_handler(args).write_reg(regno, value) ? 0 : 1;
}
int read_mem(void* args, const size_t addr, const size_t len, void* val)
{
return get_handler(args).read_mem(addr, len, val) ? 0 : 1;
}
int write_mem(void* args, const size_t addr, const size_t len, void* val)
{
return get_handler(args).write_mem(addr, len, val) ? 0 : 1;
}
bool set_bp(void* args, const size_t addr, const bp_type_t type, const size_t size)
{
return get_handler(args).set_bp(map_breakpoint_type(type), addr, size);
}
bool del_bp(void* args, const size_t addr, const bp_type_t type, const size_t size)
{
return get_handler(args).del_bp(map_breakpoint_type(type), addr, size);
}
void on_interrupt(void* args)
{
get_handler(args).on_interrupt();
}
target_ops get_target_ops()
{
target_ops ops{};
ops.cont = cont;
ops.stepi = stepi;
ops.read_reg = read_reg;
ops.write_reg = write_reg;
ops.read_mem = read_mem;
ops.write_mem = write_mem;
ops.set_bp = set_bp;
ops.del_bp = del_bp;
ops.on_interrupt = on_interrupt;
return ops;
}
}
bool run_gdb_stub(gdb_stub_handler& handler, std::string target_description, const size_t register_count,
std::string bind_address)
{
const arch_info_t info{
target_description.data(),
static_cast<int>(register_count),
sizeof(uint64_t),
};
auto ops = get_target_ops();
gdbstub_t stub{};
if (!gdbstub_init(&stub, &ops, info, bind_address.data()))
{
return false;
}
const auto _ = utils::finally([&] { gdbstub_close(&stub); });
return gdbstub_run(&stub, &handler);
}

View File

@@ -1,39 +0,0 @@
#pragma once
enum class gdb_action : uint8_t
{
none,
resume,
shutdown,
};
enum class breakpoint_type : uint8_t
{
software,
hardware_exec,
hardware_write,
hardware_read,
hardware_read_write,
};
struct gdb_stub_handler
{
virtual ~gdb_stub_handler() = default;
virtual gdb_action cont() = 0;
virtual gdb_action stepi() = 0;
virtual bool read_reg(int regno, size_t* value) = 0;
virtual bool write_reg(int regno, size_t value) = 0;
virtual bool read_mem(size_t addr, size_t len, void* val) = 0;
virtual bool write_mem(size_t addr, size_t len, void* val) = 0;
virtual bool set_bp(breakpoint_type type, size_t addr, size_t size) = 0;
virtual bool del_bp(breakpoint_type type, size_t addr, size_t size) = 0;
virtual void on_interrupt() = 0;
};
bool run_gdb_stub(gdb_stub_handler& handler, std::string target_description, size_t register_count,
std::string bind_address);

View File

@@ -12,7 +12,7 @@ class win_x64_gdb_stub_handler : public x64_gdb_stub_handler
{
}
gdb_action cont() override
gdb_stub::action run() override
{
try
{
@@ -20,13 +20,13 @@ class win_x64_gdb_stub_handler : public x64_gdb_stub_handler
}
catch (const std::exception& e)
{
puts(e.what());
this->win_emu_->log.error("%s\n", e.what());
}
return gdb_action::resume;
return gdb_stub::action::resume;
}
gdb_action stepi() override
gdb_stub::action singlestep() override
{
try
{
@@ -34,10 +34,15 @@ class win_x64_gdb_stub_handler : public x64_gdb_stub_handler
}
catch (const std::exception& e)
{
puts(e.what());
this->win_emu_->log.error("%s\n", e.what());
}
return gdb_action::resume;
return gdb_stub::action::resume;
}
std::string get_target_description() override
{
return "i386:x86-64";
}
private:

View File

@@ -1,8 +1,8 @@
#pragma once
#include <x64_emulator.hpp>
#include "gdb_stub.hpp"
#include "scoped_hook.hpp"
#include <utils/concurrency.hpp>
#include <gdb-stub/gdb_stub.hpp>
inline std::vector gdb_registers{
x64_register::rax, x64_register::rbx, x64_register::rcx, x64_register::rdx, x64_register::rsi, x64_register::rdi,
@@ -16,18 +16,20 @@ inline std::vector gdb_registers{
x64_register::gs,*/
};
inline memory_operation map_breakpoint_type(const breakpoint_type type)
inline memory_operation map_breakpoint_type(const gdb_stub::breakpoint_type type)
{
using enum gdb_stub::breakpoint_type;
switch (type)
{
case breakpoint_type::software:
case breakpoint_type::hardware_exec:
case software:
case hardware_exec:
return memory_operation::exec;
case breakpoint_type::hardware_read:
case hardware_read:
return memory_permission::read;
case breakpoint_type::hardware_write:
case hardware_write:
return memory_permission::write;
case breakpoint_type::hardware_read_write:
case hardware_read_write:
return memory_permission::read_write;
default:
throw std::runtime_error("Bad bp type");
@@ -38,7 +40,7 @@ struct breakpoint_key
{
size_t addr{};
size_t size{};
breakpoint_type type{};
gdb_stub::breakpoint_type type{};
bool operator==(const breakpoint_key& other) const
{
@@ -56,7 +58,7 @@ struct std::hash<breakpoint_key>
}
};
class x64_gdb_stub_handler : public gdb_stub_handler
class x64_gdb_stub_handler : public gdb_stub::debugging_handler
{
public:
x64_gdb_stub_handler(x64_emulator& emu)
@@ -66,7 +68,7 @@ class x64_gdb_stub_handler : public gdb_stub_handler
~x64_gdb_stub_handler() override = default;
gdb_action cont() override
gdb_stub::action run() override
{
try
{
@@ -77,10 +79,10 @@ class x64_gdb_stub_handler : public gdb_stub_handler
puts(e.what());
}
return gdb_action::resume;
return gdb_stub::action::resume;
}
gdb_action stepi() override
gdb_stub::action singlestep() override
{
try
{
@@ -91,39 +93,51 @@ class x64_gdb_stub_handler : public gdb_stub_handler
puts(e.what());
}
return gdb_action::resume;
return gdb_stub::action::resume;
}
bool read_reg(const int regno, size_t* value) override
size_t get_register_count() override
{
*value = 0;
return gdb_registers.size();
}
size_t get_max_register_size() override
{
// return 256 / 8;
return 64 / 8;
}
bool read_register(const size_t reg, void* data, const size_t max_length) override
{
try
{
if (static_cast<size_t>(regno) >= gdb_registers.size())
if (reg >= gdb_registers.size())
{
// TODO: Fix
return true;
}
this->emu_->read_register(gdb_registers[regno], value, sizeof(*value));
this->emu_->read_register(gdb_registers[reg], data, max_length);
return true;
}
catch (...)
{
// TODO: Fix
return true;
}
}
bool write_reg(const int regno, const size_t value) override
bool write_register(const size_t reg, const void* data, const size_t size) override
{
try
{
if (static_cast<size_t>(regno) >= gdb_registers.size())
if (reg >= gdb_registers.size())
{
// TODO: Fix
return true;
}
this->emu_->write_register(gdb_registers[regno], &value, sizeof(value));
this->emu_->write_register(gdb_registers[reg], data, size);
return true;
}
catch (...)
@@ -132,16 +146,16 @@ class x64_gdb_stub_handler : public gdb_stub_handler
}
}
bool read_mem(const size_t addr, const size_t len, void* val) override
bool read_memory(const uint64_t address, void* data, const size_t length) override
{
return this->emu_->try_read_memory(addr, val, len);
return this->emu_->try_read_memory(address, data, length);
}
bool write_mem(const size_t addr, const size_t len, void* val) override
bool write_memory(const uint64_t address, const void* data, const size_t length) override
{
try
{
this->emu_->write_memory(addr, val, len);
this->emu_->write_memory(address, data, length);
return true;
}
catch (...)
@@ -150,7 +164,7 @@ class x64_gdb_stub_handler : public gdb_stub_handler
}
}
bool set_bp(const breakpoint_type type, const size_t addr, const size_t size) override
bool set_breakpoint(const gdb_stub::breakpoint_type type, const uint64_t addr, const size_t size) override
{
try
{
@@ -170,7 +184,7 @@ class x64_gdb_stub_handler : public gdb_stub_handler
}
}
bool del_bp(const breakpoint_type type, const size_t addr, const size_t size) override
bool delete_breakpoint(const gdb_stub::breakpoint_type type, const uint64_t addr, const size_t size) override
{
try
{

View File

@@ -457,7 +457,7 @@ namespace
if (recevied_data < 0)
{
const auto error = GET_SOCKET_ERROR();
if (error == SOCK_WOULDBLOCK)
if (error == SERR(EWOULDBLOCK))
{
this->delay_ioctrl(c, {}, true);
return STATUS_PENDING;
@@ -512,7 +512,7 @@ namespace
if (sent_data < 0)
{
const auto error = GET_SOCKET_ERROR();
if (error == SOCK_WOULDBLOCK)
if (error == SERR(EWOULDBLOCK))
{
this->delay_ioctrl(c, {}, false);
return STATUS_PENDING;