我被困在从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("%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 <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

因为它是从160寄存器中减去%eax的结果,该寄存器保存了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。因此,汇编代码以这种方式工作(为清楚起见,我已将(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表示1-operand 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个“符号位” -s(这是一个有符号的32位整数),如果0为非负数,则等于X-1(如果X为负)。

编辑:否定X的特殊情况的详细阐述

现在来谈谈负数会发生什么。关于ecx的重要部分是它实际上是X * ((2^33 + 1) / 9)的32个最高有效位。

就像我希望您记得的那样,用二进制表示否定数字意味着将其所有位取反,然后向其添加1。然后,当我们添加1时,如果lsb是1,则将lsb转换为0,否则我们将其及其后面的所有位反转,直到找到第一个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)

Tl; dr(表示否定X):表示ecx-X的值等于ecxX的值的按位取反。我们不想要那个。因此,要获得正确的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,但希望您能理解。

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

为什么要打扰?

结果,我们将划分划分为10个周期(考虑imul也只需要一个周期)。考虑到idiv可能花费几乎两倍的周期(从11到18,正如Hans Passant在this对类似问题的回答中提到的),这种方法可以带来巨大的性能优势。

关于c - 需要说明K&R fahr-to-cels示例的组装说明,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/27260524/

10-14 06:44