📄 chap03.html
字号:
<P>In addition, the class file verifier checks that the class itself adheres to certain constraints placed upon it by the specification of the Java programming language. For example, the verifier enforces the rule that all classes, except class <FONT FACE="Courier New">Object</FONT>, must have a superclass. Thus, the class file verifier checks at run-time some of the Java language rules that should have been enforced at compile-time. Because the verifier has no way of knowing if the class file was generated by a benevolent, bug-free compiler, it checks each class file to make sure the rules are followed.</P>
<P>Once the class file verifier has successfully completed the checks for proper format and internal consistency, it turns its attention to the bytecodes. During this part of phase one, which is commonly called the "bytecode verifier," the Java Virtual Machine performs a data-flow analysis on the streams of bytecodes that represent the methods of the class. To understand the bytecode verifier, you need to understand a bit about bytecodes and frames. </P>
<P>The bytecode streams that represent Java methods are a series of one-byte instructions, called <I>opcodes</I>, each of which may be followed by one or more <I>operands</I>. The operands supply extra data needed by the Java Virtual Machine to execute the opcode instruction. The activity of executing bytecodes, one opcode after another, constitutes a thread of execution inside the Java Virtual Machine. Each thread is awarded its own <I>Java Stack</I>, which is made up of discrete <I>frames</I>. Each method invocation gets its own frame, a section of memory where it stores, among other things, local variables and intermediate results of computation. The part of the frame in which a method stores intermediate results is called the method韘 <I>operand stack</I>. An opcode and its (optional) operands may refer to the data stored on the operand stack or in the local variables of the method韘 frame. Thus, the virtual machine may use data on the operand stack, in the local variables, or both, in addition to any data stored as operands following an opcode when it executes the opcode.</P>
<P>The bytecode verifier does a great deal of checking. It checks to make sure that no matter what path of execution is taken to get to a certain opcode in the bytecode stream, the operand stack always contains the same number and types of items. It checks to make sure no local variable is accessed before it is known to contain a proper value. It checks that fields of the class are always assigned values of the proper type, and that methods of the class are always invoked with the correct number and types of arguments. The bytecode verifier also checks to make sure that each opcode is valid, that each opcode has valid operands, and that for each opcode, values of the proper type are in the local variables and on the operand stack. These are just a few of the many checks performed by the bytecode verifier, which is able, through all its checking, to verify that a stream of bytecodes is safe for the Java Virtual Machine to execute.</P>
<P>Phase one of the class file verifier makes sure the imported class file is properly formed, internally consistent, adheres to the constraints of the Java programming language, and contains bytecodes that will be safe for the Java Virtual Machine to execute. If the class file verifier finds that any of these are not true, it throws an error, and the class file is never used by the program.</P>
<H3><P>Phase Two: Verification of Symbolic References</P>
</H3><P>Although phase one happens immediately after the Java Virtual Machine loads a class file, phase two is delayed until the bytecodes contained in the class file are actually executed. During phase two, the Java Virtual Machine follows the references from the class file being verified to the referenced class files, to make sure the references are correct. Because phase two has to look at other classes external to the class file being checked, phase two may require that new classes be loaded. Most Java Virtual Machine implementations will likely delay loading classes until they are actually used by the program. If an implementation does load classes earlier, perhaps in an attempt to speed up the loading process, then it must still give the impression that it is loading classes as late as possible. If, for example, a Java Virtual Machine discovers during early loading that it can韙 find a certain referenced class, it doesn韙 throw a "class definition not found" error until (and unless) the referenced class is used for the first time by the running program. Therefore, phase two, the checking of symbolic references, is usually delayed until each symbolic reference is actually used for the first time during bytecode execution.</P>
<P>Phase two of class file verification is really just part of the process of dynamic linking. When a class file is loaded, it contains symbolic references to other classes and their fields and methods. A symbolic reference is a character string that gives the name and possibly other information about the referenced item--enough information to uniquely identify a class, field, or method. Thus, symbolic references to other classes give the full name of the class; symbolic references to the fields of other classes give the class name, field name, and field descriptor; symbolic references to the methods of other classes give the class name, method name, and method descriptor.</P>
<P>Dynamic linking is the process of <I>resolving</I> symbolic references into direct references. As the Java Virtual Machine executes bytecodes and encounters an opcode that, for the first time, uses a symbolic reference to another class, the virtual machine must resolve the symbolic reference. The virtual machine performs two basic tasks during resolution:</P>
<OL><LI>find the class being referenced (loading it if necessary)</P>
<LI>replace the symbolic reference with a direct reference, such as a pointer or offset, to the class, field, or method</OL>
<P>The virtual machine remembers the direct reference so that if it encounters the same reference again later, it can immediately use the direct reference without needing to spend time resolving the symbolic reference again.</P>
<P>When the Java Virtual Machine resolves a symbolic reference, phase two of the class file verifier makes sure the reference is valid. If the reference is not valid--for instance, if the class cannot be loaded or if the class exists but doesn韙 contain the referenced field or method--the class file verifier throws an error.</P>
<P>As an example, consider again the <FONT FACE="Courier New">Volcano</FONT> class. If a method of class <FONT FACE="Courier New">Volcano</FONT> invokes a method in a class named <FONT FACE="Courier New">Lava</FONT>, the name and descriptor of the method in <FONT FACE="Courier New">Lava</FONT> are included as part of the binary data in the class file for <FONT FACE="Courier New">Volcano</FONT>. So, during the course of execution when the <FONT FACE="Courier New">Volcano</FONT>韘 method first invokes the <FONT FACE="Courier New">Lava</FONT>韘 method, the Java Virtual Machine makes sure a method exists in class <FONT FACE="Courier New">Lava</FONT> that has a name and descriptor that matches those expected by class <FONT FACE="Courier New">Volcano</FONT>. If the symbolic reference (class name, method name and descriptor) is correct, the virtual machine replaces it with a direct reference, such as a pointer, which it will use from then on. But if the symbolic reference from class <FONT FACE="Courier New">Volcano</FONT> doesn韙 match any method in class <FONT FACE="Courier New">Lava</FONT>, phase two verification fails, and the Java Virtual Machine throws a "no such method" error.</P>
<H3><P>Binary Compatibility</P>
</H3><P>The reason phase two of the class file verifier must look at classes that refer to one<H3> </H3>nother to make sure they are compatible is because Java programs are dynamically linked. Java compilers will often recompile classes that depend on a class you have changed, and in so doing, detect any incompatibility at compile-time. But there may be times when your compiler doesn韙 recompile a dependent class. For example, if you are developing a large system, you will likely partition the various parts of the system into packages. If you compile each package separately, then a change to one class in a package would cause a recompilation of affected classes within that same package, but not necessarily in any other package. Moreover, if you are using someone else韘 packages, especially if your program downloads class files from someone else韘 package across a network as it runs, it may be impossible for you to check for compatibility at compile-time. That韘 why phase two of the class file verifier must check for compatibility at run-time.</P>
<P>As an example of incompatible changes, imagine you compiled class <FONT FACE="Courier New">Volcano</FONT> (from the above example) with a Java compiler. Because a method in <FONT FACE="Courier New">Volcano</FONT> invokes a method in another class named <FONT FACE="Courier New">Lava</FONT>, the Java compiler would look for a class file or a source file for class <FONT FACE="Courier New">Lava</FONT> to make sure there was a method in <FONT FACE="Courier New">Lava</FONT> with the appropriate name, return type, and number and types of arguments. If the compiler couldn韙 find any <FONT FACE="Courier New">Lava</FONT> class, or if it encountered a <FONT FACE="Courier New">Lava</FONT> class that didn韙 contain the desired method, the compiler would generate an error and would not create a class file for <FONT FACE="Courier New">Volcano</FONT>. Otherwise, the Java compiler would produce a class file for <FONT FACE="Courier New">Volcano</FONT> that is compatible with the class file for <FONT FACE="Courier New">Lava</FONT>. In this case, the Java compiler refused to generate a class file for <FONT FACE="Courier New">Volcano</FONT> that wasn韙 already compatible with class <FONT FACE="Courier New">Lava</FONT>.</P>
<P>The converse, however, is not necessarily true. The Java compiler could conceivably generate a class file for <FONT FACE="Courier New">Lava</FONT> that isn韙 compatible with <FONT FACE="Courier New">Volcano</FONT>. If the <FONT FACE="Courier New">Lava</FONT> class doesn韙 refer to <FONT FACE="Courier New">Volcano</FONT>, you could potentially change the name of the method <FONT FACE="Courier New">Volcano</FONT> invokes from the <FONT FACE="Courier New">Lava</FONT> class, and then recompile only the <FONT FACE="Courier New">Lava</FONT> class. If you tried to run your program using the new version of <FONT FACE="Courier New">Lava</FONT>, but still using the old version of <FONT FACE="Courier New">Volcano</FONT> that wasn韙 recompiled since you made your change to <FONT FACE="Courier New">Lava</FONT>, the Java Virtual Machine would, as a result of phase two class file verification, throw a "no such method" error when <FONT FACE="Courier New">Volcano</FONT> attempted to invoke the now non-existent method in <FONT FACE="Courier New">Lava</FONT>.</P>
<P>In this case, the change to class <FONT FACE="Courier New">Lava</FONT> broke <I>binary compatibility</I> with the pre-existing class file for <FONT FACE="Courier New">Volcano</FONT>. In practice, this situation may arise when you update a library you have been using, and your existing code isn韙 compatible with the new version of the library. To make it easier to alter the code for libraries, the Java programming language was designed to allow you to make many kinds of changes to a class that don韙 require recompilation of classes that depend upon it. The changes you are allowed to make, which are listed in the Java Language Specification, are called the rules of binary compatibility. These rules clearly define what can be changed, added, or deleted in a class without breaking binary compatibility with pre-existing class files that depend on the changed class. For example, it is always a binary compatible change to add a new method to a class, but never to delete a method that other classes may be using. So in the case of <FONT FACE="Courier New">Lava</FONT>, you violated the rules of binary compatibility when you changed the name of the method used by <FONT FACE="Courier New">Volcano</FONT>, because you in effect deleted the old method and added a new. If you had, instead, added the new method and then rewritten the old method so it calls the new, that change would have been binary compatible with any pre-existing class file that already used <FONT FACE="Courier New">Lava</FONT>, including <FONT FACE="Courier New">Volcano</FONT>.</P>
<H3><EM><P>Safety Features Built Into the Java Virtual Machine</P>
</EM></H3><P>Once the Java Virtual Machine has loaded a class and performed phase one of class file verification, the bytecodes are ready to be executed. Besides the verification of symbolic references (phase two of class file verification), the Java Virtual Machine has several other built-in security mechanisms operating as bytecodes are executed. These are the same mechanisms listed in Chapter 1 as features of the Java programming language that make Java programs robust. They are, not surprisingly, also features of the Java Virtual Machine:</P>
<UL><LI> type-safe reference casting
<LI> structured memory access (no pointer arithmetic)
<LI> automatic garbage collection (can韙 explicitly free allocated memory)
<LI> array bounds checking
<LI> checking references for <FONT FACE="Courier New">null</FONT></UL>
<P>By granting a Java program only safe, structured ways to access memory, the Java Virtual Machine makes Java programs more robust, but it also makes their execution more secure. Why? There are two reasons. First, a program that corrupts memory, crashes, and possibly causes other programs to crash represents one kind of security breach. If you are running a mission critical server process, it is critical that the process doesn韙 crash. This level of robustness is also important in embedded systems, such as a cell phone, which people don韙 usually expect to have to reboot. The second reason unrestrained memory access would be a security risk is because a wily cracker could potentially use it to subvert the security system. If, for example, a cracker could learn where in memory a class loader is stored, it could assign a pointer to that memory and manipulate the class loader韘 data. By enforcing structured access to memory, the Java Virtual Machine yields programs that are robust, but also frustrates crackers who dream of harnessing the internal memory of the Java Virtual Machine for their own devious plots.</P>
<P>Another safety feature built into the Java Virtual Machine--one that serves as a backup to structured memory access--is the unspecified manner in which the runtime data areas are laid out inside the Java Virtual Machine. The <I>runtime data areas</I> are the memory areas in which the Java Virtual Machine stores the data it needs to execute a Java application: Java stacks (one for each thread), a <I>method area</I>, where bytecodes are stored, and a <I>garbage-collected heap</I>, where the objects created by the running program are stored. If you peer into a class file, you won韙 find any memory addresses. When the Java Virtual Machine loads a class file, it decides where in its internal memory to put the bytecodes and other data it parses from the class file. When the Java Virtual Machine starts a thread, it decides where to put the Java stack it creates for the thread. When it creates a new object, it decides where in memory to put the object. Thus, a cracker cannot predict by looking at a class file where in memory the data representing that class, or objects instantiated from that class, will be kept. What韘 worse (for the cracker) is the cracker can韙 tell anything about memory layout by reading the Java Virtual Machine specification either. The manner in which a Java Virtual Machine lays out its internal data is not part of the specification. The designers of each Java Virtual Machine implementation decide which data structures their implementation will use to represent the runtime data areas, and where in memory their implementation will place them. As a result, even if a cracker were somehow able to break through the Java Virtual Machine韘 memory access restrictions, they would next be faced with the difficult task of finding something to subvert by looking around.</P>
<P>The prohibition on unstructured memory access is not something the Java Virtual Machine must actively enforce on a running program; rather, it is intrinsic to the bytecode instruction set itself. Just as there is no way to express an unstructured memory access in the Java programming language, there is also no way to express it in bytecodes--even if you write the bytecodes by hand. Thus, the prohibition on unstructured memory access is a solid barrier against the malicious manipulation of memory.</P>
<P>There is, however, a way to penetrate the security barriers erected by the Java Virtual Machine. Although the bytecode instruction set doesn韙 give you an unsafe, unstructured way to access memory, there is a way you can go around bytecodes: native methods. Basically, when you call a native method, Java韘 security sandbox becomes dust in the wind. First of all, the robustness guarantees don韙 hold for native methods. Although you can韙 corrupt memory from a Java method, you can from a native method. But most importantly, native methods don韙 go through the Java API (they are how you go around the Java API) so the security manager isn韙 checked before a native method attempts to do something that could be potentially damaging. (This is, of course, often how the Java API itself gets anything done. But the native methods used by the Java API are "trusted.") Thus, once a thread gets into a native method, no matter what security policy was established inside the Java Virtual Machine, it doesn韙 apply anymore to that thread, so long as that thread continues to execute the native method. This is why the security manager includes a method that establishes whether or not a program can load dynamic libraries, which are necessary for invoking native methods. Applets, for example, aren韙 allowed to load a new dynamic library, therefore they can韙 install their own new native methods. They can, however, call methods in the Java API, methods which may be native, but which are always trusted. When a thread invokes a native method, that thread leaps outside the sandbox. The security model for native methods is, therefore, the same security model described earlier as the traditional approach to computer security: you have to trust a native method before you call it.</P>
<P>One final mechanism that is built into the Java Virtual Machine that contributes to security is structured error handling with exceptions. Because of its support for exceptions, the Java Virtual Machine has something structured to do when a security violation occurs. Instead of crashing, the Java Virtual Machine can throw an exception or an error, which may result in the death of the offending thread, but shouldn韙 crash the system. Throwing an error (as opposed to throwing an exception) almost always results in the death of the thread in which the error was thrown. This is usually a major inconvenience to a running Java program, but won韙 necessarily result in termination of the entire program. If the program has other threads doing useful things, those threads may be able to carry on without their recently departed colleague. Throwing an exception, on the other hand, may result in the death of the thread, but is often just used as a way to transfer control from the point in the program where the exception condition arose to the point in the program where the exception condition is handled.</P>
<H3><EM><P>The Security Manager and the Java API</P>
</EM></H3><P>By using class loaders, you can prevent code loaded by different class loaders from interfering with one another inside the Java Virtual Machine, but to protect assets external to the Java Virtual Machine, you must use a security manager. The security manager defines the outer boundaries of the sandbox. Because it is customizable, the security manager allows you to establish a custom security policy for an application. The Java API enforces the custom security policy by asking the security manager for permission before it takes any action that is potentially unsafe. For each potentially unsafe action, there is a method in the security manager that defines whether that action is allowed by the sandbox. Each method韘 name starts with "check," so for example, <FONT FACE="Courier New">checkRead()</FONT>defines whether or not a thread is allowed to read to a specified file, and <FONT FACE="Courier New">checkWrite()</FONT>defines whether or not a thread is allowed to write to a specified file. The implementation of these methods is what defines the custom security policy of the application.</P>
<P>Most of the activities that are regulated by a "check" method are listed below. The classes of the Java API check with the security manager before they:</P>
<UL><LI> accept a socket connection from a specified host and port number
<LI> modify a thread (change its priority, stop it, etc
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -