OpenJDK系列(二):從ClassFileParser談Endian

對TensorFlow的研究暫時告一段落.就目前看來,AI的應用場景還有待發掘,後續如果有時間將寫點關於TF結合樹莓派的一些玩法.

Endian

Endian即所謂的字節序,通俗點說就是多於一個類型的數據在內存中存取的順序目前有兩種字節序.

  • Big-Endian: 也稱爲大端序:高位字節存放在內存的低地址端,低位字節存放在內存的高地址端.
  • Little-Endian: 也稱爲小端序:高位字節存放在內存的高地址端,低位字節存放在內存的低地址端.

Endian與內存單元

對於0x12345678而言,1234是高四位,5678是低四位.再以十進制的98來說9是高位,8是低位.現在回顧下內存的抽象模型:由不同的存儲單元的構成,每個存儲單元容量爲1個字節.

image-20180905111953115

也就是說一個內存單元可以存放C語言中一個char類型數據,如果是short類型,則需要佔用2個內存單元,而int類型則需要佔據4個內存單元,比如int類型的305419896,其十六進制爲0x12345678,需要佔據4個內存單元,那這個四個內存單元中到底該如何存放數據呢?此時就用到了剛纔的Endian.

如果按照Little-Endian方式,其內存佈局如下:

image-20180905105723144

如果按照Big-Endian方法,其內存佈局如下:

image-20180905105758961

可以看出,對於超過一個字節類型的數據按照不同Endian會在內存中呈現不同的存放順序,那爲什麼會出現大小端呢?

Endian起因

Endian產生根本原因在於CPU要想讀寫內存中的數據必須藉助於寄存器.內存單元的容量一直保持1Byte不變,但寄存器卻隨着發展其容量不斷增加,比如現代計算機的寄存器的容量都是超過1Byte的.這種寄存器容量和內存單元容量的差異最終導致字節序問題.寄存器如何保存超過一個字節數據必然涉及到某種順序,這種順序就體現在寄存器高低位的定義,而這種定義又會影響到數據在寄存器中的存放,最終在內存的存儲順序上體現出來.

Endian與Class解析

Endian和字節流解析有什麼聯繫呢?在單機上採用同一種模式進行存取操作時,CPU會自動處理這種變化,保證數據寫入和讀取之後的結果一致.但涉及到網絡傳輸或者跨平臺後,就無法保證雙方使用的是同一種模式,如果不一致則會導致數據問題,因此需要進行大小端的轉換.

對於Java這種跨平臺語言而言,同樣需要關注這種差異.Java輸出的字節信息都是大端模式,但JVM是卻由C/C++編寫的.在默認情況下C/C++的大小端模式與當前計算機硬件平臺的大小端模式保持一致,如果JVM對此不做特殊處理,最終讀取的字節碼文件會有問題.在實際開發中,我們並不會關注該問題,這是因爲JVM在讀取字節碼文件時做了特殊處理:如果檢測到當前平臺採用的是小端模式,會將其轉爲大端模式,以保證字節碼文件的在JVM中的一致性.

整個流程可以簡單描述爲:當一個類需要被加載時,最終會交給classload.cpp的load_class(),接下來由ClassFileParser.cpp的parse_stream()負責解析.class文件對應ClassFileStream,在解析的過程中會根據平臺的Endian來決定是否要進行轉換.

ClassFileStream

ClassFileStream是用於讀取.class文件的輸入流,其路徑爲:/OpenJDK10/OpenJDK10/hotspot/src/share/vm/classfile/classFileParser.hpp

class ClassFileStream: public ResourceObj {
 private:
  const u1* const _buffer_start; // Buffer bottom
  const u1* const _buffer_end;   // Buffer top (one past last element)
  mutable const u1* _current;    // Current buffer position
  const char* const _source;     // Source of stream (directory name, ZIP/JAR archive name)
  bool _need_verify;             // True if verification is on for the class file

  .......  
}

_current指針指向Java字節流中當前已經讀取到的位置.當class文件剛被加載時,_current指向當前字節流的第一個字節所在的位置,後續隨着解析操作的不斷進行,_current指針不斷的往後移動,直至當前字節流最後.

根據字節碼規範,該類中定義了用於讀取固定字節長度的方法:

class ClassFileStream: public ResourceObj {
    ......

    public: 
     ClassFileStream(const u1* buffer,
                  int length,
                  const char* source,
                  bool verify_stream = verify);

     u2 get_u2_fast() const {
        u2 res = Bytes::get_Java_u2((address)_current);
        _current += 2;
        return res;
    }   

    u4 get_u4_fast() const {
     u4 res = Bytes::get_Java_u4((address)_current);
     _current += 4;
     return res;
    }

   u8 get_u8_fast() const {
    u8 res = Bytes::get_Java_u8((address)_current);
    _current += 8;
    return res;
   }
   ......
}

