Skip to content

Commit e8f0fa3

Browse files
committed
[ObjC] Add support for removing runtime calls that perform retain count operations
A new activity is added that will remove calls to `objc_retain`, `objc_release`, `objc_autorelease`, and related functions. It is disabled by default due to the fact it changes the semantics of the code. It can be enabled on a per-function basis via the Function Settings context menu or command palette entry, or it can be enabled in Open with Options or in user settings if a user would prefer it be on for an entire file or for all files they open. For now the activity is only eligible within arm64 binaries. Supporting x86_64 will require matching some slightly different IL patterns.
1 parent 5b71d36 commit e8f0fa3

File tree

3 files changed

+216
-0
lines changed

3 files changed

+216
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod inline_stubs;
22
pub mod objc_msg_send_calls;
3+
pub mod remove_memory_management;
34
pub mod super_init;
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
use binaryninja::{
2+
architecture::{Architecture as _, CoreRegister, Register as _, RegisterInfo as _},
3+
binary_view::{BinaryView, BinaryViewExt as _},
4+
low_level_il::{
5+
expression::{ExpressionHandler, LowLevelILExpressionKind},
6+
function::{LowLevelILFunction, Mutable, NonSSA},
7+
instruction::{
8+
InstructionHandler, LowLevelILInstruction, LowLevelILInstructionKind,
9+
LowLevelInstructionIndex,
10+
},
11+
lifting::LowLevelILLabel,
12+
LowLevelILRegisterKind,
13+
},
14+
workflow::AnalysisContext,
15+
};
16+
use bstr::ByteSlice;
17+
18+
use crate::{error::ILLevel, metadata::GlobalState, Error};
19+
20+
// TODO: We should also handle `objc_retain_x` / `objc_release_x` variants
21+
// that use a custom calling convention.
22+
const IGNORABLE_MEMORY_MANAGEMENT_FUNCTIONS: &[&[u8]] = &[
23+
b"_objc_autorelease",
24+
b"_objc_autoreleaseReturnValue",
25+
b"_objc_release",
26+
b"_objc_retain",
27+
b"_objc_retainAutorelease",
28+
b"_objc_retainAutoreleaseReturnValue",
29+
b"_objc_retainAutoreleasedReturnValue",
30+
b"_objc_retainBlock",
31+
b"_objc_unsafeClaimAutoreleasedReturnValue",
32+
];
33+
34+
fn is_call_to_ignorable_memory_management_function<'func>(
35+
view: &binaryninja::binary_view::BinaryView,
36+
instr: &'func LowLevelILInstruction<'func, Mutable, NonSSA>,
37+
) -> bool {
38+
let target = match instr.kind() {
39+
LowLevelILInstructionKind::Call(call) | LowLevelILInstructionKind::TailCall(call) => {
40+
if let LowLevelILExpressionKind::ConstPtr(address) = call.target().kind() {
41+
address.value()
42+
} else {
43+
return false;
44+
}
45+
}
46+
LowLevelILInstructionKind::Goto(target) => target.address(),
47+
_ => return false,
48+
};
49+
let Some(symbol) = view.symbol_by_address(target) else {
50+
return false;
51+
};
52+
53+
let symbol_name = symbol.full_name();
54+
let mut symbol_name = symbol_name.to_bytes();
55+
56+
// Remove any j_ prefix that the shared cache workflow adds to stub functions.
57+
if symbol_name.starts_with_str("j_") {
58+
symbol_name = &symbol_name[2..];
59+
}
60+
61+
IGNORABLE_MEMORY_MANAGEMENT_FUNCTIONS.contains(&symbol_name)
62+
}
63+
64+
fn process_instruction(
65+
bv: &BinaryView,
66+
llil: &LowLevelILFunction<Mutable, NonSSA>,
67+
insn: &LowLevelILInstruction<Mutable, NonSSA>,
68+
link_register: LowLevelILRegisterKind<CoreRegister>,
69+
link_register_size: usize,
70+
) -> Result<bool, &'static str> {
71+
if !is_call_to_ignorable_memory_management_function(bv, insn) {
72+
return Ok(false);
73+
}
74+
75+
// TODO: Removing calls to `objc_release` can sometimes leave behind a load of a struct field
76+
// that appears to be unused. It's not clear whether we should be trying to detect and remove
77+
// those here, or if some later analysis pass should be cleaning them up but isn't.
78+
79+
match insn.kind() {
80+
LowLevelILInstructionKind::TailCall(_) => unsafe {
81+
llil.set_current_address(insn.address());
82+
llil.replace_expression(
83+
insn.expr_idx(),
84+
llil.ret(llil.reg(link_register_size, link_register)),
85+
);
86+
},
87+
LowLevelILInstructionKind::Call(_) => unsafe {
88+
// The memory management functions that are currently supported either return void
89+
// or return their first argument. For arm64, the first argument is passed in `x0`
90+
// and results are returned in `x0`, so we can replace the call with a nop. We'll need
91+
// to revisit this to support other architectures, and to support the `objc_retain_x`
92+
// `objc_release_x` functions that accept their argument in a different register.
93+
llil.set_current_address(insn.address());
94+
llil.replace_expression(insn.expr_idx(), llil.nop());
95+
},
96+
LowLevelILInstructionKind::Goto(_) if insn.index.0 == 0 => unsafe {
97+
// If the `objc_retain` is the first instruction in the function, this function
98+
// can only contain the call to the memory management function since when the
99+
// memory management function returns, it will return to this function's caller.
100+
llil.set_current_address(insn.address());
101+
llil.replace_expression(
102+
insn.expr_idx(),
103+
llil.ret(llil.reg(link_register_size, link_register)),
104+
);
105+
},
106+
LowLevelILInstructionKind::Goto(_) => {
107+
// The shared cache workflow inlines calls to stub functions, which causes them
108+
// to show up as a `lr = <next instruction>; goto <stub function instruction>;`
109+
// sequence. We need to remove the load of `lr` and update the `goto` to jump
110+
// to the next instruction.
111+
112+
let Some(prev) =
113+
llil.instruction_from_index(LowLevelInstructionIndex(insn.index.0 - 1_usize))
114+
else {
115+
return Ok(false);
116+
};
117+
118+
let target = match prev.kind() {
119+
LowLevelILInstructionKind::SetReg(op) if op.dest_reg() == link_register => {
120+
let LowLevelILExpressionKind::ConstPtr(value) = op.source_expr().kind() else {
121+
return Ok(false);
122+
};
123+
value.value()
124+
}
125+
_ => return Ok(false),
126+
};
127+
128+
let Some(LowLevelInstructionIndex(target_idx)) = llil.instruction_index_at(target)
129+
else {
130+
return Ok(false);
131+
};
132+
133+
// TODO: Manually creating a label like this is fragile and relies on a) knowledge of
134+
// how labels are used by core, and b) that the target is the first instruction in
135+
// a basic block. We should do this differently.
136+
let mut label = LowLevelILLabel::new();
137+
label.operand = target_idx;
138+
139+
unsafe {
140+
llil.set_current_address(prev.address());
141+
llil.replace_expression(prev.expr_idx(), llil.nop());
142+
llil.set_current_address(insn.address());
143+
llil.replace_expression(insn.expr_idx(), llil.goto(&mut label));
144+
}
145+
}
146+
_ => return Ok(false),
147+
}
148+
149+
Ok(true)
150+
}
151+
152+
pub fn process(ac: &AnalysisContext) -> Result<(), Error> {
153+
let view = ac.view();
154+
if GlobalState::should_ignore_view(&view) {
155+
return Ok(());
156+
}
157+
158+
let func = ac.function();
159+
160+
let Some(link_register) = func.arch().link_reg() else {
161+
return Ok(());
162+
};
163+
let link_register_size = link_register.info().size();
164+
let link_register = LowLevelILRegisterKind::Arch(link_register);
165+
166+
let Some(llil) = (unsafe { ac.llil_function() }) else {
167+
return Err(Error::MissingIL {
168+
level: ILLevel::Low,
169+
func_start: func.start(),
170+
});
171+
};
172+
173+
let mut function_changed = false;
174+
for block in llil.basic_blocks().iter() {
175+
for insn in block.iter() {
176+
match process_instruction(&view, &llil, &insn, link_register, link_register_size) {
177+
Ok(true) => function_changed = true,
178+
Ok(_) => {}
179+
Err(err) => {
180+
log::error!(
181+
"Error processing instruction at {:#x}: {}",
182+
insn.address(),
183+
err
184+
);
185+
continue;
186+
}
187+
}
188+
}
189+
}
190+
191+
if function_changed {
192+
// Regenerate SSA form after modifications
193+
llil.generate_ssa_form();
194+
}
195+
Ok(())
196+
}

plugins/workflow_objc/src/workflow.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,28 @@ pub fn register_activities() -> Result<(), WorkflowRegistrationError> {
6969
run(activities::super_init::process),
7070
);
7171

72+
let remove_memory_management_activity = Activity::new_with_action(
73+
activity::Config::action(
74+
"core.function.objectiveC.removeMemoryManagement",
75+
"Obj-C: Remove reference counting calls",
76+
"Remove calls to objc_retain / objc_release / objc_autorelease to simplify the resulting higher-level ILs",
77+
)
78+
.eligibility(
79+
activity::Eligibility::auto_with_default(false).matching_all_predicates(&[
80+
activity::ViewType::in_(["Mach-O", "DSCView"]).into(),
81+
activity::Platform::in_(["mac-aarch64", "ios-aarch64"]).into()
82+
])
83+
),
84+
run(activities::remove_memory_management::process),
85+
);
86+
7287
workflow
7388
.activity_after(&inline_stubs_activity, "core.function.translateTailCalls")?
7489
.activity_after(&objc_msg_send_calls_activity, &inline_stubs_activity.name())?
90+
.activity_before(
91+
&remove_memory_management_activity,
92+
"core.function.generateMediumLevelIL",
93+
)?
7594
.activity_after(&super_init_activity, "core.function.generateMediumLevelIL")?
7695
.register_with_config(WORKFLOW_INFO)?;
7796

0 commit comments

Comments
 (0)