Renamed GdbRsp directory to Gdb

This commit is contained in:
Nav
2022-03-31 21:52:46 +01:00
parent 01d52bb130
commit 2aa240a680
57 changed files with 64 additions and 64 deletions

View File

@@ -0,0 +1,47 @@
#include "AvrGdbRsp.hpp"
// Command packets
#include "CommandPackets/ReadMemory.hpp"
#include "CommandPackets/WriteMemory.hpp"
namespace Bloom::DebugServer::Gdb::AvrGdb
{
using namespace Bloom::Exceptions;
using Bloom::Targets::TargetRegisterDescriptor;
using Bloom::Targets::TargetRegisterType;
AvrGdbRsp::AvrGdbRsp(
const DebugServerConfig& debugServerConfig,
EventListener& eventListener
)
: GdbRspDebugServer(debugServerConfig, eventListener)
{}
void AvrGdbRsp::init() {
DebugServer::Gdb::GdbRspDebugServer::init();
this->gdbTargetDescriptor = TargetDescriptor(
this->targetControllerConsole.getTargetDescriptor()
);
}
std::unique_ptr<Gdb::CommandPackets::CommandPacket> AvrGdbRsp::resolveCommandPacket(
const RawPacketType& rawPacket
) {
using AvrGdb::CommandPackets::ReadMemory;
using AvrGdb::CommandPackets::WriteMemory;
if (rawPacket.size() >= 2) {
if (rawPacket[1] == 'm') {
return std::make_unique<ReadMemory>(rawPacket);
}
if (rawPacket[1] == 'M') {
return std::make_unique<WriteMemory>(rawPacket);
}
}
return GdbRspDebugServer::resolveCommandPacket(rawPacket);
}
}

View File

@@ -0,0 +1,52 @@
#pragma once
#include <cstdint>
#include "TargetDescriptor.hpp"
#include "src/DebugServer/Gdb/GdbRspDebugServer.hpp"
namespace Bloom::DebugServer::Gdb::AvrGdb
{
/**
* The AVR GDB client (avr-gdb) defines a set of parameters relating to AVR targets. These parameters are
* hardcoded in the AVR GDB source code. The client expects all compatible GDB RSP servers to be aware of
* these parameters.
*
* An example of these hardcoded parameters is target registers and the order in which they are supplied; AVR GDB
* clients expect 35 registers to be accessible via the server. 32 of these registers are general purpose CPU
* registers. The GP registers are expected to be followed by the status register (SREG), stack pointer
* register (SPH & SPL) and the program counter. These must all be given in a specific order, which is
* pre-determined by the AVR GDB client. See AvrGdbRsp::getRegisterNumberToDescriptorMapping() for more.
*
* For more on this, see the AVR GDB source code at https://github.com/bminor/binutils-gdb/blob/master/gdb/avr-tdep.c
*
* The AvrGdpRsp class extends the generic GDB RSP debug server and implements these AVR specific parameters.
*/
class AvrGdbRsp: public GdbRspDebugServer
{
public:
AvrGdbRsp(
const DebugServerConfig& debugServerConfig,
EventListener& eventListener
);
std::string getName() const override {
return "AVR GDB Remote Serial Protocol Debug Server";
}
protected:
void init() override;
const Gdb::TargetDescriptor& getGdbTargetDescriptor() override {
return this->gdbTargetDescriptor.value();
}
std::unique_ptr<Gdb::CommandPackets::CommandPacket> resolveCommandPacket(
const RawPacketType& rawPacket
) override;
private:
std::optional<TargetDescriptor> gdbTargetDescriptor;
};
}

View File

@@ -0,0 +1,17 @@
#include "AbstractMemoryAccessPacket.hpp"
#include "src/DebugServer/Gdb/ResponsePackets/TargetStopped.hpp"
#include "src/DebugServer/Gdb/ResponsePackets/ErrorResponsePacket.hpp"
#include "src/DebugServer/Gdb/Signal.hpp"
#include "src/Logger/Logger.hpp"
#include "src/Exceptions/Exception.hpp"
namespace Bloom::DebugServer::Gdb::CommandPackets
{
using ResponsePackets::ErrorResponsePacket;
using Exceptions::Exception;
}

View File

@@ -0,0 +1,54 @@
#pragma once
#include <cstdint>
#include <optional>
#include "src/DebugServer/Gdb/CommandPackets/CommandPacket.hpp"
#include "src/Targets/TargetMemory.hpp"
namespace Bloom::DebugServer::Gdb::AvrGdb::CommandPackets
{
/**
* The ReadMemory class implements a structure for "m" packets. Upon receiving these packets, the server is
* expected to read memory from the target and send it the client.
*/
class AbstractMemoryAccessPacket: public Bloom::DebugServer::Gdb::CommandPackets::CommandPacket
{
public:
explicit AbstractMemoryAccessPacket(const std::vector<unsigned char>& rawPacket): CommandPacket(rawPacket) {};
protected:
/**
* The mask used by the AVR GDB client to encode the memory type into memory addresses.
*/
static constexpr std::uint32_t AVR_GDB_MEMORY_ADDRESS_MASK = 0xFE0000U;
/**
* avr-gdb uses the most significant 15 bits in memory addresses to indicate the type of memory being
* addressed.
*
* @param address
* @return
*/
Targets::TargetMemoryType getMemoryTypeFromGdbAddress(std::uint32_t address) {
if ((address & AbstractMemoryAccessPacket::AVR_GDB_MEMORY_ADDRESS_MASK) != 0U) {
return Targets::TargetMemoryType::RAM;
}
return Targets::TargetMemoryType::FLASH;
}
/**
* Strips the most significant 15 bits from a GDB memory address.
*
* @param address
* @return
*/
std::uint32_t removeMemoryTypeIndicatorFromGdbAddress(std::uint32_t address) {
return (address & AbstractMemoryAccessPacket::AVR_GDB_MEMORY_ADDRESS_MASK) != 0U
? (address & ~(AbstractMemoryAccessPacket::AVR_GDB_MEMORY_ADDRESS_MASK))
: address;
}
};
}

View File

@@ -0,0 +1,70 @@
#include "ReadMemory.hpp"
#include "src/DebugServer/Gdb/ResponsePackets/ErrorResponsePacket.hpp"
#include "src/DebugServer/Gdb/ResponsePackets/ResponsePacket.hpp"
#include "src/Logger/Logger.hpp"
#include "src/Exceptions/Exception.hpp"
namespace Bloom::DebugServer::Gdb::AvrGdb::CommandPackets
{
using ResponsePackets::ErrorResponsePacket;
using ResponsePackets::ResponsePacket;
using Exceptions::Exception;
void ReadMemory::init() {
if (this->data.size() < 4) {
throw Exception("Invalid packet length");
}
auto packetString = QString::fromLocal8Bit(
reinterpret_cast<const char*>(this->data.data() + 1),
static_cast<int>(this->data.size() - 1)
);
/*
* The read memory ('m') packet consists of two segments, an address and a number of bytes to read.
* These are separated by a comma character.
*/
auto packetSegments = packetString.split(",");
if (packetSegments.size() != 2) {
throw Exception(
"Unexpected number of segments in packet data: " + std::to_string(packetSegments.size())
);
}
bool conversionStatus = false;
this->startAddress = packetSegments.at(0).toUInt(&conversionStatus, 16);
if (!conversionStatus) {
throw Exception("Failed to parse start address from read memory packet data");
}
this->bytes = packetSegments.at(1).toUInt(&conversionStatus, 16);
if (!conversionStatus) {
throw Exception("Failed to parse read length from read memory packet data");
}
}
void ReadMemory::handle(DebugSession& debugSession, TargetControllerConsole& targetControllerConsole) {
Logger::debug("Handling ReadMemory packet");
try {
auto memoryType = this->getMemoryTypeFromGdbAddress(this->startAddress);
auto startAddress = this->removeMemoryTypeIndicatorFromGdbAddress(this->startAddress);
auto memoryBuffer = targetControllerConsole.readMemory(memoryType, startAddress, this->bytes);
auto hexMemoryBuffer = Packet::dataToHex(memoryBuffer);
debugSession.connection.writePacket(
ResponsePacket(std::vector<unsigned char>(hexMemoryBuffer.begin(), hexMemoryBuffer.end()))
);
} catch (const Exception& exception) {
Logger::error("Failed to read memory from target - " + exception.getMessage());
debugSession.connection.writePacket(ErrorResponsePacket());
}
}
}

View File

@@ -0,0 +1,40 @@
#pragma once
#include <cstdint>
#include <optional>
#include "AbstractMemoryAccessPacket.hpp"
namespace Bloom::DebugServer::Gdb::AvrGdb::CommandPackets
{
/**
* The ReadMemory class implements a structure for "m" packets. Upon receiving these packets, the server is
* expected to read memory from the target and send it the client.
*/
class ReadMemory: public AbstractMemoryAccessPacket
{
public:
/**
* The startAddress sent from the GDB client may include additional bits used to indicate the memory type.
* These bits have to be removed from the address before it can be used as a start address. This is not done
* here, as it's target specific.
*
* For an example of where GDB does this, see the AvrGdbRsp class.
*/
std::uint32_t startAddress = 0;
/**
* Number of bytes to read.
*/
std::uint32_t bytes = 0;
explicit ReadMemory(const std::vector<unsigned char>& rawPacket): AbstractMemoryAccessPacket(rawPacket) {
init();
};
void handle(DebugSession& debugSession, TargetControllerConsole& targetControllerConsole) override;
protected:
void init();
};
}

View File

@@ -0,0 +1,86 @@
#include "WriteMemory.hpp"
#include "src/DebugServer/Gdb/ResponsePackets/ErrorResponsePacket.hpp"
#include "src/DebugServer/Gdb/ResponsePackets/OkResponsePacket.hpp"
#include "src/Logger/Logger.hpp"
#include "src/Exceptions/Exception.hpp"
namespace Bloom::DebugServer::Gdb::AvrGdb::CommandPackets
{
using ResponsePackets::ErrorResponsePacket;
using ResponsePackets::OkResponsePacket;
using namespace Bloom::Exceptions;
void WriteMemory::init() {
if (this->data.size() < 4) {
throw Exception("Invalid packet length");
}
auto packetString = QString::fromLocal8Bit(
reinterpret_cast<const char*>(this->data.data() + 1),
static_cast<int>(this->data.size() - 1)
);
/*
* The write memory ('M') packet consists of three segments, an address, a length and a buffer.
* The address and length are separated by a comma character, and the buffer proceeds a colon character.
*/
auto packetSegments = packetString.split(",");
if (packetSegments.size() != 2) {
throw Exception(
"Unexpected number of segments in packet data: " + std::to_string(packetSegments.size())
);
}
bool conversionStatus = false;
this->startAddress = packetSegments.at(0).toUInt(&conversionStatus, 16);
if (!conversionStatus) {
throw Exception("Failed to parse start address from write memory packet data");
}
auto lengthAndBufferSegments = packetSegments.at(1).split(":");
if (lengthAndBufferSegments.size() != 2) {
throw Exception(
"Unexpected number of segments in packet data: "
+ std::to_string(lengthAndBufferSegments.size())
);
}
auto bufferSize = lengthAndBufferSegments.at(0).toUInt(&conversionStatus, 16);
if (!conversionStatus) {
throw Exception("Failed to parse write length from write memory packet data");
}
this->buffer = Packet::hexToData(lengthAndBufferSegments.at(1).toStdString());
if (this->buffer.size() != bufferSize) {
throw Exception("Buffer size does not match length value given in write memory packet");
}
}
void WriteMemory::handle(DebugSession& debugSession, TargetControllerConsole& targetControllerConsole) {
Logger::debug("Handling WriteMemory packet");
try {
const auto memoryType = this->getMemoryTypeFromGdbAddress(this->startAddress);
if (memoryType == Targets::TargetMemoryType::FLASH) {
throw Exception(
"GDB client requested a flash memory write - This is not currently supported by Bloom."
);
}
const auto startAddress = this->removeMemoryTypeIndicatorFromGdbAddress(this->startAddress);
targetControllerConsole.writeMemory(memoryType, startAddress, this->buffer);
debugSession.connection.writePacket(OkResponsePacket());
} catch (const Exception& exception) {
Logger::error("Failed to write memory to target - " + exception.getMessage());
debugSession.connection.writePacket(ErrorResponsePacket());
}
}
}

