Bộ nạp lớp và kiểm tra Byte code

Chương trình dịch của Java chuyển đổi mã nguồn sang ngôn ngữ máy giả thuyết, máy ảo JVM. Mỗi lớp trong chương trình Java được tạo ra trong JVM là một tệp lớp có đuôi class. Các tệp lớp này muốn thực hiện lại phải được thông dịch để chuyền các lệnh trong JVM sang mẵ máy của những máy đích [1 ].

Lưu ý rằng, chương trình thông dịch của JVM chỉ nạp những lớp cần thiết tại mỗi thời điểm để thực hiện chương trình. Ví dụ, ta xét quá trình bắt đầu thực hiện chương trình ứng dụng độc lập MyProgramclass. JVM sẽ thực hiện các bước như sau:

  1. JVM có cơ chế để nạp các tệp lóp liên quan, đọc từ đĩa hay tải xuống từ những Website chứa các lóp đỏ. Nó sử dụng cơ chế này để nạp MyProgrclass.
  2. Nếu lớp MyProgram có các trường dữ liệu hoặc kể thừa từ một lóp cha khác thì những lớp đó cũng được nạp về.
  3. JVM sẽ thực hiện phương thức maìn() trong MyProgram, vi phương thức này là tĩnh nên không cần phải tạo ra đối tượng để gọi nó.
  4. Nếu trong main ( ) lại yêu cầu những đối tượng lớp khác thì chúng sẽ được nạp bồ sung theo yêu cầu.

Nói chung, ta không cần can thiệp vào quá trình trên, hệ thống tự dộng nạp và kiểm soát các lớp được tải xuống. Tuy nhiên, để hệ thống an toàn hơn, ta nên xây dựng bộ kiểm soát nạp lóp riêng. Nó cho phép ta kiểm tra tính toàn vẹn của các mã byte code trước khi đưa vào JVM.

1. Viết bộ nạp lớp ClassLoader riêng

Mọi bộ nạp lớp đều cài đặt ClassLoader. Phương thức loadclass ( ) ở lớp này sẽ xác định cách nap lớp ở mức đỉnh. Một khi có một lớp được nạp thì tất cả các lớp khác mà nó tham chiếu tới đều được nạp bởi cùng một bộ nạp.

Để tạo ra được một bộ nạp, ví dụ MyClassLoader, ta phải viết đè phương thức

loadciass(String className, booolean resolve) theo các bước như sau.

  1. Kiểm tra xem bộ nạp lớp này dã được nạp hay chưa. Mục đích là bộ nạp lớp của ta chỉ phải lưu lại một bàn ghi về những lớp mà nó đã nạp.
  1. Nếu là lớp mới thì kiểm tra xem nó có phải là lớp hệ thống hay không. Trường hợp không phải là lớp hệ thống, các mã bytecode cùa lớp đó được nạp từ các hệ thống tệp hoặc từ những nguồn khác.
  2. Gọi phương thức defineClass () của ClassLoader để giới thiệu các bytecode cho JVM

Thông thường, bộ nạp lớp sử dụng một phép ánh xạ (thường là bàng băm) dề lưu giữ các tham chiếu tới các lớp đã được nạp.

  • Class defineClass(String name, byte data[], int offset, int length) ; Đưa thêm một lóp mới vào JVM, trong đó có các tham số:

name là tên cùa một lớp có thể chứa cả tên của gói chứa nó, nhưng không cẩn chi rõ cả . class

data: một mảng đề chứa các byte code của lớp đó offset byte aode bắt đầu trên mảng length độ dài cùa mảng.

  • void loadciass (String name, boolean resolve): Được cài đặt để nhận được các byte code của lóp cần nạp vào JWỊ, nếu resolve là Trong đó, các tham số:

name là tên của một lóp có thể chứa cả tên của gói chứa nó, nhưng không cần chi rõ cả . class

resolve có giá trị true nếu resolveClassO cần gọi để kiểm tra sau khi lớp được nạp.

  • Class findSystemClass (String name): Tìm lớp hệ thống được nạp vào.
  • void resolveClass (Class c) : Được gọi thực hiện khi cờ resolve là true

2. Kiểm tra byte code

Khi một bộ nạp lớp giới thiệu các byte code cho JVM thi trước tiên những byte code này phải được kiểm định bời một bộ kiểm trà. Bộ kiểm tra đảm bảo rằng những lệnh được nạp vào khi thực hiện sẽ không gây ra thiệt hại nào cả. Tất cả các ngoại lệ của các lớp hệ thống sẽ được kiểm tra và được thông báo khi chúng xuất hiện.

