18.6 结构体中的位

18.6.1 CPUID 的例子

C/C++中允许给结构体的每一个成员都定义一个准确的位域。如果我们想要节省空间的话,这个对我们来说将是非常有用的。比如,对BOOL来说,1位就足矣了。但是当然,如果我们想要速度的话,必然会浪费点空间。 让我们以CPUID指令为例,这个指令返回当前CPU的信息和特性。 如果EAX在指令执行之前就设置为了1,CPUID将会返回这些内容到EAX中。

18.6 结构体中的位 - 图1

MSVC 2010有CPUID的宏,但是GCC 4.4.1没有,所以,我们就手动的利用它的内联汇编器为GCC写一个吧。

  1. #include <stdio.h>
  2. #ifdef __GNUC__
  3. static inline void cpuid(int code, int *a, int *b, int *c, int *d) {
  4. asm volatile("cpuid":"=a"(*a),"=b"(*b),"=c"(*c),"=d"(*d):"a"(code));
  5. }
  6. #endif
  7. #ifdef _MSC_VER
  8. #include <intrin.h>
  9. #endif
  10. struct CPUID_1_EAX
  11. {
  12. unsigned int stepping:4;
  13. unsigned int model:4;
  14. unsigned int family_id:4;
  15. unsigned int processor_type:2;
  16. unsigned int reserved1:2;
  17. unsigned int extended_model_id:4;
  18. unsigned int extended_family_id:8;
  19. unsigned int reserved2:4;
  20. };
  21. int main()
  22. {
  23. struct CPUID_1_EAX *tmp;
  24. int b[4];
  25. #ifdef _MSC_VER
  26. __cpuid(b,1);
  27. #endif
  28. #ifdef __GNUC__
  29. cpuid (1, &b[0], &b[1], &b[2], &b[3]);
  30. #endif
  31. tmp=(struct CPUID_1_EAX *)&b[0];
  32. printf ("stepping=%d", tmp->stepping);
  33. printf ("model=%d", tmp->model);
  34. printf ("family_id=%d", tmp->family_id);
  35. printf ("processor_type=%d", tmp->processor_type);
  36. printf ("extended_model_id=%d", tmp->extended_model_id);
  37. printf ("extended_family_id=%d", tmp->extended_family_id);
  38. return 0;
  39. };

之后CPU会填充EAX,EBX,ECX,EDX,这些寄存器的值会通过b[]数组显现出来。接着我们用一个指向CPUID_1_EAX结构体的指针,把它指向b[]数组的EAX值。 换句话说,我们将把32位的INT类型的值当作一个结构体来看。 然后我们就能从结构体中读取数据。 让我们在MSVC 2008用/Ox编译一下:

清单18.19: MSVC 2008

  1. _b$ = -16 ; size = 16
  2. _main PROC
  3. sub esp, 16 ; 00000010H
  4. push ebx
  5. xor ecx, ecx
  6. mov eax, 1
  7. cpuid
  8. push esi
  9. lea esi, DWORD PTR _b$[esp+24]
  10. mov DWORD PTR [esi], eax
  11. mov DWORD PTR [esi+4], ebx
  12. mov DWORD PTR [esi+8], ecx
  13. mov DWORD PTR [esi+12], edx
  14. mov esi, DWORD PTR _b$[esp+24]
  15. mov eax, esi
  16. and eax, 15 ; 0000000fH
  17. push eax
  18. push OFFSET $SG15435 ; stepping=%d’, 0aH, 00H
  19. call _printf
  20. mov ecx, esi
  21. shr ecx, 4
  22. and ecx, 15 ; 0000000fH
  23. push ecx
  24. push OFFSET $SG15436 ; model=%d’, 0aH, 00H
  25. call _printf
  26. mov edx, esi
  27. shr edx, 8
  28. and edx, 15 ; 0000000fH
  29. push edx
  30. push OFFSET $SG15437 ; family_id=%d’, 0aH, 00H
  31. call _printf
  32. mov eax, esi
  33. shr eax, 12 ; 0000000cH
  34. and eax, 3
  35. push eax
  36. push OFFSET $SG15438 ; processor_type=%d’, 0aH, 00H
  37. call _printf
  38. mov ecx, esi
  39. shr ecx, 16 ; 00000010H
  40. and ecx, 15 ; 0000000fH
  41. push ecx
  42. push OFFSET $SG15439 ; extended_model_id=%d’, 0aH, 00H
  43. call _printf
  44. shr esi, 20 ; 00000014H
  45. and esi, 255 ; 000000ffH
  46. push esi
  47. push OFFSET $SG15440 ; extended_family_id=%d’, 0aH, 00H
  48. call _printf
  49. add esp, 48 ; 00000030H
  50. pop esi
  51. xor eax, eax
  52. pop ebx
  53. add esp, 16 ; 00000010H
  54. ret 0
  55. _main ENDP