View File

@@ -0,0 +1,34 @@
#pragma once
#include <cstdint>
#include <optional>
#include "AbstractMemoryAccessPacket.hpp"
namespace Bloom::DebugServer::Gdb::AvrGdb::CommandPackets
{
/**
* The WriteMemory class implements the structure for "M" packets. Upon receiving this packet, the server is
* expected to write data to the target's memory, at the specified start address.
*/
class WriteMemory: public AbstractMemoryAccessPacket
{
public:
/**
* Like with the ReadMemory command packet, the start address carries additional bits that indicate
* the memory type.
*/
std::uint32_t startAddress = 0;
Targets::TargetMemoryBuffer buffer;
explicit WriteMemory(const std::vector<unsigned char>& rawPacket): AbstractMemoryAccessPacket(rawPacket) {
init();
};
void handle(DebugSession& debugSession, TargetControllerConsole& targetControllerConsole) override;
private:
void init();
};
}

View File

@@ -0,0 +1,170 @@
#include "TargetDescriptor.hpp"
#include <numeric>
#include "src/Exceptions/Exception.hpp"
#include "src/Logger/Logger.hpp"
namespace Bloom::DebugServer::Gdb::AvrGdb
{
using Bloom::Targets::TargetRegisterDescriptor;
using Bloom::Targets::TargetRegisterType;
using Bloom::Exceptions::Exception;
TargetDescriptor::TargetDescriptor(const Bloom::Targets::TargetDescriptor& targetDescriptor)
: DebugServer::Gdb::TargetDescriptor(targetDescriptor)
{
this->loadRegisterMappings();
}
std::optional<GdbRegisterNumberType> TargetDescriptor::getRegisterNumberFromTargetRegisterDescriptor(
const Targets::TargetRegisterDescriptor& registerDescriptor
) const {
return this->targetRegisterDescriptorsByGdbNumber.valueAt(registerDescriptor);
}
const RegisterDescriptor& TargetDescriptor::getRegisterDescriptorFromNumber(GdbRegisterNumberType number) const {
if (this->registerDescriptorsByGdbNumber.contains(number)) {
return this->registerDescriptorsByGdbNumber.at(number);
}
throw Exception("Unknown register from GDB - register number (" + std::to_string(number)
+ ") not mapped to any GDB register descriptor.");
}
const TargetRegisterDescriptor& TargetDescriptor::getTargetRegisterDescriptorFromNumber(
GdbRegisterNumberType number
) const {
if (this->targetRegisterDescriptorsByGdbNumber.contains(number)) {
return this->targetRegisterDescriptorsByGdbNumber.at(number);
}
throw Exception("Unknown register from GDB - register number (" + std::to_string(number)
+ ") not mapped to any target register descriptor.");
}
const std::vector<GdbRegisterNumberType>& TargetDescriptor::getRegisterNumbers() const {
return this->registerNumbers;
}
void TargetDescriptor::loadRegisterMappings() {
auto& registerDescriptorsByType = this->targetDescriptor.registerDescriptorsByType;
if (!registerDescriptorsByType.contains(TargetRegisterType::STATUS_REGISTER)) {
throw Exception("Missing status register descriptor");
}
if (!registerDescriptorsByType.contains(TargetRegisterType::STACK_POINTER)) {
throw Exception("Missing stack pointer register descriptor");
}
if (!registerDescriptorsByType.contains(TargetRegisterType::PROGRAM_COUNTER)) {
throw Exception("Missing program counter register descriptor");
}
if (!registerDescriptorsByType.contains(TargetRegisterType::GENERAL_PURPOSE_REGISTER)
|| registerDescriptorsByType.at(TargetRegisterType::GENERAL_PURPOSE_REGISTER).size() != 32
) {
throw Exception("Unexpected general purpose register count");
}
/*
* For AVR targets, avr-gdb defines 35 registers in total:
*
* Register number 0 through 31 are general purpose registers
* Register number 32 is the status register (SREG)
* Register number 33 is the stack pointer register
* Register number 34 is the program counter register
*/
// Generate 35 register numbers (0 -> 34)
std::iota(this->registerNumbers.begin(), this->registerNumbers.end(), 0);
/*
* Worth noting that gpRegisterDescriptors will always be sorted in the correct order, from register 0 to 31.
*
* Hmm, but the sorting is based on the start address (see TargetRegisterDescriptor::<() for more). So
* effectively, we're assuming that the registers will be laid out in the correct order, in memory. I think
* this assumption is fair.
*/
const auto& gpRegisterDescriptors = registerDescriptorsByType.at(
TargetRegisterType::GENERAL_PURPOSE_REGISTER
);
// General purpose registers
GdbRegisterNumberType regNumber = 0;
for (const auto& descriptor : gpRegisterDescriptors) {
this->registerDescriptorsByGdbNumber.insert(std::pair(
regNumber,
RegisterDescriptor(
regNumber,
1,
"General Purpose Register " + std::to_string(regNumber)
)
));
this->targetRegisterDescriptorsByGdbNumber.insert(std::pair(
regNumber,
descriptor
));
regNumber++;
}
// Status, stack pointer and program counter registers
const auto statusDescriptor = RegisterDescriptor(
32,
1,
"Status Register"
);
this->registerDescriptorsByGdbNumber.insert(std::pair(statusDescriptor.number, statusDescriptor));
this->targetRegisterDescriptorsByGdbNumber.insert(std::pair(
statusDescriptor.number,
*(registerDescriptorsByType.at(TargetRegisterType::STATUS_REGISTER).begin())
));
const auto stackPointerDescriptor = RegisterDescriptor(
33,
2,
"Stack Pointer Register"
);
this->registerDescriptorsByGdbNumber.insert(
std::pair(stackPointerDescriptor.number, stackPointerDescriptor)
);
this->targetRegisterDescriptorsByGdbNumber.insert(std::pair(
stackPointerDescriptor.number,
*(registerDescriptorsByType.at(TargetRegisterType::STACK_POINTER).begin())
));
const auto programCounterDescriptor = RegisterDescriptor(
34,
4,
"Program Counter"
);
this->registerDescriptorsByGdbNumber.insert(std::pair(
programCounterDescriptor.number,
programCounterDescriptor
));
this->targetRegisterDescriptorsByGdbNumber.insert(std::pair(
programCounterDescriptor.number,
*(registerDescriptorsByType.at(TargetRegisterType::PROGRAM_COUNTER).begin())
));
if (registerDescriptorsByType.at(TargetRegisterType::STATUS_REGISTER).size() > statusDescriptor.size) {
throw Exception("AVR8 status target register size exceeds the GDB register size.");
}
if (registerDescriptorsByType.at(TargetRegisterType::STACK_POINTER).size() > stackPointerDescriptor.size) {
throw Exception("AVR8 stack pointer target register size exceeds the GDB register size.");
}
if (
registerDescriptorsByType.at(TargetRegisterType::PROGRAM_COUNTER).size() > programCounterDescriptor.size
) {
throw Exception("AVR8 program counter size exceeds the GDB register size.");
}
}
}

View File

@@ -0,0 +1,63 @@
#pragma once
#include "src/DebugServer/Gdb/TargetDescriptor.hpp"
#include "src/Helpers/BiMap.hpp"
namespace Bloom::DebugServer::Gdb::AvrGdb
{
class TargetDescriptor: public DebugServer::Gdb::TargetDescriptor
{
public:
BiMap<GdbRegisterNumberType, RegisterDescriptor> registerDescriptorsByGdbNumber = {};
BiMap<GdbRegisterNumberType, Targets::TargetRegisterDescriptor> targetRegisterDescriptorsByGdbNumber = {};
explicit TargetDescriptor(const Targets::TargetDescriptor& targetDescriptor);
/**
* Should retrieve the GDB register number, given a target register descriptor. Or std::nullopt if the target
* register descriptor isn't mapped to any GDB register.
*
* @param registerDescriptor
* @return
*/
std::optional<GdbRegisterNumberType> getRegisterNumberFromTargetRegisterDescriptor(
const Targets::TargetRegisterDescriptor& registerDescriptor
) const override;
/**
* Should retrieve the GDB register descriptor for a given GDB register number.
*
* @param number
* @return
*/
const RegisterDescriptor& getRegisterDescriptorFromNumber(GdbRegisterNumberType number) const override;
/**
* Should retrieve the mapped target register descriptor for a given GDB register number.
*
* @param number
* @return
*/
const Targets::TargetRegisterDescriptor& getTargetRegisterDescriptorFromNumber(
GdbRegisterNumberType number
) const override;
const std::vector<GdbRegisterNumberType>& getRegisterNumbers() const override;
private:
std::vector<GdbRegisterNumberType> registerNumbers = std::vector<GdbRegisterNumberType>(35);
/**
* For AVR targets, avr-gdb defines 35 registers in total:
*
* Register number 0 through 31 are general purpose registers
* Register number 32 is the status register (SREG)
* Register number 33 is the stack pointer register
* Register number 34 is the program counter register
*
* This function will prepare the appropriate GDB register numbers and mappings.
*/
void loadRegisterMappings();
};
}

View File

@@ -0,0 +1,11 @@
#pragma once
namespace Bloom::DebugServer::Gdb
{
enum class BreakpointType: int
{
UNKNOWN = 0,
SOFTWARE_BREAKPOINT = 1,
HARDWARE_BREAKPOINT = 2,
};
}

View File

@@ -0,0 +1,45 @@
#include "CommandPacket.hpp"
#include "src/DebugServer/Gdb/ResponsePackets/ResponsePacket.hpp"
#include "src/DebugServer/Gdb/ResponsePackets/OkResponsePacket.hpp"
#include "src/DebugServer/Gdb/ResponsePackets/TargetStopped.hpp"
#include "src/DebugServer/Gdb/ResponsePackets/ErrorResponsePacket.hpp"
#include "src/DebugServer/Gdb/Signal.hpp"
#include "src/Logger/Logger.hpp"
#include "src/Exceptions/Exception.hpp"
namespace Bloom::DebugServer::Gdb::CommandPackets
{
using ResponsePackets::ResponsePacket;
using ResponsePackets::OkResponsePacket;
using ResponsePackets::TargetStopped;
using ResponsePackets::ErrorResponsePacket;
using Exceptions::Exception;
void CommandPacket::handle(DebugSession& debugSession, TargetControllerConsole& targetControllerConsole) {
auto packetData = this->getData();
auto packetString = std::string(packetData.begin(), packetData.end());
if (packetString[0] == '?') {
// Status report
debugSession.connection.writePacket(TargetStopped(Signal::TRAP));
} else if (packetString[0] == 'D') {
// Detach packet - there's not really anything we need to do here, so just respond with an OK
debugSession.connection.writePacket(OkResponsePacket());
} else if (packetString.find("qAttached") == 0) {
Logger::debug("Handling qAttached");
debugSession.connection.writePacket(ResponsePacket({1}));
} else {
Logger::debug("Unknown GDB RSP packet: " + packetString + " - returning empty response");
// Respond with an empty packet
debugSession.connection.writePacket(ResponsePacket({0}));
}
}
}

View File

@@ -0,0 +1,51 @@
#pragma once
#include <vector>
#include <memory>
#include "src/DebugServer/Gdb/Packet.hpp"
#include "src/DebugServer/Gdb/DebugSession.hpp"
#include "src/TargetController/TargetControllerConsole.hpp"
namespace Bloom::DebugServer::Gdb::CommandPackets
{
/**
* GDB RSP command packets are sent to the server, from the GDB client. These packets carry instructions that the
* server is expected to carry out. Upon completion, the server is expected to respond to the client with
* a ResponsePacket.
*
* For some command packets, we define a specific data structure by extending this CommandPacket class. These
* classes extend the data structure to include fields for data which may be specific to the command.
* They also implement additional methods that allow us to easily access the additional data. An example
* of this would be the SupportedFeaturesQuery class. It extends the CommandPacket class and provides access
* to additional data fields that are specific to the command (in this case, a set of GDB features reported to be
* supported by the GDB client).
*
* Typically, command packets that require specific data structures are handled in a dedicated handler method
* in the GdbRspDebugServer. This is done by double dispatching the packet object to the appropriate handler.
* See CommandPacket::dispatchToHandler(), GdbRspDebugServer::serve() and the overloads
* for GdbRspDebugServer::handleGdbPacket() for more on this.
*
* Some command packets are so simple they do not require a dedicated data structure. An example of this is
* the halt reason packet, which contains nothing more than an ? character in the packet body. These packets are
* typically handled in the generic GdbRspDebugServer::handleGdbPacket(CommandPacket&) method.
*
* See the Packet class for information on how the raw packets are formatted.
*/
class CommandPacket: public Packet
{
public:
explicit CommandPacket(const std::vector<unsigned char>& rawPacket): Packet(rawPacket) {}
/**
* Should handle the command for the current active debug session.
*
* @param debugSession
* The current active debug session.
*
* @param targetControllerConsole
*/
virtual void handle(DebugSession& debugSession, TargetControllerConsole& targetControllerConsole);
};
}

View File

@@ -0,0 +1,33 @@
#include "ContinueExecution.hpp"
#include "src/DebugServer/Gdb/ResponsePackets/ErrorResponsePacket.hpp"
#include "src/Logger/Logger.hpp"
#include "src/Exceptions/Exception.hpp"
namespace Bloom::DebugServer::Gdb::CommandPackets
{
using ResponsePackets::ErrorResponsePacket;
using Exceptions::Exception;
void ContinueExecution::init() {
if (this->data.size() > 1) {
this->fromProgramCounter = static_cast<std::uint32_t>(
std::stoi(std::string(this->data.begin(), this->data.end()), nullptr, 16)
);
}
}
void ContinueExecution::handle(DebugSession& debugSession, TargetControllerConsole& targetControllerConsole) {
Logger::debug("Handling ContinueExecution packet");
try {
targetControllerConsole.continueTargetExecution(this->fromProgramCounter);
debugSession.waitingForBreak = true;
} catch (const Exception& exception) {
Logger::error("Failed to continue execution on target - " + exception.getMessage());
debugSession.connection.writePacket(ErrorResponsePacket());
}
}
}

View File

@@ -0,0 +1,36 @@
#pragma once
#include <cstdint>
#include <optional>
#include "CommandPacket.hpp"
namespace Bloom::DebugServer::Gdb::CommandPackets
{
/**
* The ContinueExecution class implements a structure for "c" packets. These packets instruct the server
* to continue execution on the target.
*
* See @link https://sourceware.org/gdb/onlinedocs/gdb/Packets.html#Packets for more on this.
*/
class ContinueExecution: public CommandPacket
{
public:
/**
* The "c" packet can contain an address which defines the point from which the execution should be resumed on
* the target.
*
* Although the packet *can* contain this address, it is not required, hence the optional.
*/
std::optional<std::uint32_t> fromProgramCounter;
explicit ContinueExecution(const std::vector<unsigned char>& rawPacket): CommandPacket(rawPacket) {
init();
}
void handle(DebugSession& debugSession, TargetControllerConsole& targetControllerConsole) override;
private:
void init();
};
}

View File

@@ -0,0 +1,29 @@
#include "InterruptExecution.hpp"
#include "src/DebugServer/Gdb/ResponsePackets/TargetStopped.hpp"
#include "src/DebugServer/Gdb/ResponsePackets/ErrorResponsePacket.hpp"
#include "src/DebugServer/Gdb/Signal.hpp"
#include "src/Logger/Logger.hpp"
#include "src/Exceptions/Exception.hpp"
namespace Bloom::DebugServer::Gdb::CommandPackets
{
using ResponsePackets::TargetStopped;
using ResponsePackets::ErrorResponsePacket;
using Exceptions::Exception;
void InterruptExecution::handle(DebugSession& debugSession, TargetControllerConsole& targetControllerConsole) {
Logger::debug("Handling InterruptExecution packet");
try {
targetControllerConsole.stopTargetExecution();
debugSession.connection.writePacket(TargetStopped(Signal::INTERRUPTED));
debugSession.waitingForBreak = false;
} catch (const Exception& exception) {
Logger::error("Failed to interrupt execution - " + exception.getMessage());
debugSession.connection.writePacket(ErrorResponsePacket());
}
}
}

View File

@@ -0,0 +1,22 @@
#pragma once
#include "CommandPacket.hpp"
namespace Bloom::DebugServer::Gdb::CommandPackets
{
/**
* The InterruptException class represents interrupt command packets. Upon receiving an interrupt packet, the
* server is expected to interrupt execution on the target.
*
* Technically, interrupts are not sent by the client in the form of a typical GDP RSP packet. Instead, they're
* just sent as a single byte from the client. We fake the packet on our end, to save us the headache of dealing
* with this inconsistency. We do this in Connection::readRawPackets().
*/
class InterruptExecution: public CommandPacket
{
public:
explicit InterruptExecution(const std::vector<unsigned char>& rawPacket): CommandPacket(rawPacket) {}
void handle(DebugSession& debugSession, TargetControllerConsole& targetControllerConsole) override;
};
}

View File

@@ -0,0 +1,96 @@
#include "ReadRegisters.hpp"
#include "src/DebugServer/Gdb/ResponsePackets/TargetStopped.hpp"
#include "src/DebugServer/Gdb/ResponsePackets/ErrorResponsePacket.hpp"
#include "src/Targets/TargetRegister.hpp"
#include "src/Logger/Logger.hpp"
#include "src/Exceptions/Exception.hpp"
namespace Bloom::DebugServer::Gdb::CommandPackets
{
using Targets::TargetRegister;
using Targets::TargetRegisterDescriptors;
using ResponsePackets::ResponsePacket;
using ResponsePackets::ErrorResponsePacket;
using Exceptions::Exception;
void ReadRegisters::init() {
if (this->data.size() >= 2 && this->data.front() == 'p') {
// This command packet is requesting a specific register
this->registerNumber = static_cast<size_t>(
std::stoi(std::string(this->data.begin() + 1, this->data.end()))
);
}
}
void ReadRegisters::handle(DebugSession& debugSession, TargetControllerConsole& targetControllerConsole) {
Logger::debug("Handling ReadRegisters packet");
try {
const auto& targetDescriptor = debugSession.targetDescriptor;
auto descriptors = TargetRegisterDescriptors();
if (this->registerNumber.has_value()) {
Logger::debug("Reading register number: " + std::to_string(this->registerNumber.value()));
descriptors.insert(
targetDescriptor.getTargetRegisterDescriptorFromNumber(this->registerNumber.value())
);
} else {
// Read all target registers mapped to a GDB register
for (const auto& registerNumber : targetDescriptor.getRegisterNumbers()) {
descriptors.insert(targetDescriptor.getTargetRegisterDescriptorFromNumber(registerNumber));
}
}
auto registerSet = targetControllerConsole.readRegisters(descriptors);
/*
* Sort each register by their respective GDB register number - this will leave us with a collection of
* registers in the order expected by the GDB client.
*/
std::sort(
registerSet.begin(),
registerSet.end(),
[this, &targetDescriptor] (const TargetRegister& registerA, const TargetRegister& registerB) {
return targetDescriptor.getRegisterNumberFromTargetRegisterDescriptor(registerA.descriptor) <
targetDescriptor.getRegisterNumberFromTargetRegisterDescriptor(registerB.descriptor);
}
);
/*
* Finally, reverse the register values (as they're all currently in MSB, but GDB expects them in LSB), ensure
* that each register value size matches the size in the associated GDB register descriptor, implode the
* values, convert to hexadecimal form and send to the GDB client.
*/
auto registers = std::vector<unsigned char>();
for (auto& reg : registerSet) {
std::reverse(reg.value.begin(), reg.value.end());
const auto gdbRegisterNumber = targetDescriptor.getRegisterNumberFromTargetRegisterDescriptor(
reg.descriptor
).value();
const auto& gdbRegisterDescriptor = targetDescriptor.getRegisterDescriptorFromNumber(gdbRegisterNumber);
if (reg.value.size() < gdbRegisterDescriptor.size) {
reg.value.insert(reg.value.end(), (gdbRegisterDescriptor.size - reg.value.size()), 0x00);
}
registers.insert(registers.end(), reg.value.begin(), reg.value.end());
}
auto responseRegisters = Packet::dataToHex(registers);
debugSession.connection.writePacket(
ResponsePacket(std::vector<unsigned char>(responseRegisters.begin(), responseRegisters.end()))
);
} catch (const Exception& exception) {
Logger::error("Failed to read general registers - " + exception.getMessage());
debugSession.connection.writePacket(ErrorResponsePacket());
}
}
}

View File

@@ -0,0 +1,37 @@
#pragma once
#include <optional>
#include "CommandPacket.hpp"
#include "src/DebugServer/Gdb/RegisterDescriptor.hpp"
namespace Bloom::DebugServer::Gdb::CommandPackets
{
/**
* The ReadRegisters class implements a structure for "g" and "p" command packets. In response to these
* packets, the server is expected to send register values for all registers (for "g" packets) or for a single
* register (for "p" packets).
*/
class ReadRegisters: public CommandPacket
{
public:
/**
* "p" packets include a register number to indicate which register is requested for reading. When this is set,
* the server is expected to respond with only the value of the requested register.
*
* If the register number is not supplied (as is the case with "g" packets), the server is expected to respond
* with values for all registers.
*/
std::optional<GdbRegisterNumberType> registerNumber;
explicit ReadRegisters(const std::vector<unsigned char>& rawPacket): CommandPacket(rawPacket) {
init();
};
void handle(DebugSession& debugSession, TargetControllerConsole& targetControllerConsole) override;
private:
void init();
};
}

View File

@@ -0,0 +1,64 @@
#include "RemoveBreakpoint.hpp"
#include <QtCore/QString>
#include "src/DebugServer/Gdb/ResponsePackets/OkResponsePacket.hpp"
#include "src/DebugServer/Gdb/ResponsePackets/ErrorResponsePacket.hpp"
#include "src/Targets/TargetBreakpoint.hpp"
#include "src/Logger/Logger.hpp"
#include "src/Exceptions/Exception.hpp"
namespace Bloom::DebugServer::Gdb::CommandPackets
{
using Targets::TargetBreakpoint;
using ResponsePackets::OkResponsePacket;
using ResponsePackets::ErrorResponsePacket;
using Exceptions::Exception;
void RemoveBreakpoint::init() {
if (data.size() < 6) {
throw Exception("Unexpected RemoveBreakpoint packet size");
}
// z0 = SW breakpoint, z1 = HW breakpoint
this->type = (data[1] == 0) ? BreakpointType::SOFTWARE_BREAKPOINT : (data[1] == 1) ?
BreakpointType::HARDWARE_BREAKPOINT : BreakpointType::UNKNOWN;
auto packetData = QString::fromLocal8Bit(
reinterpret_cast<const char*>(this->data.data() + 2),
static_cast<int>(this->data.size() - 2)
);
auto packetSegments = packetData.split(",");
if (packetSegments.size() < 3) {
throw Exception("Unexpected number of packet segments in RemoveBreakpoint packet");
}
bool conversionStatus = true;
this->address = packetSegments.at(1).toUInt(&conversionStatus, 16);
if (!conversionStatus) {
throw Exception("Failed to convert address hex value from RemoveBreakpoint packet.");
}
}
void RemoveBreakpoint::handle(DebugSession& debugSession, TargetControllerConsole& targetControllerConsole) {
Logger::debug("Removing breakpoint at address " + std::to_string(this->address));
try {
auto breakpoint = TargetBreakpoint();
breakpoint.address = this->address;
targetControllerConsole.removeBreakpoint(breakpoint);
debugSession.connection.writePacket(OkResponsePacket());
} catch (const Exception& exception) {
Logger::error("Failed to remove breakpoint on target - " + exception.getMessage());
debugSession.connection.writePacket(ErrorResponsePacket());
}
}
}

