Added patchcompile module handling

This commit is contained in:
York Jasper Niebuhr 2026-04-11 11:31:50 +02:00
parent 447431ae45
commit 30be283e15
8 changed files with 324 additions and 71 deletions

View File

@ -48,12 +48,15 @@ static bool global_target_cmp(const TARGET& a, const TARGET& b) {
return true; return true;
} }
static std::size_t accumulate_global_target(TARGET&& target) { static std::size_t accumulate_global_target(TARGET&& target, bool& was_new) {
was_new = false;
for (const auto& [guid, gtarget] : targets) { for (const auto& [guid, gtarget] : targets) {
if (global_target_cmp(gtarget, target)) if (global_target_cmp(gtarget, target))
return guid; return guid;
} }
was_new = true;
std::size_t guid = next_global_target_uid++; std::size_t guid = next_global_target_uid++;
targets.emplace(guid, std::move(target)); targets.emplace(guid, std::move(target));
return guid; return guid;
@ -75,7 +78,7 @@ dpin <symbol> <offset> <level> <target uid>
dpin <symbol> <offset> <level> <target uid> dpin <symbol> <offset> <level> <target uid>
... ...
*/ */
static bool accumulate_file(const fs::path& path) { static bool accumulate_file(const fs::path& path, bool no_new_targets) {
std::ifstream infile(path); std::ifstream infile(path);
if (!infile) if (!infile)
return false; return false;
@ -169,7 +172,14 @@ static bool accumulate_file(const fs::path& path) {
fit++; fit++;
} }
std::size_t global_target_uid = accumulate_global_target(std::move(target)); bool was_new = false;
std::size_t global_target_uid = accumulate_global_target(std::move(target), was_new);
if (no_new_targets && was_new) {
std::cerr << "Encountered new target but --no-new-targets is set!" << std::endl;
return false;
}
cu.local_targets.emplace(local_uid, global_target_uid); cu.local_targets.emplace(local_uid, global_target_uid);
continue; continue;
} else if (type == "ipin") { } else if (type == "ipin") {
@ -223,7 +233,7 @@ static bool accumulate_file(const fs::path& path) {
return true; return true;
} }
bool accumulate(const std::vector<std::string>& spslr_files) { bool accumulate(const std::vector<std::string>& spslr_files, bool no_new_targets) {
for (const std::string& spslr_file : spslr_files) { for (const std::string& spslr_file : spslr_files) {
fs::path p { spslr_file }; fs::path p { spslr_file };
@ -237,7 +247,7 @@ bool accumulate(const std::vector<std::string>& spslr_files) {
return false; return false;
} }
if (!accumulate_file(p)) { if (!accumulate_file(p, no_new_targets)) {
std::cerr << "Failed to parse metadata file " << p << "!" << std::endl; std::cerr << "Failed to parse metadata file " << p << "!" << std::endl;
return false; return false;
} }
@ -245,3 +255,116 @@ bool accumulate(const std::vector<std::string>& spslr_files) {
return true; return true;
} }
bool dump_target_map(const std::string& path) {
std::filesystem::path p{path};
if (p.has_parent_path()) {
std::error_code ec;
std::filesystem::create_directories(p.parent_path(), ec);
if (ec) {
std::cerr << "Failed to create target-map directory: " << ec.message() << "\n";
return false;
}
}
std::ofstream out(p);
if (!out)
return false;
out << "SPSLR_TARGETS 1\n";
for (const auto& [uid, t] : targets) {
out << "target " << t.name << " " << uid << " " << t.size << " " << t.fields.size() << "\n";
for (const auto& [off, f] : t.fields) {
(void)off;
out << "f " << f.offset << " " << f.size << " " << f.alignment << " " << f.flags << "\n";
}
}
return !!out;
}
bool load_target_map(const std::string& path) {
std::ifstream in(path);
if (!in)
return false;
std::string magic;
std::size_t version = 0;
if (!(in >> magic >> version) || magic != "SPSLR_TARGETS" || version != 1) {
std::cerr << "Invalid target map header\n";
return false;
}
std::string line;
std::getline(in, line); // consume rest of header line
std::size_t max_uid = 0;
bool have_any = false;
while (std::getline(in, line)) {
if (line.empty())
continue;
std::istringstream iss(line);
std::string tag;
iss >> tag;
if (tag != "target") {
std::cerr << "Expected target entry in target map\n";
return false;
}
TARGET t{};
std::size_t uid = 0;
std::size_t field_count = 0;
if (!(iss >> t.name >> uid >> t.size >> field_count)) {
std::cerr << "Malformed target entry in target map\n";
return false;
}
for (std::size_t i = 0; i < field_count; ++i) {
std::string fline;
if (!std::getline(in, fline)) {
std::cerr << "Missing field entry in target map\n";
return false;
}
std::istringstream fiss(fline);
std::string ftag;
FIELD f{};
if (!(fiss >> ftag) || ftag != "f" || !(fiss >> f.offset >> f.size >> f.alignment >> f.flags)) {
std::cerr << "Malformed field entry in target map\n";
return false;
}
f.idx = i;
if (t.fields.contains(f.offset)) {
std::cerr << "Duplicate field offset in target map\n";
return false;
}
t.fields.emplace(f.offset, f);
}
if (targets.contains(uid)) {
std::cerr << "Duplicate target uid in target map\n";
return false;
}
targets.emplace(uid, std::move(t));
if (!have_any || uid > max_uid) {
max_uid = uid;
have_any = true;
}
}
if (have_any)
next_global_target_uid = (max_uid + 1);
return true;
}

View File

@ -49,4 +49,6 @@ struct CU {
extern std::unordered_map<std::size_t, TARGET> targets; extern std::unordered_map<std::size_t, TARGET> targets;
extern std::unordered_map<std::string, CU> units; extern std::unordered_map<std::string, CU> units;
bool accumulate(const std::vector<std::string>& spslr_files); bool accumulate(const std::vector<std::string>& spslr_files, bool no_new_targets);
bool dump_target_map(const std::string& path);
bool load_target_map(const std::string& path);

View File

@ -101,6 +101,13 @@ static bool emit_header(std::ostream& out) {
return !!out; return !!out;
} }
static bool emit_module_header(std::ostream& out) {
// Shared objects with read-only relocations cause warnings
out << ".section .data.rel.ro.spslr,\"aw\",@progbits\n";
out << ".balign 8\n";
return !!out;
}
static bool emit_u32_object(std::ostream& out, const char* name, uint32_t value) { static bool emit_u32_object(std::ostream& out, const char* name, uint32_t value) {
out << ".globl " << name << "\n"; out << ".globl " << name << "\n";
out << ".type " << name << ", @object\n"; out << ".type " << name << ", @object\n";
@ -112,6 +119,9 @@ static bool emit_u32_object(std::ostream& out, const char* name, uint32_t value)
} }
static bool emit_targets(std::ostream& out, const std::vector<TARGET_REC>& targets) { static bool emit_targets(std::ostream& out, const std::vector<TARGET_REC>& targets) {
if (!emit_u32_object(out, "spslr_target_cnt", static_cast<uint32_t>(targets.size())))
return false;
out << ".globl spslr_targets\n"; out << ".globl spslr_targets\n";
out << ".type spslr_targets, @object\n"; out << ".type spslr_targets, @object\n";
out << ".balign 4\n"; out << ".balign 4\n";
@ -128,6 +138,9 @@ static bool emit_targets(std::ostream& out, const std::vector<TARGET_REC>& targe
} }
static bool emit_target_fields(std::ostream& out, const std::vector<TARGET_FIELD_REC>& fields) { static bool emit_target_fields(std::ostream& out, const std::vector<TARGET_FIELD_REC>& fields) {
if (!emit_u32_object(out, "spslr_target_field_cnt", static_cast<uint32_t>(fields.size())))
return false;
out << ".globl spslr_target_fields\n"; out << ".globl spslr_target_fields\n";
out << ".type spslr_target_fields, @object\n"; out << ".type spslr_target_fields, @object\n";
out << ".balign 4\n"; out << ".balign 4\n";
@ -145,6 +158,9 @@ static bool emit_target_fields(std::ostream& out, const std::vector<TARGET_FIELD
} }
static bool emit_ipins(std::ostream& out, const std::vector<IPIN_REC>& ipins) { static bool emit_ipins(std::ostream& out, const std::vector<IPIN_REC>& ipins) {
if (!emit_u32_object(out, "spslr_ipin_cnt", static_cast<uint32_t>(ipins.size())))
return false;
out << ".globl spslr_ipins\n"; out << ".globl spslr_ipins\n";
out << ".type spslr_ipins, @object\n"; out << ".type spslr_ipins, @object\n";
out << ".balign 8\n"; out << ".balign 8\n";
@ -161,6 +177,9 @@ static bool emit_ipins(std::ostream& out, const std::vector<IPIN_REC>& ipins) {
} }
static bool emit_ipin_ops(std::ostream& out, const std::vector<IPIN_OP_REC>& ops) { static bool emit_ipin_ops(std::ostream& out, const std::vector<IPIN_OP_REC>& ops) {
if (!emit_u32_object(out, "spslr_ipin_op_cnt", static_cast<uint32_t>(ops.size())))
return false;
out << ".globl spslr_ipin_ops\n"; out << ".globl spslr_ipin_ops\n";
out << ".type spslr_ipin_ops, @object\n"; out << ".type spslr_ipin_ops, @object\n";
out << ".balign 4\n"; out << ".balign 4\n";
@ -177,6 +196,9 @@ static bool emit_ipin_ops(std::ostream& out, const std::vector<IPIN_OP_REC>& ops
} }
static bool emit_dpins(std::ostream& out, const std::vector<DPIN_REC>& dpins) { static bool emit_dpins(std::ostream& out, const std::vector<DPIN_REC>& dpins) {
if (!emit_u32_object(out, "spslr_dpin_cnt", static_cast<uint32_t>(dpins.size())))
return false;
out << ".globl spslr_dpins\n"; out << ".globl spslr_dpins\n";
out << ".type spslr_dpins, @object\n"; out << ".type spslr_dpins, @object\n";
out << ".balign 8\n"; out << ".balign 8\n";
@ -193,37 +215,45 @@ static bool emit_dpins(std::ostream& out, const std::vector<DPIN_REC>& dpins) {
} }
bool emit_patcher_program_asm(std::ostream& out) { bool emit_patcher_program_asm(std::ostream& out, bool is_module) {
if (!emit_header(out)) if (!is_module) {
return false; if (!emit_header(out))
return false;
} else {
if (!emit_module_header(out))
return false;
}
// Important: target UID == index in spslr_targets[] for the new interface. // Important: target UID == index in spslr_targets[] for the new interface.
std::vector<TARGET_REC> target_recs(targets.size()); std::vector<TARGET_REC> target_recs(targets.size());
std::vector<TARGET_FIELD_REC> field_recs; std::vector<TARGET_FIELD_REC> field_recs;
field_recs.reserve(64);
for (uint32_t uid = 0; uid < static_cast<uint32_t>(targets.size()); ++uid) { if (!is_module) {
if (!targets.contains(uid)) field_recs.reserve(64);
return false;
const TARGET& target = targets.at(uid); for (uint32_t uid = 0; uid < static_cast<uint32_t>(targets.size()); ++uid) {
if (!targets.contains(uid))
return false;
TARGET_REC trec{}; const TARGET& target = targets.at(uid);
trec.size = static_cast<uint32_t>(target.size);
trec.fieldoff = static_cast<uint32_t>(field_recs.size());
trec.fieldcnt = static_cast<uint32_t>(target.fields.size());
for (const auto& [off, field] : target.fields) { TARGET_REC trec{};
(void)off; trec.size = static_cast<uint32_t>(target.size);
field_recs.push_back(TARGET_FIELD_REC{ trec.fieldoff = static_cast<uint32_t>(field_recs.size());
.offset = static_cast<uint32_t>(field.offset), trec.fieldcnt = static_cast<uint32_t>(target.fields.size());
.size = static_cast<uint32_t>(field.size),
.alignment = static_cast<uint32_t>(field.alignment), for (const auto& [off, field] : target.fields) {
.flags = static_cast<uint32_t>(field.flags), (void)off;
}); field_recs.push_back(TARGET_FIELD_REC{
.offset = static_cast<uint32_t>(field.offset),
.size = static_cast<uint32_t>(field.size),
.alignment = static_cast<uint32_t>(field.alignment),
.flags = static_cast<uint32_t>(field.flags),
});
}
target_recs[uid] = trec;
} }
target_recs[uid] = trec;
} }
std::vector<IPIN_REC> ipin_recs; std::vector<IPIN_REC> ipin_recs;
@ -289,28 +319,17 @@ bool emit_patcher_program_asm(std::ostream& out) {
} }
} }
if (!emit_u32_object(out, "spslr_target_cnt", static_cast<uint32_t>(target_recs.size()))) if (!is_module) {
return false; if (!emit_targets(out, target_recs))
if (!emit_targets(out, target_recs)) return false;
return false; if (!emit_target_fields(out, field_recs))
return false;
}
if (!emit_u32_object(out, "spslr_target_field_cnt", static_cast<uint32_t>(field_recs.size())))
return false;
if (!emit_target_fields(out, field_recs))
return false;
if (!emit_u32_object(out, "spslr_ipin_cnt", static_cast<uint32_t>(ipin_recs.size())))
return false;
if (!emit_ipins(out, ipin_recs)) if (!emit_ipins(out, ipin_recs))
return false; return false;
if (!emit_u32_object(out, "spslr_ipin_op_cnt", static_cast<uint32_t>(ipin_ops.size())))
return false;
if (!emit_ipin_ops(out, ipin_ops)) if (!emit_ipin_ops(out, ipin_ops))
return false; return false;
if (!emit_u32_object(out, "spslr_dpin_cnt", static_cast<uint32_t>(dpin_recs.size())))
return false;
if (!emit_dpins(out, dpin_recs)) if (!emit_dpins(out, dpin_recs))
return false; return false;

View File

@ -1,4 +1,4 @@
#pragma once #pragma once
#include <fstream> #include <fstream>
bool emit_patcher_program_asm(std::ostream& out); bool emit_patcher_program_asm(std::ostream& out, bool is_module);

View File

@ -14,58 +14,91 @@ Notes:
Between CUs, types with the same name HAVE TO HAVE the same layout -> randomized together Between CUs, types with the same name HAVE TO HAVE the same layout -> randomized together
*/ */
struct OPTIONS {
std::string out_file;
std::string load_targets_file;
std::string dump_targets_file;
std::vector<std::string> spslr_files;
bool no_new_targets = false;
bool is_module = false;
};
int main(int argc, char** argv) { int main(int argc, char** argv) {
static option long_options[] = { static option long_options[] = {
{ "help", no_argument, 0, 0 }, { "help", no_argument, 0, 0 },
{ "out", required_argument, 0, 0 }, { "out", required_argument, 0, 0 },
{ "load-targets", required_argument, 0, 0 },
{ "dump-targets", required_argument, 0, 0 },
{ "no-new-targets", no_argument, 0, 0 },
{ "module", no_argument, 0, 0 },
{ 0, 0, 0, 0 } { 0, 0, 0, 0 }
}; };
OPTIONS opts{};
int option_index = 0; int option_index = 0;
int c; int c;
std::vector<std::string> spslr_files;
std::string out_file;
while ((c = getopt_long(argc, argv, "h:o:", long_options, &option_index)) == 0) { while ((c = getopt_long(argc, argv, "h:o:", long_options, &option_index)) == 0) {
const option& opt = long_options[option_index]; const option& opt = long_options[option_index];
std::string optname { opt.name }; std::string optname { opt.name };
if (optname == "help") { if (optname == "help") {
std::cout << "To use spslr_patchcompile, supply these arguments:" << std::endl; std::cout
std::cout << " --out=<file> (the compiled asm file to be written)" << std::endl; << "Usage:\n"
std::cout << " <file>... (one or more .spslr metadata files)" << std::endl; << " spslr_patchcompile --out=<file> [options] <file>...\n\n"
<< "Options:\n"
<< " --read-targets=<file>\n"
<< " --emit-targets=<file>\n"
<< " --no-new-targets\n"
<< " --module\n";
return 0; return 0;
} else if (optname == "out") { } else if (optname == "out") {
out_file = std::string{ optarg }; opts.out_file = optarg;
} else if (optname == "load-targets") {
opts.load_targets_file = optarg;
} else if (optname == "dump-targets") {
opts.dump_targets_file = optarg;
} else if (optname == "no-new-targets") {
opts.no_new_targets = true;
} else if (optname == "module") {
opts.is_module = true;
} else { } else {
std::cerr << "Invalid option, try \"--help\"!" << std::endl; std::cerr << "Invalid option, try \"--help\"!\n";
return 1; return 1;
} }
} }
if (out_file.empty()) {
std::cerr << "Missing output file path, supply it via --out=<file>!" << std::endl;
return 1;
}
for (int i = optind; i < argc; ++i) for (int i = optind; i < argc; ++i)
spslr_files.emplace_back(argv[i]); opts.spslr_files.emplace_back(argv[i]);
if (spslr_files.empty()) { if (opts.out_file.empty()) {
std::cerr << "Missing spslr files! Pass one or more .spslr files as positional arguments." << std::endl; std::cerr << "Missing output file path, supply it via --out=<file>!\n";
return 1; return 1;
} }
if (!accumulate(spslr_files)) { if (opts.spslr_files.empty()) {
std::cerr << "Missing spslr files! Pass one or more .spslr metadata files as positional arguments.\n";
return 1;
}
if (opts.no_new_targets && opts.load_targets_file.empty()) {
std::cerr << "--no-new-targets requires --load-targets\n";
return 1;
}
if (!opts.load_targets_file.empty()) {
if (!load_target_map(opts.load_targets_file)) {
std::cerr << "Failed to load target map: " << opts.load_targets_file << "\n";
return 1;
}
}
if (!accumulate(opts.spslr_files, opts.no_new_targets)) {
std::cerr << "Failed to accumulate data from spslr directory!" << std::endl; std::cerr << "Failed to accumulate data from spslr directory!" << std::endl;
return 1; return 1;
} }
std::cout << "Gathered a total of " << targets.size() << " distinct targets from " std::filesystem::path out_path { opts.out_file };
<< units.size() << " compilation units!" << std::endl;
std::filesystem::path out_path { out_file };
if (out_path.has_parent_path()) { if (out_path.has_parent_path()) {
std::error_code ec; std::error_code ec;
std::filesystem::create_directories(out_path.parent_path(), ec); std::filesystem::create_directories(out_path.parent_path(), ec);
@ -83,11 +116,18 @@ int main(int argc, char** argv) {
return 1; return 1;
} }
if (!emit_patcher_program_asm(out)) { if (!emit_patcher_program_asm(out, opts.is_module)) {
std::cerr << "Failed to write emit patcher program!" << std::endl; std::cerr << "Failed to write emit patcher program!" << std::endl;
return 1; return 1;
} }
if (!opts.dump_targets_file.empty()) {
if (!dump_target_map(opts.dump_targets_file)) {
std::cerr << "Failed to write target map: " << opts.dump_targets_file << "\n";
return 1;
}
}
return 0; return 0;
} }

View File

@ -1,6 +1,22 @@
#ifndef SPSLR_SELFPATCH_H #ifndef SPSLR_SELFPATCH_H
#define SPSLR_SELFPATCH_H #define SPSLR_SELFPATCH_H
#define SPSLR_MODULE_SYM_IPIN_CNT "spslr_ipin_cnt"
#define SPSLR_MODULE_SYM_IPINS "spslr_ipins"
#define SPSLR_MODULE_SYM_IPIN_OP_CNT "spslr_ipin_op_cnt"
#define SPSLR_MODULE_SYM_IPIN_OPS "spslr_ipin_ops"
#define SPSLR_MODULE_SYM_DPIN_CNT "spslr_dpin_cnt"
#define SPSLR_MODULE_SYM_DPINS "spslr_dpins"
struct spslr_module {
const void* ipin_cnt;
const void* ipins;
const void* ipin_op_cnt;
const void* ipin_ops;
const void* dpin_cnt;
const void* dpins;
};
void spslr_selfpatch(void); void spslr_selfpatch(void);
#endif #endif

View File

@ -32,17 +32,25 @@ endforeach()
set(SUBJECT_SPSLR_ASM "${CMAKE_CURRENT_BINARY_DIR}/subject_spslr_program.S") set(SUBJECT_SPSLR_ASM "${CMAKE_CURRENT_BINARY_DIR}/subject_spslr_program.S")
set(SUBJECT_SPSLR_OBJ "${CMAKE_CURRENT_BINARY_DIR}/subject_spslr_program.o") set(SUBJECT_SPSLR_OBJ "${CMAKE_CURRENT_BINARY_DIR}/subject_spslr_program.o")
set(SUBJECT_TARGET_MAP "${CMAKE_CURRENT_BINARY_DIR}/subject.spslr_targets")
add_custom_command( add_custom_command(
OUTPUT "${SUBJECT_SPSLR_ASM}" OUTPUT "${SUBJECT_SPSLR_ASM}" "${SUBJECT_TARGET_MAP}"
COMMAND $<TARGET_FILE:spslr_patchcompile> COMMAND $<TARGET_FILE:spslr_patchcompile>
--out=${SUBJECT_SPSLR_ASM} --out=${SUBJECT_SPSLR_ASM}
--dump-targets=${SUBJECT_TARGET_MAP}
${SUBJECT_SPSLR_FILES} ${SUBJECT_SPSLR_FILES}
DEPENDS DEPENDS
spslr_patchcompile spslr_patchcompile
$<TARGET_OBJECTS:subject_objs> subject_objs
${SUBJECT_SPSLR_FILES}
VERBATIM VERBATIM
) )
add_custom_target(subject_spslr_metadata
DEPENDS "${SUBJECT_SPSLR_ASM}" "${SUBJECT_TARGET_MAP}"
)
add_custom_command( add_custom_command(
OUTPUT "${SUBJECT_SPSLR_OBJ}" OUTPUT "${SUBJECT_SPSLR_OBJ}"
COMMAND ${CMAKE_C_COMPILER} COMMAND ${CMAKE_C_COMPILER}
@ -104,10 +112,15 @@ add_custom_command(
OUTPUT "${MODULE_SPSLR_ASM}" OUTPUT "${MODULE_SPSLR_ASM}"
COMMAND $<TARGET_FILE:spslr_patchcompile> COMMAND $<TARGET_FILE:spslr_patchcompile>
--out=${MODULE_SPSLR_ASM} --out=${MODULE_SPSLR_ASM}
--load-targets=${SUBJECT_TARGET_MAP}
--no-new-targets
--module
${MODULE_SPSLR_FILES} ${MODULE_SPSLR_FILES}
DEPENDS DEPENDS
spslr_patchcompile spslr_patchcompile
$<TARGET_OBJECTS:spslr_module_objs> subject_spslr_metadata
spslr_module_objs
${MODULE_SPSLR_FILES}
VERBATIM VERBATIM
) )

View File

@ -22,6 +22,37 @@ struct task_struct global = { .pid = 42, .comm = "main_global", .arrfun = {
EXPORT_SYMBOL(global); EXPORT_SYMBOL(global);
static int fetch_module_spslr_symbols(void* handle, struct spslr_module* mod) {
if (!handle || !mod)
return -1;
mod->ipin_cnt = dlsym(handle, SPSLR_MODULE_SYM_IPIN_CNT);
if (!mod->ipin_cnt)
return -1;
mod->ipins = dlsym(handle, SPSLR_MODULE_SYM_IPINS);
if (!mod->ipins)
return -1;
mod->ipin_op_cnt = dlsym(handle, SPSLR_MODULE_SYM_IPIN_OP_CNT);
if (!mod->ipin_op_cnt)
return -1;
mod->ipin_ops = dlsym(handle, SPSLR_MODULE_SYM_IPIN_OPS);
if (!mod->ipin_ops)
return -1;
mod->dpin_cnt = dlsym(handle, SPSLR_MODULE_SYM_DPIN_CNT);
if (!mod->dpin_cnt)
return -1;
mod->dpins = dlsym(handle, SPSLR_MODULE_SYM_DPINS);
if (!mod->dpins)
return -1;
return 0;
}
static int do_module_test_access_pid(const char *path, const struct task_struct *t) { static int do_module_test_access_pid(const char *path, const struct task_struct *t) {
typedef int (*module_test_access_fn)(const struct task_struct *t); typedef int (*module_test_access_fn)(const struct task_struct *t);
@ -33,6 +64,15 @@ static int do_module_test_access_pid(const char *path, const struct task_struct
dlerror(); dlerror();
struct spslr_module mod;
if (fetch_module_spslr_symbols(handle, &mod) < 0) {
fprintf(stderr, "failed to fetch spslr symbols in test module\n");
dlclose(handle);
return -1;
}
// TODO -> Patch module
module_test_access_fn fn = (module_test_access_fn)dlsym(handle, "module_test_access_pid"); module_test_access_fn fn = (module_test_access_fn)dlsym(handle, "module_test_access_pid");
const char *err = dlerror(); const char *err = dlerror();