一道 C 语言指针访存题目的引申
毕业生求职的时节,非毕业生接触到各种面试、笔试题目的几率也会相应地增加。下面请看一道经典的 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 等嵌入式开发的公司时,最好先了解清楚它们的产品常识。
(部分实验环境来源于 Unix-Center.Net,在此致谢)




