Files
Mergen/lifter/semantics/Semantics_ControlFlow.ipp
naci 605a36e8ed lifter: correctness fixes, refactors, and regression tests (#205)
* lifter: restore indirect-jump threshold to 128

* gitignore: glob output_*.ll instead of enumerating dumps

Replace output_finalnoopt.ll / output_no_opts.ll entries with
output_*.ll so ad-hoc lifter dumps (output_rets.ll, output_newpath.ll,
etc.) stop showing up in git status.

* lifter: factor REAL_return path through emitResolvedFunctionReturn

Pull the rax-zext + CreateRet + run/finished bookkeeping out of the
REAL_return branch in lift_ret() into a local lambda so future ret
exit points can reuse it without duplicating four lines of
boilerplate.

Drop the dead returnStruct/myStruct scaffolding and the
originalFunc_finalnopt local: every InsertValue call site has been
commented out for a long time and the locals had no remaining uses.
The active code emits a plain rax return.

No behavior change.

* lifter: advance RSP past continuation slot in ret-to-IAT chain

In the chained import-return pattern (`ret` to IAT slot, IAT slot
holds an external function address, the function returns and control
resumes at the next stack slot's continuation address), the lifter
collapses the two pops into a single `call @import; br contBB`. RSP
was only advanced past the IAT slot itself, so post-call register
state still claimed RSP pointed at the continuation address. Any
downstream stack read from RSP saw stale data and any solver that
constant-folded RSP picked up a value that no longer matched the
post-chain physical layout.

Bump RSP by another `ptrSize` immediately before lowering the
import call so the continuation block inherits the same RSP it would
have under a faithful two-pop lowering.

* lifter/test: regression test for ret-to-IAT chain RSP advancement

Locks in dd95fe7. The microtest stands up a LifterUnderTest, plants
[importVA, contVA] on the stack at an RSP that is intentionally NOT
equal to STACKP_VALUE (so the lift_ret REAL_return short-circuit does
not fire), registers the import in the lifter's importMap, and lifts
a single `ret` (0xC3).

It then asserts that:
- the chain handler emitted a direct call to the registered import
- RSP after the chain equals entry RSP + 16, not + 8

Without the fix the test fails with RSP = entry + 8 (only the IAT
slot pop is modeled), exactly the off-by-8 the fix closes.

Verified the test catches the regression by reverting dd95fe7
locally before re-applying — the failing message reads
"RSP after chain = 0x14FDA8; expected 0x14fdb0".

* scripts/themida: filter lifter-synthesized helpers from import diff

Calls to lifter-emitted helpers (`@exception`, `@fastfail`,
`@not_implemented`, etc.) surfaced as 'extra import (not required)'
lines on every Themida equivalence run. They are not user imports;
they are lowered from INT1/INT3/UD2/INT29/SYSCALL/segment-load
sites in the lifter's own semantics files.

Skip them in `_extract_call_names` so the equivalence diff shows
only real imports. The list of helpers lives next to the call regex
so it stays adjacent to the code that emits them; if a new helper
shows up in the IR (e.g. another illegal-instruction lowering) the
script will surface it as an 'extra import' until the entry is added
here, which is the right tripwire.

Before: example2 \xe2\x80\x94 6 distinct imports, 10 calls (3 noise calls)
After:  example2 \xe2\x80\x94 4 distinct imports, 7 calls (clean)

* lifter/analysis: replace 'TODO: fix?' marker with positive explanation

The 2-value path-solving fork's swap branch had a 'TODO: fix?'
comment from the original draft. Traced both branches and confirmed
the swap is correct:

- When the select's trueValue equals firstcase, condition is the
  select's condition as-is and firstcase\xe2\x86\x92bb_true wires correctly.
- When trueValue equals secondcase, condition still expresses 'true
  picks trueValue' but downstream code uses firstcase\xe2\x86\x92bb_true.
  Swapping firstcase\xe2\x86\x94secondcase makes firstcase refer to the trueVal
  constant so the existing CreateCondBr wiring stays correct without
  a parallel reversed-branch path.

Replaced the TODO with a comment that explains why the swap is
necessary, so future readers do not waste time investigating a
branch that is intentional.

* lifter: accept Register64/Memory64 source for punpcklqdq

Iced classifies operand types by the bytes the instruction actually
accesses, not by physical register width. PUNPCKLQDQ only reads the
low 64 bits of its second operand, so Iced reports Register64 (or
Memory64 for the m128 form) for a source whose physical encoding is
`xmm/m128`. The lift handler's accept check rejected anything other
than Register128/Memory128 and fell through to the not_implemented
exit, so every `punpcklqdq xmm, xmm/m128` site lowered to a bogus
`call @not_implemented; ret` instead of the unpack semantic.

Widen the accept set to Register64 and Memory64 too. The body
already truncates the source to i64 before OR'ing it into the high
half of the result, so a 64-bit-typed source is semantically
identical to a 128-bit one for this handler.

Fixes the two pre-existing oracle test failures
`punpcklqdq_xmm0_xmm1_basic` and
`punpcklqdq_xmm0_xmm1_zero_upper_from_zero_source`. `python test.py
all` stays at 244/244, confirming no semantic regressions.

* lifter: replace lift_jmp's fallthrough switch with an isDirectJump if

The RIP-relative add for direct jumps lived inside a 4-case switch
whose body intentionally fell through into `default: break;`. It
worked, but:

- Implicit fallthrough is a -Wimplicit-fallthrough hazard. Today the
  default does nothing; tomorrow someone adds a body and every direct
  jump silently runs it.
- The switch's discriminator is exactly `isDirectJump`, which is
  already computed two lines above for the path-solver context. The
  switch was a parallel restatement of the same predicate.

Collapse the switch into `if (isDirectJump) { trunc = add(trunc,
ripval); }` so the predicate has one definition and there is no
fallthrough to misuse. Behavior unchanged: the same immediate cases
still get the RIP-relative bump, indirect jumps still skip it, and
`python test.py all` stays at 244/244.

* lifter/test: regression test for SSE memory-form handler dispatch

Lock in that pand/por/pxor accept the `xmm, [mem]` encoding form. The
test lifts `66 0F DB 00`, `66 0F EB 00`, and `66 0F EF 00` (one
`xmm0, [rax]` site each) and asserts that the lifted function does
not contain a direct call to @not_implemented.

Pure structural acceptance: not validating bitwise-AND/OR/XOR
semantics, only that the handler dispatched at all. Iced today
reports Memory128 for these encodings so the test passes against the
existing `Register128 || Memory128` accept sets. If a future Iced
update reclassifies the source operand by bytes-actually-accessed
(the way it already does for punpcklqdq, where it reports
Register64/Memory64 even for an `xmm/m128` encoding) the handler
would silently fall through to `call @not_implemented; ret` and
miscompile every memory-form site \u2014 this test trips first.

* lifter: drop duplicate stdout print on unresolved indirect jmp

`lift_jmp` printed every UnresolvedIndirectJump twice: once as a raw
`std::cout << "[diag] lift_jmp: ..."` and once through
`diagnostics.warning(...)` on the very next line. The diagnostics
framework already persists the warning to `output_diagnostics.json`
at lift completion, and no script or test grep'd the stdout form.

Drop the std::cout. The diagnostic remains in the recorded diagnostics
list, surfaceable via the JSON dump or the in-memory entries vector.
This removes the only unguarded raw `[diag]` print in the lift path
-- the rest are gated on `liftProgressDiagEnabled` or specific hot
addresses for active debugging.

* scripts/themida: fix docstring escape leak in import-filter doc

Audit of #205 caught a literal `\\u2014` and unnecessary
`\\"` escapes in the `_extract_call_names` docstring \xe2\x80\x94 leftovers
from how the surrounding commit (#205, scripts/themida: filter
lifter-synthesized helpers) was authored. Replace the literal
escape with a plain `--` and drop the redundant backslash-quotes;
the docstring now renders cleanly at `help(_extract_call_names)`
and looks normal in the source.

Behavior unchanged: `python test.py themida` still passes with
the same import-diff filter (4 imports, 7 calls for example2).

---------

Co-authored-by: yusufcanislek <yusuf.canislek@meetdandy.com>
2026-05-02 11:58:47 +03:00

926 lines
30 KiB
C++

// Semantics_ControlFlow.ipp — mov, cmov, call, ret, jmp, conditional branches
MERGEN_LIFTER_DEFINITION_TEMPLATES(void)::lift_mov() {
LLVMContext& context = builder->getContext();
// auto Rvalue2 = GetIndexValue(src, src.size,
// std::to_string(current_address));
auto Rvalue = GetIndexValue(1);
printvalue(Rvalue);
switch (instruction.mnemonic) {
case Mnemonic::MOVSX: {
Rvalue = createSExtFolder(
Rvalue, Type::getIntNTy(context, GetTypeSize(instruction.types[0])),
"movsx-" + std::to_string(current_address) + "-");
break;
}
case Mnemonic::MOVZX: {
Rvalue = createZExtFolder(
Rvalue, Type::getIntNTy(context, GetTypeSize(instruction.types[0])),
"movzx-" + std::to_string(current_address) + "-");
break;
}
case Mnemonic::MOVSXD: {
Rvalue = createSExtFolder(
Rvalue, Type::getIntNTy(context, GetTypeSize(instruction.types[0])),
"movsxd-" + std::to_string(current_address) + "-");
break;
}
default: {
break;
}
}
printvalue(Rvalue);
switch (instruction.types[1]) {
// case OperandType::Immediate64:
case OperandType::Immediate8:
case OperandType::Immediate16:
case OperandType::Immediate32: {
Rvalue = createSExtFolder(
Rvalue, Type::getIntNTy(context, GetTypeSize(instruction.types[0])));
break;
}
default:
break;
}
printvalue(Rvalue);
SetIndexValue(0, Rvalue);
// Provenance tagging: if this is `mov reg, [rip+disp]` and disp+RIP
// resolves to an IAT slot, remember which import the register now holds.
// A later `call reg` can then emit a named external call without needing
// SSA-level back-tracing through the folded load.
if (instruction.types[0] >= OperandType::Register8 &&
instruction.types[0] <= OperandType::Register64 &&
instruction.types[1] >= OperandType::Memory8 &&
instruction.types[1] <= OperandType::Memory64 &&
instruction.mem_base == Register::RIP &&
instruction.mem_index == Register::None) {
uint64_t ea = current_address + instruction.mem_disp;
auto it = importMap.find(ea);
if (it != importMap.end()) {
registerImportSource[getBiggestEncoding(instruction.regs[0])] =
it->second;
}
}
}
MERGEN_LIFTER_DEFINITION_TEMPLATES(void)::lift_cmovcc() {
auto getCondition = [&] {
switch (instruction.mnemonic) {
case Mnemonic::CMOVZ: {
return getFlag(FLAG_ZF);
}
case Mnemonic::CMOVNZ: {
return createNotFolder(getFlag(FLAG_ZF));
}
case Mnemonic::CMOVB: {
return getFlag(FLAG_CF);
}
case Mnemonic::CMOVNB: {
return createNotFolder(getFlag(FLAG_CF));
}
case Mnemonic::CMOVBE: {
return createOrFolder(getFlag(FLAG_CF), getFlag(FLAG_ZF));
}
case Mnemonic::CMOVNBE: {
return createNotFolder(
createOrFolder(getFlag(FLAG_CF), getFlag(FLAG_ZF)));
}
case Mnemonic::CMOVL: {
return createXorFolder(getFlag(FLAG_SF), getFlag(FLAG_OF));
}
case Mnemonic::CMOVNL: {
// equal
return createNotFolder(
createXorFolder(getFlag(FLAG_SF), getFlag(FLAG_OF)));
}
case Mnemonic::CMOVLE: {
return createOrFolder(createXorFolder(getFlag(FLAG_SF), getFlag(FLAG_OF)),
getFlag(FLAG_ZF));
}
case Mnemonic::CMOVNLE: {
return createAndFolder(
createNotFolder(createXorFolder(getFlag(FLAG_SF), getFlag(FLAG_OF))),
createNotFolder(getFlag(FLAG_ZF)));
}
case Mnemonic::CMOVO: {
return getFlag(FLAG_OF);
}
case Mnemonic::CMOVNO: {
return createNotFolder(getFlag(FLAG_OF));
}
case Mnemonic::CMOVS: {
return getFlag(FLAG_SF);
}
case Mnemonic::CMOVNS: {
return createNotFolder(getFlag(FLAG_SF));
}
case Mnemonic::CMOVP: {
return getFlag(FLAG_PF);
}
case Mnemonic::CMOVNP: {
return createNotFolder(getFlag(FLAG_PF));
}
default: {
return static_cast<Value*>(nullptr);
}
}
};
auto dest = GetIndexValue(0);
auto src = GetIndexValue(1);
auto result = createSelectFolder(getCondition(), src, dest);
SetIndexValue(0, result);
}
// for now assume every call is fake
MERGEN_LIFTER_DEFINITION_TEMPLATES(void)::lift_call() {
LLVMContext& context = builder->getContext();
auto RspValue = GetRegisterValue(Register::RSP);
auto val = ConstantInt::getSigned(Type::getInt64Ty(context),
file.getMode() == arch_mode::X64 ? 8 : 4);
auto result = createSubFolder(RspValue, val, "pushing_newrsp");
uint64_t jump_address = current_address;
std::string block_name = "jmp_call-" + std::to_string(jump_address) + "-";
// Track whether we emitted an external CreateCall (not inlineable).
// When true, skip the Unflatten inlining path below.
bool emittedExternalCall = false;
// Fast path: detect `call [rip+disp]` (FF 15) direct IAT calls.
// These are memory-operand calls where the effective address is an IAT slot.
// Resolve the import name and emit a named function call without loading
// the stale on-disk IAT value.
if (instruction.types[0] >= OperandType::Memory8 &&
instruction.types[0] <= OperandType::Memory64 &&
instruction.mem_base == Register::RIP &&
instruction.mem_index == Register::None) {
// EA = RIP (post-instruction) + displacement. current_address is already
// advanced past the instruction, so it equals RIP at this point.
uint64_t ea = current_address + instruction.mem_disp;
auto it = importMap.find(ea);
if (it != importMap.end()) {
const auto& importName = it->second;
callFunctionIR(importName, nullptr);
debugging::doIfDebug([&]() {
std::cout << "[call-abi] resolved import: " << importName << "\n"
<< std::flush;
});
emittedExternalCall = true;
// Skip the switch/Unflatten path entirely.
goto call_done;
}
// Unresolved RIP-relative call: importMap has no entry for this IAT
// slot. Emit an opaque external call with strict-ABI clobber and
// continue at the post-call address. Falling through to operand
// dispatch would treat the raw on-disk IAT bytes as a jump target
// and silently corrupt the lift (the post-call block ends up
// sealed with ret undef).
{
auto fx = this->buildUnknownCallFx();
fx.target = CallTargetClass::UnknownIndirect;
auto* eaValue = builder->getInt64(ea);
auto* targetPtr = builder->CreateIntToPtr(
eaValue, PointerType::get(context, 0));
auto* callResult = builder->CreateCall(
parseArgsType(nullptr, context), targetPtr, parseArgs(nullptr));
applyPostCallEffects(callResult, fx);
abi::printCallEffectsDiag(fx, current_address - instruction.length);
diagnostics.warning(
DiagCode::CallIndirectUnresolved,
current_address - instruction.length,
"Unresolved RIP-relative IAT call at EA=0x" +
std::to_string(ea) + " (no importMap entry)");
emittedExternalCall = true;
goto call_done;
}
}
// Provenance-based fast path for register-indirect calls. If this
// register was last loaded from an IAT slot, emit the named external
// call directly — this covers the common MSVC pattern:
// mov rsi, [rip+iat]
// call rsi
// ... args setup ...
// call rsi
// where the concolic engine has folded the load to a ConstantInt
// matching the on-disk IAT value, losing SSA provenance.
if (instruction.types[0] >= OperandType::Register8 &&
instruction.types[0] <= OperandType::Register64) {
Register reg = getBiggestEncoding(instruction.regs[0]);
auto it = registerImportSource.find(reg);
if (it != registerImportSource.end()) {
const auto& importName = it->second;
callFunctionIR(importName, nullptr);
debugging::doIfDebug([&]() {
std::cout << "[call-abi] resolved import via register provenance: "
<< importName << "\n" << std::flush;
});
diagnostics.info(
DiagCode::CallOutlinedImportThunk,
current_address - instruction.length,
"Resolved register-indirect import: " + importName);
emittedExternalCall = true;
goto call_done;
}
}
{ // Non-IAT call path: operand-based dispatch.
auto registerValue = GetIndexValue(0);
switch (instruction.types[0]) {
case OperandType::Immediate8:
case OperandType::Immediate16:
case OperandType::Immediate32:
case OperandType::Immediate64: {
// Fall through to register/memory handling.
}
case OperandType::Memory8:
case OperandType::Memory16:
case OperandType::Memory32:
case OperandType::Memory64:
case OperandType::Register8:
case OperandType::Register16:
case OperandType::Register32:
case OperandType::Register64: {
registerValue =
createAddFolder(registerValue, GetRegisterValue(Register::RIP));
if (getControlFlow() == ControlFlow::Basic ||
!isa<ConstantInt>(registerValue)) {
// --- Emit external call (unknown/indirect target) ---
auto fx = this->buildUnknownCallFx();
fx.target = isa<ConstantInt>(registerValue)
? CallTargetClass::UnknownDirect
: CallTargetClass::UnknownIndirect;
auto idltvm =
builder->CreateIntToPtr(registerValue, PointerType::get(context, 0));
auto callResult =
builder->CreateCall(parseArgsType(nullptr, context), idltvm,
parseArgs(nullptr));
applyPostCallEffects(callResult, fx);
abi::printCallEffectsDiag(fx, current_address - instruction.length);
emittedExternalCall = true;
break;
}
auto registerCValue = cast<ConstantInt>(registerValue);
uint64_t rawTargetAddr = registerCValue->getZExtValue();
uint64_t normalizedTargetAddr = normalizeRuntimeTargetAddress(rawTargetAddr);
auto* normalizedTargetValue =
builder->getIntN(registerCValue->getBitWidth(), normalizedTargetAddr);
if ((inlinePolicy.isOutline(normalizedTargetAddr) ||
shouldOutlineCall(normalizedTargetAddr)) &&
!shouldInlineTinyOutlinedCall(normalizedTargetAddr)) {
// --- Emit external call (outlined known-address target) ---
auto importName = resolveImportName(normalizedTargetAddr);
if (!importName.empty()) {
// Named import: emit a proper LLVM function declaration.
callFunctionIR(importName, nullptr);
debugging::doIfDebug([&]() {
std::cout << "[call-abi] resolved import: " << importName << "\n"
<< std::flush;
});
diagnostics.info(DiagCode::CallOutlinedImportThunk,
current_address - instruction.length,
"Outlined import call: " + importName);
} else {
// Unknown outlined target: emit opaque inttoptr call.
auto fx = this->buildUnknownCallFx();
fx.target = CallTargetClass::UnknownDirect;
auto idltvm = builder->CreateIntToPtr(
normalizedTargetValue, PointerType::get(context, 0));
auto callResult = builder->CreateCall(
parseArgsType(nullptr, context), idltvm, parseArgs(nullptr));
applyPostCallEffects(callResult, fx);
abi::printCallEffectsDiag(fx, current_address - instruction.length);
diagnostics.info(DiagCode::CallAbiApplied,
current_address - instruction.length,
"Outlined unknown-target call (inttoptr)");
}
emittedExternalCall = true;
break;
}
jump_address = normalizedTargetAddr;
break;
}
default:
UNREACHABLE("unreachable in call");
break;
}
// Unflatten inlining path: push return address to stack and branch to
// the call target. Skip this when we already emitted a CreateCall for
// an external/indirect target — the call semantics are fully handled.
if (!emittedExternalCall && getControlFlow() == ControlFlow::Unflatten) {
// Speculative inlining: only start if enabled (maxCallInlineBudget > 0)
// and not already inside a speculative inline.
if (maxCallInlineBudget > 0 && !speculativeCall.active) {
auto returnBB = getOrCreateBB(current_address, "call_return_cont");
branch_backup(returnBB);
speculativeCall.active = true;
speculativeCall.returnAddr = current_address;
speculativeCall.worklistFloor = unvisitedBlocks.size();
speculativeCall.bailedOut = false;
speculativeCallBudget = maxCallInlineBudget;
}
// Normal Unflatten path: push return address, branch to callee.
SetRegisterValue(Register::RSP, result);
auto push_into_rsp = GetRegisterValue(Register::RIP);
SetMemoryValue(getSPaddress(), push_into_rsp);
auto bb = getOrCreateBB(jump_address, "bb_call");
branch_backup(bb);
builder->CreateBr(bb);
blockInfo = BBInfo(jump_address, bb);
printvalue2("pushing block");
addUnvisitedAddr(blockInfo);
run = 0;
}
} // end non-IAT call path
call_done:;
}
MERGEN_LIFTER_DEFINITION_TEMPLATES(void)::lift_ret() { // fix
LLVMContext& context = builder->getContext();
// [0] = rip
// [1] = rsp
// [2] = [rsp]
// if its ret 0x10
// then its
// [0] = 0x10
// [1] = rip
// [2] = rsp
// [3] = [rsp]
auto rspvalue = GetRegisterValue(Register::RSP);
// IMPORTANT, change logic
auto realval = GetMemoryValue(getSPaddress(), 64); // todo : based on bitness
auto block = builder->GetInsertBlock();
auto function = block->getParent();
// auto lastinst = builder->CreateRet(realval);
printvalue(rspvalue);
// remov
debugging::doIfDebug([&]() {
std::string Filename = "output_rets.ll";
std::error_code EC;
raw_fd_ostream OS(Filename, EC);
function->getParent()->print(OS, nullptr);
});
auto emitResolvedFunctionReturn = [&]() {
auto rax = GetRegisterValue(Register::RAX);
rax = createZExtFolder(
rax, builder->getIntNTy(file.getMode() == arch_mode::X64 ? 64 : 32));
builder->CreateRet(rax);
run = 0;
finished = 1;
printvalue2(finished);
};
uint64_t destination = 0;
uint8_t rop_result = REAL_return;
if (llvm::ConstantInt* constInt =
llvm::dyn_cast<llvm::ConstantInt>(rspvalue)) {
int64_t rspval = constInt->getSExtValue();
printvalue2(rspval);
rop_result = rspval == STACKP_VALUE ? REAL_return : ROP_return;
}
printvalue2(rop_result);
if (rop_result == REAL_return) {
block->setName("real_return-" + std::to_string(current_address) + "-");
emitResolvedFunctionReturn();
return;
}
// --- ROP/continuation return: pop return address and adjust RSP ---
const int ptrSize = (file.getMode() == arch_mode::X64) ? 8 : 4;
auto val = ConstantInt::getSigned(Type::getInt64Ty(context), ptrSize);
auto rsp_result = createAddFolder(
rspvalue, val, "ret-new-rsp-" + std::to_string(current_address) + "-");
// Handle `ret imm16` (callee-cleanup: stdcall, fastcall, or thunks).
if (instruction.types[0] == OperandType::Immediate16) {
rsp_result =
createAddFolder(rsp_result, ConstantInt::get(rsp_result->getType(),
instruction.immediate));
// Diagnostic: callee-cleanup detected via ret-immediate.
const auto retCleanup =
(getEffectiveAbi() == AbiKind::X86_STDCALL ||
getEffectiveAbi() == AbiKind::X86_FASTCALL)
? StackCleanup::Callee
: StackCleanup::Unknown;
debugging::doIfDebug([&]() {
std::cout << "[call-abi] ret imm=" << instruction.immediate
<< " cleanup=" << abi::stackCleanupName(retCleanup)
<< " at 0x" << std::hex
<< (current_address - instruction.length)
<< std::dec << "\n" << std::flush;
});
}
SetRegisterValue(Register::RSP, rsp_result);
if (auto* targetConst = llvm::dyn_cast<llvm::ConstantInt>(realval)) {
uint64_t targetVA =
normalizeFileBackedRuntimeTargetAddress(targetConst->getZExtValue());
auto importIt = importMap.find(targetVA);
if (importIt != importMap.end()) {
auto* contVal = GetMemoryValue(getSPaddress(), 64);
if (auto* contConst = llvm::dyn_cast<llvm::ConstantInt>(contVal)) {
uint64_t contVA = contConst->getZExtValue();
auto* contRsp = createAddFolder(
rsp_result,
ConstantInt::getSigned(Type::getInt64Ty(context), ptrSize),
"ret-chain-cont-rsp-" + std::to_string(current_address) + "-");
SetRegisterValue(Register::RSP, contRsp);
const std::string& importName = importIt->second;
// Emit `call @import` but with an EMPTY volatileRegs set so the
// lifter does not clobber caller-saved GPRs post-call. Rationale:
// VM-staged imports are invoked from a dispatcher that preserves
// its own caller-saved state across the external call in the
// real binary (otherwise the VM would be broken). Clobbering
// those regs in the lifter makes the dispatcher's next step
// non-concrete, trapping further exploration in one handler.
auto* externFuncType = parseArgsType(
signatures.getFunctionInfo(importName), builder->getContext());
llvm::Function* externFunc = llvm::cast<llvm::Function>(
fnc->getParent()
->getOrInsertFunction(importName, externFuncType)
.getCallee());
std::vector<llvm::Value*> args =
parseArgs(signatures.getFunctionInfo(importName));
auto* callResult = builder->CreateCall(externFunc, args);
auto fx = buildUnknownCallFx();
fx.target = CallTargetClass::KnownByName;
fx.volatileRegs = {};
applyPostCallEffects(callResult, fx);
auto* contBB = getOrCreateBB(contVA,
"bb_after_import_" + importName + "_" + std::to_string(contVA));
builder->CreateBr(contBB);
if (!visitedAddresses.contains(contVA)) {
addUnvisitedAddr(BBInfo(contVA, contBB));
}
chainedImportRetSites.insert(current_address - instruction.length);
destination = contVA;
// Stop the outer per-instruction lift loop for this block: the
// chain has emitted the block's terminator (br to contBB). Any
// further instruction lift here would emit a SECOND terminator,
// leaving the block malformed - the second br silently orphans
// everything the chain's first br reached, so the new import's
// call ends up in a dead BB that O2 then DCEs.
run = 0;
finished = 1;
return;
}
}
}
ScopedPathSolveContext pathSolveContext(this, PathSolveContext::Ret);
auto pathResult = solvePath(function, destination, realval);
if (pathResult == PATH_unsolved) {
uint64_t diagAddr = current_address - instruction.length;
const bool chained =
chainedImportRetSites.find(diagAddr) != chainedImportRetSites.end();
if (!chained) {
++liftStats.blocks_unreachable;
diagnostics.warning(DiagCode::UnresolvedRetChain, diagAddr,
"Unresolved ROP chain (ret to symbolic address)");
}
// Block is currently unterminated: solvePath did not resolve the popped
// RIP, no chain fired, and we are at a `ret`. The most accurate
// semantic is "return to a caller we do not have context for" -
// degrade to REAL_return behaviour: emit `ret rax` and stop the
// block. This keeps the IR well-formed, lets DCE collapse the block
// if it ends up unreachable, and prevents the outer per-instruction
// lift loop from advancing past the ret and emitting a second
// terminator into the same block.
if (!builder->GetInsertBlock()->getTerminator()) {
auto rax = GetRegisterValue(Register::RAX);
rax = createZExtOrTruncFolder(
rax,
llvm::Type::getIntNTy(
context, file.getMode() == arch_mode::X64 ? 64 : 32));
builder->CreateRet(rax);
}
run = 0;
finished = 1;
return;
}
// If the callee returned to our speculative call's return address,
// the inline succeeded — deactivate the budget.
if (speculativeCall.active && destination == speculativeCall.returnAddr) {
speculativeCall.active = false;
speculativeCallBudget = 0;
}
}
MERGEN_LIFTER_DEFINITION_TEMPLATES(void)::lift_jmp() {
LLVMContext& context = builder->getContext();
// auto dest = operands[0];
auto Value = GetIndexValue(0);
auto ripval = GetRegisterValue(Register::RIP);
Value = createSExtFolder(Value, ripval->getType());
// TODO:
// if its an imm, sext
// if its r/m then we probably need to zext
// auto newRip = createAddFolder(
// Value, ripval, "jump-xd-" + std::to_string(current_address) + "-");
jmpcount++;
auto targetv = Value;
auto trunc = createSExtOrTruncFolder(targetv, Type::getInt64Ty(context),
"jmp-register");
printvalue(ripval);
printvalue(trunc);
uint64_t destination = 0;
auto function = builder->GetInsertBlock()->getParent();
const bool isDirectJump = instruction.types[0] == OperandType::Immediate8 ||
instruction.types[0] == OperandType::Immediate16 ||
instruction.types[0] == OperandType::Immediate32 ||
instruction.types[0] == OperandType::Immediate64;
// For direct jumps the immediate is RIP-relative, so add it to the
// current RIP to get the absolute target. Indirect jumps already hold
// an absolute address (or a computed pointer) in `trunc`.
if (isDirectJump) {
trunc = createAddFolder(trunc, ripval);
printvalue(trunc);
}
ScopedPathSolveContext pathSolveContext(
this, isDirectJump ? PathSolveContext::DirectJump
: PathSolveContext::IndirectJump);
auto pathResult = solvePath(function, destination, trunc);
if (pathResult == PATH_unsolved) {
++liftStats.blocks_unreachable;
uint64_t diagAddr = current_address - instruction.length;
diagnostics.warning(DiagCode::UnresolvedIndirectJump, diagAddr,
"Unresolved indirect jump (symbolic target)");
}
printvalue2(destination);
// printvalue(newRip);
// SetRegisterValueWrapper(Register::RIP, newRip);
}
// jnz and jne
MERGEN_LIFTER_DEFINITION_TEMPLATES(void)::lift_jnz() {
auto zf = getFlag(FLAG_ZF);
// auto dest = operands[0];
// auto Value = GetIndexValue( dest, 64);
// auto ripval = GetRegisterValue( Register::RIP);
// auto newRip = createAddFolder( Value, ripval, "jnz");
printvalue(zf);
branchHelper(zf, "jnz", branchnumber, 1);
branchnumber++;
}
MERGEN_LIFTER_DEFINITION_TEMPLATES(void)::lift_js() {
auto sf = getFlag(FLAG_SF);
// auto dest = operands[0];
// auto Value = GetIndexValue( dest, 64);
// auto ripval = GetRegisterValue( Register::RIP);
// auto newRip = createAddFolder( Value, ripval, "js");
branchHelper(sf, "js", branchnumber);
branchnumber++;
}
MERGEN_LIFTER_DEFINITION_TEMPLATES(void)::lift_jns() {
auto sf = getFlag(FLAG_SF);
// auto dest = operands[0];
// auto Value = GetIndexValue( dest, 64);
// auto ripval = GetRegisterValue( Register::RIP);
// auto newRip = createAddFolder( Value, ripval, "jns");
branchHelper(sf, "jns", branchnumber, 1);
branchnumber++;
}
MERGEN_LIFTER_DEFINITION_TEMPLATES(void)::lift_jz() {
// if 0, then jmp, if not then not jump
auto zf = getFlag(FLAG_ZF);
// auto dest = operands[0];
// auto Value = GetIndexValue( dest, 64);
// auto ripval = GetRegisterValue( Register::RIP);
// auto newRip = createAddFolder( Value, ripval, "jnz");
branchHelper(zf, "jz", branchnumber);
branchnumber++;
}
MERGEN_LIFTER_DEFINITION_TEMPLATES(void)::lift_jle() {
// If SF != OF or ZF = 1, then jump. Otherwise, do not jump.
auto sf = getFlag(FLAG_SF);
auto of = getFlag(FLAG_OF);
auto zf = getFlag(FLAG_ZF);
// auto dest = operands[0];
// auto Value = GetIndexValue( dest, 64);
// auto ripval = GetRegisterValue( Register::RIP);
// auto newRip = createAddFolder( Value, ripval, "jle");
// Check if SF != OF or ZF is set
auto sf_neq_of = createXorFolder(sf, of, "jle_SF_NEQ_OF");
auto condition = createOrFolder(sf_neq_of, zf, "jle_Condition");
branchHelper(condition, "jle", branchnumber);
branchnumber++;
}
MERGEN_LIFTER_DEFINITION_TEMPLATES(void)::lift_jl() {
auto sf = getFlag(FLAG_SF);
auto of = getFlag(FLAG_OF);
// auto dest = operands[0];
// auto Value = GetIndexValue( dest, 64);
// auto ripval = GetRegisterValue( Register::RIP);
// auto newRip = createAddFolder( Value, ripval, "jl");
printvalue(sf);
printvalue(of);
auto condition = createXorFolder(sf, of, "jl_Condition");
branchHelper(condition, "jl", branchnumber);
branchnumber++;
}
MERGEN_LIFTER_DEFINITION_TEMPLATES(void)::lift_jnl() {
auto sf = getFlag(FLAG_SF);
auto of = getFlag(FLAG_OF);
// auto dest = operands[0];
// auto Value = GetIndexValue( dest, 64);
// auto ripval = GetRegisterValue( Register::RIP);
// auto newRip = createAddFolder( Value, ripval, "jnl");
printvalue(sf);
printvalue(of);
auto condition = createXorFolder(sf, of, "jl_condition");
branchHelper(condition, "jnl", branchnumber, 1);
branchnumber++;
}
MERGEN_LIFTER_DEFINITION_TEMPLATES(void)::lift_jnle() {
// If SF != OF or ZF = 1, then jump. Otherwise, do not jump.
auto sf = getFlag(FLAG_SF);
auto of = getFlag(FLAG_OF);
auto zf = getFlag(FLAG_ZF);
// auto dest = operands[0];
// auto Value = GetIndexValue( dest, 64);
// auto ripval = GetRegisterValue( Register::RIP);
// auto newRip = createAddFolder( Value, ripval, "jle");
// Check if SF != OF or ZF is set
auto sf_neq_of = createXorFolder(sf, of, "jle_SF_NEQ_OF");
auto condition = createOrFolder(sf_neq_of, zf, "jle_Condition");
branchHelper(condition, "jnle", branchnumber, 1);
branchnumber++;
}
MERGEN_LIFTER_DEFINITION_TEMPLATES(void)::lift_jbe() {
auto cf = getFlag(FLAG_CF);
auto zf = getFlag(FLAG_ZF);
printvalue(cf) printvalue(zf) // auto dest = operands[0];
// auto Value = GetIndexValue( dest, 64);
// auto ripval = GetRegisterValue( Register::RIP);
// auto newRip = createAddFolder( Value, ripval, "jbe");
auto condition = createOrFolder(cf, zf, "jbe_Condition");
branchHelper(condition, "jbe", branchnumber);
branchnumber++;
}
MERGEN_LIFTER_DEFINITION_TEMPLATES(void)::lift_jb() {
auto cf = getFlag(FLAG_CF);
printvalue(cf);
// auto dest = operands[0];
// auto Value = GetIndexValue( dest, 64);
// auto ripval = GetRegisterValue( Register::RIP);
// auto newRip = createAddFolder( Value, ripval, "jb");
auto condition = cf;
branchHelper(condition, "jb", branchnumber);
branchnumber++;
}
MERGEN_LIFTER_DEFINITION_TEMPLATES(void)::lift_jnb() {
auto cf = getFlag(FLAG_CF);
printvalue(cf);
// auto dest = operands[0];
// auto Value = GetIndexValue( dest, 64);
// auto ripval = GetRegisterValue( Register::RIP);
// auto newRip = createAddFolder( Value, ripval, "jnb");
auto condition = cf;
branchHelper(condition, "jnb", branchnumber, 1);
branchnumber++;
}
MERGEN_LIFTER_DEFINITION_TEMPLATES(void)::lift_jnbe() {
auto cf = getFlag(FLAG_CF);
auto zf = getFlag(FLAG_ZF);
printvalue(cf) printvalue(zf); // auto dest = operands[0];
// auto Value = GetIndexValue( dest, 64);
// auto ripval = GetRegisterValue( Register::RIP);
// auto newRip = createAddFolder( Value, ripval, "jbe");
auto condition = createOrFolder(cf, zf, "jnbe_Condition");
branchHelper(condition, "jnbe", branchnumber, 1);
branchnumber++;
}
MERGEN_LIFTER_DEFINITION_TEMPLATES(void)::lift_jo() {
auto of = getFlag(FLAG_OF);
// auto dest = operands[0];
// auto Value = GetIndexValue( dest, 64);
// auto ripval = GetRegisterValue( Register::RIP);
// auto newRip = createAddFolder( Value, ripval, "jo");
printvalue(of);
branchHelper(of, "jo", branchnumber);
branchnumber++;
}
MERGEN_LIFTER_DEFINITION_TEMPLATES(void)::lift_jno() {
auto of = getFlag(FLAG_OF);
// auto dest = operands[0];
// auto Value = GetIndexValue( dest, 64);
// auto ripval = GetRegisterValue( Register::RIP);
// auto newRip = createAddFolder( Value, ripval, "jno");
branchHelper(of, "jno", branchnumber, 1);
branchnumber++;
}
MERGEN_LIFTER_DEFINITION_TEMPLATES(void)::lift_jp() {
auto pf = getFlag(FLAG_PF);
printvalue(pf);
// auto dest = operands[0];
// auto Value = GetIndexValue( dest, 64);
// auto ripval = GetRegisterValue( Register::RIP);
// auto newRip = createAddFolder( Value, ripval, "jp");
branchHelper(pf, "jp", branchnumber);
branchnumber++;
}
MERGEN_LIFTER_DEFINITION_TEMPLATES(void)::lift_jnp() {
auto pf = getFlag(FLAG_PF);
// auto dest = operands[0];
// auto Value = GetIndexValue( dest, 64);
// auto ripval = GetRegisterValue( Register::RIP);
// auto newRip = createAddFolder( Value, ripval, "jnp");
printvalue(pf);
branchHelper(pf, "jnp", branchnumber, 1);
branchnumber++;
}
MERGEN_LIFTER_DEFINITION_TEMPLATES(void)::lift_loopx() {
if (instruction.length != 2 || instruction.attributes != InstructionPrefix::None) {
Function* externFunc = cast<Function>(
fnc->getParent()
->getOrInsertFunction("not_implemented", fnc->getReturnType())
.getCallee());
builder->CreateRet(builder->CreateCall(externFunc));
run = 0;
finished = 1;
return;
}
const auto counterRegister =
getRegOfSize(Register::RCX, file.getMode() == arch_mode::X64 ? 64 : 32);
auto counterValue = GetRegisterValue(counterRegister);
auto one = ConstantInt::get(counterValue->getType(), 1);
auto decrementedCount =
createSubFolder(counterValue, one, "loop-count-" + std::to_string(current_address));
SetRegisterValue(counterRegister, decrementedCount);
auto countNonZero = createICMPFolder(
CmpInst::ICMP_NE, decrementedCount, ConstantInt::get(counterValue->getType(), 0),
"loop-count-nonzero");
Value* branchCondition = nullptr;
switch (instruction.mnemonic) {
case Mnemonic::LOOP:
branchCondition = countNonZero;
break;
case Mnemonic::LOOPE:
branchCondition = createAndFolder(countNonZero, getFlag(FLAG_ZF), "loope-cond");
break;
case Mnemonic::LOOPNE:
branchCondition = createAndFolder(
countNonZero, createNotFolder(getFlag(FLAG_ZF)), "loopne-cond");
break;
default:
UNREACHABLE("unreachable mnemonic in lift_loopx");
}
branchHelper(branchCondition, "loop", branchnumber);
branchnumber++;
}