From 4f935153b9f031d955ec5492e068ba04d94500a5 Mon Sep 17 00:00:00 2001 From: Angelo DeLuca <43831545+AngeloD2022@users.noreply.github.com> Date: Sun, 6 Sep 2026 00:54:28 -0400 Subject: [PATCH] [Pseudo-ObjC] promote autorelease pool runtime idioms to `@autoreleasepool` blocks --- lang/c/pseudoc.cpp | 269 ++++++++++++++++++++++++------------------ lang/c/pseudoc.h | 12 ++ lang/c/pseudoobjc.cpp | 135 ++++++++++++++++++++- lang/c/pseudoobjc.h | 4 + 4 files changed, 307 insertions(+), 113 deletions(-) diff --git a/lang/c/pseudoc.cpp b/lang/c/pseudoc.cpp index 394778d655..03008bc9b1 100644 --- a/lang/c/pseudoc.cpp +++ b/lang/c/pseudoc.cpp @@ -751,6 +751,161 @@ void PseudoCFunction::GetExprText(const HighLevelILInstruction& instr, HighLevel } +void PseudoCFunction::GetExpr_VAR_INIT( + const HighLevelILInstruction& instr, HighLevelILTokenEmitter& tokens, DisassemblySettings* settings, bool statement) +{ + const auto srcExpr = instr.GetSourceExpr(); + const auto destExpr = instr.GetDestVariable(); + + const auto variableType = GetHighLevelILFunction()->GetFunction()->GetVariableType(destExpr); + const auto platform = GetHighLevelILFunction()->GetFunction()->GetPlatform(); + const auto prevTypeTokens = variableType.GetValue() ? + GetTypePrinter()->GetTypeTokensBeforeName(variableType.GetValue(), platform, variableType.GetConfidence()) : + vector {}; + const auto postTypeTokens = variableType.GetValue() ? + GetTypePrinter()->GetTypeTokensAfterName(variableType.GetValue(), platform, variableType.GetConfidence()) : + vector {}; + + // Check to see if the variable appears live + bool appearsDead = false; + if (const auto ssaForm = instr.GetSSAForm(); ssaForm.operation == HLIL_VAR_INIT_SSA) + { + const auto ssaDest = ssaForm.GetDestSSAVariable(); + appearsDead = !GetHighLevelILFunction()->IsSSAVarLive(ssaDest); + } + + // If the variable does not appear live, show the assignment as zero confidence (grayed out) + if (appearsDead) + tokens.BeginForceZeroConfidence(); + + if (variableType.GetValue()) + { + for (auto typeToken : prevTypeTokens) + { + typeToken.context = LocalVariableTokenContext; + typeToken.address = destExpr.ToIdentifier(); + tokens.Append(typeToken); + } + tokens.Append(TextToken, " "); + } + tokens.AppendVarTextToken(destExpr, instr, instr.size); + if (variableType.GetValue()) + { + for (auto typeToken : postTypeTokens) + { + typeToken.context = LocalVariableTokenContext; + typeToken.address = destExpr.ToIdentifier(); + tokens.Append(typeToken); + } + } + tokens.Append(OperationToken, " = "); + + // For the right side of the assignment, only use zero confidence if the instruction does + // not have any side effects + if (appearsDead && GetHighLevelILFunction()->HasSideEffects(srcExpr)) + { + tokens.EndForceZeroConfidence(); + appearsDead = false; + } + + GetExprTextInternal(srcExpr, tokens, settings, AssignmentOperatorPrecedence); + + if (appearsDead) + tokens.EndForceZeroConfidence(); + if (statement) + tokens.AppendSemicolon(); +} + + +void PseudoCFunction::EmitStandardBlockStatement(const HighLevelILInstruction& statement, bool isFirst, + bool& out_needsSeparator, HighLevelILTokenEmitter& tokens, DisassemblySettings* settings) +{ + // If the statement is one that contains additional blocks of code, insert a scope separator + // to visually separate the logic. + bool hasBlocks = false; + switch (statement.operation) + { + case HLIL_IF: + case HLIL_WHILE: + case HLIL_WHILE_SSA: + case HLIL_DO_WHILE: + case HLIL_DO_WHILE_SSA: + case HLIL_FOR: + case HLIL_FOR_SSA: + case HLIL_SWITCH: + hasBlocks = true; + break; + default: + hasBlocks = false; + break; + } + + if (out_needsSeparator || (!isFirst && hasBlocks)) + { + tokens.ScopeSeparator(); + } + + out_needsSeparator = hasBlocks; + + // Emit the lines for the statement itself + if (!ShouldSkipStatement(statement)) + { + GetExprTextInternal(statement, tokens, settings, TopLevelOperatorPrecedence, true); + tokens.NewLine(); + } +} + +size_t PseudoCFunction::TryEmitNewBlockRegion(std::span statements, size_t index, + HighLevelILTokenEmitter& tokens, DisassemblySettings* settings) +{ + // This method can be overridden by subclasses to handle creation of new block-like syntax elements. + // Returns the number of statements consumed. + return 0; +} + +void PseudoCFunction::EmitBlockStatements(std::span exprs, bool isBlockRoot, + HighLevelILTokenEmitter& tokens, DisassemblySettings* settings) +{ + bool needSeparator = false; + for (size_t i = 0; i < exprs.size();) + { + bool isFirst = i == 0; + const auto& statement = exprs[i]; + + const size_t consumedStmts = TryEmitNewBlockRegion(exprs, i, tokens, settings); + if (consumedStmts != 0) + { + i += consumedStmts; + needSeparator = true; + continue; + } + + // Don't show void returns at the very end of the function when printing + // the root of an AST, as it is implicit and almost always omitted in + // normal source code. + const bool isVoidReturnAtFuncRootEnd = isBlockRoot + && exprs.size() > 1 + && i+1 == exprs.size() + && statement.operation == HLIL_RET + && statement.GetSourceExprs().size() == 0; + + if (!isVoidReturnAtFuncRootEnd) + { + EmitStandardBlockStatement(statement, isFirst, needSeparator, tokens, settings); + } + ++i; + } +} + +void PseudoCFunction::GetExpr_BLOCK( + const HighLevelILInstruction& instr, HighLevelILTokenEmitter& tokens, DisassemblySettings* settings) +{ + const std::vector exprs = instr.GetBlockExprs(); + const bool isBlockRoot = instr.ast && + instr.exprIndex == GetHighLevelILFunction()->GetRootExpr().exprIndex; + EmitBlockStatements(exprs, isBlockRoot, tokens, settings); +} + void PseudoCFunction::GetExprTextInternal(const HighLevelILInstruction& instr, HighLevelILTokenEmitter& tokens, DisassemblySettings* settings, BNOperatorPrecedence precedence, bool statement, optional signedHint) { @@ -790,54 +945,7 @@ void PseudoCFunction::GetExprTextInternal(const HighLevelILInstruction& instr, H switch (instr.operation) { case HLIL_BLOCK: - [&]() { - const auto exprs = instr.GetBlockExprs(); - bool needSeparator = false; - for (auto i = exprs.begin(); i != exprs.end(); ++i) - { - // Don't show void returns at the very end of the function when printing - // the root of an AST, as it is implicit and almost always omitted in - // normal source code. - auto next = i; - ++next; - if (instr.ast && (instr.exprIndex == GetHighLevelILFunction()->GetRootExpr().exprIndex) - && (exprs.size() > 1) && (next == exprs.end()) && ((*i).operation == HLIL_RET) - && ((*i).GetSourceExprs().size() == 0)) - continue; - - // If the statement is one that contains additional blocks of code, insert a scope separator - // to visually separate the logic. - bool hasBlocks = false; - switch ((*i).operation) - { - case HLIL_IF: - case HLIL_WHILE: - case HLIL_WHILE_SSA: - case HLIL_DO_WHILE: - case HLIL_DO_WHILE_SSA: - case HLIL_FOR: - case HLIL_FOR_SSA: - case HLIL_SWITCH: - hasBlocks = true; - break; - default: - hasBlocks = false; - break; - } - if (needSeparator || (i != exprs.begin() && hasBlocks)) - { - tokens.ScopeSeparator(); - } - needSeparator = hasBlocks; - - // Emit the lines for the statement itself - if (!ShouldSkipStatement(*i)) - { - GetExprTextInternal(*i, tokens, settings, TopLevelOperatorPrecedence, true); - tokens.NewLine(); - } - } - }(); + GetExpr_BLOCK(instr, tokens, settings); break; case HLIL_FOR: @@ -1217,70 +1325,7 @@ void PseudoCFunction::GetExprTextInternal(const HighLevelILInstruction& instr, H break; case HLIL_VAR_INIT: - [&]() { - const auto srcExpr = instr.GetSourceExpr(); - const auto destExpr = instr.GetDestVariable(); - - const auto variableType = GetHighLevelILFunction()->GetFunction()->GetVariableType(destExpr); - const auto platform = GetHighLevelILFunction()->GetFunction()->GetPlatform(); - const auto prevTypeTokens = variableType.GetValue() ? - GetTypePrinter()->GetTypeTokensBeforeName( - variableType.GetValue(), platform, variableType.GetConfidence()) : - vector {}; - const auto postTypeTokens = variableType.GetValue() ? - GetTypePrinter()->GetTypeTokensAfterName( - variableType.GetValue(), platform, variableType.GetConfidence()) : - vector {}; - - // Check to see if the variable appears live - bool appearsDead = false; - if (const auto ssaForm = instr.GetSSAForm(); ssaForm.operation == HLIL_VAR_INIT_SSA) - { - const auto ssaDest = ssaForm.GetDestSSAVariable(); - appearsDead = !GetHighLevelILFunction()->IsSSAVarLive(ssaDest); - } - - // If the variable does not appear live, show the assignment as zero confidence (grayed out) - if (appearsDead) - tokens.BeginForceZeroConfidence(); - - if (variableType.GetValue()) - { - for (auto typeToken: prevTypeTokens) - { - typeToken.context = LocalVariableTokenContext; - typeToken.address = destExpr.ToIdentifier(); - tokens.Append(typeToken); - } - tokens.Append(TextToken, " "); - } - tokens.AppendVarTextToken(destExpr, instr, instr.size); - if (variableType.GetValue()) - { - for (auto typeToken: postTypeTokens) - { - typeToken.context = LocalVariableTokenContext; - typeToken.address = destExpr.ToIdentifier(); - tokens.Append(typeToken); - } - } - tokens.Append(OperationToken, " = "); - - // For the right side of the assignment, only use zero confidence if the instruction does - // not have any side effects - if (appearsDead && GetHighLevelILFunction()->HasSideEffects(srcExpr)) - { - tokens.EndForceZeroConfidence(); - appearsDead = false; - } - - GetExprTextInternal(srcExpr, tokens, settings, AssignmentOperatorPrecedence); - - if (appearsDead) - tokens.EndForceZeroConfidence(); - if (statement) - tokens.AppendSemicolon(); - }(); + GetExpr_VAR_INIT(instr, tokens, settings, statement); break; case HLIL_VAR_DECLARE: diff --git a/lang/c/pseudoc.h b/lang/c/pseudoc.h index b0648c3ba5..c42914a7da 100644 --- a/lang/c/pseudoc.h +++ b/lang/c/pseudoc.h @@ -62,6 +62,10 @@ class PseudoCFunction: public BinaryNinja::LanguageRepresentationFunction BinaryNinja::HighLevelILTokenEmitter& tokens, BinaryNinja::DisassemblySettings* settings, BNOperatorPrecedence precedence = TopLevelOperatorPrecedence, bool statement = false, std::optional signedHint = std::nullopt); + void GetExpr_VAR_INIT(const BinaryNinja::HighLevelILInstruction& instr, + BinaryNinja::HighLevelILTokenEmitter& tokens, BinaryNinja::DisassemblySettings* settings, bool statement); + void GetExpr_BLOCK(const BinaryNinja::HighLevelILInstruction& instr, BinaryNinja::HighLevelILTokenEmitter& tokens, + BinaryNinja::DisassemblySettings* settings); protected: void InitTokenEmitter(BinaryNinja::HighLevelILTokenEmitter& tokens) override; @@ -70,12 +74,20 @@ class PseudoCFunction: public BinaryNinja::LanguageRepresentationFunction bool statement = false) override; void BeginLines( const BinaryNinja::HighLevelILInstruction& instr, BinaryNinja::HighLevelILTokenEmitter& tokens) override; + void EmitBlockStatements(std::span exprs, bool isBlockRoot, + BinaryNinja::HighLevelILTokenEmitter& tokens, BinaryNinja::DisassemblySettings* settings); + void EmitStandardBlockStatement(const BinaryNinja::HighLevelILInstruction& statement, bool isFirst, + bool& out_needsSeparator, BinaryNinja::HighLevelILTokenEmitter& tokens, + BinaryNinja::DisassemblySettings* settings); void EndLines( const BinaryNinja::HighLevelILInstruction& instr, BinaryNinja::HighLevelILTokenEmitter& tokens) override; BinaryNinja::TypePrinter* GetTypePrinter() const; virtual bool ShouldSkipStatement(const BinaryNinja::HighLevelILInstruction& instr); + + virtual size_t TryEmitNewBlockRegion(std::span statements, size_t index, + BinaryNinja::HighLevelILTokenEmitter& tokens, BinaryNinja::DisassemblySettings* settings); virtual void GetExpr_CALL_OR_TAILCALL(const BinaryNinja::HighLevelILInstruction& instr, BinaryNinja::HighLevelILTokenEmitter& tokens, BinaryNinja::DisassemblySettings* settings, BNOperatorPrecedence precedence, bool statement); diff --git a/lang/c/pseudoobjc.cpp b/lang/c/pseudoobjc.cpp index 101156f985..58aa96324e 100644 --- a/lang/c/pseudoobjc.cpp +++ b/lang/c/pseudoobjc.cpp @@ -122,6 +122,8 @@ struct RuntimeCall Retain, Release, Autorelease, + AutoreleasePoolPush, + AutoreleasePoolPop, RetainAutorelease, Class, Self, @@ -139,6 +141,8 @@ constexpr std::array RUNTIME_CALLS = { std::make_pair("_objc_alloc_init", RuntimeCall::AllocInit), std::make_pair("_objc_alloc", RuntimeCall::Alloc), std::make_pair("_objc_autorelease", RuntimeCall::Autorelease), + std::make_pair("_objc_autoreleasePoolPush", RuntimeCall::AutoreleasePoolPush), + std::make_pair("_objc_autoreleasePoolPop", RuntimeCall::AutoreleasePoolPop), std::make_pair("_objc_autoreleaseReturnValue", RuntimeCall::Autorelease), std::make_pair("_objc_msgSend", RuntimeCall::MessageSend), std::make_pair("_objc_msgSendSuper", RuntimeCall::MessageSendSuper), @@ -154,9 +158,13 @@ constexpr std::array RUNTIME_CALLS = { std::make_pair("_objc_retainAutoreleaseReturnValue", RuntimeCall::RetainAutorelease), std::make_pair("_objc_retainBlock", RuntimeCall::Retain), std::make_pair("_objc_exception_throw", RuntimeCall::ExceptionThrow), + std::make_pair("__objc_autoreleasePoolPush", RuntimeCall::AutoreleasePoolPush), + std::make_pair("__objc_autoreleasePoolPop", RuntimeCall::AutoreleasePoolPop), std::make_pair("j__objc_alloc_init", RuntimeCall::AllocInit), std::make_pair("j__objc_alloc", RuntimeCall::Alloc), std::make_pair("j__objc_autorelease", RuntimeCall::Autorelease), + std::make_pair("j__objc_autoreleasePoolPush", RuntimeCall::AutoreleasePoolPush), + std::make_pair("j__objc_autoreleasePoolPop", RuntimeCall::AutoreleasePoolPop), std::make_pair("j__objc_autoreleaseReturnValue", RuntimeCall::Autorelease), std::make_pair("j__objc_msgSend", RuntimeCall::MessageSend), std::make_pair("j__objc_msgSendSuper", RuntimeCall::MessageSendSuper), @@ -248,6 +256,53 @@ bool IsAssignmentToObjCSuperStructField(const HighLevelILInstruction& assignInst return VariableIsObjCSuperStruct(variable, function); } +bool DoesVarInitBeginAutoreleasepool(const HighLevelILInstruction& varInit, const Function& function, Variable& out_poolHandle) +{ + if (varInit.operation != HLIL_VAR_INIT) + return false; + + // we're matching for variable initializations with a value of an autoreleasePoolPush runtime call. + // example: void* context = _objc_autoreleasePoolPush() + const auto initCall = varInit.GetSourceExpr(); + if (initCall.operation != HLIL_CALL) + return false; + + const auto callee = initCall.GetDestExpr(); + const auto params = initCall.GetParameterExprs(); // this doesn't seem like it's used meaningfully. + const auto rtCall = DetectObjCRuntimeCall(callee, params, function); + if (!rtCall) + return false; + + if (rtCall.value().type == RuntimeCall::AutoreleasePoolPush) + { + out_poolHandle = varInit.GetDestVariable(); + return true; + } + + return false; +} + +bool DoesCallTerminateAutoreleasePool(const HighLevelILInstruction& call, const Function& function, Variable& out_variable) +{ + if (call.operation != HLIL_CALL) + return false; + + const auto callee = call.GetDestExpr(); + const auto params = call.GetParameterExprs(); + + const auto rtCall = DetectObjCRuntimeCall(callee, params, function); + const bool isPoolPop = rtCall && rtCall.value().type == RuntimeCall::AutoreleasePoolPop; + + // ensure that the first parameter of the call is a variable reference. + if (params.size() != 1 || params[0].operation != HLIL_VAR) + return false; + + const auto paramVar = params[0].GetVariable(); + out_variable = paramVar; + + return isPoolPop; +} + } // unnamed namespace PseudoObjCFunction::PseudoObjCFunction(LanguageRepresentationFunctionType* type, Architecture* arch, Function* owner, @@ -344,6 +399,78 @@ void PseudoObjCFunction::GetExpr_CALL_OR_TAILCALL(const BinaryNinja::HighLevelIL return PseudoCFunction::GetExpr_CALL_OR_TAILCALL(instr, tokens, settings, precedence, statement); } +size_t PseudoObjCFunction::TryEmitNewBlockRegion(std::span statements, size_t index, + HighLevelILTokenEmitter& tokens, DisassemblySettings* settings) +{ + const auto& statement = statements[index]; + auto function = GetFunction(); + + if (Variable poolHandle; DoesVarInitBeginAutoreleasepool(statement, *function, poolHandle)) + { + const size_t start = index; + size_t end = index; + + // Find the call that pops poolHandle in the current block. + for (auto i = index + 1; i < statements.size(); i++) + { + if (Variable popped; + DoesCallTerminateAutoreleasePool(statements[i], *function, popped) + && popped == poolHandle) + { + end = i; + break; + } + } + + if (start == end) + // Function is either compiled oddly somehow, or we're in a stub/trampoline; + // render normally. + return 0; + + // Emit the autoreleasepool block once we have a valid span. + const auto& poolStart = statements[start]; + auto guard = tokens.SetCurrentExpr(poolStart); + const auto collapsed = function->IsInstructionCollapsed(poolStart); + + if (index != 0) + tokens.ScopeSeparator(); + + tokens.PrependCollapseIndicator( + collapsed ? ContentCollapsedContext : ContentExpandedContext, + poolStart.GetInstructionHash() + ); + + tokens.InitLine(); + + if (collapsed) + { + tokens.Append(KeywordToken, "@autoreleasepool"); + tokens.Append(CollapsedInformationToken, " {...}"); + } else + { + tokens.Append(KeywordToken, "@autoreleasepool"); + tokens.BeginScope(BlockScopeType); + + // Push the handle to the active handle stack during emission so that ShouldSkipStatement + // knows when to elide calls. + activePoolHandles.push_back(poolHandle); + EmitBlockStatements(statements.subspan(start + 1, end - start - 1), false, tokens, settings); + activePoolHandles.pop_back(); + + tokens.EndScope(BlockScopeType); + tokens.FinalizeScope(); + } + + // insert a line break if at `end` we're not the last statement in the block. + if (end + 1 != statements.size()) + tokens.NewLine(); + + return end - start + 1; + } + + return 0; +} + bool PseudoObjCFunction::GetExpr_ObjCMsgSend(uint64_t msgSendAddress, bool isSuper, bool isRewritten, const HighLevelILInstruction& instr, HighLevelILTokenEmitter& tokens, DisassemblySettings* settings, const std::vector& parameterExprs) @@ -579,6 +706,13 @@ bool PseudoObjCFunction::ShouldSkipStatement(const BinaryNinja::HighLevelILInstr // used for `objc_msgSendSuper` calls. switch (instr.operation) { + case HLIL_CALL: + { + if (Variable popped; DoesCallTerminateAutoreleasePool(instr, *GetFunction(), popped) && + std::find(activePoolHandles.begin(), activePoolHandles.end(), popped) != activePoolHandles.end()) + return true; + break; + } case HLIL_VAR_DECLARE: if (VariableIsObjCSuperStruct(instr.GetVariable(), *GetFunction())) return true; @@ -594,7 +728,6 @@ bool PseudoObjCFunction::ShouldSkipStatement(const BinaryNinja::HighLevelILInstr return PseudoCFunction::ShouldSkipStatement(instr); } - PseudoObjCFunctionType::PseudoObjCFunctionType() : PseudoCFunctionType("Pseudo Objective-C") {} Ref PseudoObjCFunctionType::Create( diff --git a/lang/c/pseudoobjc.h b/lang/c/pseudoobjc.h index ecf2db4779..ec315b8f2f 100644 --- a/lang/c/pseudoobjc.h +++ b/lang/c/pseudoobjc.h @@ -20,10 +20,14 @@ class PseudoObjCFunction : public PseudoCFunction void GetExpr_IMPORT(const BinaryNinja::HighLevelILInstruction& instr, BinaryNinja::HighLevelILTokenEmitter& tokens, BinaryNinja::DisassemblySettings* settings, BNOperatorPrecedence precedence, bool statement) override; + size_t TryEmitNewBlockRegion(std::span statements, size_t index, + BinaryNinja::HighLevelILTokenEmitter& tokens, BinaryNinja::DisassemblySettings* settings) override; bool ShouldSkipStatement(const BinaryNinja::HighLevelILInstruction& instr) override; private: + std::vector activePoolHandles; + bool GetExpr_ObjCMsgSend(uint64_t msgSendAddress, bool isSuper, bool isRewritten, const BinaryNinja::HighLevelILInstruction& expr, BinaryNinja::HighLevelILTokenEmitter& tokens, BinaryNinja::DisassemblySettings* settings, const std::vector& parameterExprs);