View File

@@ -0,0 +1,38 @@
#pragma once
#include <cstdint>
#include <string>
#include <set>
#include "CommandPacket.hpp"
#include "src/DebugServer/Gdb/BreakpointType.hpp"
namespace Bloom::DebugServer::Gdb::CommandPackets
{
/**
* The RemoveBreakpoint class implements the structure for "z" command packets. Upon receiving this command, the
* server is expected to remove a breakpoint at the specified address.
*/
class RemoveBreakpoint: public CommandPacket
{
public:
/**
* Breakpoint type (Software or Hardware)
*/
BreakpointType type = BreakpointType::UNKNOWN;
/**
* Address at which the breakpoint should be located.
*/
std::uint32_t address = 0;
explicit RemoveBreakpoint(const std::vector<unsigned char>& rawPacket): CommandPacket(rawPacket) {
this->init();
};
void handle(DebugSession& debugSession, TargetControllerConsole& targetControllerConsole) override;
private:
void init();
};
}

View File

@@ -0,0 +1,64 @@
#include "SetBreakpoint.hpp"
#include <QtCore/QString>
#include "src/DebugServer/Gdb/ResponsePackets/OkResponsePacket.hpp"
#include "src/DebugServer/Gdb/ResponsePackets/ErrorResponsePacket.hpp"
#include "src/Targets/TargetBreakpoint.hpp"
#include "src/Logger/Logger.hpp"
#include "src/Exceptions/Exception.hpp"
namespace Bloom::DebugServer::Gdb::CommandPackets
{
using Targets::TargetBreakpoint;
using ResponsePackets::OkResponsePacket;
using ResponsePackets::ErrorResponsePacket;
using Exceptions::Exception;
void SetBreakpoint::init() {
if (data.size() < 6) {
throw Exception("Unexpected SetBreakpoint packet size");
}
// Z0 = SW breakpoint, Z1 = HW breakpoint
this->type = (data[1] == 0) ? BreakpointType::SOFTWARE_BREAKPOINT : (data[1] == 1) ?
BreakpointType::HARDWARE_BREAKPOINT : BreakpointType::UNKNOWN;
auto packetData = QString::fromLocal8Bit(
reinterpret_cast<const char*>(this->data.data() + 2),
static_cast<int>(this->data.size() - 2)
);
auto packetSegments = packetData.split(",");
if (packetSegments.size() < 3) {
throw Exception("Unexpected number of packet segments in SetBreakpoint packet");
}
bool conversionStatus = true;
this->address = packetSegments.at(1).toUInt(&conversionStatus, 16);
if (!conversionStatus) {
throw Exception("Failed to convert address hex value from SetBreakpoint packet.");
}
}
void SetBreakpoint::handle(DebugSession& debugSession, TargetControllerConsole& targetControllerConsole) {
Logger::debug("Handling SetBreakpoint packet");
try {
auto breakpoint = TargetBreakpoint();
breakpoint.address = this->address;
targetControllerConsole.setBreakpoint(breakpoint);
debugSession.connection.writePacket(OkResponsePacket());
} catch (const Exception& exception) {
Logger::error("Failed to set breakpoint on target - " + exception.getMessage());
debugSession.connection.writePacket(ErrorResponsePacket());
}
}
}

View File

@@ -0,0 +1,38 @@
#pragma once
#include <cstdint>
#include <string>
#include <set>
#include "CommandPacket.hpp"
#include "src/DebugServer/Gdb/BreakpointType.hpp"
namespace Bloom::DebugServer::Gdb::CommandPackets
{
/**
* The SetBreakpoint class implements the structure for "Z" command packets. Upon receiving this command, the
* server is expected to set a breakpoint at the specified address.
*/
class SetBreakpoint: public CommandPacket
{
public:
/**
* Breakpoint type (Software or Hardware)
*/
BreakpointType type = BreakpointType::UNKNOWN;
/**
* Address at which the breakpoint should be located.
*/
std::uint32_t address = 0;
explicit SetBreakpoint(const std::vector<unsigned char>& rawPacket): CommandPacket(rawPacket) {
this->init();
};
void handle(DebugSession& debugSession, TargetControllerConsole& targetControllerConsole) override;
private:
void init();
};
}

View File

@@ -0,0 +1,34 @@
#include "StepExecution.hpp"
#include "src/DebugServer/Gdb/ResponsePackets/ErrorResponsePacket.hpp"
#include "src/Logger/Logger.hpp"
#include "src/Exceptions/Exception.hpp"
namespace Bloom::DebugServer::Gdb::CommandPackets
{
using ResponsePackets::ErrorResponsePacket;
using Exceptions::Exception;
void StepExecution::init() {
if (this->data.size() > 1) {
this->fromProgramCounter = static_cast<std::uint32_t>(
std::stoi(std::string(this->data.begin(), this->data.end()), nullptr, 16)
);
}
}
void StepExecution::handle(DebugSession& debugSession, TargetControllerConsole& targetControllerConsole) {
Logger::debug("Handling StepExecution packet");
try {
targetControllerConsole.stepTargetExecution(this->fromProgramCounter);
debugSession.waitingForBreak = true;
} catch (const Exception& exception) {
Logger::error("Failed to step execution on target - " + exception.getMessage());
debugSession.connection.writePacket(ErrorResponsePacket());
}
}
}

View File

@@ -0,0 +1,31 @@
#pragma once
#include <cstdint>
#include <optional>
#include "CommandPacket.hpp"
namespace Bloom::DebugServer::Gdb::CommandPackets
{
/**
* The StepExecution class implements the structure for "s" command packets. Upon receiving this command, the
* server is expected to step execution on the target.
*/
class StepExecution: public CommandPacket
{
public:
/**
* The address from which to begin the step.
*/
std::optional<std::size_t> fromProgramCounter;
explicit StepExecution(const std::vector<unsigned char>& rawPacket): CommandPacket(rawPacket) {
init();
};
void handle(DebugSession& debugSession, TargetControllerConsole& targetControllerConsole) override;
private:
void init();
};
}

View File

@@ -0,0 +1,72 @@
#include "SupportedFeaturesQuery.hpp"
#include <QtCore/QString>
#include "src/DebugServer/Gdb/Feature.hpp"
#include "src/DebugServer/Gdb/ResponsePackets/SupportedFeaturesResponse.hpp"
#include "src/DebugServer/Gdb/ResponsePackets/ErrorResponsePacket.hpp"
#include "src/Logger/Logger.hpp"
#include "src/Exceptions/Exception.hpp"
#include "src/DebugServer/Gdb/Exceptions/ClientNotSupported.hpp"
namespace Bloom::DebugServer::Gdb::CommandPackets
{
using ResponsePackets::SupportedFeaturesResponse;
using ResponsePackets::ErrorResponsePacket;
using Bloom::Exceptions::Exception;
using Gdb::Exceptions::ClientNotSupported;
void SupportedFeaturesQuery::init() {
/*
* For qSupported packets, supported and unsupported GDB features are reported in the packet
* data, where each GDB feature is separated by a semicolon.
*/
// The "qSupported:" prefix occupies 11 bytes
if (data.size() > 11) {
auto packetData = QString::fromLocal8Bit(
reinterpret_cast<const char*>(this->data.data() + 11),
static_cast<int>(this->data.size() - 11)
);
auto featureList = packetData.split(";");
auto gdbFeatureMapping = getGdbFeatureToNameMapping();
for (int i = 0; i < featureList.size(); i++) {
auto featureString = featureList.at(i);
// We only care about supported features. Supported features will precede a '+' character.
if (featureString[featureString.size() - 1] == '+') {
featureString.remove('+');
auto feature = gdbFeatureMapping.valueAt(featureString.toStdString());
if (feature.has_value()) {
this->supportedFeatures.insert(static_cast<decltype(feature)::value_type>(feature.value()));
}
}
}
}
}
void SupportedFeaturesQuery::handle(DebugSession& debugSession, TargetControllerConsole& targetControllerConsole) {
Logger::debug("Handling QuerySupport packet");
if (!this->isFeatureSupported(Feature::HARDWARE_BREAKPOINTS)
&& !this->isFeatureSupported(Feature::SOFTWARE_BREAKPOINTS)
) {
// All GDB clients are expected to support breakpoints!
throw ClientNotSupported("GDB client does not support HW or SW breakpoints");
}
// Respond with a SupportedFeaturesResponse packet, listing all supported GDB features by Bloom
auto response = SupportedFeaturesResponse({
{Feature::SOFTWARE_BREAKPOINTS, std::nullopt},
{Feature::PACKET_SIZE, std::to_string(debugSession.connection.getMaxPacketSize())},
});
debugSession.connection.writePacket(response);
}
}

View File

@@ -0,0 +1,44 @@
#pragma once
#include <string>
#include <set>
#include "CommandPacket.hpp"
#include "../Feature.hpp"
namespace Bloom::DebugServer::Gdb::CommandPackets
{
/**
* The SupportedFeaturesQuery command packet is a query from the GDB client, requesting a list of GDB features
* supported by the GDB server. The body of this packet also contains a list GDB features that are supported or
* unsupported by the GDB client.
*
* The command packet is identified by its 'qSupported' prefix in the command packet data. Following the prefix is
* a list of GDB features that are supported/unsupported by the client. For more info on this command
* packet, see the GDP RSP documentation.
*
* Responses to this command packet should take the form of a ResponsePackets::SupportedFeaturesResponse.
*/
class SupportedFeaturesQuery: public CommandPacket
{
public:
explicit SupportedFeaturesQuery(const std::vector<unsigned char>& rawPacket): CommandPacket(rawPacket) {
this->init();
};
[[nodiscard]] bool isFeatureSupported(const Feature& feature) const {
return this->supportedFeatures.find(feature) != this->supportedFeatures.end();
}
[[nodiscard]] const std::set<Feature>& getSupportedFeatures() const {
return this->supportedFeatures;
}
void handle(DebugSession& debugSession, TargetControllerConsole& targetControllerConsole) override;
private:
std::set<Feature> supportedFeatures;
void init();
};
}

View File

