2014-12-02 75 views
7

我被卡在学习汇编语言的基础知识中,用华氏温度对K & R的摄氏例子进行了学习。下面是我所指的C代码:需要解释K&R fahr-to-cels的汇编指令示例

#include <stdio.h> 

main() 
{ 
    int fahr, celsius; 
    int lower, upper, step; 

    lower = 0; 
    upper = 300; 
    step = 20; 

    fahr = lower; 
    while (fahr <= upper) { 
     celsius = 5 * (fahr-32)/9; 
     printf("%d\t%d\n", fahr, celsius); 
     fahr = fahr + step; 
    } 
} 

随着GCC 4.4.7(GNU/Linux的x86-64的),我获得以下拆解:

$ gcc -O0 -g -ansi -pedantic l1-2a.c 
$ gdb -q a.out 
(gdb) disas /m main 
(gdb) disas /m main 
Dump of assembler code for function main: 
6 { 
    0x00000000004004c4 <+0>: push %rbp 
    0x00000000004004c5 <+1>: mov %rsp,%rbp 
    0x00000000004004c8 <+4>: sub $0x20,%rsp 

7  int fahr, celsius; 
8  int lower, upper, step; 
9 
10  lower = 0; 
    0x00000000004004cc <+8>: movl $0x0,-0xc(%rbp) 

11  upper = 300; 
    0x00000000004004d3 <+15>: movl $0x12c,-0x8(%rbp) 

12  step = 20; 
    0x00000000004004da <+22>: movl $0x14,-0x4(%rbp) 

13 
14  fahr = lower; 
    0x00000000004004e1 <+29>: mov -0xc(%rbp),%eax 
    0x00000000004004e4 <+32>: mov %eax,-0x14(%rbp) 

15  while (fahr <= upper) { 
    0x00000000004004e7 <+35>: jmp 0x400532 <main+110> 
    0x0000000000400532 <+110>: mov -0x14(%rbp),%eax 
    0x0000000000400535 <+113>: cmp -0x8(%rbp),%eax 
    0x0000000000400538 <+116>: jle 0x4004e9 <main+37> 

16   celsius = 5 * (fahr-32)/9; 
    0x00000000004004e9 <+37>: mov -0x14(%rbp),%edx 
    0x00000000004004ec <+40>: mov %edx,%eax 
    0x00000000004004ee <+42>: shl $0x2,%eax 
    0x00000000004004f1 <+45>: add %edx,%eax 
    0x00000000004004f3 <+47>: lea -0xa0(%rax),%ecx 
    0x00000000004004f9 <+53>: mov $0x38e38e39,%edx 
    0x00000000004004fe <+58>: mov %ecx,%eax 
    0x0000000000400500 <+60>: imul %edx 
    0x0000000000400502 <+62>: sar %edx 
    0x0000000000400504 <+64>: mov %ecx,%eax 
    0x0000000000400506 <+66>: sar $0x1f,%eax 
    0x0000000000400509 <+69>: mov %edx,%ecx 
    0x000000000040050b <+71>: sub %eax,%ecx 
    0x000000000040050d <+73>: mov %ecx,%eax 
    0x000000000040050f <+75>: mov %eax,-0x10(%rbp) 

17   printf("%d\t%d\n", fahr, celsius); 
    0x0000000000400512 <+78>: mov $0x400638,%eax 
    0x0000000000400517 <+83>: mov -0x10(%rbp),%edx 
    0x000000000040051a <+86>: mov -0x14(%rbp),%ecx 
    0x000000000040051d <+89>: mov %ecx,%esi 
    0x000000000040051f <+91>: mov %rax,%rdi 
    0x0000000000400522 <+94>: mov $0x0,%eax 
    0x0000000000400527 <+99>: callq 0x4003b8 <[email protected]> 

18   fahr = fahr + step; 
    0x000000000040052c <+104>: mov -0x4(%rbp),%eax 
    0x000000000040052f <+107>: add %eax,-0x14(%rbp) 

19  } 
20 } 
    0x000000000040053a <+118>: leaveq 
    0x000000000040053b <+119>: retq 

End of assembler dump. 

什么是不明确的,我是这个片段:

16   celsius = 5 * (fahr-32)/9; 
    0x00000000004004e9 <+37>: mov -0x14(%rbp),%edx 
    0x00000000004004ec <+40>: mov %edx,%eax 
    0x00000000004004ee <+42>: shl $0x2,%eax 
    0x00000000004004f1 <+45>: add %edx,%eax 
    0x00000000004004f3 <+47>: lea -0xa0(%rax),%ecx 
    0x00000000004004f9 <+53>: mov $0x38e38e39,%edx 
    0x00000000004004fe <+58>: mov %ecx,%eax 
    0x0000000000400500 <+60>: imul %edx 
    0x0000000000400502 <+62>: sar %edx 
    0x0000000000400504 <+64>: mov %ecx,%eax 
    0x0000000000400506 <+66>: sar $0x1f,%eax 
    0x0000000000400509 <+69>: mov %edx,%ecx 
    0x000000000040050b <+71>: sub %eax,%ecx 
    0x000000000040050d <+73>: mov %ecx,%eax 
    0x000000000040050f <+75>: mov %eax,-0x10(%rbp) 

我的意思是,我明白了一切达:

lea -0xa0(%rax),%ecx 

因为它是从%eax寄存器,其保持5*fahr从其减去160,如:

5 * (fahr-32)/9 <=> (5*fahr - 5*32)/9 <=> (5*fahr - 160)/9 

从而%ecx后(以及完整%rcx)存储5*fahr - 160。然而,我不知道它如何除以9。这似乎是一些骗局,如“乘以逆”,以避免分裂,但我不知道它是如何工作的。

+4

这就是所谓的魔数分割。有关说明,请参阅[这里](http://ridiculousfish.com/blog/posts/labor-of-division-episode-i.html)。 – Jester 2014-12-02 22:52:58

+1

'0x38e38e39' = 2^33/9. – 2014-12-02 22:53:02

+4

试图通过观察编译器生成的程序集来学习程序集可能是一个糟糕的主意,要知道编译器可能会做得比在各种方式下更好地接受可读性! – Clifford 2014-12-02 23:14:10

回答

4

总结评论中说的内容:0x38e38e39954437177十进制,这正好是(2^33 + 1)/9。所以,汇编代码以这种方式工作(我把它换成(5 * fahr - 160)X为清楚起见):

mov $0x38e38e39,%edx /* edx is now 0x38e38e39 == (2^33 + 1)/9 */ 
mov %ecx,%eax  /* eax is now X */ 
imul %edx    /* edx:eax is now eax * edx == X * ((2^33 + 1)/9) */ 

这是最有趣的部分开始的位置。 edx:eax代表第一个操作数imul,先填充其操作数(在这种情况下为edx)32位,然后将其余的低位置入eax

实际上,我们在两个寄存器中得到了64位结果!它看起来像这样:

edx(X * ((2^33 + 1)/9)) >> 32的32个最低有效位。

eax(X * ((2^33 + 1)/9)) % 2^32(但是这马上就会丢弃)

然后我们得到这个东西成形状:

sar %edx    /* edx is now edx >> 1 == (X * ((2^33 + 1)/9)) >> 33 */ 
mov %ecx,%eax  /* eax is now X again */ 
sar $0x1f,%eax  /* eax is now X >> 0x1f == X >> 31 */ 
mov %edx,%ecx  /* ecx is now (X * ((2^33 + 1)/9)) >> 33 */ 

所以现在ecx(X * ((2^33 + 1)/9)) >> 33 32个最低显著位和eaxX >> 31 ,即X(它是一个有符号的32位整数)的32“符号位”-s,如果X是非负的,则它等于0,并且如果是-1i f X为负数。

编辑:对负X

现在有点什么负数发生特殊情况详细的阐述。关于ecx的重要部分是它实际上是X * ((2^33 + 1)/9)的32个最高有效位。

正如我希望你记得,在二进制,否定数字意味着反转所有的位,然后加入1它。当我们添加1时,如果它是0,我们将lsb反转为1,否则我们将它反转并在它之后的所有位'直到我们找到第一个0,然后将其反转。

所以,当我们试图否定(X * ((2^33 + 1)/9))发生了什么(或者等价地,我们怎么得到,如果我们用-X进行计算,考虑X积极为这个例子)?当然,首先我们反转所有的位,然后我们加上1。但是对于后者(将其添加1)影响数字的32个最重要位,32个最低有效位将必须等于0xFFFFFFFF。并且(相信我这个)没有32位整数,当乘以0x38e38e39时,得出这样的结果。

如此有效,虽然(-X * ((2^33 + 1)/9)) == -(X * ((2^33 + 1)/9)),它与32位最高有效位不同:((-X * ((2^33 + 1)/9)) >> 33) & 0xFFFFFFFF != -(((X * ((2^33 + 1)/9)) >> 33) & 0xFFFFFFFF)

而是,(-X * ((2^33 + 1)/9))的32个最高有效位等于(X * ((2^33 + 1)/9))的32个最高有效位的按位否定:((-X * ((2^33 + 1)/9)) >> 33) & 0xFFFFFFFF != ~(((X * ((2^33 + 1)/9)) >> 33) & 0xFFFFFFFF)

铊组成; dr为负X情况:ecx-X的值将等于的ecxX值的按位求反。我们不希望这样。因此,要获得的X负值正确的结果,我们必须添加1ecx(或等价地,减-1):

sub %eax,%ecx  /* ecx is now X/9 */ 

然后是最后一部分:

mov %ecx,%eax  /* eax is now X/9 */ 
mov %eax,-0x10(%rbp) /* Aaand mov the result into the variable "cels" */ 

我如果我混淆了某些东西,我会非常抱歉,我无法用GAS语法编写自己的头脑,但我希望你能理解这个想法。

T1; dr:这里的技巧是将乘以大数的逆乘以大数,用算术移位舍弃大数,然后如果结果为负数则将结果舍入为零。

为什么所有的麻烦?

因此,我们将分成10个周期(考虑imul只需要一个周期)。考虑到idiv可能会花费几乎两倍的周期(从Hans Passant在this回答类似的问题中提到的11到18),这种方法可以产生巨大的性能优势。