SHR指令将EAX寄存器的值右移位,移出去的值必须被忽略,例如我们会忽略右边的位。 AND指令将清除左边不需要的位,换句话说,它处理过后EAX将只留下我们需要的值。 让我们在GCC4.4.1下用-O3编译。

清单18.20: GCC 4.4.1

  1. main proc near ; DATA XREF: _start+17
  2. push ebp
  3. mov ebp, esp
  4. and esp, 0FFFFFFF0h
  5. push esi
  6. mov esi, 1
  7. push ebx
  8. mov eax, esi
  9. sub esp, 18h
  10. cpuid
  11. mov esi, eax
  12. and eax, 0Fh
  13. mov [esp+8], eax
  14. mov dword ptr [esp+4], offset aSteppingD ; "stepping=%d"
  15. mov dword ptr [esp], 1
  16. call ___printf_chk
  17. mov eax, esi
  18. shr eax, 4
  19. and eax, 0Fh
  20. mov [esp+8], eax
  21. mov dword ptr [esp+4], offset aModelD ; "model=%d"
  22. mov dword ptr [esp], 1
  23. call ___printf_chk
  24. mov eax, esi
  25. shr eax, 8
  26. and eax, 0Fh
  27. mov [esp+8], eax
  28. mov dword ptr [esp+4], offset aFamily_idD ; "family_id=%d"
  29. mov dword ptr [esp], 1
  30. call ___printf_chk
  31. mov eax, esi
  32. shr eax, 0Ch
  33. and eax, 3
  34. mov [esp+8], eax
  35. mov dword ptr [esp+4], offset aProcessor_type ; "processor_type=%d"
  36. mov dword ptr [esp], 1
  37. call ___printf_chk
  38. mov eax, esi
  39. shr eax, 10h
  40. shr esi, 14h
  41. and eax, 0Fh
  42. and esi, 0FFh
  43. mov [esp+8], eax
  44. mov dword ptr [esp+4], offset aExtended_model ; "extended_model_id=%d"
  45. mov dword ptr [esp], 1
  46. call ___printf_chk
  47. mov [esp+8], esi
  48. mov dword ptr [esp+4], offset unk_80486D0
  49. mov dword ptr [esp], 1
  50. call ___printf_chk
  51. add esp, 18h
  52. xor eax, eax
  53. pop ebx
  54. pop esi
  55. mov esp, ebp
  56. pop ebp
  57. retn
  58. main endp

几乎一样。只有一个需要注意的地方就是GCC在调用每个printf()之前会把extended_model_id和extended_family_id的计算联合到一块去,而不是把它们分开计算。

18.6.2 将浮点数当作结构体看待

我们已经在FPU(15章)中注意到了float和double两个类型都是有符号的,他们分为符号、有效数字和指数部分。但是我们能直接用上这些位嘛?让我们试一试float。

18.6 结构体中的位 - 图2

  1. #include <stdio.h>
  2. #include <assert.h>
  3. #include <stdlib.h>
  4. #include <memory.h>
  5. struct float_as_struct
  6. {
  7. unsigned int fraction : 23; // fractional part
  8. unsigned int exponent : 8; // exponent + 0x3FF
  9. unsigned int sign : 1; // sign bit
  10. };
  11. float f(float _in)
  12. {
  13. float f=_in;
  14. struct float_as_struct t;
  15. assert (sizeof (struct float_as_struct) == sizeof (float));
  16. memcpy (&t, &f, sizeof (float));
  17. t.sign=1; // set negative sign
  18. t.exponent=t.exponent+2; // multiple d by 2^n (n here is 2)
  19. memcpy (&f, &t, sizeof (float));
  20. return f;
  21. };
  22. int main()
  23. {
  24. printf ("%f", f(1.234));
  25. };

