畢業生求職的時節,非畢業生接觸到各種面試、筆試題目的機率也會相應地增加。下面請看一道經典的 C 語言指針訪存題目,稍有些經驗的朋友應該很快可以看出這個題目考查的是字節序、內存佈局等知識點。然後在大腦中略排列一下,就能夠給出答案(2000000)。
- #include < stdio.h >
- int main ()
- {
- int a [ 5 ] = { 1 , 2 , 3 , 4 , 5 } ;
- int * pa = ( int )( & a ) + 1 ;
- printf ( " %x / n " , * pa ) ;
- return 0 ;
- }
不過,這個答案是否絕對正確,還要看題目所處的上下文了。如果題目明確說是在常見的 32 位 x86
平臺上運行,那就無可厚非;但如果沒有指明機器架構,那就要小心一點了,也許命題者真想考查一下求職者對非 x86
平臺的瞭解程度呢。如果考慮機器架構,這個題目應當如何作答呢?粗想一下,我們需要考慮的是字長、字節序和對齊(alignment)訪問規則。不過真要
做實驗看看,會發現這裏面還是有一些花樣的。如果沒有實際經驗,只憑教條加推測,很可能想不到其它平臺上的一些細節之處。
我們換用一段信息量更豐富的程序來進行後續的實驗。在不同的平臺上,均使用未加特殊參數的 gcc 來編譯這段程序——
- #include < stdio.h >
- int main ()
- {
- int x ;
- int a [ 5 ] = { 0x11121314 , 0x21222324 , 0x31323334 , 0x41424344 , 0x51525354 } ;
- for ( x = 0 ; x < 20 ; x ++ ) {
- printf ( " %02x " , * ( char * )(( int )( & a ) + x )) ;
- }
- printf ( " / n " ) ;
- for ( x = 0 ; x < 8 ; x ++ ) {
- printf ( " %08x " , * ( int * )(( int )( & a ) + x )) ;
- }
- printf ( " / n " ) ;
- return 0 ;
- }
在 32 位 x86 下的結果不需要多解釋。
- uname -a
- Linux ubuntu 2.6.31-14-generic #48-Ubuntu SMP Fri Oct 16 14:04:26 UTC 2009 i686 GNU/Linux
- ./a.out
- 14 13 12 11 24 23 22 21 34 33 32 31 44 43 42 41 54 53 52 51
- 11121314 24111213 23241112 22232411 21222324 34212223 33342122 32333421
而在 64 位的 x86_64 下,由於 8 字節的指針被截斷到了 4 字節的整型長度,故會引發段錯誤。同樣的情況出現在 64 位的 Alpha 機器下。解決辦法自然是把運算地址時的 int 修改成 long 或某種顯式的 64 位類型。修改後的結果應該與 32 位 x86 一致。
- uname -a
- Linux ubuntu 2.6.24-22-generic #1 SMP Mon Nov 24 19:35:06 UTC 2008 x86_64 GNU/Linux
- ./a.out
- Segmentation fault
- uname -a
- NetBSD sdf 2.1.0_STABLE NetBSD 2.1.0_STABLE (sdf) #0: Fri Mar 30 02:24:32 UTC 2007 root@ol:/var/sys/arch/alpha/compile/sdf alpha
- ./a.out
- Memory fault (core dumped)
有趣的是在 XScale(Intel 實現的 ARMv5)下,雖然同屬 little-endian,但非對齊取數時出現了在字內按字節循環的移位的結果。查查 ARM 的官方文檔 ,這確實是 ARMv5 的特性;而在 ARMv6 以後,非對齊訪問則是完全支持的。
- uname -a
- Linux zaurus 2.4.18-rmk7-pxa3-embedix #1 Sat, 06 Aug 2005 12:22:55 +0000 armv5tel unknown
- ./a.out
- 14 13 12 11 24 23 22 21 34 33 32 31 44 43 42 41 54 53 52 51
- 11121314 14111213 13141112 12131411 21222324 24212223 23242122 22232421
接下來看看 PowerPC,它是 big-endian 的代表,允許 32 位以內的非對齊訪問,結果是容易理解的。有關 PowerPC 非對齊訪問的一些細節可以參考這篇文章 。
- uname -a
- AIX aix 3 5 00C97AC04C00 powerpc unknown AIX
- ./a.out
- 11 12 13 14 21 22 23 24 31 32 33 34 41 42 43 44 51 52 53 54
- 11121314 12131421 13142122 14212223 21222324 22232431 23243132 24313233
同樣是 big-endium 的 SPARC 則不允許非對齊訪問。它會對非對齊訪問拋出 SIGBUS。
- uname -a
- SunOS t1000 5.10 Generic_118833-33 sun4v sparc SUNW,Sun-Fire-T1000 Solaris
- ./a.out
- 11 12 13 14 21 22 23 24 31 32 33 34 41 42 43 44 51 52 53 54
- Bus Error (core dumped)
最後看看我們中科院計算所的龍芯(Loongson)2E,它是兼容 MIPS 架構的處理器。很多教科書告訴我們說通常的 MIPS 是不允許非對齊訪問的(部分 MIPS 實現提供了非對齊訪問指令,並申請了專利),但我們在龍芯下卻得到了和 x86 相同的、允許非對齊訪問的結果,這又是爲什麼呢?初步查到的原因是“(針對龍芯修改過的 Linux)內核裏確實有一個異常處理函數負責處理 lw 訪問非對齊地址引起的異常 ”。這也許是龍芯繞開 MIPS 專利的一種辦法?我會向龍芯團隊的同學求證一下,也希望熟悉 MIPS 或龍芯的朋友給我一個確切的答案。
- uname -a
- Linux Loongson-1 2.6.18.1lemote #1 Sat Jan 13 16:02:26 CST 2007 mips GNU/Linux
- ./a.out
- 14 13 12 11 24 23 22 21 34 33 32 31 44 43 42 41 54 53 52 51
- 11121314 24111213 23241112 22232411 21222324 34212223 33342122 32333421
不過用心思考的朋友也許會發現上面一系列實驗存在的一個疏漏:沒有考慮編譯器的影響。一方面,編譯器可能對整型的字長有不同的規定(例如 Windows 下的某些編譯器即使在 32 位 x86 上也會把 int 定義爲 16 位);另一方面,編譯器可以對不支持非對齊訪問的處理器生成一定的指令序列、通過多次訪存來模擬非對齊訪問。我們看下面的例子:還是在 SPARC 平臺上,改用 Solaris 自帶的 Sun CC 來編譯實驗程序,這時就不會出現“Bus Error”,而會輸出和 PowerPC 一樣的結果。因爲 SunCC 默認會使用“-xmemalign ”參數來生成適當的訪存指令序列。
- uname -a
- SunOS t1000 5.10 Generic_118833-33 sun4v sparc SUNW,Sun-Fire-T1000 Solaris
- cc data.c
- ./a.out
- 11 12 13 14 21 22 23 24 31 32 33 34 41 42 43 44 51 52 53 54
- 11121314 12131421 13142122 14212223 21222324 22232431 23243132 24313233
這樣看來,在不指定機器架構和編譯器等上下文的情況下,要正確且完美地回答一開始的那道題目還是需要一定知識積累的。答案省略,留給大家自己求 解。在面試、筆試諸如 Sun SPARC、IBM PowerPC、中科院計算所微處理器中心等部門或者做 ARM 等嵌入式開發的公司時,最好先了解清楚它們的產品常識。