最近在看《UNIX網路編程》,發現一個問題,始終無法解決,請大神幫忙答疑解惑。

我們知道,通常情況下,結構體是有對齊的需求的,我在win32下使用gcc,int大小是4, char大小是1,默認採用自然對齊,定義了兩個結構體和一個字元數組:

typedef struct
{
char a1;
char a2;
char a3;
}A;
typedef struct
{
int b1;
int b2;
}B;
char buff[1024] = {0};

很明顯,char是1位元組對齊,A是1位元組對齊,sizeof(A) = 3, B是4位元組對齊. sizeof(B) = 8;

在《UNIX網路編程》一書中,經常有類似於下面的這種做法:

A* a = (A*) buff;
XXXXXXXXX; //對a進行操作
B* b = (B*) (buff +sizeof(A));
b-&>b1......;
b-&>b2......; //對b進行操作

我之前對位元組對齊的理解是這樣的:char*強轉成A*沒有問題,因為他們都只要求起始地址是1的整數倍,但是強轉成B*似乎會出問題。假設buff本身恰好處於4的整數倍的地址(貌似有些系統默認棧內存起始都是4對齊),那麼這樣一來buff +sizeof(A)就一定不能滿足是4的整數倍,從而使得強轉後的指針b無法滿足B的對齊條件(B是4位元組對齊所以B的首地址必須是4 的整數倍),這個時候使用b-&>b1或者b-&>b2這樣的方式訪問數據似乎就會有一些問題。

看到網上有些資料說,這種情況在x86上是允許的,只是會使存取效率降低,而在ARM上會導致錯誤。我本人之前在ARM上也踩過類似的坑,所以對這個特別關注。不過我感覺Steven大神的代碼不應該有問題的,所以有些懷疑自己之前所理解的內容。不知為各位大神對此有什麼見解呢?

P.S.因為之前踩過強轉的坑,所以我一般盡量不用強轉,必須用的時候我會重新定義一個結構體變數或者在堆上申請一塊內存,然後用memcpy拷貝過來。假設buff是發送緩衝區,代碼類似下面這種方式:

B bs;
bs.b1... ;
bs.b2...; //填寫和修改bs
memcpy(buff + sizeof(A), bs, sizeof(B));
XXXXXXXX buff; //將buff發送給接收方
//而在接收方,也不採用直接強制轉換的方式取出結構體的內容,而是採用下面的方式:
char recv[1024] = {0}; //定義接收緩衝區
XXXXXXXX recv; // 將數據接收到緩衝區中
B rs;
memcpy(B, recv + sizeof(A), sizeof(B));
rs.b1...;
rs.b2...; //讀取接收到的B結構體內容

使用這種方式是不是就可以從根本上解決我之前提出的問題,從而實現無需指定對齊方式的情況下跨平臺移植呢?(假設發送端和接受端平臺相同)


你想到的做法沒錯,可能正是標準期望的:未對齊的情況下需要逐個位元組處理數據。標準 C 中未對齊的訪問是未定義行為。

(另外有個事是標準 C 不允許 char 數組為其他類型提供存儲,而 C++ 可以。雖然這個題目關係不大。)

不過如果執意這麼寫的話,需要注意一下編譯器有沒有提供允許未對齊訪問的擴展,如 GCC 的 -munaligned-access 。畢竟標準不管這問題。

https://gcc.gnu.org/onlinedocs/gcc/ARM-Options.html


ARM下沒接觸過,就說下x86的情況。

x86下非對齊訪問是允許的,當然效率會低一點。非對齊訪問也可以被禁止,只要設置了http://CR0.AM和EFLAG.AC,ring3下非對齊訪問就會觸發異常。另外cpu的部分指令對地址對齊也存在要求,比如movdqa,這些一般在使用sse、avx等高級指令集時要注意。

那麼怎麼看待那段代碼?很簡單,沒有什麼特別的看法,那樣寫又不是不可以。那部分代碼一般用來對外來的數據反序列化,性能一般不是問題,除非對外來數據有大量的讀取操作;所以怎麼方便怎麼來。但是如果結構體成員存在__m128i、__m128等類型的成員時,務必在確保地址對齊的情況下訪問,因為這種情況編譯器會採用movdqa指令訪問,沒對齊的話就是Access Violation。還有在內核裏,務必將外來數據copy到一個已對齊的結構體、然後再對已對齊的結構體訪問;這麼做一來儘可能提升效率,二來有些內核API對內存對齊是有要求的,沒對齊就可能觸發BSOD。


是的,C 指針強轉需要考慮位元組對齊。正如你所說的,假如指針不對齊,就去讀寫其中數據,x86 速度會慢些,而 ARM 可能直接崩掉。

為了安全,從網路或文件讀數據的時候,有時需要使用 memcpy 來複制,不直接強轉。

但並非有指針強轉就不對。memcpy 速度會慢些,直接強轉會快些。有時為了速度,會使用強轉。當讀取代碼有強轉時,在寫入數據的時候,就需要考慮對齊問題。

比如你例子的結構 A, 原始定義 sizeof(A) = 3。假設在 32 位機子上,想在讀取時候強轉,需要在結構中填充一些位元組。

