當前位置:
首頁 > 新聞 > C/C+中未對齊訪問導致的問題和解決方法

C/C+中未對齊訪問導致的問題和解決方法

眾所周知,當指針值是對齊值的倍數時,用於執行內存訪問時使用的CPU性能更好。這種現象仍然存在於當前的CPU中,並且仍有一些僅具有執行對齊訪問的指令。考慮到這個問題,C標準已經有了相應的對齊規則,所以編譯器可以利用它們來儘可能的生成有效代碼。正如我們將在本文中看到的那樣,我們在投射指針時需要小心,以確保不會破壞這些規則。本文的目的是通過展示問題並提供一些解決方案來輕鬆克服它,有一定的教育意義。

對於那些只想查看最終代碼的人來說,可以直接跳到【C++輔助程序庫】部分。

註:本文提供的解決方案沒有任何破壞性,並且是相當標準的解決方案!互聯網上的其他資源[1]?[2]也涵蓋了這個問題。

產生的問題

讓我們來看看這個哈希函數,它計算緩衝區中的64位整數:

#include

#include

static uint64_t load64_le(uint8_t const* V)

{

#if !defined(__LITTLE_ENDIAN__)

#error This code only works with little endian systems

#endif

uint64_t Ret = *((uint64_t const*)V);

return Ret;

}

uint64_t hash(const uint8_t* Data, const size_t Len)

{

uint64_t Ret = 0;

const size_t NBlocks = Len/8;

for (size_t I = 0; I

const uint64_t V = load64_le(&Data[I*sizeof(uint64_t)]);

Ret = (Ret ^ V)*CST;

}

uint64_t LastV = 0;

for (size_t I = 0; I

LastV |= ((uint64_t)Data[NBlocks*8+I])

}

Ret = (Ret^LastV)*CST;

return Ret;

}

完整源代碼可以在這裡下載:https://gist.github.com/aguinet/4b631959a2cb4ebb7e1ea20e679a81af。

它基本上將輸入數據作為64位小端整數塊進行處理,使用當前哈希值和乘法執行XOR,用剩餘的位元組填充64位數字。

如果我們想讓這個哈希跨體系結構可移植(可移植的意義是它將在每個可能的CPU/OS上生成相同的值),我們需要處理目標的位元組順序——我將在本文末尾回顧這個主題。

讓我們在經典的Linux x64計算機上編譯並運行該程序:

$ clang -O2 hash.c -o hash && ./hash "hello world"

527F7DD02E1C1350

一切順利。現在,讓我們在Thumb模式下為具有ARMv5 CPU的Android手機交叉編譯此代碼並運行它。假設ANDROID_NDK是一個指向Android NDK安裝的環境變數,我們這樣操做:

$ $ANDROID_NDK/build/tools/make_standalone_toolchain.py --arch arm --install-dir arm

$ ./arm/bin/clang -fPIC -pie -O2 hash.c -o hash_arm -march=thumbv5 -mthumb

$ adb push hash_arm /data/local/tmp && adb shell "/data/local/tmp/hash_arm "hello world""

hash_arm: 1 file pushed. 4.7 MB/s (42316 bytes in 0.009s)

Bus error

有錯誤。讓我們試試另一個字元串:

$ adb push hash_arm && adb shell "/data/local/tmp/hash_arm "dragons""

hash_arm: 1 file pushed. 4.7 MB/s (42316 bytes in 0.009s)

39BF423B8562D6A0

調試

我們檢索了內核日誌以了解詳細信息,發現有:

$ dmesg |grep hash_arm

[13598.809744] [2: hash_arm:22351] Unhandled fault: alignment fault (0x92000021) at 0x00000000ffdc8977

看來我們在對齊方面存在問題。讓我們看一下編譯器生成的程序集:

所述LDMIA指令從存儲器將數據載入到多個寄存器。在我們的例子中,它將我們的64位整數載入到兩個32位寄存器中。該指令的ARM文檔[3]指出存儲器指針必須是字對齊的(在我們的例子中,一個字是2個位元組)。問題出現是因為我們的main函數使用libc載入器傳遞給argv的緩衝區,它沒有保證對齊。

為什麼會這樣?

為什麼編譯器會發出這樣的指令?是什麼讓它認為數據指向的內存是字對齊的?

問題發生在load64_le函數中,其中發生了這種強制轉換:

uint64_t Ret = *((uint64_t const*)V);

