需要解释K&R fahr的组装说明

我坚持学习汇编语言的基础知识,用华氏温度对K&R书中的摄氏例子进行学习。 这里是我所指的C代码:

#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("%dt%dn", 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("%dt%dn", 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 <printf@plt>

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寄存器减去160 ,保持5*fahr ,如下所示:

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

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


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

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表示首先填充其操作数(本例中为edx )32位的1操作数imul ,然后将其余的低位放入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是的32个最低位显著(X * ((2^33 + 1) / 9)) >> 33eaxX >> 31 ,即32 “符号位”的-s X (它是一符号的32位整数),其等于0 ,如果X是非负的,并-1如果X为负。

编辑:详细阐述负面X的特殊情况

现在谈谈负数的情况。 关于ecx的重要部分是它实际上是X * ((2^33 + 1) / 9)的32个最重要的位。

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

那么当我们试图否定(X * ((2^33 + 1) / 9)) (或者等价地,如果我们用-X进行计算,在这个例子中考虑X positive),会发生什么? 当然,首先我们倒置它的所有位,然后我们加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))((-X * ((2^33 + 1) / 9)) >> 33) & 0xFFFFFFFF != ~(((X * ((2^33 + 1) / 9)) >> 33) & 0xFFFFFFFF)

Tl; dr表示负X情况: -Xecx值等于Xecx值的按位否定。 我们不希望这样。 因此,为了得到X负值的正确结果,我们必须将1加到ecx (或等价减去-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语法编写asm,但我希望你能理解这个想法。

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

为什么所有的麻烦?

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

链接地址: http://www.djcxy.com/p/82765.html

上一篇: Need explanation on assembly instructions of K&R fahr

下一篇: How to play webm file in iOS video player