@@ -0,0 +1,77 @@
#include "WriteRegister.hpp"
#include "src/DebugServer/Gdb/ResponsePackets/TargetStopped.hpp"
#include "src/DebugServer/Gdb/ResponsePackets/OkResponsePacket.hpp"
#include "src/DebugServer/Gdb/ResponsePackets/ErrorResponsePacket.hpp"
#include "src/Targets/TargetRegister.hpp"
#include "src/Logger/Logger.hpp"
#include "src/Exceptions/Exception.hpp"
namespace Bloom::DebugServer::Gdb::CommandPackets
{
using Targets::TargetRegister;
using Targets::TargetRegisterDescriptors;
using ResponsePackets::ResponsePacket;
using ResponsePackets::OkResponsePacket;
using ResponsePackets::ErrorResponsePacket;
using Exceptions::Exception;
void WriteRegister::init() {
// The P packet updates a single register
auto packet = std::string(this->data.begin(), this->data.end());
if (packet.size() < 4) {
throw Exception("Invalid P command packet - insufficient data in packet.");
}
if (packet.find('=') == std::string::npos) {
throw Exception("Invalid P command packet - unexpected format");
}
auto packetSegments = QString::fromStdString(packet).split("=");
this->registerNumber = static_cast<int>(packetSegments.front().mid(1).toUInt(nullptr, 16));
this->registerValue = Packet::hexToData(packetSegments.back().toStdString());
std::reverse(this->registerValue.begin(), this->registerValue.end());
}
void WriteRegister::handle(DebugSession& debugSession, TargetControllerConsole& targetControllerConsole) {
Logger::debug("Handling WriteRegister packet");
try {
auto targetRegisterDescriptor = debugSession.targetDescriptor.getTargetRegisterDescriptorFromNumber(this->registerNumber);
const auto valueSize = this->registerValue.size();
if (valueSize > 0 && valueSize > targetRegisterDescriptor.size) {
// Attempt to trim the higher zero-value bytes from the register value, until we reach the correct size.
for (auto i = this->registerValue.size() - 1; i >= targetRegisterDescriptor.size; i--) {
if (this->registerValue.at(i) != 0x00) {
// If we reach a non-zero byte, we cannot trim anymore without changing the data
break;
}
this->registerValue.erase(this->registerValue.begin() + i);
}
if (this->registerValue.size() > targetRegisterDescriptor.size) {
const auto& gdbRegisterDescriptor = debugSession.targetDescriptor.getRegisterDescriptorFromNumber(this->registerNumber);
throw Exception("Cannot set value for " + gdbRegisterDescriptor.name
+ " - value size exceeds register size."
);
}
}
targetControllerConsole.writeRegisters({
TargetRegister(targetRegisterDescriptor, this->registerValue)
});
debugSession.connection.writePacket(OkResponsePacket());
} catch (const Exception& exception) {
Logger::error("Failed to write registers - " + exception.getMessage());
debugSession.connection.writePacket(ErrorResponsePacket());
}
}
}

View File

@@ -0,0 +1,29 @@
#pragma once
#include <optional>
#include "CommandPacket.hpp"
#include "src/Targets/TargetRegister.hpp"
namespace Bloom::DebugServer::Gdb::CommandPackets
{
/**
* The WriteRegisters class implements the structure for "P" packets. Upon receiving this packet,
* server is expected to update a register value to the target.
*/
class WriteRegister: public CommandPacket
{
public:
int registerNumber = 0;
std::vector<unsigned char> registerValue;
explicit WriteRegister(const std::vector<unsigned char>& rawPacket): CommandPacket(rawPacket) {
init();
};
void handle(DebugSession& debugSession, TargetControllerConsole& targetControllerConsole) override;
private:
void init();
};
}

View File

@@ -0,0 +1,269 @@
#include "Connection.hpp"
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <cerrno>
#include <fcntl.h>
#include "Exceptions/ClientDisconnected.hpp"
#include "Exceptions/ClientCommunicationError.hpp"
#include "src/Exceptions/Exception.hpp"
#include "src/Exceptions/DebugServerInterrupted.hpp"
#include "src/Logger/Logger.hpp"
namespace Bloom::DebugServer::Gdb
{
using namespace Exceptions;
using namespace Bloom::Exceptions;
using ResponsePackets::ResponsePacket;
Connection::Connection(int serverSocketFileDescriptor, EventNotifier& interruptEventNotifier)
: interruptEventNotifier(interruptEventNotifier)
{
this->accept(serverSocketFileDescriptor);
::fcntl(
this->socketFileDescriptor.value(),
F_SETFL,
::fcntl(this->socketFileDescriptor.value(), F_GETFL, 0) | O_NONBLOCK
);
this->epollInstance.addEntry(
this->socketFileDescriptor.value(),
static_cast<std::uint16_t>(EpollEvent::READ_READY)
);
this->enableReadInterrupts();
}
Connection::~Connection() {
this->close();
}
std::string Connection::getIpAddress() const {
std::array<char, INET_ADDRSTRLEN> ipAddress = {};
if (::inet_ntop(AF_INET, &(socketAddress.sin_addr), ipAddress.data(), INET_ADDRSTRLEN) == nullptr) {
throw Exception("Failed to convert client IP address to text form.");
}
return std::string(ipAddress.data());
}
std::vector<RawPacketType> Connection::readRawPackets() {
std::vector<RawPacketType> output;
const auto bytes = this->read();
std::size_t bufferSize = bytes.size();
for (std::size_t byteIndex = 0; byteIndex < bufferSize; byteIndex++) {
auto byte = bytes[byteIndex];
if (byte == 0x03) {
/*
* This is an interrupt packet - it doesn't carry any of the usual packet frame bytes, so we'll just
* add them here, in order to keep things consistent.
*
* Because we're effectively faking the packet frame, we can use any value for the checksum.
*/
output.push_back({'$', byte, '#', 'F', 'F'});
} else if (byte == '$') {
// Beginning of packet
RawPacketType rawPacket;
rawPacket.push_back('$');
auto packetIndex = byteIndex;
bool validPacket = false;
bool isByteEscaped = false;
for (packetIndex++; packetIndex < bufferSize; packetIndex++) {
byte = bytes[packetIndex];
if (byte == '}' && !isByteEscaped) {
isByteEscaped = true;
continue;
}
if (!isByteEscaped) {
if (byte == '$') {
// Unexpected end of packet
validPacket = false;
break;
}
if (byte == '#') {
// End of packet data
if ((bufferSize - 1) < (packetIndex + 2)) {
// There should be at least two more bytes in the buffer, for the checksum.
break;
}
rawPacket.push_back(byte);
// Add the checksum bytes and break the loop
rawPacket.push_back(bytes[++packetIndex]);
rawPacket.push_back(bytes[++packetIndex]);
validPacket = true;
break;
}
} else {
// Escaped bytes are XOR'd with a 0x20 mask.
byte ^= 0x20;
isByteEscaped = false;
}
rawPacket.push_back(byte);
}
if (validPacket) {
// Acknowledge receipt
this->write({'+'});
output.push_back(rawPacket);
byteIndex = packetIndex;
}
}
}
return output;
}
void Connection::writePacket(const ResponsePacket& packet) {
// Write the packet repeatedly until the GDB client acknowledges it.
int attempts = 0;
auto rawPacket = packet.toRawPacket();
do {
if (attempts > 10) {
throw ClientCommunicationError("Failed to write GDB response packet - client failed to "
"acknowledge receipt - retry limit reached");
}
this->write(rawPacket);
attempts++;
} while (this->readSingleByte(false).value_or(0) != '+');
}
void Connection::accept(int serverSocketFileDescriptor) {
int socketAddressLength = sizeof(this->socketAddress);
const auto socketFileDescriptor = ::accept(
serverSocketFileDescriptor,
reinterpret_cast<sockaddr*>(&(this->socketAddress)),
reinterpret_cast<socklen_t*>(&socketAddressLength)
);
if (socketFileDescriptor < 0) {
throw Exception("Failed to accept GDB Remote Serial Protocol connection");
}
this->socketFileDescriptor = socketFileDescriptor;
}
void Connection::close() noexcept {
if (this->socketFileDescriptor.value_or(-1) >= 0) {
::close(this->socketFileDescriptor.value());
this->socketFileDescriptor = std::nullopt;
}
}
std::vector<unsigned char> Connection::read(
size_t bytes,
bool interruptible,
std::optional<std::chrono::milliseconds> timeout
) {
auto output = std::vector<unsigned char>();
constexpr size_t bufferSize = 1024;
std::array<unsigned char, bufferSize> buffer = {};
ssize_t bytesRead = 0;
if (interruptible) {
if (this->readInterruptEnabled != interruptible) {
this->enableReadInterrupts();
} else {
// Clear any previous interrupts that are still hanging around
this->interruptEventNotifier.clear();
}
}
if (this->readInterruptEnabled != interruptible && !interruptible) {
this->disableReadInterrupts();
}
const auto eventFileDescriptor = this->epollInstance.waitForEvent(timeout);
if (
!eventFileDescriptor.has_value()
|| eventFileDescriptor.value() == this->interruptEventNotifier.getFileDescriptor()
) {
// Interrupted
this->interruptEventNotifier.clear();
throw DebugServerInterrupted();
}
size_t bytesToRead = (bytes > bufferSize || bytes == 0) ? bufferSize : bytes;
while (
bytesToRead > 0
&& (bytesRead = ::read(this->socketFileDescriptor.value(), buffer.data(), bytesToRead)) > 0
) {
output.insert(output.end(), buffer.begin(), buffer.begin() + bytesRead);
if (bytesRead < bytesToRead) {
// No more data available
break;
}
bytesToRead = ((bytes - output.size()) > bufferSize || bytes == 0) ? bufferSize : (bytes - output.size());
}
if (output.empty()) {
// EOF means the client has disconnected
throw ClientDisconnected();
}
return output;
}
std::optional<unsigned char> Connection::readSingleByte(bool interruptible) {
auto bytes = this->read(1, interruptible, std::chrono::milliseconds(300));
if (!bytes.empty()) {
return bytes.front();
}
return std::nullopt;
}
void Connection::write(const std::vector<unsigned char>& buffer) {
Logger::debug("Writing packet: " + std::string(buffer.begin(), buffer.end()));
if (::write(this->socketFileDescriptor.value(), buffer.data(), buffer.size()) == -1) {
if (errno == EPIPE || errno == ECONNRESET) {
// Connection was closed
throw ClientDisconnected();
}
throw ClientCommunicationError("Failed to write " + std::to_string(buffer.size())
+ " bytes to GDP client socket - error no: "
+ std::to_string(errno));
}
}
void Connection::disableReadInterrupts() {
this->epollInstance.removeEntry(this->interruptEventNotifier.getFileDescriptor());
this->readInterruptEnabled = false;
}
void Connection::enableReadInterrupts() {
this->epollInstance.addEntry(
this->interruptEventNotifier.getFileDescriptor(),
static_cast<std::uint16_t>(EpollEvent::READ_READY)
);
this->readInterruptEnabled = true;
}
}

View File

@@ -0,0 +1,159 @@
#pragma once
#include <sys/socket.h>
#include <netinet/in.h>
#include <cstdint>
#include <utility>
#include <vector>
#include <queue>
#include <array>
#include <chrono>
#include "src/Helpers/EventNotifier.hpp"
#include "src/Helpers/EpollInstance.hpp"
#include "src/DebugServer/Gdb/Packet.hpp"
#include "src/DebugServer/Gdb/ResponsePackets/ResponsePacket.hpp"
namespace Bloom::DebugServer::Gdb
{
/**
* The Connection class represents an active connection between the GDB RSP server and client.
*/
class Connection
{
public:
explicit Connection(int serverSocketFileDescriptor, EventNotifier& interruptEventNotifier);
Connection() = delete;
Connection(const Connection&) = delete;
Connection& operator = (Connection&) = delete;
/*
* TODO: Implement this. For now, use the move constructor.
*/
Connection& operator = (Connection&&) = delete;
Connection(Connection&& other) noexcept
: interruptEventNotifier(other.interruptEventNotifier)
, socketFileDescriptor(other.socketFileDescriptor)
, epollInstance(std::move(other.epollInstance))
, readInterruptEnabled(other.readInterruptEnabled)
{
other.socketFileDescriptor = std::nullopt;
}
~Connection();
/**
* Obtains the human readable IP address of the connected client.
*
* @return
*/
[[nodiscard]] std::string getIpAddress() const;
/**
* Waits for incoming data from the client and returns the raw GDB packets.
*
* @return
*/
std::vector<RawPacketType> readRawPackets();
/**
* Sends a response packet to the client.
*
* @param packet
*/
void writePacket(const ResponsePackets::ResponsePacket& packet);
[[nodiscard]] int getMaxPacketSize() const {
return this->maxPacketSize;
}
private:
std::optional<int> socketFileDescriptor;
struct sockaddr_in socketAddress = {};
int maxPacketSize = 1024;
/**
* The interruptEventNotifier (instance of EventNotifier) allows us to interrupt blocking I/O calls on this
* connection's socket. Under the hood, the EventNotifier class is just an RAII wrapper for a Linux eventfd
* object.
*
* The file descriptors of the eventfd object and the socket are both added to an EpollInstance (which is just
* an RAII wrapper for a Linux epoll instance). The EpollInstance object is then used to wait for events on
* either of the two file descriptors. See any of the Connection I/O functions (e.g Connection::read()) for
* more on this.
*
* See the EventNotifier and EpollInstance classes for more.
*/
EventNotifier& interruptEventNotifier;
EpollInstance epollInstance = EpollInstance();
bool readInterruptEnabled = false;
/**
* Accepts a connection on serverSocketFileDescriptor.
*
* @param serverSocketFileDescriptor
*/
void accept(int serverSocketFileDescriptor);
/**
* Closes the connection with the client.
*/
void close() noexcept;
/**
* Reads data from the client into a raw buffer.
*
* @param bytes
* Number of bytes to read.
*
* @param interruptible
* If this flag is set to false, no other component within Bloom will be able to gracefully interrupt
* the read (via means of this->interruptEventNotifier). This flag has no effect if this->readInterruptEnabled
* is false.
*
* @param timeout
* The timeout in milliseconds. If not supplied, no timeout will be applied.
*
* @return
*/
std::vector<unsigned char> read(
std::size_t bytes = 0,
bool interruptible = true,
std::optional<std::chrono::milliseconds> timeout = std::nullopt
);
/**
* Does the same as Connection::read(), but only reads a single byte.
*
* @param interruptible
* See Connection::read().
*
* @return
*/
std::optional<unsigned char> readSingleByte(bool interruptible = true);
/**
* Writes data from a raw buffer to the client connection.
*
* @param buffer
*/
void write(const std::vector<unsigned char>& buffer);
/**
* Removes this->interruptEventNotifier's file descriptor from the EpollInstance (this->epollInstance),
* preventing subsequent I/O operations on this->socketFileDescriptor from being interrupted.
*/
void disableReadInterrupts();
/**
* Inserts this->interruptEventNotifier's file descriptor into the EpollInstance (this->epollInstance),
* allowing for subsequent I/O operations on this->socketFileDescriptor to be interrupted.
*/
void enableReadInterrupts();
};
}

View File

@@ -0,0 +1,15 @@
#include "DebugSession.hpp"
#include "src/Logger/Logger.hpp"
namespace Bloom::DebugServer::Gdb
{
DebugSession::DebugSession(Connection&& connection, const TargetDescriptor& targetDescriptor)
: connection(std::move(connection))
, targetDescriptor(targetDescriptor)
{}
void DebugSession::terminate() {
}
}

View File

@@ -0,0 +1,27 @@
#pragma once
#include <cstdint>
#include "TargetDescriptor.hpp"
#include "Connection.hpp"
namespace Bloom::DebugServer::Gdb
{
class DebugSession
{
public:
Connection connection;
const TargetDescriptor& targetDescriptor;
/**
* When the GDB client is waiting for the target to halt, this is set to true so we know when to notify the
* client.
*/
bool waitingForBreak = false;
DebugSession(Connection&& connection, const TargetDescriptor& targetDescriptor);
void terminate();
};
}

View File

@@ -0,0 +1,26 @@
#pragma once
#include "src/Exceptions/Exception.hpp"
namespace Bloom::DebugServer::Gdb::Exceptions
{
/**
* In the event that communication between the GDB RSP client and Bloom fails, a ClientCommunicationFailure
* exception should be thrown. The GDB debug server handles this by severing the connection.
*
* See GdbRspDebugServer::serve() for handling code.
*/
class ClientCommunicationError: public Bloom::Exceptions::Exception
{
public:
explicit ClientCommunicationError(const std::string& message): Bloom::Exceptions::Exception(message) {
this->message = message;
}
explicit ClientCommunicationError(const char* message): Bloom::Exceptions::Exception(message) {
this->message = std::string(message);
}
explicit ClientCommunicationError() = default;
};
}

View File

@@ -0,0 +1,27 @@
#pragma once
#include "src/Exceptions/Exception.hpp"
namespace Bloom::DebugServer::Gdb::Exceptions
{
/**
* When a GDB RSP client unexpectedly drops the connection in the middle of an IO operation, a ClientDisconnected
* exception should be thrown. The GDB debug server handles this by clearing the connection and waiting for a new
* one.
*
* See GdbRspDebugServer::serve() for handling code.
*/
class ClientDisconnected: public Bloom::Exceptions::Exception
{
public:
explicit ClientDisconnected(const std::string& message): Bloom::Exceptions::Exception(message) {
this->message = message;
}
explicit ClientDisconnected(const char* message): Bloom::Exceptions::Exception(message) {
this->message = std::string(message);
}
explicit ClientDisconnected() = default;
};
}

View File

@@ -0,0 +1,26 @@
#pragma once
#include "src/Exceptions/Exception.hpp"
namespace Bloom::DebugServer::Gdb::Exceptions
{
/**
* In the event that the GDB debug server determines that the connected client cannot be served,
* the ClientNotSupported exception should be thrown.
*
* See GdbRspDebugServer::serve() for handling code.
*/
class ClientNotSupported: public Bloom::Exceptions::Exception
{
public:
explicit ClientNotSupported(const std::string& message): Bloom::Exceptions::Exception(message) {
this->message = message;
}
explicit ClientNotSupported(const char* message): Bloom::Exceptions::Exception(message) {
this->message = std::string(message);
}
explicit ClientNotSupported() = default;
};
}

View File

@@ -0,0 +1,26 @@
#pragma once
#include "src/Exceptions/Exception.hpp"
namespace Bloom::DebugServer::Gdb::Exceptions
{
/**
* The GDB server may abort a debug session with the client, if an internal error occurs. One circumstance where
* this can happen is when the TargetController is not able to service the debug session for whatever reason.
*
* See GdbRspDebugServer::serve() for handling code.
*/
class DebugSessionAborted: public Bloom::Exceptions::Exception
{
public:
explicit DebugSessionAborted(const std::string& message): Bloom::Exceptions::Exception(message) {
this->message = message;
}
explicit DebugSessionAborted(const char* message): Bloom::Exceptions::Exception(message) {
this->message = std::string(message);
}
explicit DebugSessionAborted() = default;
};
}

View File

@@ -0,0 +1,23 @@
#pragma once
#include "src/Helpers/BiMap.hpp"
namespace Bloom::DebugServer::Gdb
{
enum class Feature: int
{
SOFTWARE_BREAKPOINTS,
HARDWARE_BREAKPOINTS,
PACKET_SIZE,
MEMORY_MAP_READ,
};
static inline BiMap<Feature, std::string> getGdbFeatureToNameMapping() {
return BiMap<Feature, std::string>{
{Feature::HARDWARE_BREAKPOINTS, "hwbreak"},
{Feature::SOFTWARE_BREAKPOINTS, "swbreak"},
{Feature::PACKET_SIZE, "PacketSize"},
{Feature::MEMORY_MAP_READ, "qXfer:memory-map:read"},
};
}
}

View File

@@ -0,0 +1,19 @@
#include "GdbDebugServerConfig.hpp"
namespace Bloom::DebugServer::Gdb
{
GdbDebugServerConfig::GdbDebugServerConfig(const DebugServerConfig& debugServerConfig)
: DebugServerConfig(debugServerConfig)
{
if (debugServerConfig.jsonObject.contains("ipAddress")) {
this->listeningAddress = debugServerConfig.jsonObject.value("ipAddress").toString().toStdString();
}
if (debugServerConfig.jsonObject.contains("port")) {
const auto portValue = debugServerConfig.jsonObject.value("port");
this->listeningPortNumber = static_cast<std::uint16_t>(
portValue.isString() ? portValue.toString().toInt(nullptr, 10) : portValue.toInt()
);
}
}
}

View File

@@ -0,0 +1,29 @@
#pragma once
#include "src/ProjectConfig.hpp"
namespace Bloom::DebugServer::Gdb
{
/**
* Extending the generic DebugServerConfig struct to accommodate GDB debug server configuration parameters.
*/
class GdbDebugServerConfig: public DebugServerConfig
{
public:
/**
* The port number for the GDB server to listen on.
*
* This parameter is optional. If not specified, the default value set here will be used.
*/
std::uint16_t listeningPortNumber = 1442;
/**
* The address for the GDB server to listen on.
*
* This parameter is optional. If not specified, the default value set here will be used.
*/
std::string listeningAddress = "127.0.0.1";
explicit GdbDebugServerConfig(const DebugServerConfig& debugServerConfig);
};
}

View File

