借助HotSpot SA来反汇编

57 篇文章 0 订阅
26 篇文章 0 订阅
接[url=http://rednaxelafx.iteye.com/blog/727938]前一篇日志[/url],再记一些HotSpot中Serviceability Agent(以下简称SA)的有趣用法。
前面提到过,要在Windows上使用SA的话,可以使用[url=http://dlc.sun.com.edgesuite.net/jdk7/binaries/index.html]Sun JDK 7 build 64或者更新的版本[/url]。下面我们用JDK 7 build 96来跑[url=https://visualvm.dev.java.net/]VisualVM[/url] 1.3和[url=http://groovy.codehaus.org/]Groovy[/url] 1.7.2的groovysh做个例子。
(要用JDK7来启动groovysh的话,一个简单的办法是在命令行里把JAVA_HOME设置到JDK7的目录上。
另外,要让Groovy使用某些Java参数启动的话,可以设置JAVA_OPTS环境变量)

在启动VisualVM的时候,确保它使用的是JDK7。可以用--jdkhome参数或者在VisualVM安装目录下的etc/visualvm.conf文件中设置jdkhome。启动后在VisualVM主菜单的Tools -> Plugins里,选择Available Plugins选项卡,在列表里可以看到一个称为[url=https://visualvm.dev.java.net/saplugin.html]SAPlugin[/url]的插件,把它装上。SAPlugin是VisualVM为SA提供的图形界面插件,可以方便的使用SA的部分封装好的功能。

然后用JDK7来启动一个groovysh,再在VisualVM里通过SAPlugin接进去,可以看到:
[img]http://dl.iteye.com/upload/attachment/288278/8cfb7237-7867-338b-a17e-b26ec5829f57.png[/img]
SAPlugin还有这样的功能,可以看到被JIT编译过的方法以及解释器的汇编代码:
[img]http://dl.iteye.com/upload/attachment/288280/9d1ca318-fee0-3962-8dac-f2474c7caee6.png[/img]

那SA是不是有内建的反汇编器,可以把任意x86、x64、IA64、SPARC等架构的机器码给反汇编为汇编代码呢?答案是肯定的,有足够方便的现成的API可用。(x64版尚未实现完,暂时还用了)
(顺带提一下:“反汇编”(disassemble)与“反编译”(decompile)是不一样的,请不要弄混了。)

设想有个场景,HotSpot VM突然crash了,并在退出过程中打出了一个hs_err_pidXXXX.log的日志文件。其中开头的部分是:
#
# A fatal error has been detected by the Java Runtime Environment:
#
# EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x6d9039f1, pid=3628, tid=3664
#
# JRE version: 6.0_18-b07
# Java VM: Java HotSpot(TM) Client VM (16.0-b13 mixed mode, sharing windows-x86 )
# Problematic frame:
# V [jvm.dll+0x1039f1]
#
# If you would like to submit a bug report, please visit:
# http://java.sun.com/webapps/bugreport/crash.jsp
#

--------------- T H R E A D ---------------

Current thread (0x0084c000): JavaThread "main" [_thread_in_vm, id=3664, stack(0x008e0000,0x00930000)]

siginfo: ExceptionCode=0xc0000005, reading address 0x00000008

Registers:
EAX=0x00000000, EBX=0x0084c000, ECX=0x00000008, EDX=0x0092e134
ESP=0x0092dbf8, EBP=0x0092dc3c, ESI=0x00000000, EDI=0x0084c8ec
EIP=0x6d9039f1, EFLAGS=0x00010246

Top of Stack: (sp=0x0092dbf8)
0x0092dbf8: 0084c110 0092e134 0092e134 0092e134
0x0092dc08: 04506fef 0092dc30 1000781f 00000000
0x0092dc18: 00000001 00000000 0092e054 0084c000
0x0092dc28: 00000000 00000002 0084c110 f910dbcd
0x0092dc38: 0084c000 0084c110 62f01296 0084c8e8
0x0092dc48: 0092e134 62f0f17c 00000049 00000000
0x0092dc58: 00000000 00000000 00000000 00000000
0x0092dc68: 00000000 00000000 00000000 00000000

Instructions: (pc=0x6d9039f1)
0x6d9039e1: 8b f0 eb 07 89 51 08 89 30 8b f0 8b 00 8d 48 08
0x6d9039f1: 8b 01 53 ff 50 2c 8b 43 04 85 c0 0f 85 8f 00 00

可以看到HotSpot遇到了[url=http://msdn.microsoft.com/en-us/library/aa363082(VS.85).aspx]访问异常(EXCEPTION_ACCESS_VIOLATION)[/url]。访问异常有很多时候是由本地代码中的空指针访问而引起的,这里是否也是如此呢?
至少得看看出错的指令长啥样吧。可以看到日志里有一块写着“Instructions”,有两行十六进制表示的数据跟在后面。这就是出错位置前后的机器码,其中第二行开头处是出错指令的开头位置。

借助SA里内建的反汇编器,我们可以写段代码来看看日志里出错的机器码到底是什么:
import sun.jvm.hotspot.asm.CPUHelper;
import sun.jvm.hotspot.asm.Disassembler;
import sun.jvm.hotspot.asm.Instruction;
import sun.jvm.hotspot.asm.InstructionVisitor;
import sun.jvm.hotspot.asm.SymbolFinder;
import sun.jvm.hotspot.asm.x86.X86Helper;

/**
* @author sajia
*
*/
public class TestX86Disassembler {
/**
* @param args
*/
public static void main(String[] args) {
CodeSnippet code = makeSampleCode();

CPUHelper cpuHelper = new X86Helper();
Disassembler dasm = cpuHelper.createDisassembler(code.startPc, code.code);
StringBuilder buf = new StringBuilder();
dasm.decode(new RawCodeVisitor(buf));
String dasmStr = buf.toString();

System.out.println(dasmStr);
}

private static CodeSnippet makeSampleCode() {
CodeSnippet code = new CodeSnippet();
code.startPc = 0x6d9039f1;
code.code = new byte[] {
(byte) 0x8b, (byte) 0x01,
(byte) 0x53,
(byte) 0xff, (byte) 0x50, (byte) 0x2c,
(byte) 0x8b, (byte) 0x43, (byte) 0x04,
(byte) 0x85, (byte) 0xc0,
(byte) 0x0f, (byte) 0x85, (byte) 0x8f, (byte) 0x00, (byte) 0x00
};
return code;
}

private static class CodeSnippet {
long startPc;
byte[] code;
}

private static class RawCodeVisitor implements InstructionVisitor {
private final StringBuilder buf;
private final SymbolFinder symFinder = new DummySymbolFinder();

public RawCodeVisitor(StringBuilder buf) {
this.buf = buf;
}

@Override
public void prologue() {
// do nothing
}

@Override
public void visit(long currentPc, Instruction instr) {
buf.append("0x")
.append(Long.toHexString(currentPc))
.append(": ")
.append(instr.asString(currentPc, symFinder))
.append("\n");
}

@Override
public void epilogue() {
// do nothing
}
}

public static class DummySymbolFinder implements SymbolFinder {
public String getSymbolFor(long address) {
return "0x" + Long.toHexString(address);
}
}
}

确保$JAVA_HOME/lib/sa-jdi.jar在classpath上,运行该程序可以看到输出为:
0x6d9039f1:  movl	%eax, [%ecx]
0x6d9039f3: pushl %ebx
0x6d9039f4: call 44[%eax]
0x6d9039f7: movl %eax, 4[%ebx]
0x6d9039fa: testl %eax, %eax
0x6d9039fc: jne 0x6d903a91

可以看到在地址0x6d9039f1的指令是mov eax, [ecx],对ECX寄存器有一个间接读访问,而从日志上可以看到此时ECX寄存器的值为0x00000008。在32位Windows XP上0x00000000-0x0000FFFF是保护区域,尝试对该区域读写会引发访问异常。这里程序试图从0x00000008读取一个DWORD赋值给EAX,印证了它是日志中访问异常的事发地。当然,真正导致该问题的代码肯定在更前面的地方,但确认事发地的状况也算是个好的开始。

SA反汇编出来的汇编代码语法既不是AT&T系的([url=http://www.gnu.org/software/binutils/]GNU as[/url]用这种),也不是Intel系的([url=http://www.masm32.com/]MASM[/url]、[url=http://www.nasm.us/]NASM[/url]用这种),感觉有那么点怪。
与Intel系的相似点是参数顺序,目标在前,源在后;间接引用以方括号表示。
与AT&T系相似的是寄存器带有%前缀,而且间接引用的偏移量写在括号的前面;指令带有宽度后缀。
总之需要点时间来习惯…

反汇编器这部分的实现与SA的其它相对独立,要抽出来单独用也没什么问题。有需要的话抽个出来改造成纯MASM语法输出或许会有用。

Alright,今天就废话到这里~
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值