一道 C 语言指针访存题目的引申

2009/11/08 | 13:41 | 分类:计算机科学与编程 | 标签: | 804次阅读

  毕业生求职的时节,非毕业生接触到各种面试、笔试题目的几率也会相应地增加。下面请看一道经典的 C 语言指针访存题目,稍有些经验的朋友应该很快可以看出这个题目考查的是字节序、内存布局等知识点。然后在大脑中略排列一下,就能够给出答案(2000000)。

  1. #include <stdio.h>
  2.  
  3. int main()
  4. {
  5.     int a[5] = {1, 2, 3, 4, 5};
  6.     int *pa = (int)(&a) + 1;
  7.     printf("%x\n", *pa);
  8.     return 0;
  9. }

  不过,这个答案是否绝对正确,还要看题目所处的上下文了。如果题目明确说是在常见的 32 位 x86 平台上运行,那就无可厚非;但如果没有指明机器架构,那就要小心一点了,也许命题者真想考查一下求职者对非 x86 平台的了解程度呢。如果考虑机器架构,这个题目应当如何作答呢?粗想一下,我们需要考虑的是字长、字节序和对齐(alignment)访问规则。不过真要做实验看看,会发现这里面还是有一些花样的。如果没有实际经验,只凭教条加推测,很可能想不到其它平台上的一些细节之处。
  我们换用一段信息量更丰富的程序来进行后续的实验。在不同的平台上,均使用未加特殊参数的 gcc 来编译这段程序——

  1. #include <stdio.h>
  2.  
  3. int main()
  4. {
  5.     int x;
  6.     int a[5] = {0x11121314, 0x21222324, 0x31323334, 0x41424344, 0x51525354};
  7.     for (x = 0; x < 20; x++) {
  8.         printf("%02x ", *(char *)((int)(&a) + x));
  9.     }
  10.     printf("\n");
  11.     for (x = 0; x < 8; x++) {
  12.         printf("%08x ", *(int *)((int)(&a) + x));
  13.     }
  14.     printf("\n");
  15.     return 0;
  16. }

  在 32 位 x86 下的结果不需要多解释。

  1. uname -a
  2. Linux ubuntu 2.6.31-14-generic #48-Ubuntu SMP Fri Oct 16 14:04:26 UTC 2009 i686 GNU/Linux
  3. ./a.out
  4. 14 13 12 11 24 23 22 21 34 33 32 31 44 43 42 41 54 53 52 51
  5. 11121314 24111213 23241112 22232411 21222324 34212223 33342122 32333421

  而在 64 位的 x86_64 下,由于 8 字节的指针被截断到了 4 字节的整型长度,故会引发段错误。同样的情况出现在 64 位的 Alpha 机器下。解决办法自然是把运算地址时的 int 修改成 long 或某种显式的 64 位类型。修改后的结果应该与 32 位 x86 一致。

  1. uname -a
  2. Linux ubuntu 2.6.24-22-generic #1 SMP Mon Nov 24 19:35:06 UTC 2008 x86_64 GNU/Linux
  3. ./a.out
  4. Segmentation fault
  1. uname -a
  2. 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
  3. ./a.out
  4. Memory fault (core dumped)

  有趣的是在 XScale(Intel 实现的 ARMv5)下,虽然同属 little-endian,但非对齐取数时出现了在字内按字节循环的移位的结果。查查 ARM 的官方文档,这确实是 ARMv5 的特性;而在 ARMv6 以后,非对齐访问则是完全支持的。

  1. uname -a
  2. Linux zaurus 2.4.18-rmk7-pxa3-embedix #1 Sat, 06 Aug 2005 12:22:55 +0000 armv5tel unknown
  3. ./a.out
  4. 14 13 12 11 24 23 22 21 34 33 32 31 44 43 42 41 54 53 52 51
  5. 11121314 14111213 13141112 12131411 21222324 24212223 23242122 22232421

  接下来看看 PowerPC,它是 big-endian 的代表,允许 32 位以内的非对齐访问,结果是容易理解的。有关 PowerPC 非对齐访问的一些细节可以参考这篇文章

  1. uname -a
  2. AIX aix 3 5 00C97AC04C00 powerpc unknown AIX
  3. ./a.out
  4. 11 12 13 14 21 22 23 24 31 32 33 34 41 42 43 44 51 52 53 54
  5. 11121314 12131421 13142122 14212223 21222324 22232431 23243132 24313233

  同样是 big-endium 的 SPARC 则不允许非对齐访问。它会对非对齐访问抛出 SIGBUS。

  1. uname -a
  2. SunOS t1000 5.10 Generic_118833-33 sun4v sparc SUNW,Sun-Fire-T1000 Solaris
  3. ./a.out
  4. 11 12 13 14 21 22 23 24 31 32 33 34 41 42 43 44 51 52 53 54
  5. Bus Error (core dumped)

  最后看看我们中科院计算所的龙芯(Loongson)2E,它是兼容 MIPS 架构的处理器。很多教科书告诉我们说通常的 MIPS 是不允许非对齐访问的(部分 MIPS 实现提供了非对齐访问指令,并申请了专利),但我们在龙芯下却得到了和 x86 相同的、允许非对齐访问的结果,这又是为什么呢?初步查到的原因是“(针对龙芯修改过的 Linux)内核里确实有一个异常处理函数负责处理 lw 访问非对齐地址引起的异常”。这也许是龙芯绕开 MIPS 专利的一种办法?我会向龙芯团队的同学求证一下,也希望熟悉 MIPS 或龙芯的朋友给我一个确切的答案。

  1. uname -a
  2. Linux Loongson-1 2.6.18.1lemote #1 Sat Jan 13 16:02:26 CST 2007 mips GNU/Linux
  3. ./a.out
  4. 14 13 12 11 24 23 22 21 34 33 32 31 44 43 42 41 54 53 52 51
  5. 11121314 24111213 23241112 22232411 21222324 34212223 33342122 32333421

  不过用心思考的朋友也许会发现上面一系列实验存在的一个疏漏:没有考虑编译器的影响。一方面,编译器可能对整型的字长有不同的规定(例如 Windows 下的某些编译器即使在 32 位 x86 上也会把 int 定义为 16 位);另一方面,编译器可以对不支持非对齐访问的处理器生成一定的指令序列、通过多次访存来模拟非对齐访问。我们看下面的例子:还是在 SPARC 平台上,改用 Solaris 自带的 Sun CC 来编译实验程序,这时就不会出现“Bus Error”,而会输出和 PowerPC 一样的结果。因为 SunCC 默认会使用“-xmemalign”参数来生成适当的访存指令序列。

  1. uname -a
  2. SunOS t1000 5.10 Generic_118833-33 sun4v sparc SUNW,Sun-Fire-T1000 Solaris
  3. cc data.c
  4. ./a.out
  5. 11 12 13 14 21 22 23 24 31 32 33 34 41 42 43 44 51 52 53 54
  6. 11121314 12131421 13142122 14212223 21222324 22232431 23243132 24313233

  这样看来,在不指定机器架构和编译器等上下文的情况下,要正确且完美地回答一开始的那道题目还是需要一定知识积累的。答案省略,留给大家自己求解。在面试、笔试诸如 Sun SPARC、IBM PowerPC、中科院计算所微处理器中心等部门或者做 ARM 等嵌入式开发的公司时,最好先了解清楚它们的产品常识。
  (部分实验环境来源于 Unix-Center.Net,在此致谢)

试用Windows的UNIX/POSIX子系统(SUA)

2009/05/04 | 20:07 | 分类:Windows应用 | 标签: | 1,635次阅读

  以前研究Windows的基本概念时,我就知道它有一个POSIX子系统,可以在Windows下编译运行使用了POSIX库的程序。但这一直停留在书本概念层面,直到昨天看到Jeep同学的Windows系统上安装了一个Subsystem for UNIX-based Applications时,我便决定也安装试用一下。
  有关Windows的POSIX子系统是什么、怎么用的问题,可以参考Wikipedia或Microsoft TechNet [英文][中文]上的介绍。它历经了NT时代的Microsoft POSIX subsystem、XP/2000时代的Microsoft Windows Services for UNIX (SFU)以及2003 R2/Vista/2008时代的Subsystem for UNIX-based Applications (SUA)等版本,对POSIX标准的支持日臻完善。我的系统是来自MSDNAA/IEEE的Windows Server 2008,自然要使用最新版的SUA。至于SUA和cygwin在实现机理和功能性能上有什么区别,我还没有仔细研究。但从直观感觉上,Windows原生支持的SUA是比cygwin快一点儿;按照Wikipedia上的这个说法,cygwin是对POSIX是“partial”兼容,而SFU/SUA则是“full”兼容。
  很多人安装SUA的目的并不是要向Windows移植什么重要的UNIX/Linux应用,有时候我们仅仅是为了在Windows中使用一个类UNIX的Shell以及丰富的GNU utilities,毕竟这类久经考验的命令行工具比Windows Command Prompt的那些命令好使很多。对于工作环境要求在Windows和Linux间来来回回切换的人们,也省得敲错命令。安装SUA之后,预装的Shell是C Shell和Korn Shell,还安装了包括vi、gcc在内的300多个命令行工具。同时,Windows的Path环境变量中自动添加了SUA相关目录,这样在Windows Command Prompt和Power Shell中也可以使用很多GNU utilities了。当然,UNIX Shell的内部命令是不可以在这里使用的。此外,在Power Shell中,Power Shell命令别名(如ls、cp)会优先于同名的GNU utilities调用。总之,使用SUA或cygwin这类UNIX Shell+GNU utilities的模拟环境,相比手工添加的“容错”命令或者GnuWin32这类独立命令级的移植要“真实”和顺手得多,但缺点就是体积庞大。
  为了检验SUA的能力,我拿我相对熟悉的GNU bash做了实验。从官方下载bash-4.0版源代码,在SUA的C Shell环境中解压,运行./configure通过。但在make时报错:

  1. execute_cmd.c: In function `time_command':
  2. execute_cmd.c:1145: error: storage size of `dtz' isn't known

  查看execute_cmd.c源代码,发现这句代码有注释:

  1. struct timezone dtz; /* posix doesn't define this */

  看来这个struct timezone不是POSIX标准的东西。通过上下文,我发现这里是处理time(计时)关键字的函数。于是查看configure的帮助,得知只要加一个“--disable-command-timing”参数即可禁用time关键字(这时输入time将改用/bin/time程序做计时)。再次make,execute_cmd.c通过,但又出现以下错误:

  1. getcwd.c: In function `_path_checkino':
  2. getcwd.c:80: error: `MP_RMDOT' undeclared (first use in this function)
  3. getcwd.c:80: error: (Each undeclared identifier is reported only once
  4. getcwd.c:80: error: for each function it appears in.)

  查看./lib/sh/getcwd.c的源代码,果然没有找到MP_RMDOT宏的定义。在全部源代码中搜索,发现这一定义在externs.h中,在./lib/sh/makepath.c中也有使用,唯独在./lib/sh/getcwd.c中使用了却没有引用其定义。也许这是bash-4.0的一个bug?使用Linux下的gcc编译连接是通过的,但SUA下的gcc版本也许有更严格的名字连接策略,导致无法编译通过。于是我将“#define MP_RMDOT 0x04”的定义手工加入./lib/sh/getcwd.c,再次make,全部通过。实验运行无误!
  需要注意的是,SUA提供的是编译和运行使用了POSIX库的程序的环境,并不提供UNIX二进制文件的运行支持。它编译生成和支持运行的可执行文件仍然是Windows的PE格式,而不是ELF之类。SUA支持的只是仅使用了标准库和POSIX库的程序的源代码级移植,对于使用了Linux等环境特有的系统调用的程序,也不可能不加修改地编译运行。
  有空再研究一下SUA在我们的工程中能有什么实际点的应用。

基于FD-CES-B实验仪的计算机整机实验

2007/09/18 | 09:49 | 分类:Windows开发 | 标签: | 503次阅读

  在小学期结束之际,贴出我和柴祎同学合作的基于FD-CES-B实验仪的计算机整机实验报告(节选),希望对使用类似器件进行计算机整机实验的同学有一定的参考价值。

  下载:
http://www.linjian.cn/files/works/FD-CES-B-cpu.pdf
http://files.linjian.org/works/FD-CES-B-cpu.pdf

  另外,提供一个本人用C#编写的辅助程序(含源代码),用于查找微指令位与有效电平的对应关系,“磨刀不误砍柴工”,此工具有助于快速而正确地书写或分析微指令。

  下载:
http://www.linjian.cn/files/dotNet/bitdeffer.rar
http://files.linjian.org/dotNet/bitdeffer.rar