果然啊,内容越往后面,就越来越往安全上转移了,转移了有木有,看来得开始制定新的学习计划,这再往后学没神马用啊,或者说短时间用不上。写完这期,开始找找新的学习路线。
这节介绍的是字节码的效验,严格来讲和“ClassLoader学习”这个题目有点不贴切,因为介绍的偏重于
效验器(verifier)。不过还是能学到不少东西。
先回顾一下
基本知识,在执行一个java文件的时候一般的步骤是:
javac Hello.java -> Hello.class
java Hello
(上面两行,忘记的或者记不清的,去面壁去!)
大家好,我刚面壁回来,给大家继续讲解:
如果执行命令 javac Hello.java -verbose,我们可以回顾下前面学习的知识,
打印出的消息是:
D:\code>javac Hello.java -verbose
[解析开始时间 Hello.java]
[解析已完成时间 30ms]
[源文件的搜索路径: .,C:\Program Files\Java\jdk1.6.0_29\lib\dt.jar,C:\Program Fi
les\Java\jdk1.6.0_29\lib\tools.jar]
[类文件的搜索路径: C:\Program Files\Java\jdk1.6.0_29\jre\lib\resources.jar,C:\P
rogram Files\Java\jdk1.6.0_29\jre\lib\rt.jar,C:\Program Files\Java\jdk1.6.0_29\j
re\lib\sunrsasign.jar,C:\Program Files\Java\jdk1.6.0_29\jre\lib\jsse.jar,C:\Prog
ram Files\Java\jdk1.6.0_29\jre\lib\jce.jar,C:\Program Files\Java\jdk1.6.0_29\jre
\lib\charsets.jar,C:\Program Files\Java\jdk1.6.0_29\jre\lib\modules\jdk.boot.jar
,C:\Program Files\Java\jdk1.6.0_29\jre\classes,C:\Program Files\Java\jdk1.6.0_29
\jre\lib\ext\dnsns.jar,C:\Program Files\Java\jdk1.6.0_29\jre\lib\ext\localedata.
jar,C:\Program Files\Java\jdk1.6.0_29\jre\lib\ext\sunjce_provider.jar,C:\Program
Files\Java\jdk1.6.0_29\jre\lib\ext\sunmscapi.jar,C:\Program Files\Java\jdk1.6.0
_29\jre\lib\ext\sunpkcs11.jar,.,C:\Program Files\Java\jdk1.6.0_29\lib\dt.jar,C:\
Program Files\Java\jdk1.6.0_29\lib\tools.jar]
[正在装入 java\lang\Object.class(java\lang:Object.class)]
[正在装入 java\lang\String.class(java\lang:String.class)]
[
正在检查 Hello]
[正在装入 java\lang\System.class(java\lang:System.class)]
[正在装入 java\io\PrintStream.class(java\io:PrintStream.class)]
[正在装入 java\io\FilterOutputStream.class(java\io:FilterOutputStream.class)]
[正在装入 java\io\OutputStream.class(java\io:OutputStream.class)]
[已写入 Hello.class]
[总时间 234ms]
代码就是一个Hello World。通过输出,我们可以看见前面学过得加载类的过程。
然后使用java Hello就可以输出Hello World了。
本章的开始部分引入这个命令: java
-noverify Hello。
该命令特别之处就是在执行Hello的时候使用非正式的-noverify选项来钝化效验。
那么,我们就先来看看效验,都要
效验什么东西:
- 变量要在使用之前进行初始化。
- 方法调用与对象引用类型之间要匹配。
- 访问私有数据和方法的规则没有被违反。
- 对本地变量的访问都在运行时堆栈内。
- 运行时堆栈没有溢出。
如果以上这些检查中任何一条没有通过,那么该类就被认为遭到了破坏,并且不予加载。
肯定很多人有疑问了,在javac Hello.java的过程中,如果你违反了规则,就不会得到Hello.class,(这事
编译器都做了)那还要效验器来干嘛?因为,一个具有汇编程序经验的人,可以很容易的制造出这样的字节码出来,然后送给虚拟机来执行。所以效验器是很有必要的,要记住,效验器总是在防范被故意篡改的类文件,而不仅仅只是检查编译器产生的类文件。
为了深入了解效验器的工作,也为了了解java字节码的载入原理,我们继续来看下面的代码:
static int fun(){
int m;
int n;
int m = 1;
int n = 2;
int r = m + n;
return r;
}
这个方法被编译以后会生成16进制的.class文件。
要了解编译后的数据,我们要借助javap工具,来查看助记(mnemonic)格式的字节码。
javap -c VerifierTest
得到:
0 |
iconst_1 |
1 |
istore_0 |
2 |
iconst_2 |
3 |
istore_1 |
4 |
iload_0 |
5 |
iload_1 |
6 |
iadd |
7 |
istore_2 |
8 |
iload_2 |
9 |
ireturn |
那么如果我们把
int n = 2 替换成 int m = 2,也就是
istore_1改成istore_0; 使得n没有被初始化。我们就要修改16进制的class文件。先列出代码对应的16进制数:
0 |
iconst_1 |
04 |
1 |
istore_0 |
3B |
2 |
iconst_2 |
05 |
3 |
istore_1 |
3C |
4 |
iload_0 |
1A |
5 |
iload_1 |
1B |
6 |
iadd |
60 |
7 |
istore_2 |
3D |
8 |
iload_2 |
1C |
9 |
ireturn |
AC |
使用16进制数编辑器,将
3C改为3B。这样执行VerifierTest程序,我们会得到java.lang.VerifyError的错误。
没错,虚拟机发现了我们的修改。
如果加入刚才的java -noverify VerifierTest参数,那么程序就可以产生输出了,为15102330,一个随机数,典型的输出错误。
所以,可以看出效验器果然给我们提供了很好的安全机制。