float_as_struct结构占用了和float一样多的内存空间,也就是4字节,或者说,32位。 现在我们给输入值设置一个负值,然后指数加2,这样我们就能把整个数按照22的值来倍乘,也就是乘以4。 让我们在MSVC2008无优化模式下编译它。

清单18.21: MSVC 2008

  1. _t$ = -8 ; size = 4
  2. _f$ = -4 ; size = 4
  3. __in$ = 8 ; size = 4
  4. ?f@@YAMM@Z PROC ; f
  5. push ebp
  6. mov ebp, esp
  7. sub esp, 8
  8. fld DWORD PTR __in$[ebp]
  9. fstp DWORD PTR _f$[ebp]
  10. push 4
  11. lea eax, DWORD PTR _f$[ebp]
  12. push eax
  13. lea ecx, DWORD PTR _t$[ebp]
  14. push ecx
  15. call _memcpy
  16. add esp, 12 ; 0000000cH
  17. mov edx, DWORD PTR _t$[ebp]
  18. or edx, -2147483648 ; 80000000H - set minus sign
  19. mov DWORD PTR _t$[ebp], edx
  20. mov eax, DWORD PTR _t$[ebp]
  21. shr eax, 23 ; 00000017H - drop significand
  22. and eax, 255 ; 000000ffH - leave here only exponent
  23. add eax, 2 ; add 2 to it
  24. and eax, 255 ; 000000ffH
  25. shl eax, 23 ; 00000017H - shift result to place of bits 30:23
  26. mov ecx, DWORD PTR _t$[ebp]
  27. and ecx, -2139095041 ; 807fffffH - drop exponent
  28. or ecx, eax ; add original value without exponent with new calculated exponent
  29. mov DWORD PTR _t$[ebp], ecx
  30. push 4
  31. lea edx, DWORD PTR _t$[ebp]
  32. push edx
  33. lea eax, DWORD PTR _f$[ebp]
  34. push eax
  35. call _memcpy
  36. add esp, 12 ; 0000000cH
  37. fld DWORD PTR _f$[ebp]
  38. mov esp, ebp
  39. pop ebp
  40. ret 0
  41. ?f@@YAMM@Z ENDP ; f

有点多余。如果用/Ox编译的话,这里就没有memcpy调用了。f变量会被直接使用,但是没有优化的版本看起来会更容易理解一点。 GCC 4.4.1的-O3选项会怎么做?

清单18.22: Gcc 4.4.1

  1. ; f(float)
  2. public _Z1ff
  3. _Z1ff proc near
  4. var_4 = dword ptr -4
  5. arg_0 = dword ptr 8
  6. push ebp
  7. mov ebp, esp
  8. sub esp, 4
  9. mov eax, [ebp+arg_0]
  10. or eax, 80000000h ; set minus sign
  11. mov edx, eax
  12. and eax, 807FFFFFh ; leave only significand and exponent in EAX
  13. shr edx, 23 ; prepare exponent
  14. add edx, 2 ; add 2
  15. movzx edx, dl ; clear all bits except 7:0 in EAX
  16. shl edx, 23 ; shift new calculated exponent to its place
  17. or eax, edx ; add new exponent and original value without exponent
  18. mov [ebp+var_4], eax
  19. fld [ebp+var_4]
  20. leave
  21. retn
  22. _Z1ff endp
  23. public main
  24. main proc near
  25. push ebp
  26. mov ebp, esp
  27. and esp, 0FFFFFFF0h
  28. sub esp, 10h
  29. fld ds:dword_8048614 ; -4.936
  30. fstp qword ptr [esp+8]
  31. mov dword ptr [esp+4], offset asc_8048610 ; "%f
  32. "
  33. mov dword ptr [esp], 1
  34. call ___printf_chk
  35. xor eax, eax
  36. leave
  37. retn
  38. main endp

f()函数基本可以理解,但是有趣的是,GCC可以在编译阶段就通过我们这堆大杂烩一样的代码计算出f(1.234)的值,从而会把他当作参数直接给printf()。