除此之外也定義用於跳過固定字節碼長度的常用方法,比如:skip_u4_fast(int length)等.在後續的字節碼解析過程中,這幾個方法非常常見.

ClassFileParser

ClassFileParser負責class文件解析,並嘗試創建oops.創建ClassFileParser對象後會繼續調用其parse_stream()`對當前類文件的字節碼流進行解析.由於class文件解析相對複雜,因此這裏只介紹magic number是如何被解析出來的.

void ClassFileParser::parse_stream(const ClassFileStream* const stream,
                                   TRAPS) {

  assert(stream != NULL, "invariant");
  assert(_class_name != NULL, "invariant");

  // BEGIN STREAM PARSING
  stream->guarantee_more(8, CHECK);  // magic, major, minor
  // Magic value
  const u4 magic = stream->get_u4_fast();
  guarantee_property(magic == JAVA_CLASSFILE_MAGIC,
                     "Incompatible magic value %u in class file %s",
                     magic, CHECK);

  // Version numbers
  _minor_version = stream->get_u2_fast();
  _major_version = stream->get_u2_fast();

  ......

}

按照字節碼規範,字節碼前三部分依次是magic number,minor_version及major_version,分別佔用u4,u2,u2,即4個字節,2個字節,2個字節,總共是8個字節,guarantee_more(8, CHECK)中的參數8含義就是如此:比較當前字節流文件剩餘的長度是否大於想要讀取的字節長度,否則報錯.

校驗通過後,調用stream的get_u4_fast()方法從字節碼流中讀取u4長度的字節序,即ClassFileStream中get_u4_fast():

  u4 get_u4_fast() const {
    u4 res = Bytes::get_Java_u4((address)_current);
    // 讀取完4個字節後,需要後移_current,因此需要對其進行+4  
    _current += 4;
    return res;
  }

在該方法中,從字節流中讀取4個字節的操作由Bytes::get_Java_u4((address)_current)實現.其中Bytes是與CPU架構相關的類.我這邊CPU採用的是x86架構,因此調用的是/OpenJDK10/hotspot/src/cpu/x86/vm/bytes_x86.hpp`中Bytes類:

class Bytes: AllStatic {
    ......
    static inline u4 get_Java_u4(address p) {
        // 調用模板方法get_Java()
        return get_Java<u4>(p); 
    }

    ......

    template <typename T>
    static inline T get_Java(const address p) {
       // 1.讀取u4,即get_native<u4>(p) 
       T x = get_native<T>(p);
       // 2.如果當前平臺的字節序和Java不一樣,即不是Big-Endian,需要進行轉換
       // 也就是將Little_Endian轉爲Big_Endian 
       if (Endian::is_Java_byte_ordering_different()) {
         //3.大小端轉換,即swap<u4>(x)  
         x = swap<T>(x);
       }
       return x;
    }       

}

在模板方法get_Java()先是調用與平臺相關的函數get_native<u4>()來讀取4個字節:

class Bytes: AllStatic {

  template <typename T>
  static inline T get_native(const void* p) {
    assert(p != NULL, "null pointer");

    T x;
    // is_aligned()用於判斷當前值是否對齊與給定值,未對齊則使用memcpy從p指針出拷貝u4數據到x
    if (is_aligned(p, sizeof(T))) {
      // 此處由於是讀取u4,因此最終將指針p強轉爲u4*類型的指針.  
      x = *(T*)p;
    } else {
      memcpy(&x, p, sizeof(T));
    }

    return x;
  }   

  ......

}

讀取完成後判斷當前平臺的模式是否和Java中的一致,即當前是否是大端模式,如果不是則繼續調用swap<u4>()實現小端到大端的轉換.

class Bytes: AllStatic {
  ......

  // Efficient swapping of byte ordering
  template <typename T>
  static T swap(T x) {
    switch (sizeof(T)) {
    case sizeof(u1): return x;
    case sizeof(u2): return swap_u2(x);
    case sizeof(u4): return swap_u4(x);
    case sizeof(u8): return swap_u8(x);
    default:
      guarantee(false, "invalid size: " SIZE_FORMAT "\n", sizeof(T));
      return 0;
    }
  }

  static inline u2   swap_u2(u2 x);                   // compiler-dependent implementation
  static inline u4   swap_u4(u4 x);                   // compiler-dependent implementation
  static inline u8   swap_u8(u8 x);
}

需要注意swap_u4()是誇平臺,爲了兼容,可以看到在/OpenJDK10/OpenJDK10/hotspot/src/os_cpu根據平臺進行了不同的實現,比如我這邊用的是:
/OpenJDK10/hotspot/src/os_cpu/bsd_x86/vm/bytes_bsd_x86.inline.hpp

image-20180905154857898

此處內嵌了一段彙編代碼來實現大小端的轉換.至此,我們已經清楚JVM是如何統一成大端模式的.最新文章見浮游.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章