Merge pull request #431 from adriadam10/feat/relinker-e2e-fixture
test(relinker): add C++ end-to-end fixture and --to-intel Linux stub test
This commit is contained in:
commit
1e5bb880bb
|
|
@ -274,4 +274,11 @@ if(BUILD_TESTING)
|
|||
${CMAKE_CURRENT_SOURCE_DIR}/relinker/include
|
||||
)
|
||||
add_test(NAME strict_nid_filter COMMAND strict_nid_filter_tests)
|
||||
|
||||
if(UNIX AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
|
||||
add_test_executable(to_intel_linux_stubs_tests
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/ToIntelLinuxStubsTests.cpp
|
||||
)
|
||||
add_test(NAME to_intel_linux_stubs COMMAND to_intel_linux_stubs_tests $<TARGET_FILE:relinker>)
|
||||
endif()
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,129 @@
|
|||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace RelinkerTests {
|
||||
|
||||
using Bytes = std::vector<std::uint8_t>;
|
||||
|
||||
inline constexpr std::uint64_t kCodeOffset = 0x4000;
|
||||
inline constexpr std::uint64_t kCodeSize = 0x1000;
|
||||
inline constexpr std::uint64_t kDynamicVaddr = 0x600;
|
||||
inline constexpr std::uint16_t kSpareProgramHeaders = 3;
|
||||
|
||||
template<typename TValue>
|
||||
void Write(Bytes& bytes, const std::size_t offset, const TValue value) {
|
||||
if (offset > bytes.size() || sizeof(value) > bytes.size() - offset) throw std::runtime_error("ELF fixture write is out of bounds");
|
||||
std::memcpy(bytes.data() + offset, &value, sizeof(value));
|
||||
}
|
||||
|
||||
template<typename TValue>
|
||||
TValue Read(const Bytes& bytes, const std::size_t offset) {
|
||||
TValue value;
|
||||
if (offset > bytes.size() || sizeof(value) > bytes.size() - offset) throw std::runtime_error("ELF read is out of bounds");
|
||||
std::memcpy(&value, bytes.data() + offset, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
inline void WriteProgramHeader(Bytes& image, const std::uint16_t index, const std::uint32_t type, const std::uint32_t flags, const std::uint64_t offset, const std::uint64_t vaddr, const std::uint64_t size, const std::uint64_t align) {
|
||||
const std::size_t header = 64 + index * 56;
|
||||
Write<std::uint32_t>(image, header, type);
|
||||
Write<std::uint32_t>(image, header + 4, flags);
|
||||
Write<std::uint64_t>(image, header + 8, offset);
|
||||
Write<std::uint64_t>(image, header + 16, vaddr);
|
||||
Write<std::uint64_t>(image, header + 24, vaddr);
|
||||
Write<std::uint64_t>(image, header + 32, size);
|
||||
Write<std::uint64_t>(image, header + 40, size);
|
||||
Write<std::uint64_t>(image, header + 48, align);
|
||||
}
|
||||
|
||||
inline Bytes MakeExecutable(const std::span<const std::uint8_t> code) {
|
||||
if (code.size() > kDynamicVaddr) throw std::runtime_error("ELF fixture code overlaps the dynamic section");
|
||||
Bytes image(0x8000);
|
||||
const std::uint8_t ident[16] = {0x7F, 'E', 'L', 'F', 2, 1, 1};
|
||||
std::memcpy(image.data(), ident, sizeof(ident));
|
||||
Write<std::uint16_t>(image, 16, 3);
|
||||
Write<std::uint16_t>(image, 18, 62);
|
||||
Write<std::uint32_t>(image, 20, 1);
|
||||
Write<std::uint64_t>(image, 24, 0);
|
||||
Write<std::uint64_t>(image, 32, 64);
|
||||
Write<std::uint16_t>(image, 52, 64);
|
||||
Write<std::uint16_t>(image, 54, 56);
|
||||
Write<std::uint16_t>(image, 56, 2 + kSpareProgramHeaders);
|
||||
Write<std::uint16_t>(image, 58, 64);
|
||||
const std::pair<std::int64_t, std::uint64_t> tags[] = {{5, 0x600}, {10, 1}, {6, 0x620}, {11, 24}, {7, 0x700}, {8, 0}, {9, 24}, {0, 0}};
|
||||
WriteProgramHeader(image, 0, 1, 5, kCodeOffset, 0, kCodeSize, 0x4000);
|
||||
WriteProgramHeader(image, 1, 2, 6, kCodeOffset + kDynamicVaddr, kDynamicVaddr, sizeof(tags) / sizeof(tags[0]) * 16, 8);
|
||||
for (std::uint16_t index = 2; index < 2 + kSpareProgramHeaders; ++index) WriteProgramHeader(image, index, 0x6FFFFF01, 0, 0, 0, 0, 1);
|
||||
std::fill(image.begin() + kCodeOffset, image.begin() + kCodeOffset + kDynamicVaddr, 0xCC);
|
||||
std::copy(code.begin(), code.end(), image.begin() + kCodeOffset);
|
||||
for (std::size_t index = 0; index < sizeof(tags) / sizeof(tags[0]); ++index) {
|
||||
Write<std::int64_t>(image, kCodeOffset + kDynamicVaddr + index * 16, tags[index].first);
|
||||
Write<std::uint64_t>(image, kCodeOffset + kDynamicVaddr + index * 16 + 8, tags[index].second);
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
class MappedImage {
|
||||
public:
|
||||
explicit MappedImage(const Bytes& elf) {
|
||||
const auto phOffset = Read<std::uint64_t>(elf, 32);
|
||||
const auto phSize = Read<std::uint16_t>(elf, 54);
|
||||
const auto phCount = Read<std::uint16_t>(elf, 56);
|
||||
for (std::uint16_t index = 0; index < phCount; ++index) {
|
||||
const auto header = phOffset + index * phSize;
|
||||
if (Read<std::uint32_t>(elf, header) != 1) continue;
|
||||
size = std::max<std::size_t>(size, Read<std::uint64_t>(elf, header + 16) + Read<std::uint64_t>(elf, header + 40));
|
||||
}
|
||||
if (size == 0) throw std::runtime_error("ELF has no PT_LOAD segment");
|
||||
base = mmap(nullptr, size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
if (base == MAP_FAILED) throw std::runtime_error("Cannot map executable memory for the ELF image");
|
||||
for (std::uint16_t index = 0; index < phCount; ++index) {
|
||||
const auto header = phOffset + index * phSize;
|
||||
if (Read<std::uint32_t>(elf, header) != 1) continue;
|
||||
const auto offset = Read<std::uint64_t>(elf, header + 8);
|
||||
const auto fileSize = Read<std::uint64_t>(elf, header + 32);
|
||||
if (offset > elf.size() || fileSize > elf.size() - offset) throw std::runtime_error("PT_LOAD exceeds the ELF file");
|
||||
std::memcpy(static_cast<std::uint8_t*>(base) + Read<std::uint64_t>(elf, header + 16), elf.data() + offset, fileSize);
|
||||
}
|
||||
if (mprotect(base, size, PROT_NONE) != 0) throw std::runtime_error("Cannot protect the ELF image");
|
||||
const auto page = static_cast<std::uint64_t>(sysconf(_SC_PAGESIZE));
|
||||
for (std::uint16_t index = 0; index < phCount; ++index) {
|
||||
const auto header = phOffset + index * phSize;
|
||||
if (Read<std::uint32_t>(elf, header) != 1) continue;
|
||||
const auto flags = Read<std::uint32_t>(elf, header + 4);
|
||||
const auto vaddr = Read<std::uint64_t>(elf, header + 16);
|
||||
const auto start = vaddr / page * page;
|
||||
const auto end = (vaddr + Read<std::uint64_t>(elf, header + 40) + page - 1) / page * page;
|
||||
const int protection = ((flags & 4) != 0 ? PROT_READ : 0) | ((flags & 2) != 0 ? PROT_WRITE : 0) | ((flags & 1) != 0 ? PROT_EXEC : 0);
|
||||
if (mprotect(static_cast<std::uint8_t*>(base) + start, std::min<std::uint64_t>(end, size) - start, protection) != 0) throw std::runtime_error("Cannot apply PT_LOAD permissions");
|
||||
}
|
||||
}
|
||||
|
||||
MappedImage(const MappedImage&) = delete;
|
||||
MappedImage& operator=(const MappedImage&) = delete;
|
||||
|
||||
~MappedImage() {
|
||||
munmap(base, size);
|
||||
}
|
||||
|
||||
template<typename TFunction>
|
||||
TFunction* At(const std::uint64_t vaddr) const {
|
||||
if (vaddr >= size) throw std::runtime_error("Address is outside the mapped ELF image");
|
||||
return reinterpret_cast<TFunction*>(static_cast<std::uint8_t*>(base) + vaddr);
|
||||
}
|
||||
|
||||
private:
|
||||
void* base = nullptr;
|
||||
std::size_t size = 0;
|
||||
};
|
||||
|
||||
} // namespace RelinkerTests
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <fcntl.h>
|
||||
#include <spawn.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
extern char** environ;
|
||||
|
||||
namespace RelinkerTests {
|
||||
|
||||
class TempDirectory {
|
||||
public:
|
||||
TempDirectory() {
|
||||
auto pattern = (std::filesystem::temp_directory_path() / "anyps5-relinker-XXXXXX").string();
|
||||
if (mkdtemp(pattern.data()) == nullptr) throw std::runtime_error("Cannot create a temporary directory");
|
||||
path = pattern;
|
||||
}
|
||||
|
||||
TempDirectory(const TempDirectory&) = delete;
|
||||
TempDirectory& operator=(const TempDirectory&) = delete;
|
||||
|
||||
~TempDirectory() {
|
||||
std::error_code error;
|
||||
std::filesystem::remove_all(path, error);
|
||||
}
|
||||
|
||||
const std::filesystem::path& Path() const {
|
||||
return path;
|
||||
}
|
||||
|
||||
private:
|
||||
std::filesystem::path path;
|
||||
};
|
||||
|
||||
struct RelinkerRun {
|
||||
int ExitCode = -1;
|
||||
std::string Output;
|
||||
};
|
||||
|
||||
inline RelinkerRun RunRelinker(const std::filesystem::path& relinker, const std::vector<std::string>& arguments, const std::filesystem::path& logPath) {
|
||||
std::vector<std::string> owned{relinker.string()};
|
||||
owned.insert(owned.end(), arguments.begin(), arguments.end());
|
||||
std::vector<char*> argv;
|
||||
for (auto& argument : owned) argv.push_back(argument.data());
|
||||
argv.push_back(nullptr);
|
||||
posix_spawn_file_actions_t actions;
|
||||
posix_spawn_file_actions_init(&actions);
|
||||
posix_spawn_file_actions_addopen(&actions, STDOUT_FILENO, logPath.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
posix_spawn_file_actions_adddup2(&actions, STDOUT_FILENO, STDERR_FILENO);
|
||||
pid_t pid = 0;
|
||||
const auto spawned = posix_spawn(&pid, argv[0], &actions, nullptr, argv.data(), environ);
|
||||
posix_spawn_file_actions_destroy(&actions);
|
||||
if (spawned != 0) throw std::runtime_error("Cannot start the relinker");
|
||||
int status = 0;
|
||||
if (waitpid(pid, &status, 0) != pid) throw std::runtime_error("Cannot wait for the relinker");
|
||||
RelinkerRun run;
|
||||
if (WIFEXITED(status)) run.ExitCode = WEXITSTATUS(status);
|
||||
std::ifstream log(logPath, std::ios::binary);
|
||||
run.Output.assign(std::istreambuf_iterator<char>(log), std::istreambuf_iterator<char>());
|
||||
return run;
|
||||
}
|
||||
|
||||
inline std::vector<std::uint8_t> ReadFile(const std::filesystem::path& path) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) throw std::runtime_error("Cannot read " + path.string());
|
||||
return {std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()};
|
||||
}
|
||||
|
||||
inline void WriteFile(const std::filesystem::path& path, const std::vector<std::uint8_t>& bytes) {
|
||||
std::ofstream file(path, std::ios::binary);
|
||||
file.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
|
||||
if (!file) throw std::runtime_error("Cannot write " + path.string());
|
||||
}
|
||||
|
||||
} // namespace RelinkerTests
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
#include "ElfFixture.hpp"
|
||||
#include "RelinkerProcess.hpp"
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace RelinkerTests;
|
||||
|
||||
void require(const bool condition, const std::string& message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
const Bytes kCode = {
|
||||
0x66, 0x48, 0x0F, 0x6E, 0xCF,
|
||||
0x66, 0x48, 0x0F, 0x6E, 0xD6,
|
||||
0x66, 0x0F, 0x78, 0xC1, 0x08, 0x08,
|
||||
0xF2, 0x0F, 0x78, 0xD1, 0x08, 0x10,
|
||||
0x66, 0x48, 0x0F, 0x7E, 0xD0,
|
||||
0xC3,
|
||||
};
|
||||
constexpr std::uint64_t kSites[] = {10, 16};
|
||||
constexpr std::size_t kSiteLength = 6;
|
||||
|
||||
std::uint64_t expected(const std::uint64_t value, const std::uint64_t destination) {
|
||||
return (destination & ~0xFF0000ull) | (((value >> 8) & 0xFF) << 16);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main(const int argc, char** argv) {
|
||||
try {
|
||||
require(argc == 2, "usage: to_intel_linux_stubs_tests <relinker>");
|
||||
const TempDirectory directory;
|
||||
const auto input = directory.Path() / "input.elf";
|
||||
const auto output = directory.Path() / "output.elf";
|
||||
WriteFile(input, MakeExecutable(kCode));
|
||||
const auto run = RunRelinker(argv[1], {"--skip-sce-module", "--to-intel", input.string(), output.string()}, directory.Path() / "relinker.log");
|
||||
require(run.ExitCode == 0, "Relinker failed:\n" + run.Output);
|
||||
require(run.Output.find("Intel conversion: 0 in place, 2 stubs") != std::string::npos, "Relinker did not report two stubs:\n" + run.Output);
|
||||
|
||||
const MappedImage image(ReadFile(output));
|
||||
for (const auto site : kSites) {
|
||||
const auto* bytes = image.At<const std::uint8_t>(site);
|
||||
require(bytes[0] == 0xE9, "SSE4a site was not replaced by a jump");
|
||||
for (std::size_t index = 5; index < kSiteLength; ++index) require(bytes[index] == 0x90, "SSE4a site tail is not padded with NOPs");
|
||||
std::int32_t displacement;
|
||||
std::memcpy(&displacement, bytes + 1, sizeof(displacement));
|
||||
require(site + 5 + displacement >= kCodeSize, "SSE4a stub is inside the original code segment");
|
||||
}
|
||||
|
||||
const auto function = image.At<std::uint64_t(std::uint64_t, std::uint64_t)>(0);
|
||||
for (const auto [value, destination] : {std::pair{0x1122334455667788ull, 0x0123456789ABCDEFull}, {0ull, ~0ull}, {~0ull, 0ull}, {0x9E3779B97F4A7C15ull, 0x0F1E2D3C4B5A6978ull}})
|
||||
require(function(value, destination) == expected(value, destination), "Relinked SSE4a stubs computed the wrong value");
|
||||
std::cout << "--to-intel Linux stub tests passed\n";
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue