3 内存容量检查(2)(harib06c)

这种做法本身没有问题,笔者在OSASK上确认过,所以看到上述结果很纳闷。这种内存检查方法在很多机型上都能运行,所以笔者非常自信地向大家推荐了它。虽然笔者坚信程序没有问题,可运行结果……

经过多方调查,终于搞清楚了原因。如果我们不用“make run”,而是用“make -r bootpack.nas”来运行的话,就可以确认bootpack.c被编译成了什么样的机器语言。用文本编辑器看一看生成的bootpack.nas会发现,最下边有memtest_sub的编译结果。我们将编译结果列在下面。(为了读起来方便,笔者还添加了注释。)

harib06b中,memtest_sub的编译结果

  1. _memtest_sub:
  2. PUSH EBP ; C编译器的固定语句
  3. MOV EBP,ESP
  4. MOV EDX,DWORD [12+EBP] ; EDX = end;
  5. MOV EAX,DWORD [8+EBP] ; EAX = start; /* EAX是i */
  6. CMP EAX,EDX ; if (EAX > EDX) goto L30;
  7. JA L30
  8. L36:
  9. L34:
  10. ADD EAX,4096 ; EAX += 0x1000;
  11. CMP EAX,EDX ; if (EAX <= EDX) goto L36;
  12. JBE L36
  13. L30:
  14. POP EBP ; 接收前文中PUSHEBP
  15. RET ; return;

有些细节大家可能不太明白,但是可以跟memtest_sub比较一下。可以发现,以上的编译结果有点不正常。

harib06b中,memtest_sub的内容

  1. unsigned int memtest_sub(unsigned int start, unsigned int end)
  2. {
  3. unsigned int i, *p, old, pat0 = 0xaa55aa55, pat1 = 0x55aa55aa;
  4. for (i = start; i <= end; i += 0x1000) {
  5. p = (unsigned int *) (i + 0xffc);
  6. old = *p; /* 先记住修改前的值 */
  7. *p = pat0; /* 试写 */
  8. *p ^= 0xffffffff; /* 反转 */
  9. if (*p != pat1) { /* 检查反转结果 */
  10. not_memory:
  11. *p = old;
  12. break;
  13. }
  14. *p ^= 0xffffffff; /* 再次反转 */
  15. if (*p != pat0) { /* 检查值是否恢复 */
  16. goto not_memory;
  17. }
  18. *p = old; /* 恢复为修改前的值 */
  19. }
  20. return i;
  21. }

大家会发现,编译后没有XOR等指令,而且,好像编译后只剩下了for语句。怪不得显示结果是3GB呢。但是,为什么会这样呢?

■■■■■

笔者开始以为这是C编译器的bug,但仔细查一查,发现并非如此。反倒是编译器太过优秀了。

编译器在编译时,应该是按下面思路考虑问题的。

首先将内存的内容保存到old里,然后写入pat0的值,再反转,最后跟pat1进行比较。这不是肯定相等的吗?if语句不成立,得不到执行,所以把它删掉。怎么?下面还要反转吗?这家伙好像就喜欢反转。这次是不是要比较p和pat0呀?这不是也肯定相等吗?这些处理不是多余么?为了提高速度,将这部分也删掉吧。这样一来,程序就变成了:

编译器脑中所想的(1)

  1. unsigned int memtest_sub(unsigned int start, unsigned int end)
  2. {
  3. unsigned int i, p, old, pat0 = 0xaa55aa55, pat1 = 0x55aa55aa;
  4. for (i = start; i <= end; i += 0x1000) {
  5. p = (unsigned int ) (i + 0xffc);
  6. old = p; / 先记住修改前的值/
  7. p = pat0; / 试写 /
  8. p ^= 0xffffffff; / 反转 /
  9. p ^= 0xffffffff; / 再次反转 /
  10. p = old; / 恢复为修改前的值 /
  11. }
  12. return i;
  13. }

反转了两次会变回之前的状态,所以这些处理也可以不要嘛。因此程序就变成了这样:

编译器脑中所想的(2)

  1. unsigned int memtest_sub(unsigned int start, unsigned int end)
  2. {
  3. unsigned int i, p, old, pat0 = 0xaa55aa55, pat1 = 0x55aa55aa;
  4. for (i = start; i <= end; i += 0x1000) {
  5. p = (unsigned int ) (i + 0xffc);
  6. old = p; / 先记住修改前的值 /
  7. p = pat0; / 试写 /
  8. p = old; / 恢复为修改前的值 /
  9. }
  10. return i;
  11. }

还有,“p = pat0;”本来就没有意义嘛。反正要将old的值赋给p。因此程序就变成了:

编译器脑中所想的(3)

  1. unsigned int memtest_sub(unsigned int start, unsigned int end)
  2. {
  3. unsigned int i, p, old, pat0 = 0xaa55aa55, pat1 = 0x55aa55aa;
  4. for (i = start; i <= end; i += 0x1000) {
  5. p = (unsigned int ) (i + 0xffc);
  6. old = p; / 先记住修改前的值 /
  7. p = old; / 恢复为修改前的值 /
  8. }
  9. return i;
  10. }

这程序是什么嘛?结果,p里面不是没写进任何内容吗?这有什么意义?

编译器脑中所想的(4)

  1. unsigned int memtest_sub(unsigned int start, unsigned int end)
  2. {
  3. unsigned int i, p, old, pat0 = 0xaa55aa55, pat1 = 0x55aa55aa;
  4. for (i = start; i <= end; i += 0x1000) {
  5. p = (unsigned int ) (i + 0xffc);
  6. }
  7. return i;
  8. }

这里的地址变量p,虽然计算了地址,却一次也没有用到。这么说来,old、pat0、pat1也都是用不到的变量。全部都舍弃掉吧。

编译器脑中所想的(5)

  1. unsigned int memtest_sub(unsigned int start, unsigned int end)
  2. {
  3. unsigned int i;
  4. for (i = start; i <= end; i += 0x1000) { }
  5. return i;
  6. }

好了,这样修改后,速度能提高许多。用户肯定会说:“这编译器真好,速度特别快!”

根据以上编译器的思路,我们可以看出,它进行了最优化处理。但其实这个工作本来是不需要的。用于应用程序的C编译器,根本想不到会对没有内存的地方进行读写。

如果更改编译选项,是可以停止最优化处理的。可是在其他地方,我们还是需要如此考虑周密的最优化处理的,所以不想更改编译选项。那怎样来解决这个问题呢?想来想去,还是觉得很麻烦,于是决定memtest_sub也用汇编来写算了。

这次C编译器只是好心干了坏事,但意外的是,它居然会考虑得如此周到、缜密来进行最优化处理……这个编译器真是聪明啊!顺便说一句,这种事偶尔还会有的,所以能够看见中途结果很有用。而且懂汇编语言很重要。

■■■■■

笔者用汇编语言写的程序列举如下。

本次的naskfunc.nas节选

  1. _memtest_sub: ; unsigned int memtest_sub(unsigned int start, unsigned int end)
  2. PUSH EDI ; (由于还要使用EBX, ESI, EDI
  3. PUSH ESI
  4. PUSH EBX
  5. MOV ESI,0xaa55aa55 ; pat0 = 0xaa55aa55;
  6. MOV EDI,0x55aa55aa ; pat1 = 0x55aa55aa;
  7. MOV EAX,[ESP+12+4] ; i = start;
  8. mts_loop:
  9. MOV EBX,EAX
  10. ADD EBX,0xffc ; p = i + 0xffc;
  11. MOV EDX,[EBX] ; old = *p;
  12. MOV [EBX],ESI ; *p = pat0;
  13. XOR DWORD [EBX],0xffffffff ; *p ^= 0xffffffff;
  14. CMP EDI,[EBX] ; if (*p != pat1) goto fin;
  15. JNE mts_fin
  16. XOR DWORD [EBX],0xffffffff ; *p ^= 0xffffffff;
  17. CMP ESI,[EBX] ; if (*p != pat0) goto fin;
  18. JNE mts_fin
  19. MOV [EBX],EDX ; *p = old;
  20. ADD EAX,0x1000 ; i += 0x1000;
  21. CMP EAX,[ESP+12+8] ; if (i <= end) goto mts_loop;
  22. JBE mts_loop
  23. POP EBX
  24. POP ESI
  25. POP EDI
  26. RET
  27. mts_fin:
  28. MOV [EBX],EDX ; *p = old;
  29. POP EBX
  30. POP ESI
  31. POP EDI
  32. RET

笔者好久没写过这么长的汇编程序了。程序里加上了足够的注释,应该很好懂。虽然XOR指令(异或)是第一次出现,不过不用特别解释大家也应该能明白。

那好,我们删除bootpack.c中的memtest_sub函数,运行一下看看。“make run”。结果怎么样呢?

3 内存容量检查(2)(harib06c) - 图1

内存容量显示正常了

太好了!现在可以回到内存管理这个正题上来了。