diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 8b3246c..161a5d9 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1 +1,2 @@ add_subdirectory(cpuinfo) +add_subdirectory(literal) diff --git a/examples/literal/CMakeLists.txt b/examples/literal/CMakeLists.txt new file mode 100644 index 0000000..2004033 --- /dev/null +++ b/examples/literal/CMakeLists.txt @@ -0,0 +1,3 @@ +add_executable(literal literal.cpp) +target_link_libraries(literal biscuit) +set_property(TARGET literal PROPERTY CXX_STANDARD 20) diff --git a/examples/literal/literal.cpp b/examples/literal/literal.cpp new file mode 100644 index 0000000..acc6d8b --- /dev/null +++ b/examples/literal/literal.cpp @@ -0,0 +1,73 @@ +#include +#include + +#include + +using namespace biscuit; + +constexpr const static uint64_t literal1_value = 0x1234567890ABCDEF; +constexpr const static uint64_t literal2_value = 0x1122334455667788; +constexpr const static uint64_t literal3_value = 0xFEDCBA0987654321; +constexpr const static uint64_t literal4_value = 0xAABBCCDDEEFF0011; + +void print_literals(uint64_t literal1, uint64_t literal2, uint64_t literal3, uint64_t literal4) { + std::cout << "Literal 1: " << std::hex << literal1 << std::endl; + std::cout << "Literal 2: " << std::hex << literal2 << std::endl; + std::cout << "Literal 3: " << std::hex << literal3 << std::endl; + std::cout << "Literal 4: " << std::hex << literal4 << std::endl; + + BISCUIT_ASSERT(literal1 == literal1_value); + BISCUIT_ASSERT(literal2 == literal2_value); + BISCUIT_ASSERT(literal3 == literal3_value); + BISCUIT_ASSERT(literal4 == literal4_value); +} + +int main() { + Assembler as(0x5000); + + Literal64 literal1(literal1_value); + Literal64 literal2(literal2_value); + Literal64 literal3(literal3_value); + Literal64 literal4(literal4_value); + + // Literal placed before the code, more than 0x1000 bytes away + as.Place(&literal1); + + as.AdvanceBuffer(as.GetCodeBuffer().GetCursorOffset() + 0x1000); + + // Literal placed before the code, less than 0x1000 bytes away + as.Place(&literal2); + + void (*code)() = reinterpret_cast(as.GetCursorPointer()); + as.ADDI(sp, sp, -8); + as.SD(ra, 0, sp); + + as.LD(a0, &literal1); + + as.LD(a1, &literal2); + + as.LD(a2, &literal3); + + as.LD(a3, &literal4); + + as.LI(t0, (uint64_t)print_literals); + as.JALR(t0); + + as.LD(ra, 0, sp); + as.ADDI(sp, sp, 8); + as.RET(); + + // Literal placed after the code, less than 0x1000 bytes away + as.Place(&literal3); + + as.AdvanceBuffer(as.GetCodeBuffer().GetCursorOffset() + 0x1000); + + // Literal placed after the code, more than 0x1000 bytes away + as.Place(&literal4); + + as.GetCodeBuffer().SetExecutable(); + + code(); + + return 0; +} diff --git a/include/biscuit/assembler.hpp b/include/biscuit/assembler.hpp index 6c7e59a..173aef8 100644 --- a/include/biscuit/assembler.hpp +++ b/include/biscuit/assembler.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -151,6 +152,13 @@ class Assembler { */ void Bind(Label* label); + /** + * Places a literal at the current offset within the code buffer. + * + * @param literal A non-null valid literal to place. + */ + void Place(Literal64* literal); + // RV32I Instructions void ADD(GPR rd, GPR lhs, GPR rhs) noexcept; @@ -269,6 +277,7 @@ class Assembler { void ADDIW(GPR rd, GPR rs, int32_t imm) noexcept; void ADDW(GPR rd, GPR lhs, GPR rhs) noexcept; void LD(GPR rd, int32_t imm, GPR rs) noexcept; + void LD(GPR rd, Literal64* literal) noexcept; void LWU(GPR rd, int32_t imm, GPR rs) noexcept; void SD(GPR rs2, int32_t imm, GPR rs1) noexcept; @@ -1523,6 +1532,19 @@ class Assembler { // requires them. void ResolveLabelOffsets(Label* label); + // Places a literal at the given offset. + template + void PlaceAtOffset(Literal* literal, Literal::LocationOffset offset); + + // Links the given literal and returns the offset to it. + template + ptrdiff_t LinkAndGetOffset(Literal* literal); + + // Resolves all literal offsets and patches any necessary + // offsets into the load instructions that require them. + template + void ResolveLiteralOffsets(Literal* literal); + CodeBuffer m_buffer; ArchFeature m_features = ArchFeature::RV64; }; diff --git a/include/biscuit/literal.hpp b/include/biscuit/literal.hpp new file mode 100644 index 0000000..f6c4be0 --- /dev/null +++ b/include/biscuit/literal.hpp @@ -0,0 +1,168 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace biscuit { + +/** + * A Literal is a representation of a constant value that can be loaded into a register. + * This is useful for avoiding multiple instructions for loading big constants. + * + * Literals, like Labels, don't need to be placed immediately. They can be created + * and used with loads that require a Literal, and placed in the buffer at a later point. + * + * @note Any literal that is created, is used with a load instruction, + * but is *not* placed to a location (via Place() in the assembler) + * will result in an assertion being invoked when the literal instance's + * destructor is executed. + * + * @note A literal may only be placed to one location. Any attempt to place + * a literal that is already placed will result in an assertion being + * invoked. + * + * @par + * An example of placing a literal: + * @code{.cpp} + * Assembler as{...}; + * Literal64 literal(0x1234567890ABCDEF); + * + * as.LD(x2, &literal); // Load the literal (emits a AUIPC+LD sequence) + * as.JR(x2); // Execution continues elsewhere + * as.Place(&literal); // Place the literal at this location in the buffer + * @endcode +*/ +template +class Literal { +public: + using Location = std::optional; + using LocationOffset = Location::value_type; + + /** + * This constructor results in a literal being constructed that is not + * placed at a particular location yet. + * + * @param value The value that this literal represents. + */ + explicit Literal(T value) : m_value{value} {} + + /// Destructor + ~Literal() noexcept { + // It's a logic bug if something references a literal and hasn't been handled. + // + // This is usually indicative of a scenario where a literal is referenced but + // hasn't been placed at a location. + // + BISCUIT_ASSERT(IsResolved()); + } + + // Copying disabled for the same reasons as Labels. + Literal(const Literal&) = delete; + Literal& operator=(const Literal&) = delete; + + Literal(Literal&&) noexcept = default; + Literal& operator=(Literal&&) noexcept = default; + + /** + * Determines whether or not this literal instance has a location assigned to it. + * + * A literal is considered placed if it has an assigned location. + */ + [[nodiscard]] bool IsPlaced() const noexcept { + return m_location.has_value(); + } + + /** + * Determines whether or not this literal is resolved. + * + * A literal is considered resolved when all referencing offsets have been handled. + */ + [[nodiscard]] bool IsResolved() const noexcept { + return m_offsets.empty(); + } + + /** + * Determines whether or not this literal is unresolved. + * + * A literal is considered unresolved if it still has any unhandled referencing offsets. + */ + [[nodiscard]] bool IsUnresolved() const noexcept { + return !IsResolved(); + } + + /** + * Retrieves the location for this literal. + * + * @note If the returned location is empty, then this literal has not been assigned + * a location yet. + */ + [[nodiscard]] Location GetLocation() const noexcept { + return m_location; + } + +private: + // A literal instance is inherently bound to the assembler it's + // used with, as the offsets within the literal set depend on + // said assemblers code buffer. + friend class Assembler; + + /** + * Places a literal to the given location. + * + * @param offset The offset to place this literal at. + * + * @returns The literal value so it can be copied to memory by the assembler. + * + * @pre The literal must not have already been placed at a previous location. + * Attempting to place a literal multiple times is typically, in almost all scenarios, + * the source of bugs. + * Attempting to place an already placed literal will result in an assertion + * being triggered. + */ + [[nodiscard]] const T& Place(LocationOffset offset) noexcept { + BISCUIT_ASSERT(!IsPlaced()); + m_location = offset; + return m_value; + } + + /** + * Marks the given address as dependent on this literal. + * + * This is used in scenarios where a literal exists, but has not yet been + * placed at a location yet. It's important to track these addresses, + * as we'll need to patch the dependent load instructions with the + * proper offset once the literal is finally placed by the assembler. + * + * During literal placement, the offset will be calculated and inserted + * into dependent instructions. + */ + void AddOffset(LocationOffset offset) { + // If a literal is already placed at a location, then offset tracking + // isn't necessary. Tripping this assert means we have a bug somewhere. + BISCUIT_ASSERT(!IsPlaced()); + BISCUIT_ASSERT(IsNewOffset(offset)); + + m_offsets.insert(offset); + } + + // Clears all the underlying offsets for this literal. + void ClearOffsets() noexcept { + m_offsets.clear(); + } + + // Determines whether or not this address has already been added before. + [[nodiscard]] bool IsNewOffset(LocationOffset offset) const noexcept { + return m_offsets.find(offset) == m_offsets.cend(); + } + + std::set m_offsets; + Location m_location; + const T m_value; +}; + +using Literal64 = Literal; + +} // namespace biscuit \ No newline at end of file diff --git a/src/assembler.cpp b/src/assembler.cpp index 10b2d8f..1959b23 100644 --- a/src/assembler.cpp +++ b/src/assembler.cpp @@ -29,6 +29,10 @@ void Assembler::Bind(Label* label) { BindToOffset(label, m_buffer.GetCursorOffset()); } +void Assembler::Place(Literal64* literal) { + PlaceAtOffset(literal, m_buffer.GetCursorOffset()); +} + void Assembler::ADD(GPR rd, GPR lhs, GPR rhs) noexcept { EmitRType(m_buffer, 0b0000000, rhs, lhs, 0b000, rd, 0b0110011); } @@ -531,6 +535,15 @@ void Assembler::LD(GPR rd, int32_t imm, GPR rs) noexcept { EmitIType(m_buffer, static_cast(imm), rs, 0b011, rd, 0b0000011); } +void Assembler::LD(GPR rd, Literal64* literal) noexcept { + BISCUIT_ASSERT(IsRV64(m_features)); + const auto offset = LinkAndGetOffset(literal); + const auto hi20 = static_cast((static_cast(offset) + 0x800) >> 12 & 0xFFFFF); + const auto lo12 = static_cast(offset << 20) >> 20; + AUIPC(rd, hi20); + LD(rd, lo12, rd); +} + void Assembler::LWU(GPR rd, int32_t imm, GPR rs) noexcept { BISCUIT_ASSERT(IsRV64(m_features)); BISCUIT_ASSERT(IsValidSigned12BitImm(imm)); @@ -1508,4 +1521,83 @@ void Assembler::ResolveLabelOffsets(Label* label) { } } +template +void Assembler::PlaceAtOffset(Literal* literal, Literal::LocationOffset offset) { + BISCUIT_ASSERT(literal != nullptr); + BISCUIT_ASSERT(offset >= 0 && offset <= m_buffer.GetCursorOffset()); + + const T& value = literal->Place(offset); + ResolveLiteralOffsets(literal); + literal->ClearOffsets(); + + m_buffer.Emit(value); +} + +template +ptrdiff_t Assembler::LinkAndGetOffset(Literal* literal) { + BISCUIT_ASSERT(literal != nullptr); + + // If we have a placed literal, then it's straightforward to calculate + // the offsets. + if (literal->IsPlaced()) { + const auto cursor_address = m_buffer.GetCursorAddress(); + const auto literal_offset = m_buffer.GetOffsetAddress(*literal->GetLocation()); + return static_cast(literal_offset - cursor_address); + } + + // If we don't have a placed literal, we return an offset of zero. + // While the emitter will emit a bogus load instruction initially, + // the offset will be patched over once the literal has been properly + // placed at a location. + literal->AddOffset(m_buffer.GetCursorOffset()); + return 0; +} + +template +void Assembler::ResolveLiteralOffsets(Literal* literal) { + const auto is_auipc_type = [](uint32_t instruction) { + return (instruction & 0x7F) == 0b0010111; + }; + + const auto is_gpr_load_type = [](uint32_t instruction) { + return (instruction & 0x7F) == 0b0000011; + }; + + const auto literal_location = *literal->GetLocation(); + + for (const auto offset : literal->m_offsets) { + const auto address = m_buffer.GetOffsetAddress(offset); + auto* const ptr = reinterpret_cast(address); + + uint32_t instructions[2] = {0, 0}; + std::memcpy(&instructions[0], ptr, sizeof(uint32_t)); + std::memcpy(&instructions[1], ptr + sizeof(uint32_t), sizeof(uint32_t)); + + // Given all load instructions we need to patch have 0 encoded as + // their load offset, we don't need to worry about any masking work. + // + // It's enough to verify that the immediate is going to be valid + // and then OR it into the instruction. + + const auto encoded_offset = literal_location - offset; + + BISCUIT_ASSERT(is_auipc_type(instructions[0])); + + // Make sure the distance is within the bounds of a 32-bit signed integer. + BISCUIT_ASSERT((static_cast(encoded_offset << 32) >> 32) == encoded_offset); + + if (is_gpr_load_type(instructions[1])) { + const auto high20 = static_cast(encoded_offset & 0xFFFFF000); + const auto low12 = static_cast(encoded_offset & 0xFFF); + instructions[0] |= high20; + instructions[1] |= low12 << 20; + } else { + BISCUIT_ASSERT(false); + } + + std::memcpy(ptr, &instructions[0], sizeof(uint32_t)); + std::memcpy(ptr + sizeof(uint32_t), &instructions[1], sizeof(uint32_t)); + } +} + } // namespace biscuit