Languages: English | 简体中文 | 繁體中文 | 日本語 | 한국어 | Français | Deutsch | Español | Italiano | Русский | العربية
This document explains how to extend the dyncode compiler to new target platforms. Currently supported: arm64 / x86_64 on macOS / Linux / Android / Windows (8 triples), each with independent User / Kernel contexts (16 variants total). Adding a new platform typically requires a few hundred lines of code.
Every pass is target-independent. Platform differences are concentrated in two places:
TargetDesc.cpp'sdescribeTriple()table entries- Three extractors (Mach-O / ELF / COFF) arch switches
Adding a new platform = one more row in (1), one more case in (2).
Add the corresponding OS branch in describeTriple():
if (TT.isOSFreeBSD()) {
D.OS = DynCodeOS::FreeBSD;
D.Format = ObjectFormat::ELF;
D.TextSectionName = ".text";
if (D.Arch == DynCodeArch::X86_64) {
D.Syscall = SyscallABI::FreeBSDSyscall;
D.AsmTemplate = "syscall";
D.SyscallNumberReg = "rax";
D.SyscallRetReg = "rax";
D.ArgRegs = kX86_64FreeBSDArgRegs;
D.NumArgRegs = 6;
D.DriverInjectFlags = kX86_64UnixInjectFlags;
}
return D;
}Required fields (all defined in TargetDesc.h):
| Field | Purpose | If missing |
|---|---|---|
OS / Arch / Format |
Dispatch key | describeTriple returns Unknown → driver rejects early |
TextSectionName |
Extractor finds entry section | Extractor cannot find .text → reject |
Syscall |
SyscallStubPass replacement decision | None → SyscallStubPass no-op |
AsmTemplate / SyscallNumberReg / SyscallRetReg / ArgRegs |
SyscallStubPass inline-asm generation | Any empty → SyscallStubPass no-op |
TCBReadAsm / TCBReadConstraint |
WinPEBImportPass TEB read inline-asm | Empty → PEB walk generates empty InlineAsm (Windows: required) |
DriverInjectFlags |
Platform-specific flags injected in dyncode mode | null → none injected |
- Add an enum value to
SyscallABIinTargetDesc.h - Add a
kXxxTableinSyscallTables.cpp - Add a case in
lookupSyscall's switch SyscallStubPassrequires no changes — InlineAsm templates/constraints are read fromTargetDesc
Windows has no stable syscall ABI; user calls to WriteFile / CreateThread / VirtualAlloc go through WinPEBImportPass. The whitelist is a multi-DLL table:
- Defined in
Tables/Win32Apis.def - Each row:
NEVERC_WIN32_API(ApiName, "dll.dll") - The resolver already supports arbitrary DLLs via the two-parameter
__neverc_win_resolve(dll_hash, api_hash)
Adding a new API (e.g., DeviceIoControl):
- Add one row to
Win32Apis.def - Mirror the declaration in
lib/Headers/windows.h's dyncode branch - No pass changes needed
Adding a new DLL bucket (e.g., winhttp.dll):
- Just add rows with the new DLL name in
Win32Apis.def
Three things to handle:
- Identify reloc types → patch bytes or reject
- Update forbidden data section name list (new OS may have its own sections)
- Update entry-at-offset-0 relocation target range validation
For an entirely new object format (e.g., WASM modules):
- Add an
ObjectFormatenum value - Add a case in
DynCodeExtractor.cpp's dispatch switch - Write
<Format>Extractor.cpp(followingELFExtractor.cpp's structure)
- Reference
tests/neverc/dyncode/loader_linux.candloader_windows.c - Typically:
mmap(RWX) → memcpy → icache flush → call
- Add a cross-compile check in
tests/neverc/DynCodeCrossTargetTests.cpp - If CI can execute on the platform, add a loader round-trip test
- Endianness: NeverC only supports little-endian (LE), covering all mainstream targets.
- ABI differences: Win64 (rcx/rdx/r8/r9) vs System V AMD64 (rdi/rsi/rdx/rcx/r8/r9) have completely different argument registers. This is handled at the NeverC frontend layer; the dyncode pipeline does not need to care.
- Syscall numbers: differ per architecture on Linux, Android matches Linux, Darwin has its own BSD numbers, Windows has no stable numbers (hence PEB walk). Indexed by (OS, arch) in the table.
- Cache coherency: ARM requires explicit i-cache flush; x86 does not. macOS arm64 JIT also needs
pthread_jit_write_protect_np; Linux arm64 uses__builtin___clear_cache; Windows usesFlushInstructionCache(no-op on x86). - SELinux / W^X: Android is constrained by SELinux
execmem; iOS on non-jailbroken devices completely rejectsmmap(RWX), requiringMAP_JIT+ code signing.
| Target | Estimated effort | Dependencies |
|---|---|---|
iOS arm64 (jailbreak / MAP_JIT) |
1 day | Reuse Mach-O extractor, modify loader |
| FreeBSD / OpenBSD x86_64 | Half day | Reuse ELF extractor + new syscall table |
| RISC-V64 Linux | 2 days | Needs RISC-V TargetDesc + new AllBlr variant + RISC-V reloc patching |
The dyncode pipeline exposes 11 interpose points via Pipeline.h::ObfuscationInterposes for third-party obfuscation libraries:
PipelineStartEP:
RunBeforePrep → [ZeroReloc Prep] → RunAfterPrep →
[IndirectBr → MemIntrin → CompilerRt → SyscallStub →
WinPEBImport → KernelImport → Data2Text phase 1] →
RunBeforeInlining
OptimizerLastEP:
RunAfterInlining → [Data2Text phase 2 → ZeroReloc Stackify] →
RunAfterStackify → [AllBlrPass] → RunAfterFinalIR
MIR: RunBeforePreEmit → [MIRPrepPass] → RunAfterPreEmit →
[LLVM addPreEmitPass/addPreEmitPass2] → RunAfterFinalMIR
Byte-stream: RunPostExtract → [finalize chain] → RunPostFinalize
IR-level usage:
neverc::dyncode::ObfuscationInterposes H;
H.RunAfterInlining = [](llvm::ModulePassManager &MPM,
const neverc::dyncode::DynCodeOptions &Opts) {
MPM.addPass(MyCFFPass(Opts.ObfuscateSpec));
};
// Register via Plugin API: NEVERC_INTERPOSE_SC_* interposes (see plugin-api docs)MIR-level usage:
H.RunAfterPreEmit = [](llvm::TargetPassConfig &TPC,
const neverc::dyncode::DynCodeOptions &Opts) {
TPC.addExternalPass(new MyInstructionSubstitutionPass(Opts.MirObfuscateSpec));
};Built-in MIR patching is also table-driven: Tables/MIRRewritePatterns.def records pattern diagnostic names, arch filters, and helper names; Tables/MIRRewriteOpcodes.def records backend opcode names. When adding new dyncode-friendly backend forms, prefer adding table entries and narrow helpers over scattering target-specific branches in the pass body.