@@ -0,0 +1,292 @@
#include "GdbRspDebugServer.hpp"
#include <sys/socket.h>
#include <sys/epoll.h>
#include "src/Logger/Logger.hpp"
#include "Exceptions/ClientDisconnected.hpp"
#include "Exceptions/ClientNotSupported.hpp"
#include "Exceptions/ClientCommunicationError.hpp"
#include "Exceptions/DebugSessionAborted.hpp"
#include "src/Exceptions/Exception.hpp"
#include "src/Exceptions/InvalidConfig.hpp"
#include "src/Exceptions/DebugServerInterrupted.hpp"
// Command packets
#include "CommandPackets/CommandPacket.hpp"
#include "CommandPackets/SupportedFeaturesQuery.hpp"
#include "CommandPackets/InterruptExecution.hpp"
#include "CommandPackets/ContinueExecution.hpp"
#include "CommandPackets/StepExecution.hpp"
#include "CommandPackets/ReadRegisters.hpp"
#include "CommandPackets/WriteRegister.hpp"
#include "CommandPackets/SetBreakpoint.hpp"
#include "CommandPackets/RemoveBreakpoint.hpp"
// Response packets
#include "ResponsePackets/TargetStopped.hpp"
namespace Bloom::DebugServer::Gdb
{
using namespace Exceptions;
using namespace Bloom::Exceptions;
using CommandPackets::CommandPacket;
GdbRspDebugServer::GdbRspDebugServer(
const DebugServerConfig& debugServerConfig,
EventListener& eventListener
)
: debugServerConfig(GdbDebugServerConfig(debugServerConfig))
, eventListener(eventListener)
, interruptEventNotifier(eventListener.getInterruptEventNotifier())
{
assert(this->interruptEventNotifier != nullptr && this->interruptEventNotifier->isInitialised());
}
void GdbRspDebugServer::init() {
this->socketAddress.sin_family = AF_INET;
this->socketAddress.sin_port = htons(this->debugServerConfig->listeningPortNumber);
if (::inet_pton(
AF_INET,
this->debugServerConfig->listeningAddress.c_str(),
&(this->socketAddress.sin_addr)
) == 0
) {
// Invalid IP address
throw InvalidConfig(
"Invalid IP address provided in config file: (\"" + this->debugServerConfig->listeningAddress
+ "\")"
);
}
int socketFileDescriptor = 0;
if ((socketFileDescriptor = ::socket(AF_INET, SOCK_STREAM, 0)) == 0) {
throw Exception("Failed to create socket file descriptor.");
}
if (::setsockopt(
socketFileDescriptor,
SOL_SOCKET,
SO_REUSEADDR,
&(this->enableReuseAddressSocketOption),
sizeof(this->enableReuseAddressSocketOption)
) < 0
) {
Logger::error("Failed to set socket SO_REUSEADDR option.");
}
if (::bind(
socketFileDescriptor,
reinterpret_cast<const sockaddr*>(&(this->socketAddress)),
sizeof(this->socketAddress)
) < 0
) {
throw Exception("Failed to bind address. The selected port number ("
+ std::to_string(this->debugServerConfig->listeningPortNumber) + ") may be in use.");
}
this->serverSocketFileDescriptor = socketFileDescriptor;
this->epollInstance.addEntry(
this->serverSocketFileDescriptor,
static_cast<std::uint16_t>(EpollEvent::READ_READY)
);
this->epollInstance.addEntry(
this->interruptEventNotifier->getFileDescriptor(),
static_cast<std::uint16_t>(EpollEvent::READ_READY)
);
Logger::info("GDB RSP address: " + this->debugServerConfig->listeningAddress);
Logger::info("GDB RSP port: " + std::to_string(this->debugServerConfig->listeningPortNumber));
this->eventListener.registerCallbackForEventType<Events::TargetControllerStateReported>(
std::bind(&GdbRspDebugServer::onTargetControllerStateReported, this, std::placeholders::_1)
);
this->eventListener.registerCallbackForEventType<Events::TargetExecutionStopped>(
std::bind(&GdbRspDebugServer::onTargetExecutionStopped, this, std::placeholders::_1)
);
}
void GdbRspDebugServer::close() {
this->terminateActiveDebugSession();
if (this->serverSocketFileDescriptor > 0) {
::close(this->serverSocketFileDescriptor);
}
}
void GdbRspDebugServer::run() {
try {
if (!this->activeDebugSession.has_value()) {
Logger::info("Waiting for GDB RSP connection");
auto connection = this->waitForConnection();
if (!connection.has_value()) {
// Likely an interrupt
return;
}
Logger::info("Accepted GDP RSP connection from " + connection->getIpAddress());
this->activeDebugSession.emplace(
DebugSession(std::move(connection.value()), this->getGdbTargetDescriptor())
);
EventManager::triggerEvent(std::make_shared<Events::DebugSessionStarted>());
/*
* Before proceeding with a new debug session, we must ensure that the TargetController is able to
* service it.
*/
if (!this->targetControllerConsole.isTargetControllerInService()) {
this->terminateActiveDebugSession();
throw DebugSessionAborted("TargetController not in service");
}
}
auto commandPacket = this->waitForCommandPacket();
if (commandPacket == nullptr) {
// Likely an interrupt
return;
}
commandPacket->handle(this->activeDebugSession.value(), this->targetControllerConsole);
} catch (const ClientDisconnected&) {
Logger::info("GDB RSP client disconnected");
this->terminateActiveDebugSession();
return;
} catch (const ClientCommunicationError& exception) {
Logger::error(
"GDB RSP client communication error - " + exception.getMessage() + " - closing connection"
);
this->terminateActiveDebugSession();
return;
} catch (const ClientNotSupported& exception) {
Logger::error("Invalid GDB RSP client - " + exception.getMessage() + " - closing connection");
this->terminateActiveDebugSession();
return;
} catch (const DebugSessionAborted& exception) {
Logger::warning("GDB debug session aborted - " + exception.getMessage());
this->terminateActiveDebugSession();
return;
} catch (const DebugServerInterrupted&) {
// Server was interrupted
Logger::debug("GDB RSP interrupted");
return;
}
}
std::optional<Connection> GdbRspDebugServer::waitForConnection() {
if (::listen(this->serverSocketFileDescriptor, 3) != 0) {
throw Exception("Failed to listen on server socket");
}
const auto eventFileDescriptor = this->epollInstance.waitForEvent();
if (
!eventFileDescriptor.has_value()
|| eventFileDescriptor.value() == this->interruptEventNotifier->getFileDescriptor()
) {
this->interruptEventNotifier->clear();
return std::nullopt;
}
return std::make_optional<Connection>(this->serverSocketFileDescriptor, *(this->interruptEventNotifier));
}
std::unique_ptr<CommandPacket> GdbRspDebugServer::waitForCommandPacket() {
const auto rawPackets = this->activeDebugSession->connection.readRawPackets();
if (rawPackets.empty()) {
// The wait was interrupted
return nullptr;
}
// We only process the last packet - any others will probably be duplicates from an impatient client.
return this->resolveCommandPacket(rawPackets.back());
}
std::unique_ptr<CommandPacket> GdbRspDebugServer::resolveCommandPacket(const RawPacketType& rawPacket) {
if (rawPacket.size() == 5 && rawPacket[1] == 0x03) {
// This is an interrupt request - create a fake packet for it
return std::make_unique<CommandPackets::InterruptExecution>(rawPacket);
}
const auto rawPacketString = std::string(rawPacket.begin(), rawPacket.end());
if (rawPacketString.size() >= 2) {
/*
* First byte of the raw packet will be 0x24 ('$'), so find() should return 1, not 0, when
* looking for a command identifier string.
*/
if (rawPacketString.find("qSupported") == 1) {
return std::make_unique<CommandPackets::SupportedFeaturesQuery>(rawPacket);
}
if (rawPacketString[1] == 'g' || rawPacketString[1] == 'p') {
return std::make_unique<CommandPackets::ReadRegisters>(rawPacket);
}
if (rawPacketString[1] == 'P') {
return std::make_unique<CommandPackets::WriteRegister>(rawPacket);
}
if (rawPacketString[1] == 'c') {
return std::make_unique<CommandPackets::ContinueExecution>(rawPacket);
}
if (rawPacketString[1] == 's') {
return std::make_unique<CommandPackets::StepExecution>(rawPacket);
}
if (rawPacketString[1] == 'Z') {
return std::make_unique<CommandPackets::SetBreakpoint>(rawPacket);
}
if (rawPacketString[1] == 'z') {
return std::make_unique<CommandPackets::RemoveBreakpoint>(rawPacket);
}
}
return std::make_unique<CommandPacket>(rawPacket);
}
void GdbRspDebugServer::terminateActiveDebugSession() {
if (this->activeDebugSession.has_value()) {
this->activeDebugSession->terminate();
this->activeDebugSession = std::nullopt;
EventManager::triggerEvent(std::make_shared<Events::DebugSessionFinished>());
}
}
void GdbRspDebugServer::onTargetControllerStateReported(const Events::TargetControllerStateReported& event) {
if (event.state == TargetControllerState::SUSPENDED && this->activeDebugSession.has_value()) {
Logger::warning("Terminating debug session - TargetController suspended unexpectedly");
this->terminateActiveDebugSession();
}
}
void GdbRspDebugServer::onTargetExecutionStopped(const Events::TargetExecutionStopped&) {
if (this->activeDebugSession.has_value() && this->activeDebugSession->waitingForBreak) {
this->activeDebugSession->connection.writePacket(
ResponsePackets::TargetStopped(Signal::TRAP)
);
this->activeDebugSession->waitingForBreak = false;
}
}
}

View File

@@ -0,0 +1,175 @@
#pragma once
#include <cstdint>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <vector>
#include <queue>
#include <optional>
#include "src/DebugServer/ServerInterface.hpp"
#include "GdbDebugServerConfig.hpp"
#include "src/EventManager/EventListener.hpp"
#include "src/Helpers/EpollInstance.hpp"
#include "src/TargetController/TargetControllerConsole.hpp"
#include "Connection.hpp"
#include "TargetDescriptor.hpp"
#include "DebugSession.hpp"
#include "Signal.hpp"
#include "RegisterDescriptor.hpp"
#include "Feature.hpp"
#include "CommandPackets/CommandPacket.hpp"
#include "src/EventManager/Events/TargetControllerStateReported.hpp"
#include "src/EventManager/Events/TargetExecutionStopped.hpp"
#include "src/Helpers/BiMap.hpp"
namespace Bloom::DebugServer::Gdb
{
/**
* The GdbRspDebugServer is an implementation of a GDB server using the GDB Remote Serial Protocol.
*
* This DebugServer employs TCP/IP sockets to interface with GDB clients. The listening address can be configured
* in the user's project config file.
*
* See https://sourceware.org/gdb/onlinedocs/gdb/Remote-Protocol.html for more info on the GDB Remote
* Serial Protocol.
*
* @TODO: This could do with some cleaning.
*/
class GdbRspDebugServer: public ServerInterface
{
public:
explicit GdbRspDebugServer(
const DebugServerConfig& debugServerConfig,
EventListener& eventListener
);
[[nodiscard]] std::string getName() const override {
return "GDB Remote Serial Protocol DebugServer";
};
/**
* Prepares the GDB server for listing on the selected address and port.
*/
void init() override;
/**
* Terminates any active debug session and closes the listening socket.
*/
void close() override;
/**
* Waits for a connection from a GDB client or services an active one.
*
* This function will return when any blocking operation is interrupted via this->interruptEventNotifier.
*/
void run() override;
protected:
std::optional<GdbDebugServerConfig> debugServerConfig;
EventListener& eventListener;
EventNotifier* interruptEventNotifier = nullptr;
TargetControllerConsole targetControllerConsole = TargetControllerConsole(this->eventListener);
/**
* Listening socket address
*/
struct sockaddr_in socketAddress = {};
/**
* Listening socket file descriptor
*/
int serverSocketFileDescriptor = -1;
/**
* When waiting for a connection, we don't listen on the this->serverSocketFileDescriptor directly. Instead,
* we use an EpollInstance to monitor both this->serverSocketFileDescriptor and this->interruptEventNotifier.
* This allows us to interrupt any blocking socket IO calls when EventNotifier::notify() is called on
* this->interruptEventNotifier.
*
* See GdbRspDebugServer::init()
* See DebugServer::interruptEventNotifier
* See EpollInstance
* See EventNotifier
*/
EpollInstance epollInstance = EpollInstance();
/**
* SO_REUSEADDR option value for listening socket.
*/
int enableReuseAddressSocketOption = 1;
/**
* When a connection with a GDB client is established, a new instance of the DebugSession class is created and
* held here. A value of std::nullopt means there is no active debug session present.
*/
std::optional<DebugSession> activeDebugSession;
/**
* Waits for a GDB client to connect on the listening socket.
*
* This function may return an std::nullopt, if the waiting was interrupted by some other event.
*/
std::optional<Connection> waitForConnection();
/**
* Waits for a command packet from the connected GDB client.
*
* @return
*/
std::unique_ptr<CommandPackets::CommandPacket> waitForCommandPacket();
/**
* Should construct a derived instance of the CommandPackets::CommandPacket class, from a raw packet.
*
* Derived implementations of this GDB server class can override this function to include support for more
* specific GDB commands. For example, the AVR GDB implementation overrides this function to support AVR
* specific implementations of the read and write memory GDB commands.
*
* This function should never return a nullptr. If the command is unknown, it should simply return an
* instance of the CommandPackets::CommandPacket base class. The handler (CommandPacket::handle()) for the base
* class will handle unknown packets by responding with an empty response packet (as is expected by the GDB
* client).
*
* @param rawPacket
* @return
*/
virtual std::unique_ptr<CommandPackets::CommandPacket> resolveCommandPacket(const RawPacketType& rawPacket);
/**
* Terminates any active debug session (if any) by closing the connection to the GDB client.
*/
void terminateActiveDebugSession();
/**
* Should return the GDB target descriptor for the connected target.
*
* NOTE: This function returns a target descriptor specific to GDB and the target. It's not the same data
* struct as Targets::TargetDescriptor. But the GDB target descriptor does hold an instance to
* Targets::TargetDescriptor. See the Gdb::TargetDescriptor::targetDescriptor class member.
*
* @return
*/
virtual const TargetDescriptor& getGdbTargetDescriptor() = 0;
/**
* Responds to any unexpected TargetController state changes.
*
* @param event
*/
void onTargetControllerStateReported(const Events::TargetControllerStateReported& event);
/**
* If the GDB client is currently waiting for the target execution to stop, this event handler will issue
* a "stop reply" packet to the client once the target execution stops.
*/
void onTargetExecutionStopped(const Events::TargetExecutionStopped&);
};
}

