我被困在从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个最低有效位,而eax
是X >> 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
的值等于ecx
的X
的值的按位取反。我们不想要那个。因此,要获得正确的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/