Bộ kiểm tra có thể thấm đinh:

  • Các biến phải được khỏi tạo giá trị trước khi chúng được sử dựng.
  • Trong các lời goi hàm, các kieu tham chiếu phải phù hợp, tưong thích với các tham số hình thức.
  • Đảm báo các luật truy câp vào các thành phần riêng không bị VI phạm.
  • Đảm báo chương trình khi thực hiện không bị tràn bộ nhớ, V V.

Một khi phát hiên ra một sai sót bất kỳ thì lớp dó được xem là không hợp lệ và sẽ không được nạp vào JVM

Quan trọng hơn, trong thế giới mới với Internet, ta phải tìm cách bảo vệ để chống lai những người khác cố tình phá hoại. Vi dụ, họ có thể thay đổi kết quả tính toán của chương trình, thay đổi các trường dữ liệu riêng của các dối tượng trong hệ thống, hay một chương trình có thế bị ngắt bởi hệ thống bao mật của môt trình duyệt

Song, bạn có thể phân vân tại sao không có môt bộ kiểm tra đặc biệt đế kiểm định tất cả những điều nêu trên?. Trước hết phải biết rằng, trong Java chương trình biên dịch luôn kiểm duyệt và không cho phép tạo ra những tệp lớp, trong đó có những biến được sử dụng nhưng chưa khởi tạo giá trị hay những biến riêng (private) mà lại bị các đối tượng của lớp khác truy nhập, V V. Nhân đây cũng cần lưu ý là những vấn đề kiểm soát này không đươc đảm bảo ở những ngôn ngữ khác như C/C1-+, hay Pascal. Tuy nhiên, dạng byte code sử dụng trong tệp lớp là đang tài liêu hóa và việc sửa nó không có gì khó khăn đối VỚI những người lập trình Assembly có kinh nghiệm, họ có thể sử dụng nhũng hệ soạn thảo hexa và sửa bằng tay để tao ra các tệp lớp hợp lệ nhưng có những lệnh không an toàn trong JVM

Trong trường hợp này, biến n chưa được khởi tạo giá trị, và do vậy nó có thể nhận giá trị ngẫu nhiên trong bộ nhớ mà những ngôn ngữ lập trình khác như C/C++, Pascal không kiểm soát được. Riêng đối với Java, chương trình dịch phát hiện ra vấn đề này và nó thông báo là có lỗi.

Để tạo ra một tệp lớp nhập nhằng, ta phải can thiệp vào các câu lệnh ở mức sâu hơn, mức mã lệnh. Trước tiên, sử dụng javap để dịch chương trình Verif ierTest .java. Câu lệnh

javap -c VerífíerTest

cho các byte code trong tệp lóp dưới dạng các câu lệnh ký hiệu gợi nhớ.

Phương thức int fun () đã được dịch

  • iconst_l
  • istore_0
  • iconst_2
  • ìstore_l
  • iload_0
  • iload_l
  • iadd
  • istore_2
  • iload_2
  • ireturn

Tiếp theo, sử dụng hệ soạn thảo hexa (như Hex Workshop) để thay đổi lệnh thứ ba là istore 1 bằng istore_0. Bởi vì biến cục bộ istore_0 ứng với m, do vậy nó được khởi tạo hai lần và biến n không được khởi tạo giá trị. Ghi lại tệp vừa sùa vào tên cũ

VerifierTest.class.

Thử chạy chương trình VerifierTest với chế độ kiểm tra mặc định ta nhận được thông báo lỗi:

Exception in thread “main” java.lang.VerifyError: (class. VerifìerTest, method:                            fun signature:      ()I) Accessing

value frorrt unitialized register 1

Nhưng, nếu ta chạy chưcmg trình trên với tuỳ chọn -nover i f y, java-noverify VerifierTest nghĩa là không cần kiểm tra thì chương trình thực hiện và cho một kết quả ngẫu nhiên, ví dụ 1 + 2 = 15102330

vì n chưa được khởi tạo nên nó có thể nhận kết quả ngẫu nhiên trong bộ nhớ.

Lưu ý: Bộ kiểm tra luôn canh trừng đế chống lại sự thay đổi ác ý đối với những tệp lớp, nhưng không kiềm tra những tệp được sinh bởi chương trình dịch.