Contents

C 語言 type punning

根據維基百科的定義,type punning 是能夠繞過或顛覆一門程式語言型別系統的技巧統稱,例如在 C/C++,指標的型別轉換與 union,另外 C++ 還有 reference 的型別轉換與 reinterpret_cast,這些機制來達成記憶體 data 相同,卻能改變 represent value,當然這也牽扯到 type 之間 memory layout 是否相同的問題。

從計算機組織的層次來看,硬體本身有 CPU instruction size、page boundary 及 cache line boundary 的特性,因此 memory layout 必須考慮 data 要如何在記憶體對齊,才能有效的 access data,因此本篇文章會討論:

  • 為何硬體特性造成需要 data alignment
  • C 語言 data type 對齊規則
  • C99 規範的指標型別轉換規則
  • 網路封包轉換資料結構的應用 (待完成)

Data punning 常使用在 Berkeley sockets 的介面、封包 char buffer[] 與資料結構之間的轉換。例如 bind() 的 IP 位址資料結構的轉換:

1
2
3
4
5
6
7
// int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);

struct sockaddr_in sa = {0};
int sockfd = ...;
sa.sin_family = AF_INET;
sa.sin_port = htons(port);
bind(sockfd, (struct sockaddr *)&sa, sizeof sa);

因為 struct sockaddr_instruct sockaddr 具有相同的 memory layout,兩者的指標可互相轉換,所以 &my_addr->sin_family == &sa.sin_family。socket 函示庫使用 data punning 的技巧來達到多型與繼承的概念。

C99 [3.14] object

  • region of data storage in the execution environment, the contents of which can represent values
  • 在 C 語言的物件就指在執行時期,資料儲存的區域,可以明確表示數值的內容
  • 很多人誤認在 C 語言程式中,(int) 7 和 (float) 7.0 是等價的,其實以資料表示的角度來看,這兩者截然不同,前者對應到二進位的 “111”,而後者以 IEEE 754 表示則大異於 “111”

一個 data object 具有兩個特性:

  • value representation
  • storage location (address)

而 data alignment 的意思就是, data 的 address 可以公平的被 1, 2, 4, 8,這些數字整除,從這些數字可以發現他們都是 2 的 N 次方 (N為從 0 開始的整數)。換句話說,這些 data object 可以用 1, 2, 4, 8 byte 去 alignment。

電腦的 cpu 又是如何抓取資料呢?cpu 不會一次只抓取 1 byte 的資料,因為這樣太慢了,如果有個資料型態是 int 的 資料,如果你只抓取 1 byte ,就必須要抓 4 次 (int 為 4 byte),有夠慢。所以 cpu 通常一次會取 4 byte (要看電腦的規格 32 位元的 cpu 一次可以讀取 32 bit 的資料,64 位元一次可以讀取 64 bit),並且是按照順序取的,如下圖左側,而右側則是 unaligned memory access。

/type-punning-c/access_unaligned_object1.png

有些微處理器可以支援 unaligned memory access,實作細節可能不相同,例如 intel x86/x86_64 有 alignment interrupts 來幫助處理 unaligned memory access。但 unaligned memory access 有以下缺點,詳細可參考 Linux kernel: unaligned memory access:

  • 有些微處理器則無法處理 unaligned memory access (例如 Sun SPARC),執行時期會發生不可預期的結果,難以除錯
  • address location 可能會跨越 cache line boundary 和 page boundary,造成執行效能較差,行為如下面的範例

例如要取得的記憶體地址為 1~4,要做以下步驟:

  • 第一次取:0~3 將,0 的資料去掉,留下 1~3
  • 第二次取:4~7 將,5~7 的資料去掉,留下 4
  • 再將 1~3 4 合起來

/type-punning-c/access_unaligned_object2.png

  • Array: uses the same alignment as its elements, except that a local or global array variable of length at least 16 bytes or a C99 variable-length array variable always has alignment of at least 16 bytes
  • Structure and union:
    • member 之間填入 padding 來維持 member 都是 aligned
    • 用 widest scalar member 的 alignment 需求,為整個 struct/union 的 alignment 需求,整個 struct/union 大小為其倍數,不足則在最後加 padding
    • 詳細請參考 The Lost Art of Structure Packing 的範例
  • Malloc:

malloc 本來配置出來的記憶體位置就有做 alignment,根據 malloc 的 man page 裡提到 :

The malloc() and calloc() functions return a pointer to the allocated memory, which is suitably aligned for any built-in type.

實際上到底 malloc 做了怎樣的 data alignment,繼續翻閱 The GNU C Library - Malloc Example,裡面特別提到:

In the GNU system, the address is always a multiple of eight on most systems, and a multiple of 16 on 64-bit systems.

ISO/IEC 9899 (a.k.a C99 Standard)

  • 6.2.5 (27) 提到:

    A pointer to void shall have the same representation and alignment requirements as a pointer to a character type.

    All pointers to structure types shall have the same representation and alignment requirements as each other.

    Pointers to other types need not have the same representation or alignment requirements.

  • 6.3.2.3 (1) 提到:

    A pointer to void may be converted to or from a pointer to any object type. A pointer to any object type may be converted to a pointer to void and back again; the result shall compare equal to the original pointer.

  • void * 的設計,導致開發者必須透過顯式強制轉型,才能存取最終的 object,否則就會丟出編譯器的錯誤訊息,從而避免危險的指標操作。也就是說,我們無法直接對 void * 做數值操作
    1
    2
    
    void *p = ...;
    void *p2 = p + 1; /* what exactly is the size of void? */
    
  • 換言之,void * 存在的目的就是為了強迫使用者使用顯式轉型或是強制轉型,以避免 Undefined behavior 產生
  • 6.3.2.3 (7) 提到:

    A pointer to an object or incomplete type may be converted to a pointer to a different object or incomplete type. If the resulting pointer is not correctly aligned for the pointed-to type, the behavior is undefined. Otherwise, when converted back again, the result shall compare equal to the original pointer. When a pointer to an object is converted to a pointer to a character type, the result points to the lowest addressed byte of the object. Successive increments of the result, up to the size of the object, yield pointers to the remaining bytes of the object.





Комментарии