如何修改ARM64二进制文件替换所有函数调用至指定函数以实现调用日志
替换ARM64二进制中所有函数调用为自定义跟踪函数的方案
要实现给所有函数调用插入日志并转发的功能,需要分二进制指令修改和自定义跟踪函数实现两部分完成,以下是具体步骤:
一、核心思路
把原二进制中所有的直接/间接函数调用(bl/blr指令),修改为调用自定义的my_func,同时把原目标函数的地址传递给my_func;my_func负责打印调用日志,再转发调用到原目标函数,最后返回原函数的结果。
二、二进制指令修改
1. 直接调用(bl target)的替换
原ARM64汇编的直接函数调用是单条bl指令,需要替换为加载原目标地址到临时寄存器+**调用my_func**的两条指令:
; 原指令 bl bar ; 修改后 adr x16, bar ; 用相对寻址把bar的地址加载到x16(x16是调用者可临时使用的寄存器) bl my_func
注:如果是PIE(位置无关可执行文件),
adr可以正确加载相对地址;如果是非PIE,也可以用ldr x16, =bar加载绝对地址。
2. 间接调用(blr xN)的替换
对于通过函数指针的间接调用,需要先把函数指针的值保存到临时寄存器,再调用my_func:
; 原指令 blr x0 ; 修改后 mov x16, x0 ; 把原函数指针地址存到x16 bl my_func
3. 工具实现方式
可以用以下工具批量修改二进制:
- IDA Pro/Ghidra:编写脚本遍历所有函数调用指令,自动替换为上述指令序列;注意需要处理指令长度变化(单条4字节指令变两条8字节),如果原位置没有空间,需要把指令移到空闲区域(比如段末尾的padding),再修改跳转。
- radare2:用命令行脚本遍历所有
bl/blr指令,通过asm命令替换为目标指令。
三、my_func的汇编实现(适配ARM64调用约定)
由于ARM64采用寄存器传参的调用约定,直接用C++模板无法完美适配(无法直接捕获所有寄存器参数),因此需要用汇编实现my_func,保证参数和寄存器状态的正确传递:
.global my_func .type my_func, %function my_func: ; 保存调用者保存的寄存器(ARM64约定x29/x30为callee-saved,需保存) stp x29, x30, [sp, #-16]! mov x29, sp ; 打印原函数地址日志 adr x0, fmt_str ; 格式化字符串地址作为printf的第一个参数 mov x1, x16 ; 原函数地址作为printf的第二个参数 bl printf ; 调用printf打印日志 ; 转发调用到原函数:原参数已在x0-x7,直接用blr跳转 blr x16 ; 恢复寄存器并返回 ldp x29, x30, [sp], #16 ret fmt_str: .asciz "calling: %p\n"
实现说明
- 用
x16传递原函数地址:x16属于ARM64的临时寄存器,调用者可以自由使用,不需要被my_func保存/恢复。 - 参数转发:原函数的输入参数已经在x0-x7寄存器中,
blr x16会直接使用这些寄存器调用原函数,无需额外处理。 - 返回值传递:原函数的返回值会存在x0-x3中,
ret指令会直接把这些值返回给上层调用者,保证逻辑一致。
四、注意事项
- 栈对齐:ARM64要求栈始终保持16字节对齐,修改指令时要确保栈操作(如
stp/ldp)符合对齐要求。 - PIE/非PIE适配:加载原函数地址时,根据二进制类型选择
adr(相对寻址,适配PIE)或ldr(绝对寻址,非PIE)。 - 异常处理:汇编实现的
my_func会保留原函数的栈帧结构,因此原函数抛出的异常可以正常向上传递。 - 性能开销:每个函数调用都会增加一次跳转和日志打印,仅适合调试场景,不建议在生产环境使用。
内容的提问来源于stack exchange,提问作者A. K.
相关产品推荐
相关产品推荐

