安卓修改大师Smali插桩实战

—— Log.e打印各类变量值完整教程

Log插桩调试示意图

一、Log插桩技术概述

Log插桩(Log Injection)是指在反编译APK后,在对应的Smali代码中添加日志输出指令,将程序中的关键变量值以Log日志的形式输出到logcat中,从而帮助逆向分析人员定位关键逻辑、追踪数据流、破解验证机制的技术手段。

在安卓逆向工程中,Log插桩是五种核心定位技巧之一:

  • 信息反馈法:通过搜索资源ID字符串进行定位
  • 特征函数法:搜索关键API函数调用
  • 顺序查看法:分析程序执行流程
  • 代码注入法(Log插桩):插入Log日志查看关键值
  • 栈跟踪法:动态调试分析函数调用栈

1.1 Log插桩的核心原理

Log插桩的本质是向Smali代码中插入invoke-static指令,调用android.util.Log类的静态方法。Log.e是其中日志级别最高的方法,用于输出错误级别的日志信息,其方法签名为:

// Log.e方法签名 public static int e(String tag, String msg) // 参数tag: 日志标签,用于过滤 // 参数msg: 日志内容,即要输出的变量值

在Smali中调用Log.e的完整指令序列为:

# 步骤1:定义日志标签 const-string vX, "MyTag" # 步骤2:准备要输出的变量值(已在寄存器中) # 假设vY中已经存储了要输出的字符串变量 # 步骤3:调用Log.e方法 invoke-static {vX, vY}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I

1.2 寄存器使用的核心规则

⚠️ 最重要的注意事项:不要使用被插桩方法中已有的寄存器!

Log插桩时需要额外添加寄存器,不要使用原方法中已经占用的默认寄存器,否则会覆盖原有数据导致程序崩溃或逻辑错误。

正确的做法是:

  1. 找到原方法中.locals声明的寄存器数量N
  2. 将.locals N修改为.locals N+1以上(根据需要使用额外寄存器数量)
  3. 使用原方法中未使用的最高编号寄存器(如vN、vN+1等)

💡 示例:如果原方法声明.locals 4(使用了v0-v3),则Log插桩时应使用v4作为新寄存器。将.locals 4改为.locals 5即可。

二、Log.e打印字符串类型变量

字符串类型是Log插桩中最简单、最直接的类型,因为Log.e的第二个参数本身就是String类型,无需进行类型转换。

2.1 基本字符串变量输出

假设在目标方法中寄存器v0存储了一个字符串变量,需要将其输出到日志:

# 假设原始方法:.locals 3,使用了v0, v1, v2 # v0中存储了要输出的字符串变量 # 第一步:增加.locals数量 # 将 .locals 3 改为 .locals 4 # 第二步:使用新的寄存器v3作为日志标签 const-string v3, "MyDebug" # 第三步:v0中已经是要输出的字符串,直接调用Log.e invoke-static {v3, v0}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I

2.2 字符串拼接后输出

如果需要在日志中添加描述性文字,可以使用String.concat方法拼接字符串:

# 假设原始方法:.locals 3,使用v0,v1,v2 # v0中存储了要输出的字符串变量 # 第一步:将.locals 3改为.locals 5 # 第二步:准备标签和描述文字 const-string v3, "MyDebug" const-string v4, "返回值 = " # 第三步:拼接字符串 "返回值 = " + v0 invoke-static {v4, v0}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String; move-result-object v4 # 第四步:输出拼接后的完整日志 invoke-static {v3, v4}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I

三、Log.e打印整数类型变量

整数类型(int)不能直接传给Log.e的String参数,需要先通过String.valueOf(int)或Integer.toString(int)转换为字符串。

3.1 使用String.valueOf转换

# 假设原始方法:.locals 2,使用了v0, v1 # v0中存储了需要输出的int值 # 第一步:将.locals 2改为.locals 5 # 第二步:定义日志标签 const-string v2, "MyDebug" # 第三步:将int值转换为String invoke-static {v0}, Ljava/lang/String;->valueOf(I)Ljava/lang/String; move-result-object v3 # 第四步:输出日志 invoke-static {v2, v3}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I

3.2 使用Integer.toString转换

另一种同样有效的方式是使用Integer.toString(int)方法:

# v0中存储了需要输出的int值 const-string v2, "MyDebug" # 使用Integer.toString转换为字符串 invoke-static {v0}, Ljava/lang/Integer;->toString(I)Ljava/lang/String; move-result-object v3 invoke-static {v2, v3}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I

3.3 带描述文字的整数输出

# 假设原始方法:.locals 2,v0中存储了int值 # 将.locals 2改为.locals 6 # 定义标签和描述文字 const-string v2, "MyDebug" const-string v3, "用户金币数 = " # 将int转换为String invoke-static {v0}, Ljava/lang/String;->valueOf(I)Ljava/lang/String; move-result-object v4 # 拼接描述文字 + 转换后的字符串 invoke-static {v3, v4}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String; move-result-object v5 # 输出日志 invoke-static {v2, v5}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I

四、Log.e打印浮点数类型变量

浮点数类型(float)同样需要先转换为String才能传递给Log.e。转换方式与整数类似,但调用的是String.valueOf(float)或Float.toString(float)。

4.1 float变量输出

# 假设原始方法:.locals 2,使用了v0, v1 # v0中存储了需要输出的float值 # 将.locals 2改为.locals 5 # 定义日志标签 const-string v2, "MyDebug" # 将float转换为String invoke-static {v0}, Ljava/lang/String;->valueOf(F)Ljava/lang/String; move-result-object v3 # 输出日志 invoke-static {v2, v3}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I

4.2 使用Float.toString转换

# v0中存储了需要输出的float值 const-string v2, "MyDebug" # 使用Float.toString转换为字符串 invoke-static {v0}, Ljava/lang/Float;->toString(F)Ljava/lang/String; move-result-object v3 invoke-static {v2, v3}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I

五、Log.e打印双精度浮点数类型变量

双精度浮点数(double)在Smali中占用两个连续寄存器,这是与float、int类型最大的区别。在调用String.valueOf(double)时需要传入double值所在的两个寄存器。

5.1 double变量输出

# 假设v0, v1中存储了double值(double占用2个连续寄存器) # 原始方法:.locals 3,使用v0,v1,v2 # 将.locals 3改为.locals 5 # 定义日志标签 const-string v3, "MyDebug" # 将double转换为String(传入v0, v1两个连续寄存器) invoke-static {v0, v1}, Ljava/lang/String;->valueOf(D)Ljava/lang/String; move-result-object v4 # 输出日志 invoke-static {v3, v4}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I

5.2 使用Double.toString转换

# v0, v1中存储了double值 const-string v3, "MyDebug" # 使用Double.toString转换为字符串 invoke-static {v0, v1}, Ljava/lang/Double;->toString(D)Ljava/lang/String; move-result-object v4 invoke-static {v3, v4}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I

⚠️ double类型注意事项:

  • double和long类型在Smali中都占用两个连续寄存器,如v0和v1
  • 在调用String.valueOf(D)或Double.toString(D)时,必须传入两个寄存器作为参数
  • 传入的参数需要以{v0, v1}的格式,而不是{v0}

六、Log.e打印对象类型变量

对象类型变量(如StringBuilder、JSONObject、ArrayList等)需要先调用toString()方法将其转换为字符串,再输出到日志。

6.1 对象类型变量输出

# 假设v0中存储了一个对象引用(如JSONObject、ArrayList等) # 原始方法:.locals 2,将.locals 2改为.locals 5 # 定义日志标签 const-string v2, "MyDebug" # 调用对象的toString()方法获取字符串表示 invoke-virtual {v0}, Ljava/lang/Object;->toString()Ljava/lang/String; move-result-object v3 # 输出日志 invoke-static {v2, v3}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I

