25.2 浮点数值

清单11.1: MSVC 2010

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. int main()
  4. {
  5. double celsius, fahr;
  6. printf ("Enter temperature in Fahrenheit:\n");
  7. if (scanf ("%lf", &fahr)!=1)
  8. {
  9. printf ("Error while parsing your input\n");
  10. exit(0);
  11. };
  12. celsius = 5 * (fahr-32) / 9;
  13. if (celsius<-273)
  14. {
  15. printf ("Error: incorrect temperature!\n");
  16. exit(0);
  17. };
  18. printf ("Celsius: %lf\n", celsius);
  19. };

MSVC 2010 x86使用FPU指令…

清单25.2: MSVC 2010 x86 /Ox

  1. $SG4038 DB Enter temperature in Fahrenheit:’, 0aH, 00H
  2. $SG4040 DB ’%lf’, 00H
  3. $SG4041 DB Error while parsing your input’, 0aH, 00H
  4. $SG4043 DB Error: incorrect temperature!’, 0aH, 00H
  5. $SG4044 DB Celsius: %lf’, 0aH, 00H
  6. __real@c071100000000000 DQ 0c071100000000000r ; -273
  7. __real@4022000000000000 DQ 04022000000000000r ; 9
  8. __real@4014000000000000 DQ 04014000000000000r ; 5
  9. __real@4040000000000000 DQ 04040000000000000r ; 32
  10. _fahr$ = -8 ; size = 8
  11. _main PROC
  12. sub esp, 8
  13. push esi
  14. mov esi, DWORD PTR __imp__printf
  15. push OFFSET $SG4038 ; Enter temperature in Fahrenheit:’
  16. call esi ; call printf
  17. lea eax, DWORD PTR _fahr$[esp+16]
  18. push eax
  19. push OFFSET $SG4040 ; ’%lf
  20. call DWORD PTR __imp__scanf
  21. add esp, 12 ; 0000000cH
  22. cmp eax, 1
  23. je SHORT $LN2@main
  24. push OFFSET $SG4041 ; Error while parsing your input
  25. call esi ; call printf
  26. add esp, 4
  27. push 0
  28. call DWORD PTR __imp__exit
  29. $LN2@main:
  30. fld QWORD PTR _fahr$[esp+12]
  31. fsub QWORD PTR __real@4040000000000000 ; 32
  32. fmul QWORD PTR __real@4014000000000000 ; 5
  33. fdiv QWORD PTR __real@4022000000000000 ; 9
  34. fld QWORD PTR __real@c071100000000000 ; -273
  35. fcomp ST(1)
  36. fnstsw ax
  37. test ah, 65 ; 00000041H
  38. jne SHORT $LN1@main
  39. push OFFSET $SG4043 ; Error: incorrect temperature!’
  40. fstp ST(0)
  41. call esi ; call printf
  42. add esp, 4
  43. push 0
  44. call DWORD PTR __imp__exit
  45. $LN1@main:
  46. sub esp, 8
  47. fstp QWORD PTR [esp]
  48. push OFFSET $SG4044 ; Celsius: %lf
  49. call esi
  50. add esp, 12 ; 0000000cH
  51. ; return 0
  52. xor eax, eax
  53. pop esi
  54. add esp, 8
  55. ret 0
  56. $LN10@main:
  57. _main ENDP

但是MSVC从2012年开始又改成了使用SIMD指令:

清单25.3: MSVC 2010 x86 /Ox

  1. $SG4228 DB Enter temperature in Fahrenheit:’, 0aH, 00H
  2. $SG4230 DB ’%lf’, 00H
  3. $SG4231 DB Error while parsing your input’, 0aH, 00H
  4. $SG4233 DB Error: incorrect temperature!’, 0aH, 00H
  5. $SG4234 DB Celsius: %lf’, 0aH, 00H
  6. __real@c071100000000000 DQ 0c071100000000000r ; -273
  7. __real@4040000000000000 DQ 04040000000000000r ; 32
  8. __real@4022000000000000 DQ 04022000000000000r ; 9
  9. __real@4014000000000000 DQ 04014000000000000r ; 5
  10. _fahr$ = -8 ; size = 8
  11. _main PROC
  12. sub esp, 8
  13. push esi
  14. mov esi, DWORD PTR __imp__printf
  15. push OFFSET $SG4228 ; Enter temperature in Fahrenheit:’
  16. call esi ; call printf
  17. lea eax, DWORD PTR _fahr$[esp+16]
  18. push eax
  19. push OFFSET $SG4230 ; ’%lf
  20. call DWORD PTR __imp__scanf
  21. add esp, 12 ; 0000000cH
  22. cmp eax, 1
  23. je SHORT $LN2@main
  24. push OFFSET $SG4231 ; Error while parsing your input
  25. call esi ; call printf
  26. add esp, 4
  27. push 0
  28. call DWORD PTR __imp__exit
  29. $LN9@main:
  30. $LN2@main:
  31. movsd xmm1, QWORD PTR _fahr$[esp+12]
  32. subsd xmm1, QWORD PTR __real@4040000000000000 ; 32
  33. movsd xmm0, QWORD PTR __real@c071100000000000 ; -273
  34. mulsd xmm1, QWORD PTR __real@4014000000000000 ; 5
  35. divsd xmm1, QWORD PTR __real@4022000000000000 ; 9
  36. comisd xmm0, xmm1
  37. jbe SHORT $LN1@main
  38. push OFFSET $SG4233 ; Error: incorrect temperature!’
  39. call esi ; call printf
  40. add esp, 4
  41. push 0
  42. call DWORD PTR __imp__exit
  43. $LN10@main:
  44. $LN1@main:
  45. sub esp, 8
  46. movsd QWORD PTR [esp], xmm1
  47. push OFFSET $SG4234 ; Celsius: %lf
  48. call esi ; call printf
  49. add esp, 12 ; 0000000cH
  50. ; return 0
  51. xor eax, eax
  52. pop esi
  53. add esp, 8
  54. ret 0
  55. $LN8@main:
  56. _main ENDP

当然,SIMD在x86下也是可用的,包括这些浮点数的运算。使用他们计算起来也确实方便点,所以微软编译器使用了他们。 我们也可以注意到 -273 这个值会很早的被载入XMM0。这个没问题,因为编译器并不一定会按照源代码里面的顺序产生代码。 # C99的限制

这个例子说明了为什么某些情况下FORTRAN的速度比C/C++要快

  1. void f1 (int* x, int* y, int* sum, int* product, int* sum_product, int* update_me, size_t s)
  2. {
  3. for (int i=0; i<s; i++)
  4. {
  5. sum[i]=x[i]+y[i];
  6. product[i]=x[i]*y[i];
  7. update_me[i]=i*123; // some dummy value
  8. sum_product[i]=sum[i]+product[i];
  9. };
  10. };

这是一个十分简单的例子,但是有一点需要注意:指向update_me数组的指针也可以指向sum数组,甚至是sum_product数组。但是这不是严重的错误,对吗? 编译器很清楚这一点,所以他在循环体中产生了四个阶段: 1.计算下一个sum[i] 2.计算下一个product[i] 3.计算下一个unpdate_me[i] 4.计算下一个sum_product[i],在这个阶段,我们需要从已经计算过sum[i]和product[i]的内存中载入数据

最后一个阶段可以优化吗?既然已经计算过的sum[i]和product[i]是不需要再次从内存装载的(因为我们已经计算过他们了)。但是编译器不能保证在第三个阶段没有东西被覆盖掉!这就叫“指针别名”,在这种情况下编译器无法确定指针指向区域的内存是否已经被改变。

C99标准中的限制给解决这一问题带来了一线曙光。由设计器传送给编译器的函数单元在标记这种关键字(restrict)后,它会指向不同的内存区域,并且不 会被混用。 如果要更加准确地描述这种情况,restrict表明了只有指针是可以访问对象的。这样的话我们可以通过特定的指针进行工作,并且不会用到其他指针。也就是说一个对象如果被标记为restrict,那么它只能通过一个指针访问。 我们把每个指向变量的指针标记为restrict关键字:

  1. void f2 (int* restrict x, int* restrict y, int* restrict sum, int* restrict product, int*
  2. restrict sum_product,
  3. int* restrict update_me, size_t s)
  4. {
  5. for (int i=0; i<s; i++)
  6. {
  7. sum[i]=x[i]+y[i];
  8. product[i]=x[i]*y[i];
  9. update_me[i]=i*123; // some dummy value
  10. sum_product[i]=sum[i]+product[i];
  11. };
  12. };