根據C標準[10]:「完整對象類型具有對齊要求,這些要求對可以分配該類型的對象的地址施加限制。對齊是一個實現定義的整數值,表示給定對象可以被分配的連續地址之間的位元組數。「 換句話說,這意味著我們應該這樣:

V % (alignof(uint64_t)) == 0

仍然是根據C標準,在不遵守這種對齊規則的情況下,將指針從一種類型轉換為另一種類型是未定義的行為(http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf頁面74,7)。

在我們的例子中,uint64_t的對齊是8個位元組(可以像這樣進行檢查:https://godbolt.org/z/SJjN9y),因此我們遇到的是未定義的行為。更確切地說,前面的拋出器直接告訴我們的編譯器「 ret是8的倍數,因此也是2的倍數。你可以安全的使用LDMIA 」。

在x86-64下不會出現這個問題,因為Intel mov指令支持未對齊的負載[4](如果不啟用對齊檢查的話[5],就只能由操作系統啟用[6])。這就是為什麼「老」代碼有這麼一個不可忽略的隱藏bug,因為它從未出現在x86計算機上(已被開發)。實際上,ARM Debian內核有一種模式可以捕獲未對齊的訪問並正確處理它們[7]!

解決方案

多次載入

一種經典的解決方案是通過逐位元組從內存載入來「手動」生成64位整數,這裡採用小端方式:

uint64_t load64_le(uint8_t const* V)

{

uint64_t Ret = 0; Ret |= (uint64_t) V[0];

Ret |= ((uint64_t) V[1])

Ret |= ((uint64_t) V[2])

Ret |= ((uint64_t) V[3])

Ret |= ((uint64_t) V[4])

Ret |= ((uint64_t) V[5])

Ret |= ((uint64_t) V[6])

Ret |= ((uint64_t) V[7])

return Ret;

}

這個代碼有很多優點:它是一種從內存載入小端64位整數的可移植方式,並且不會破壞先前的對齊規則。缺點是,如果我們只想要CPU的自然位元組順序為整數,則需要編寫兩個版本並使用ifdef編譯好的版本。此外,寫入有點單調乏味並且容易出錯。

無論如何,讓我們看看-O2模式中的clang 6.0 為各種架構生成了什麼:

· x86-64:mov rax,[rdi](參見https://godbolt.org/z/bMS0jd)。這是我們所期望的,因為x86上的mov指令支持非對齊訪問。

· ARM64 ldr x0,[x0](https://godbolt.org/z/qlXpDB)。實際上,ldr ARM64指令似乎沒有任何對齊限制[8]。

· Thumb模式下的ARMv5:https://godbolt.org/z/wCBfcV。這基本上就是我們編寫的代碼,它逐位元組的載入整數並構造它。我們可以注意到,這是一些不可忽略的代碼量(與之前的情況相比)。

因此,只要優化技術被激活的話,Clang能夠檢測到這個模式,並且儘可能的生成高效代碼(請注意在各種godbolt.org鏈接中的-o1標誌)。

memcpy

另一個解決方案是使用memcpy:

uint64_t load64_le(uint8_t const* V) {

uint64_t Ret;

memcpy(&Ret, V, sizeof(uint64_t));

#ifdef __BIG_ENDIAN__

Ret = __builtin_bswap64(Ret);

#endif

returnRet;

}

參考

[1] http://pzemtsov.github.io/2016/11/06/bug-story-alignment-on-x86.html

[2] https://research.csiro.au/tsblog/debugging-stories-stack-alignment-matters/

[3] //infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0068b/BABEFCIB.html

[4] https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-instruction-set-reference-manual-325383.pdf,第690頁

[5] https://xem.github.io/minix86/manual/intel-x86-and-64-manual-vol3/o_fe12b1e2a880e0ce-231.html

[6] 據我所知沒有x86操作系統可以激活它,這樣做可能會導致編譯器生成錯誤的代碼!

[7] https://wiki.debian.org/ArmEabiFixes#word_accesses_must_be_aligned_to_a_multiple_of_their_size

[8] //infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0802b/LDR_reg_gen.html

[9] https://queue.acm.org/detail.cfm?id=3212479

[10] http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf,第66頁,6.2.8.1


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 嘶吼RoarTalk 的精彩文章:

千面萬化的Android特洛伊木馬GPlayed
2018年12月31日起 PHP 5.6.x的安全支持將正式停止

TAG:嘶吼RoarTalk |