Java 访问标志验证
Java Access Flag Verification
考虑以下场景:
package packA;
public class A
{
private static int i = 0;
}
package packB;
public class B
{
public static void main(String[] args)
{
int i = A.i + 1;
}
}
因为 A.i
是 private
,所以无法从 class B
访问它,这样做会导致编译器错误。
但是,如果我像这样检测 B
class
public class B
{
public static void main(String[] args)
{
// getstatic packA/A.i : I
// iconst_1
// iadd
// istore_1
}
}
并调用main
方法,JVM验证器会检查访问A.i
是否有效吗?同样,如果 A.i
被声明为 final
?
,它是否会将以下字节码传递为有效
public class B
{
public static void main(String[] args)
{
// iconst_2
// putstatic packA/A.i : I
}
}
我想知道这一点,因为验证者需要加载 class 来检查字段的访问标志。此外,如果它不验证它们,则可能会检测恶意字节码以更改字段的值或提供 'hack methods' 以允许在不进行反射的情况下访问字段。
访问对访问 class 不可见的字段会导致验证错误。为了应用此验证,JVM 需要加载并 link 声明此字段的 class。这是正常的验证过程,例如,JVM 还需要加载 classes 来检查方法是否存在。然而,可以延迟应用验证和所需的 class 加载,即在第一次执行方法之前。
HotSpot 是一个例外,其中 class 扩展了 MagicAccessorImpl,它是一个内部接口。验证者跳过此类 classes 的访问级别验证。这是 JVM 内部代码生成所必需的,其中反射代码被优化为字节代码。另一个例外是通过匿名 class 加载程序加载的类,它继承了另一个 classes 可见性上下文,例如 lambda classes.
对于内部 classes,javac 将包私有访问器方法插入到声明 class 中,这样内部 classes 就可以访问其外部 class 的字段.
编辑:我在评论后编辑了我的答案,但现在有时间验证这个和最终字段确实与我的第一个答案略有不同,但也与评论所说的相比:
对于构造函数,验证器仅验证最终字段赋值是从构造函数内部执行的,即在字节代码级别上名为 <init>
的方法。因此,以下字节码是可能的,Java代码中不允许的是:
class Foo {
final int bar; // is 0
Foo() { }
}
class Foo {
final int bar; // is 2
Foo() {
bar = 1;
bar = 2;
}
}
class Foo {
final int bar; // is 2
Foo() {
this(null);
bar = 2;
}
Foo(Void v) {
bar = 1;
}
}
对于静态字段,Java编译器没有强制执行这样的限制,即以下Java代码在字节码中是合法的:
class Foo {
static final int bar; // is 0
static { }
}
class Foo {
static final int bar; // is 2
static {
bar = 1;
bar = 2;
}
}
class Foo {
static final int bar; // is 2
static {
bar = 1;
foobar(2);
}
static foobar(int i) {
bar = i;
}
}
Foo.foobar(3); // bar is 3
考虑以下场景:
package packA;
public class A
{
private static int i = 0;
}
package packB;
public class B
{
public static void main(String[] args)
{
int i = A.i + 1;
}
}
因为 A.i
是 private
,所以无法从 class B
访问它,这样做会导致编译器错误。
但是,如果我像这样检测 B
class
public class B
{
public static void main(String[] args)
{
// getstatic packA/A.i : I
// iconst_1
// iadd
// istore_1
}
}
并调用main
方法,JVM验证器会检查访问A.i
是否有效吗?同样,如果 A.i
被声明为 final
?
public class B
{
public static void main(String[] args)
{
// iconst_2
// putstatic packA/A.i : I
}
}
我想知道这一点,因为验证者需要加载 class 来检查字段的访问标志。此外,如果它不验证它们,则可能会检测恶意字节码以更改字段的值或提供 'hack methods' 以允许在不进行反射的情况下访问字段。
访问对访问 class 不可见的字段会导致验证错误。为了应用此验证,JVM 需要加载并 link 声明此字段的 class。这是正常的验证过程,例如,JVM 还需要加载 classes 来检查方法是否存在。然而,可以延迟应用验证和所需的 class 加载,即在第一次执行方法之前。
HotSpot 是一个例外,其中 class 扩展了 MagicAccessorImpl,它是一个内部接口。验证者跳过此类 classes 的访问级别验证。这是 JVM 内部代码生成所必需的,其中反射代码被优化为字节代码。另一个例外是通过匿名 class 加载程序加载的类,它继承了另一个 classes 可见性上下文,例如 lambda classes.
对于内部 classes,javac 将包私有访问器方法插入到声明 class 中,这样内部 classes 就可以访问其外部 class 的字段.
编辑:我在评论后编辑了我的答案,但现在有时间验证这个和最终字段确实与我的第一个答案略有不同,但也与评论所说的相比:
对于构造函数,验证器仅验证最终字段赋值是从构造函数内部执行的,即在字节代码级别上名为
<init>
的方法。因此,以下字节码是可能的,Java代码中不允许的是:class Foo { final int bar; // is 0 Foo() { } } class Foo { final int bar; // is 2 Foo() { bar = 1; bar = 2; } } class Foo { final int bar; // is 2 Foo() { this(null); bar = 2; } Foo(Void v) { bar = 1; } }
对于静态字段,Java编译器没有强制执行这样的限制,即以下Java代码在字节码中是合法的:
class Foo { static final int bar; // is 0 static { } } class Foo { static final int bar; // is 2 static { bar = 1; bar = 2; } } class Foo { static final int bar; // is 2 static { bar = 1; foobar(2); } static foobar(int i) { bar = i; } } Foo.foobar(3); // bar is 3