来看下结果:

清单26.1: GCC x64: f1()

  1. f1:
  2. push r15 r14 r13 r12 rbp rdi rsi rbx
  3. mov r13, QWORD PTR 120[rsp]
  4. mov rbp, QWORD PTR 104[rsp]
  5. mov r12, QWORD PTR 112[rsp]
  6. test r13, r13
  7. je .L1
  8. add r13, 1
  9. xor ebx, ebx
  10. mov edi, 1
  11. xor r11d, r11d
  12. jmp .L4
  13. .L6:
  14. mov r11, rdi
  15. mov rdi, rax
  16. .L4:
  17. lea rax, 0[0+r11*4]
  18. lea r10, [rcx+rax]
  19. lea r14, [rdx+rax]
  20. lea rsi, [r8+rax]
  21. add rax, r9
  22. mov r15d, DWORD PTR [r10]
  23. add r15d, DWORD PTR [r14]
  24. mov DWORD PTR [rsi], r15d ; store to sum[]
  25. mov r10d, DWORD PTR [r10]
  26. imul r10d, DWORD PTR [r14]
  27. mov DWORD PTR [rax], r10d ; store to product[]
  28. mov DWORD PTR [r12+r11*4], ebx ; store to update_me[]
  29. add ebx, 123
  30. mov r10d, DWORD PTR [rsi] ; reload sum[i]
  31. add r10d, DWORD PTR [rax] ; reload product[i]
  32. lea rax, 1[rdi]
  33. cmp rax, r13
  34. mov DWORD PTR 0[rbp+r11*4], r10d ; store to sum_product[]
  35. jne .L6
  36. .L1:
  37. pop rbx rsi rdi rbp r12 r13 r14 r15
  38. ret

清单26.2: GCC x64: f2()

  1. f2:
  2. push r13 r12 rbp rdi rsi rbx
  3. mov r13, QWORD PTR 104[rsp]
  4. mov rbp, QWORD PTR 88[rsp]
  5. mov r12, QWORD PTR 96[rsp]
  6. test r13, r13
  7. je .L7
  8. add r13, 1
  9. xor r10d, r10d
  10. mov edi, 1
  11. xor eax, eax
  12. jmp .L10
  13. .L11:
  14. mov rax, rdi
  15. mov rdi, r11
  16. .L10:
  17. mov esi, DWORD PTR [rcx+rax*4]
  18. mov r11d, DWORD PTR [rdx+rax*4]
  19. mov DWORD PTR [r12+rax*4], r10d ; store to update_me[]
  20. add r10d, 123
  21. lea ebx, [rsi+r11]
  22. imul r11d, esi
  23. mov DWORD PTR [r8+rax*4], ebx ; store to sum[]
  24. mov DWORD PTR [r9+rax*4], r11d ; store to product[]
  25. add r11d, ebx
  26. mov DWORD PTR 0[rbp+rax*4], r11d ; store to sum_product[]
  27. lea r11, 1[rdi]
  28. cmp r11, r13
  29. jne .L11
  30. .L7:
  31. pop rbx rsi rdi rbp r12 r13
  32. ret

被编译过的f1()和f2()的不同点是:在f1()中,sum[i]和product[i]在循环中途被装入,但是在f2()中没有这样的特性。已经计算过的变量将被使用,既然我们已经向编译器“保证”在循环执行期间,sum[i]和product[i]不会发生改变,所以编译器“确信”变量的值不用从内存被再装入。很明显,第二个例子的程序更快。 但是如果函数变量中的指针发生混淆的情况又能如何呢?这与一个程序员的认知有关,并且结果是不正确的。 回到FORTRAN。FORTRAN语言编译器按照指针的本身含义对待他,所以当FORTRAN程序在这种情况下不可能使用restrict的时候,它可以生成生成执行更快的代码。

这有什么实用价值?当函数处理内存中很多大“块”的时候,比如说用超级计算机解决线性代数问题。或许这就是为什么FORTRAN语言还在这个领域被使用。 但是当迭代步骤不是很多的时候,速度的增加并不是显著的。 # 内联函数

内联代码是指当编译的时候,将函数体直接嵌入正确位置,而不是在这个位置放上函数声明。

  1. #include <stdio.h>
  2. int celsius_to_fahrenheit (int celsius)
  3. {
  4. return celsius * 9 / 5 + 32;
  5. };
  6. int main(int argc, char *argv[])
  7. {
  8. int celsius=atol(argv[1]);
  9. printf ("%d\n", celsius_to_fahrenheit (celsius));
  10. };

这个编译是意料之中的,但是如果换成GCC的优化方案,我们会看到:

清单27.2: GCC 4.8.1 -O3

  1. _main:
  2. push ebp
  3. mov ebp, esp
  4. and esp, -16
  5. sub esp, 16
  6. call ___main
  7. mov eax, DWORD PTR [ebp+12]
  8. mov eax, DWORD PTR [eax+4]
  9. mov DWORD PTR [esp], eax
  10. call _atol
  11. mov edx, 1717986919
  12. mov DWORD PTR [esp], OFFSET FLAT:LC2 ; "%d\12\0"
  13. lea ecx, [eax+eax*8]
  14. mov eax, ecx
  15. imul edx
  16. sar ecx, 31
  17. sar edx
  18. sub edx, ecx
  19. add edx, 32
  20. mov DWORD PTR [esp+4], edx
  21. call _printf
  22. leave
  23. ret

这里的除法由乘法完成。 是的,我们的小函数被放到了printf()调用之前。为什么?因为这比直接执行函数之前的“调用/返回”过程速度更快。 在过去,这样的函数在函数声明的时候必须被标记为“内联”。在现代,这样的函数会自动被编译器识别。 另外一个普通的自动优化的例子是内联字符串函数,比如strcpy(),strcmp()等

清单27.3 : 另一个简单的例子

  1. bool is_bool (char *s)
  2. {
  3. if (strcmp (s, "true")==0)
  4. return true;
  5. if (strcmp (s, "false")==0)
  6. return false;
  7. assert(0);
  8. };

清单27.4: GCC 4.8.1 -O3

  1. _is_bool:
  2. push edi
  3. mov ecx, 5
  4. push esi
  5. mov edi, OFFSET FLAT:LC0 ; "true\0"
  6. sub esp, 20
  7. mov esi, DWORD PTR [esp+32]
  8. repz cmpsb
  9. je L3
  10. mov esi, DWORD PTR [esp+32]
  11. mov ecx, 6
  12. mov edi, OFFSET FLAT:LC1 ; "false\0"
  13. repz cmpsb
  14. seta cl
  15. setb dl
  16. xor eax, eax
  17. cmp cl, dl
  18. jne L8
  19. add esp, 20
  20. pop esi
  21. pop edi
  22. ret

这是一个经常可以见到的关于MSVC生成的strcmp()的例子。

清单27.5: MSVC

  1. mov dl, [eax]
  2. cmp dl, [ecx]
  3. jnz short loc_10027FA0
  4. test dl, dl
  5. jz short loc_10027F9C
  6. mov dl, [eax+1]
  7. cmp dl, [ecx+1]
  8. jnz short loc_10027FA0
  9. add eax, 2
  10. add ecx, 2
  11. test dl, dl
  12. jnz short loc_10027F80
  13. loc_10027F9C: ; CODE XREF: f1+448
  14. xor eax, eax
  15. jmp short loc_10027FA5
  16. ; ---------------------------------------------------------------------------
  17. loc_10027FA0: ; CODE XREF: f1+444
  18. ; f1+450
  19. sbb eax, eax
  20. sbb eax, 0FFFFFFFFh

我写了一个小的用于搜索和归纳的IDA脚本,这样的脚本经常能在内联代码中看到:IDA_scripts. # 处理不当的反汇编代码

逆向工程师经常需要处理不当的反汇编代码