6.2 带描述文字的对象输出

# v0中存储了对象引用 # 将.locals 2改为.locals 6 # 定义标签和描述文字 const-string v2, "MyDebug" const-string v3, "JSON数据 = " # 调用对象的toString方法 invoke-virtual {v0}, Ljava/lang/Object;->toString()Ljava/lang/String; move-result-object v4 # 拼接描述文字 + 对象字符串 invoke-static {v3, v4}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String; move-result-object v5 # 输出日志 invoke-static {v2, v5}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I

6.3 特殊场景:对象可能为null

如果对象可能为null,直接调用toString()会抛出空指针异常,导致程序崩溃。此时需要先进行null判断:

# v0中存储了可能为null的对象引用 # 将.locals 2改为.locals 6 const-string v2, "MyDebug" # 判断对象是否为null if-nez v0, :not_null # 如果v0不为null,跳转到not_null标签 # 对象为null的情况 const-string v3, "对象为null" invoke-static {v2, v3}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I goto :end_log :not_null # 对象不为null,调用toString invoke-virtual {v0}, Ljava/lang/Object;->toString()Ljava/lang/String; move-result-object v3 invoke-static {v2, v3}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I :end_log # 后续代码继续执行

七、大文本量字符串的日志输出方案

当需要输出的字符串较大(如网络请求返回的JSON数据、长文本内容、HTML页面源码等)时,Logcat单条日志存在长度限制(约4KB)。超出部分会被截断,导致无法查看完整信息。以下是几种解决方案。

7.1 分段截断输出法

将大字符串按固定长度分段,逐段输出到日志。这是最常用的解决方案,无需额外工具类。

# 假设v0中存储了大字符串变量 # 注意:此方法需要循环结构,适用于在Smali中手动实现 # 简化方案:直接使用String.substring方法分段截取 # 假设我们截取前2000个字符 const/16 v3, 0x0 # start = 0 const/16 v4, 0x7D0 # end = 2000 # 调用substring方法截取 invoke-virtual {v0, v3, v4}, Ljava/lang/String;->substring(II)Ljava/lang/String; move-result-object v5 # 输出第一段 const-string v6, "MyDebug_Part1" invoke-static {v6, v5}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I # 截取第二段 const/16 v3, 0x7D0 # start = 2000 const/16 v4, 0xFA0 # end = 4000 invoke-virtual {v0, v3, v4}, Ljava/lang/String;->substring(II)Ljava/lang/String; move-result-object v5 # 输出第二段 const-string v6, "MyDebug_Part2" invoke-static {v6, v5}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I

7.2 使用自定义日志辅助类

更好的方案是创建一个自定义的日志辅助类,专门用于处理大文本输出。在Java中编写好该类,然后通过安卓修改大师将生成的Smali文件复制到目标项目中。

步骤1:编写Java工具类

// 大文本日志辅助类 public class BigLogUtil { private static final int MAX_LOG_LENGTH = 3000; public static void logBigString(String tag, String content) { if (content == null) { Log.e(tag, "null"); return; } int len = content.length(); int start = 0; int end = MAX_LOG_LENGTH; int part = 0; while (start < len) { if (end > len) end = len; Log.e(tag + "_part" + part, content.substring(start, end)); start = end; end += MAX_LOG_LENGTH; part++; } } }

步骤2:在目标Smali中调用

# 假设v0中存储了大字符串 # 将.locals 2改为.locals 5 # 定义标签 const-string v2, "MyDebug" # 调用大文本日志工具类 invoke-static {v2, v0}, Lcom/example/utils/BigLogUtil;->logBigString(Ljava/lang/String;Ljava/lang/String;)V

7.3 写入文件方案

对于极大量文本(如完整的HTML页面、几十KB的JSON数据),日志输出效率较低,可以考虑将文本写入文件后通过ADB pull命令拉取到本地查看。

自定义工具类方法:

// 将大文本写入文件的工具方法 public static void writeToFile(String fileName, String content) { try { FileOutputStream fos = new FileOutputStream( Environment.getExternalStorageDirectory() + "/" + fileName); fos.write(content.getBytes()); fos.close(); Log.e("MyDebug", "文件已写入: " + fileName); } catch (Exception e) { Log.e("MyDebug", "写入文件失败: " + e.getMessage()); } }

在Smali中调用后,使用ADB命令拉取文件:

# ADB命令拉取文件到本地 adb pull /sdcard/output_data.txt ./

八、完整实战案例:综合多类型变量输出

8.1 案例场景

假设目标APK的某个方法中,需要同时输出一个int类型的用户ID、一个String类型的用户名、一个double类型的余额,以及一个对象类型的JSON数据。我们通过安卓修改大师进行插桩。

综合插桩实战

8.2 完整插桩代码

假设原始方法声明为.locals 3,寄存器分配为:v0=int用户ID,v1=String用户名,v2=double余额(占用v2,v3),v4=JSONObject对象。

由于原始方法已使用v0-v4,我们需将.locals 3改为.locals 9,使用v5-v8作为临时寄存器。

# 原始方法:.locals 3 —> 改为 .locals 9 # ====== 日志输出开始 ====== # 1. 输出int类型:用户ID const-string v5, "MyDebug" const-string v6, "用户ID = " invoke-static {v0}, Ljava/lang/String;->valueOf(I)Ljava/lang/String; move-result-object v7 invoke-static {v6, v7}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String; move-result-object v8 invoke-static {v5, v8}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I # 2. 输出String类型:用户名 const-string v6, "用户名 = " invoke-static {v6, v1}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String; move-result-object v8 invoke-static {v5, v8}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I # 3. 输出double类型:余额(v2,v3为double值) const-string v6, "余额 = " invoke-static {v2, v3}, Ljava/lang/String;->valueOf(D)Ljava/lang/String; move-result-object v7 invoke-static {v6, v7}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String; move-result-object v8 invoke-static {v5, v8}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I # 4. 输出对象类型:JSON数据 const-string v6, "JSON数据 = " invoke-virtual {v4}, Ljava/lang/Object;->toString()Ljava/lang/String; move-result-object v7 invoke-static {v6, v7}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String; move-result-object v8 invoke-static {v5, v8}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I # ====== 日志输出结束 ======

8.3 查看日志输出

插桩完成后,通过安卓修改大师重新打包签名,安装到设备上运行,在Android Studio的Logcat或命令行中使用以下命令查看日志:

# ADB命令过滤查看MyDebug标签的日志 adb logcat -s MyDebug # 或使用更详细的过滤 adb logcat MyDebug:D *:S

预期输出:

E/MyDebug: 用户ID = 10086 E/MyDebug: 用户名 = JohnDoe E/MyDebug: 余额 = 9999.99 E/MyDebug: JSON数据 = {"status":0,"msg":"success","data":{...}}

⭐ @逆向新手小张 ★★★★★

“之前用命令行改Smali经常寄存器报错,分不清locals和registers。安卓修改大师可视化编辑,改完自动校验寄存器数量,跟着多类型变量案例一次跑通Log插桩,效率比命令行高很多。”

—— 来自官网用户评论

九、使用安卓修改大师进行Log插桩的优势

安卓修改大师作为专业的APK反编译与定制工具,在Log插桩方面提供了以下核心优势:

9.1 可视化编辑与自动校验

安卓修改大师将Smali代码编辑集成在图形化界面中,提供语法高亮、行号显示、查找替换等功能,大大降低了Smali的学习和操作门槛。在修改.locals寄存器数量时,软件会自动校验并提示,避免因寄存器数量不足导致的编译错误。

9.2 一键反编译与打包

软件支持一键反编译目标APK,自动生成完整的Smali代码树,无需手动配置JDK、apktool、ADB等环境。修改完成后,内置的打包签名模块可一键完成编译、对齐、签名流程,省去90%的手动配置步骤。

9.3 内置ADB调试模块

安卓修改大师内置了完整的ADB调试功能,包括一键安装/卸载APK、实时查看Logcat日志、管理设备文件等。插桩完成后,可以直接在软件中查看日志输出,无需额外打开命令行工具。

9.4 深度调试打包支持

对于复杂的调试需求,安卓修改大师支持生成调试安装包,自动携带JDWP调试标记和8700调试端口,可配合Android Studio和Smalidea插件进行Smali代码的动态调试。

⭐ @汉化博主阿泽 ★★★★★

“汉化海外游戏经常需要定位字符串来源,用安卓修改大师的Log插桩功能,快速输出变量值,比对不同条件下的结果,逆向效率翻倍。不用手动改AndroidManifest调试属性,节省大量试错时间。”

—— 来自官网用户评论

十、常见问题与解决方案

问题现象可能原因解决方案
程序崩溃闪退寄存器数量未增加,导致覆盖了原方法使用的寄存器检查.locals数值,确保增加了足够数量的寄存器
日志无输出插桩代码被放置在了不可达路径确认插桩位置在方法的可执行路径内,而非条件分支的死代码区域
输出为null变量值本身就是null,未做空值判断增加if-nez/if-eqz判断,避免直接调用toString()或valueOf()
日志被截断字符串长度超过Logcat单条限制(约4KB)使用分段输出法或自定义日志工具类
double类型输出异常传入的寄存器数量不正确double类型需要传入两个连续寄存器,如{v0, v1}
编译报错:Invalid register寄存器编号超过v15范围使用invoke-static/range指令或拆分方法

十一、总结与实践建议

通过本文的系统讲解,你已经掌握了使用安卓修改大师在Smali代码中进行Log插桩的完整技术栈,包括:

  1. 字符串类型:直接传入Log.e,或拼接描述文字后输出
  2. 整数类型:通过String.valueOf(int)或Integer.toString(int)转换后输出
  3. 浮点数类型:通过String.valueOf(float)或Float.toString(float)转换后输出
  4. 双精度浮点数:注意占用两个连续寄存器,传入{v0, v1}格式参数
  5. 对象类型:调用toString()方法转换后输出,注意null判断
  6. 大文本字符串:分段截断、自定义日志工具类、写入文件三种方案

11.1 学习路径建议

  1. 从简单开始:先练习字符串类型的Log插桩,熟悉基本指令格式
  2. 逐步进阶:依次尝试int、float、double、对象类型的输出
  3. 实战应用:选择一款简单的APK,在关键方法中插入多种类型的日志
  4. 熟练掌握:将Log插桩与条件跳转修改、返回值篡改等技术结合使用

11.2 工具优势总结

安卓修改大师作为专业的APK修改工具,其Log插桩功能的核心优势在于:

  • 无需配置环境:不用安装JDK、Android SDK、baksmali命令行
  • 全图形化操作:拖拽APK即可一键反编译得到完整Smali源码
  • 可视化代码编辑:语法高亮、行号显示、查找替换
  • 自动校验寄存器:修改.locals后自动提示,避免编译错误
  • 一站式流程:从反编译到打包签名,全部在同一软件内完成

⭐ @资深逆向工程师老周 ★★★★★

“对比过数十款PC端APK修改工具,安卓修改大师对Smali语法支持最完善,区分locals/registers提示清晰。多类型变量插桩的全套方案都有现成操作案例,官网持续更新适配新版Android系统dex格式,兼容性拉满。”

—— 来自官网用户评论

安卓修改大师
官方网站:www.apkeditor.cn
最新版本 v11.14.00.00 | 更新日期:2026-05-28 | 大小:12.45 MB
开发公司:上海空宇软件科技有限公司

*本文内容综合整理自安卓修改大师官网(www.apkeditor.cn)及相关技术文档,旨在帮助逆向爱好者掌握Smali Log插桩技术。所有操作请严格遵守相关法律法规,严禁将反编译后的代码用于商业用途。*