Linux C 語言字節對齊的那些事

最近一口君在做一個項目,遇到一個問題,運行於 ARM 上的 threadx 在與 DSP 通信採用消息隊列的方式傳遞消息(最終實現原理是中斷 + 共享內存的方式),在實際操作過程中發現 threadx 總是 crash,於是經過排查,是因爲傳遞消息的結構體沒有考慮字節對齊的問題。

隨手整理一下 C 語言中字節對齊的問題與大家一起分享。

一、概念

對齊跟數據在內存中的位置有關。如果一個變量的內存地址正好位於它長度的整數倍,他就被稱做自然對齊。比如在 32 位 cpu 下,假設一個整型變量的地址爲 0x00000004,那它就是自然對齊的。

首先了解什麼位、字節、字

3yQ8m0

字長

一個字的位數,現代電腦的字長通常爲 16,32, 64 位。(一般 N 位系統的字長是 N/8 字節。)

不同的 CPU 一次可以處理的數據位數是不同的,32 位 CPU 可以一次處理 32 位數據,64 位 CPU 可以一次處理 64 位數據,這裏的位,指的就是字長。

而所謂的字長,我們有時會稱爲字(word)。在 16 位的 CPU 中,一個字剛好爲兩個字節,而 32 位 CPU 中,一個字是四個字節。若以字爲單位,向上還有雙字(兩個字),四字(四個字)。

二、對齊規則

對於標準數據類型,它的地址只要是它的長度的整數倍就行了,而非標準數據類型按下面的原則對齊:   數組 :按照基本數據類型對齊,第一個對齊了後面的自然也就對齊了。聯合 :按其包含的長度最大的數據類型對齊。結構體:結構體中每個數據類型都要對齊。

三、如何限制定字節對齊位數?

1. 缺省

在缺省情況下,C 編譯器爲每一個變量或是數據單元按其自然對界條件分配空間。一般地,可以通過下面的方法來改變缺省的對界條件:

2. #pragma pack(n)

· 使用僞指令 #pragma pack (n),C 編譯器將按照 n 個字節對齊。· 使用僞指令 #pragma pack (),取消自定義字節對齊方式。

#pragma pack(n) 用來設定變量以 n 字節對齊方式。n 字節對齊就是說變量存放的起始地址的偏移量有兩種情況:

  1. 如果 n 大於等於該變量所佔用的字節數,那麼偏移量必須滿足默認的對齊方式

  2. 如果 n 小於該變量的類型所佔用的字節數,那麼偏移量爲 n 的倍數,不用滿足默認的對齊方式。

結構的總大小也有一個約束條件,如果 n 大於等於所有成員變量類型所佔用的字節數,那麼結構的總大小必須爲佔用空間最大的變量佔用的空間數的倍數;否則必須是 n 的倍數。

3. __attribute

另外,還有如下的一種方式:· __attribute((aligned (n))),讓所作用的結構成員對齊在 n 字節自然邊界上。如果結構中有成員的長度大於 n,則按照最大成員的長度來對齊。· attribute ((packed)),取消結構在編譯過程中的優化對齊,按照實際佔用字節數進行對齊。

3. 彙編. align

彙編代碼通常用. align 來制定字節對齊的位數。

.align: 用來指定數據的對齊方式, 格式如下:

.align [absexpr1, absexpr2]

以某種對齊方式, 在未使用的存儲區域填充值. 第一個值表示對齊方式, 4, 8,16 或 32. 第二個表達式值表示填充的值。

四、爲什麼要對齊?

操作系統並非一個字節一個字節訪問內存,而是按 2,4,8 這樣的字長來訪問。因此,當 CPU 從存儲器讀數據到寄存器,IO 的數據長度通常是字長。如 32 位系統訪問粒度是 4 字節 (bytes), 64 位系統的是 8 字節。當被訪問的數據長度爲 n 字節且該數據地址爲 n 字節對齊時,那麼操作系統就可以高效地一次定位到數據, 無需多次讀取,處理對齊運算等額外操作。數據結構應該儘可能地在自然邊界上對齊。如果訪問未對齊的內存,CPU 需要做兩次內存訪問。

字節對齊可能帶來的隱患:

代碼中關於對齊的隱患,很多是隱式的。比如在強制類型轉換的時候。例如:

unsigned int i = 0x12345678;
unsigned char *p=NULL;
unsigned short *p1=NULL;

p=&i;
*p=0x00;
p1=(unsigned short *)(p+1);
*p1=0x0000;

最後兩句代碼,從奇數邊界去訪問 unsignedshort 型變量,顯然不符合對齊的規定。在 x86 上,類似的操作只會影響效率,但是在 MIPS 或者 sparc 上,可能就是一個 error, 因爲它們要求必須字節對齊.

五、舉例

例 1:os 基本數據類型佔用的字節數

首先查看操作系統的位數在 64 位操作系統下查看基本數據類型佔用的字節數:

#include <stdio.h>

int main()
{
    printf("sizeof(char) = %ld\n", sizeof(char));
    printf("sizeof(int) = %ld\n", sizeof(int));
    printf("sizeof(float) = %ld\n", sizeof(float));
    printf("sizeof(long) = %ld\n", sizeof(long));                                      
    printf("sizeof(long long) = %ld\n", sizeof(long long));
    printf("sizeof(double) = %ld\n", sizeof(double));
    return 0;
}

例 2:結構體佔用的內存大小 -- 默認規則

考慮下面的結構體佔用的位數

struct yikou_s
{
    double d;
    char c;
    int i;
} yikou_t;

執行結果

sizeof(yikou_t) = 16

在內容中各變量位置關係如下:

其中成員 C 的位置還受字節序的影響,有的可能在位置 8

編譯器給我們進行了內存對齊,各成員變量存放的起始地址相對於結構的起始地址的偏移量必須爲該變量類型所佔用的字節數的倍數, 且結構的大小爲該結構中佔用最大空間的類型所佔用的字節數的倍數。

對於偏移量:變量 type n 起始地址相對於結構體起始地址的偏移量必須爲 sizeof(type(n)) 的倍數結構體大小:必須爲成員最大類型字節的倍數

char: 偏移量必須爲sizeof(char) 即1的倍數
int: 偏移量必須爲sizeof(int) 即4的倍數
float: 偏移量必須爲sizeof(float) 即4的倍數
double: 偏移量必須爲sizeof(double) 即8的倍數

例 3:調整結構體大小

我們將結構體中變量的位置做以下調整:

struct yikou_s
{
    char c;
    double d;
    int i;
} yikou_t;

執行結果

sizeof(yikou_t) = 24

各變量在內存中佈局如下:

當結構體中有嵌套符合成員時,複合成員相對於結構體首地址偏移量是複合成員最寬基本類型大小的整數倍。

例 4:#pragma pack(4)

#pragma pack(4)

struct yikou_s
{
    char c;
    double d;
    int i;
} yikou_t;
sizeof(yikou_t) = 16

例 5:#pragma pack(8)

#pragma pack(8)

struct yikou_s
{
    char c;
    double d;
    int i;
} yikou_t;
sizeof(yikou_t) = 24

例 6:彙編代碼

舉例:以下是截取的 uboot 代碼中異常向量 irq、fiq 的入口位置代碼:

六、彙總實力

有手懶的同學,直接貼一個完整的例子給你們:

#include <stdio.h>
main()
{
struct A {
    int a;
    char b;
    short c;
};
 
struct B {
    char b;
    int a;
    short c;
};
struct AA {
   // int a;
    char b;
    short c;
};

struct BB {
    char b;
   // int a;
    short c;
}; 
#pragma pack (2) /*指定按2字節對齊*/
struct C {
    char b;
    int a;
    short c;
};
#pragma pack () /*取消指定對齊,恢復缺省對齊*/
 
 
 
#pragma pack (1) /*指定按1字節對齊*/
struct D {
    char b;
    int a;
    short c;
};
#pragma pack ()/*取消指定對齊,恢復缺省對齊*/
 
int s1=sizeof(struct A);
int s2=sizeof(struct AA);
int s3=sizeof(struct B);
int s4=sizeof(struct BB);
int s5=sizeof(struct C);
int s6=sizeof(struct D);
printf("%d\n",s1);
printf("%d\n",s2);
printf("%d\n",s3);
printf("%d\n",s4);
printf("%d\n",s5);
printf("%d\n",s6);
}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/-wJKA_VBaL98eS9wVTE9jg