View File

@@ -0,0 +1,132 @@
#pragma once
#include <vector>
#include <memory>
#include <numeric>
#include <QString>
#include <sstream>
#include <iomanip>
namespace Bloom::DebugServer::Gdb
{
using RawPacketType = std::vector<unsigned char>;
/**
* The Packet class implements the data structure for GDB RSP packets.
*
* Fore more information on the packet data structure, see https://sourceware.org/gdb/onlinedocs/gdb/Overview.html#Overview
*/
class Packet
{
public:
explicit Packet(const RawPacketType& rawPacket) {
this->init(rawPacket);
}
Packet() = default;
virtual ~Packet() = default;
Packet(const Packet& other) = default;
Packet(Packet&& other) = default;
Packet& operator = (const Packet& other) = default;
Packet& operator = (Packet&& other) = default;
[[nodiscard]] virtual std::vector<unsigned char> getData() const {
return this->data;
}
void setData(const std::vector<unsigned char>& data) {
this->data = data;
}
/**
* Generates a raw packet.
*
* @return
*/
[[nodiscard]] std::vector<unsigned char> toRawPacket() const {
std::vector<unsigned char> packet = {'$'};
auto data = this->getData();
for (const auto& byte : data) {
// Escape $ and # characters
switch (byte) {
case '$':
case '#': {
packet.push_back('}');
packet.push_back(byte ^ 0x20);
}
default: {
packet.push_back(byte);
}
}
}
auto dataSum = std::accumulate(packet.begin() + 1, packet.end(), 0);
packet.push_back('#');
auto checkSum = QStringLiteral("%1").arg(dataSum % 256, 2, 16, QLatin1Char('0')).toStdString();
if (checkSum.size() < 2) {
packet.push_back('0');
if (checkSum.size() < 1) {
packet.push_back('0');
} else {
packet.push_back(static_cast<unsigned char>(checkSum[0]));
}
}
packet.push_back(static_cast<unsigned char>(checkSum[0]));
packet.push_back(static_cast<unsigned char>(checkSum[1]));
return packet;
}
/**
* Converts raw data to hexadecimal form, the form in which responses are expected to be delivered from the
* server.
*
* @param data
* @return
*/
static std::string dataToHex(const std::vector<unsigned char>& data) {
std::stringstream stream;
stream << std::hex << std::setfill('0');
for (const auto& byte : data) {
stream << std::setw(2) << static_cast<unsigned int>(byte);
}
return stream.str();
}
/**
* Converts data in hexadecimal form to raw data.
*
* @param hexData
* @return
*/
static std::vector<unsigned char> hexToData(const std::string& hexData) {
std::vector<unsigned char> output;
for (auto i = 0; i < hexData.size(); i += 2) {
auto hexByte = std::string((hexData.begin() + i), (hexData.begin() + i + 2));
output.push_back(static_cast<unsigned char>(std::stoi(hexByte, nullptr, 16)));
}
return output;
}
protected:
std::vector<unsigned char> data;
void init(const RawPacketType& rawPacket) {
this->data.insert(
this->data.begin(),
rawPacket.begin() + 1,
rawPacket.end() - 3
);
}
};
}

View File

@@ -0,0 +1,67 @@
#pragma once
#include <cstdint>
#include <string>
namespace Bloom::DebugServer::Gdb
{
using GdbRegisterNumberType = int;
/*
* GDB defines a set of registers for each target architecture.
*
* Each register in the set is assigned a register number, which is used to identify the register.
*/
struct RegisterDescriptor
{
GdbRegisterNumberType number;
std::uint16_t size;
std::string name;
RegisterDescriptor(GdbRegisterNumberType number, std::uint16_t size, const std::string& name)
: number(number), size(size), name(name) {};
bool operator == (const RegisterDescriptor& other) const {
return this->number == other.number;
}
bool operator != (const RegisterDescriptor& other) const {
return !(*this == other);
}
bool operator < (const RegisterDescriptor& rhs) const {
return this->number < rhs.number;
}
bool operator > (const RegisterDescriptor& rhs) const {
return rhs < *this;
}
bool operator <= (const RegisterDescriptor& rhs) const {
return !(rhs < *this);
}
bool operator >= (const RegisterDescriptor& rhs) const {
return !(*this < rhs);
}
};
}
namespace std
{
/**
* Hashing function for RegisterDescriptor type.
*
* This is required in order to use RegisterDescriptor as a key in an std::unordered_map (see the BiMap
* class).
*/
template<>
class hash<Bloom::DebugServer::Gdb::RegisterDescriptor>
{
public:
std::size_t operator () (const Bloom::DebugServer::Gdb::RegisterDescriptor& descriptor) const {
// We use the GDB register number as the hash, as it is unique to the register.
return static_cast<size_t>(descriptor.number);
}
};
}

View File

@@ -0,0 +1,19 @@
#pragma once
#include "ResponsePacket.hpp"
namespace Bloom::DebugServer::Gdb::ResponsePackets
{
/**
* Error response packet expected by the GDB client, to indicate an error, in response to certain commands.
*/
class ErrorResponsePacket: public ResponsePacket
{
public:
ErrorResponsePacket() = default;
[[nodiscard]] std::vector<unsigned char> getData() const override {
return {'E', '0', '1'};
}
};
}

View File

@@ -0,0 +1,19 @@
#pragma once
#include "ResponsePacket.hpp"
namespace Bloom::DebugServer::Gdb::ResponsePackets
{
/**
* OK response packet expected by the GDB client, in response to certain commands.
*/
class OkResponsePacket: public ResponsePacket
{
public:
OkResponsePacket() = default;
[[nodiscard]] std::vector<unsigned char> getData() const override {
return {'O', 'K'};
}
};
}

View File

@@ -0,0 +1,22 @@
#pragma once
#include <vector>
#include <memory>
#include "src/DebugServer/Gdb/Packet.hpp"
namespace Bloom::DebugServer::Gdb::ResponsePackets
{
/**
* Upon receiving a CommandPacket from the connected GDB RSP client, the server is expected to respond with a
* response packet.
*/
class ResponsePacket: public Packet
{
public:
ResponsePacket() = default;
explicit ResponsePacket(const std::vector<unsigned char>& data) {
this->data = data;
}
};
}

View File

@@ -0,0 +1,25 @@
#include "SupportedFeaturesResponse.hpp"
namespace Bloom::DebugServer::Gdb::ResponsePackets
{
std::vector<unsigned char> SupportedFeaturesResponse::getData() const {
std::string output = "qSupported:";
auto gdbFeatureMapping = getGdbFeatureToNameMapping();
for (const auto& supportedFeature : this->supportedFeatures) {
auto featureString = gdbFeatureMapping.valueAt(supportedFeature.first);
if (featureString.has_value()) {
if (supportedFeature.second.has_value()) {
output.append(featureString.value() + "=" + supportedFeature.second.value() + ";");
} else {
output.append(featureString.value() + "+;");
}
}
}
return std::vector<unsigned char>(output.begin(), output.end());
}
}

View File

@@ -0,0 +1,26 @@
#pragma once
#include <set>
#include <utility>
#include "ResponsePacket.hpp"
#include "../Feature.hpp"
namespace Bloom::DebugServer::Gdb::ResponsePackets
{
/**
* The SupportedFeaturesResponse class implements the response packet structure for the "qSupported" command.
*/
class SupportedFeaturesResponse: public ResponsePacket
{
public:
SupportedFeaturesResponse() = default;
explicit SupportedFeaturesResponse(std::set<std::pair<Feature, std::optional<std::string>>> supportedFeatures)
: supportedFeatures(std::move(supportedFeatures)) {};
[[nodiscard]] std::vector<unsigned char> getData() const override;
protected:
std::set<std::pair<Feature, std::optional<std::string>>> supportedFeatures;
};
}

View File

@@ -0,0 +1,37 @@
#pragma once
#include "ResponsePacket.hpp"
#include "../Signal.hpp"
#include "../StopReason.hpp"
#include "src/Targets/TargetRegister.hpp"
namespace Bloom::DebugServer::Gdb::ResponsePackets
{
/**
* The TargetStopped class implements the response packet structure for any commands that expect a "StopReply"
* packet in response.
*/
class TargetStopped: public ResponsePacket
{
public:
Signal signal;
std::optional<StopReason> stopReason;
explicit TargetStopped(Signal signal): signal(signal) {}
[[nodiscard]] std::vector<unsigned char> getData() const override {
std::string output = "T" + Packet::dataToHex({static_cast<unsigned char>(this->signal)});
if (this->stopReason.has_value()) {
auto stopReasonMapping = getStopReasonToNameMapping();
auto stopReasonName = stopReasonMapping.valueAt(this->stopReason.value());
if (stopReasonName.has_value()) {
output += stopReasonName.value() + ":;";
}
}
return std::vector<unsigned char>(output.begin(), output.end());
}
};
}

View File

@@ -0,0 +1,10 @@
#pragma once
namespace Bloom::DebugServer::Gdb
{
enum class Signal: unsigned char
{
TRAP = 5,
INTERRUPTED = 2,
};
}

View File

@@ -0,0 +1,19 @@
#pragma once
#include "src/Helpers/BiMap.hpp"
namespace Bloom::DebugServer::Gdb
{
enum class StopReason: int
{
SOFTWARE_BREAKPOINT = 0,
HARDWARE_BREAKPOINT = 1,
};
static inline BiMap<StopReason, std::string> getStopReasonToNameMapping() {
return BiMap<StopReason, std::string>({
{StopReason::HARDWARE_BREAKPOINT, "hwbreak"},
{StopReason::SOFTWARE_BREAKPOINT, "swbreak"},
});
}
}

View File

@@ -0,0 +1,58 @@
#pragma once
#include <cstdint>
#include <optional>
#include <vector>
#include "src/Targets/TargetDescriptor.hpp"
#include "src/Targets/TargetRegister.hpp"
#include "RegisterDescriptor.hpp"
namespace Bloom::DebugServer::Gdb
{
struct TargetDescriptor
{
Targets::TargetDescriptor targetDescriptor;
explicit TargetDescriptor(const Targets::TargetDescriptor& targetDescriptor)
: targetDescriptor(targetDescriptor)
{}
/**
* Should retrieve the GDB register number, given a target register descriptor. Or std::nullopt if the target
* register descriptor isn't mapped to any GDB register.
*
* @param registerDescriptor
* @return
*/
virtual std::optional<GdbRegisterNumberType> getRegisterNumberFromTargetRegisterDescriptor(
const Targets::TargetRegisterDescriptor& registerDescriptor
) const = 0;
/**
* Should retrieve the GDB register descriptor for a given GDB register number.
*
* @param number
* @return
*/
virtual const RegisterDescriptor& getRegisterDescriptorFromNumber(GdbRegisterNumberType number) const = 0;
/**
* Should retrieve the mapped target register descriptor for a given GDB register number.
*
* @param number
* @return
*/
virtual const Targets::TargetRegisterDescriptor& getTargetRegisterDescriptorFromNumber(
GdbRegisterNumberType number
) const = 0;
/**
* Should return all allocated GDB register numbers for the target.
*
* @return
*/
virtual const std::vector<GdbRegisterNumberType>& getRegisterNumbers() const = 0;
};
}