typedef struct
{
char a1;
char a2;
char a3;
char padding; // 填充位元組
} A;

static_assert(sizeof(A) == 4, "");

添加填充位元組後,A 就是 4 位元組對齊。於是先寫入 A, 再寫入 B。在讀取時候,只要起始指針是對齊(這點容易保證,編譯時候就可以確定),就可以保證 A、B 都一定是對齊的。

再比如,寫入字元串 "123456789",有 9 個位元組。可以在寫入 9 個位元組後,再寫入 3 個填充位元組 0。就可以保證下一批數據是 4 位元組對齊。

將結構序列化成二進位數據,之後寫入文件、壓入命令隊列、放進網路。都是在設計一種協議。假如你的協議在設計的時候就考慮位元組對齊,讀取時候就可以直接強轉。這樣會更快。

實際上很多文件格式,網路協議,在設計的時候確實就會考慮位元組對齊,有時會補一些填充位元組。機器也經常有 nop 的空指令,什麼也不做,這些 nop 指令可以作為填充位元組,讓後續指令對齊。

為了讀取安全,很多時候會 memcpy,但並不一定有強轉就不對。需要具體代碼具體分析。

具體到《UNIX網路編程》的代碼,假如遇到有指針強轉的情況,可以去分析一下它定義的結構。可能它的結構一開始就是位元組對齊的,因而強轉也不會出錯。


嚴格來講確實會有問題。我以前寫 dsp 程序,裡面就有一大堆手動處理對齊的代碼。


首先,x86平臺不要求對齊,或者說x86平臺會自動把未對齊的訪存處理成兩次對齊的訪存,所以x86平臺上的對齊完全是軟體行為。

有些平臺不會處理這種情況,而是報未對齊異常,有些編譯器在你指定這些平臺的時候會幫你把未對齊的訪存翻譯成兩次對齊的訪存,但如果編譯器沒有提供這個翻譯就的確得自己做了。


你的想法是正確的。下面的鏈接也有相應的討論,其中第二個例子就是你補充的內容。

EXP36-C. Do not cast pointers into more strictly aligned pointer types?

wiki.sei.cmu.edu


以前在SPARC機器上處理過這樣的問題,在x86上運行的好好的到了SPARC上就崩了。SPARC和ARM一樣,都不支持CPU跨line訪問。


兩段代碼都沒有任何問題,只不過作者顯然比你更懂一個道理:編譯器會把你的強轉以及指針統統編譯成mov指令和dword變數。


嚴格來說你把buf轉成A*或B*是一個ub,不過很多時候可以用-fno-strict-aliasing來解決

strict-aliasing只允許某類型的指針到void *或char *轉。。


對沒對齊好像和強轉沒有關係。對於指針就是一個取地址過程。學了彙編的定址方式以後對指針這個東西理解就會很直觀了,結構體對齊這個問題我記得是編譯器優化問題。在32位機器上4位元組對齊取數據更高效,雖然損失了空間。我記得有的編譯器是可以不對齊的,或者指定對齊方式。好像是在很舊的嵌入式設備上來著。強轉會導致數據讀取異常,類似和讀取野指針數據一樣。


必須會啊。

有位哥們兒講的好:為什麼要強轉啊?

首先,構築正確的結構架構,不要一有問題就用強轉什麼的強搞,一定要注意安全。比如在同一個程序內部,各個模板的編譯選項要一致,使用的數據結構定義要統一,統一使用一份結構定義,這樣程序內部絕對無問題。題目

其次,與外部交流。數據解析和檢查的代碼不能省,要一直追蹤介面定義,確保一直使用正確的介面定義。能解析就盡量解析而不強轉,要強轉就一定要完整地檢查所有數據。

題目所說的指針強轉基本只出現在程序內部,與外界交流一定不能有內部地址指針內容。


我們一般用強轉都是 void 指針轉別的類型,前提是那個類型已經被定義好了,本身就是按照正確的對齊方式存放的,就不存在對齊的問題。

至於 char 指針直接轉 int 之類的,之前我這邊從來沒有過這種需求,頂多也就是結構體前面幾個成員相同,後面幾個成員有不同的互轉,這種也不會有對齊問題。

我覺得大多數互轉都是轉換的兩者具有某些共同結構部分點的,很少有完全不相干的 char 轉 int。

還有就是一般是比較長的類型結構轉比較短的類型結構,所以不管怎麼說都應該盡量保證轉換前後的對齊。


有可能會。你可以在Cortex M7上試試,拿指針強制轉換出個float數,如果地址不是四位元組對齊的話就會引發HardFault中斷。不過這個錯誤中斷並不是CPU發的,而是內置DSP核心發的。因此關了硬浮點運算說不定能轉的出來。。。


有意思的是

為什麼非要強轉


會,我之前也踩過ARM下要求struct起始地址要對齊4的的坑。映像中x86隻要求對齊到2。當時就直接來signal然後coredump了。

你的這種做法,不跨編譯器的話應該是可以的。但是之前好像看到過有些編譯器在內部對象的佈局上會有一些差異。所以我現在在實際應用中還是逐個成員打包。


你的疑問是有道理的。這種就是缺乏可移植性的代碼。


相關文章