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
※千面萬化的Android特洛伊木馬GPlayed
※2018年12月31日起 PHP 5.6.x的安全支持將正式停止
TAG:嘶吼RoarTalk |