字節序轉換與結構體位域(bit field)值的讀取 Part 2 - 深入理解字節序和結構體位域存儲方式


上一篇文章講解了帶位域的結構體,在從大端機(Big Endian)傳輸到小端機(Little Endian)后如何解析位域值。下面繼續深入詳解字節序,以及位域存儲的方式。

 

(1) 我們知道,存儲數字時,對小端機而言,數字的低位,存在低地址,高位存在高地址。大端機正相反。

 

(2) 讀取的方式,也是一樣的。對於小端機,讀出的低地址位作為數字的低位。

 

(3) 此外Big-Endian/Little-Endian存儲順序,不僅僅針對字節,還針對字節內的比特位。對於小端機而言,字節內的8個比特,低地址端比特位,對應二進制數字的低位。

 

(4) 對於結構體的多個位域,和普通成員一樣,編譯器同樣按照地址由低到高順序存儲,無論是大端機還是小端機。只是位域內的比特順序有區別罷了。

 

(5) 表述一個數值,可以使用兩種視圖 :

第一個是“邏輯視圖”,通俗的表述方式,也就是我們平時在書本上看到的,手寫數字時的方式。左邊為高位,右邊為低位。例如 375,-4.1,036,0xAF4D215B

另一個是“內存視圖”,即數字在內存中的存儲方式,是我們程序員專有的一種表述方式。左邊為低地址字節,右邊為高地址字節。字節內左邊為低地址比特位,右邊為高地址比特位。很明顯,同一個unsigned int值,在大端機、小端機上,分別有兩種不同的“內存視圖”。

例如,uint16 0x2A1F,二進制比特位為0010 1010 0001 1111 (顯然這一行使用的就是“邏輯視圖”)

在小端機上的“內存視圖”為:1111 1000 0101 0100 (低地址 -> 高地址)

在大端機上的“內存視圖”為:0010 1010 0001 1111 (低地址 -> 高地址)

另外可以看到,大端機的"內存視圖"和"邏輯視圖"是相同的。在很多相關的文章里,並沒有去區分數字的兩種表述方式,導致了很多混淆。其次,很多例子使用16進制,只能用於表達字節序,無法精確表達內部的比特順序。

 

再舉一個上一節使用過的例子:

typedef struct _exam_
{
  unsigned int tag : 6;
  unsigned int field1 : 3;
  unsigned int field2 : 7;
  unsigned int field3 : 11;
  unsigned int pad : 5;
}Exam;

Exam ex;
ex.tag = 4;
ex.field1 = 2;
ex.field2 = 0x3a;
ex.field3 = 0x4C1;
ex.pad = 0;

  

變量ex的6個位域的"內存視圖",在大端機是000100 010 0111010 10010110001 00000(低地址->高地址),在小端機是001000 010 0101110 10001101001 00000(低地址->高地址)。可見位域順序是一樣的,但是位域內比特位順序不同。

若按照4位一組,大端機"內存視圖"為 0001 0001 0011 1010 1001 0110 0010 0000,如果按照unsigned int的方式讀取這塊內存,結果是0x113A9620,四個比特位對應一個16進制數,和"邏輯視圖"完全一樣

在小端機上4位一組排列,"內存視圖"為 0010 0001 0010 1110 1000 1101 0010 0000,如果按照unsigned int的方式讀取這塊內存,就會按照小端機的方式來解析內存。可以先把二進制翻譯為"邏輯視圖" - 把整個"內存視圖"32位顛倒順序,結果是0x04B17484,注意不是0x212E8D20.

 

 

 

那么這些規則,對位域值的讀取有什么影響呢?

字節流在網絡上傳輸是按照網絡字節序傳輸的,也就是大端序。網卡不知道數據的含義(到底是int還是double,還是什么image),只能看到一個個字節,因此它做的就是把每個字節的8個比特位轉換為本機的位序。而具體的內容,則由我們的程序處理。比如對於整形等,調用socket接口的ntohl(),htonl()...等函數轉換字節序。順便提一句,對於float/double類型,可以直接memcpy到一個整形里面,之后按照整形正常的處理流程,到了目標機后,再memcpy到一個float/double里。

char,short,int,long等2次冪大小的整形,作為一個單獨的整體,經過整個流程梳理是沒有任何問題的。但無法保證結構體內的多個位域,按照定義的先后順序,從低地址到高地址排列。這意味着,無論如何,直接在代碼中使用ex.tag的方式,是讀不出tag位域的數據的。


細分有如下幾種情況:
(1) 主機內部傳輸無任何影響,畢竟是一樣的CPU架構。

 

(2) 相同字節序的主機間傳輸,同樣沒有影響。因為經過二次socket+網卡轉換后,碼流是相同的。讀者可自行驗證。

 

(3) 大端機傳輸到小端機(上一節所描述的)。下列二進制值如沒有特殊說明,都是"內存視圖"。

還以上面的位域為例,在大端機的為000100 010 0111010 10010110001 00000(低地址->高地址),按照四比特一組為: 0001 0001  0011 1010  1001 0110  0010 0000

傳輸到網絡中,由於大端序和網絡序相同,所以網卡不做轉換,字節流按照先后,依然是 0001 0001  0011 1010  1001 0110  0010 0000

傳輸到小端機,網卡自動轉換每個字節的比特序,但字節順序維持原狀, 1000 1000 0101 1100 0110 1001 0000 0100,可見原先跨字節相連的位域被"打散了"。字節內的位域,雖然比特順序對了,但是從低比特位挪到了高比特位,位置錯了。

調用ntohl,比特序不變,轉換字節序,0000 0100 0110 1001 0101 1100 1000 1000,效果是跨字節位域再次連通了。位域內存地址順序,正好和原先相反。如果把大端機的內存視圖畫到一張紙上,相當於翻到紙的背面。

此時,將這4個字節碼流當作unsigned int,得到一個"無符號整形",其"邏輯視圖"等於大端機上的“內存視圖”。左邊恰好是結構體最開始的位域:0001 0001  0011 1010  1001 0110  0010 0000。因此我們將錯就錯,直接使用位操作符來左移相應的位數(需要計算后邊所有位域的總比特數),即可得到對應的位域值。位移操作符等,都是對"邏輯視圖"操作的。

 

(4) 小端機傳到大端機。網卡轉換+ntohl轉換后,依然在內存中得到一個位域順序和正常順序相反的"無符號整形"。只是這次使用位運算符要注意,第一個位域在"邏輯視圖"的最右邊,依次向左類推,和(3)的情形是相反的。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM