我有下面的C代码片段:
int main() {
int tablica [100];
bool visited [100];
int counter;
int i;
for(i=0;i<=99;i++) {
if (visited[i]==0) {
counter=counter+1;
}
}
}
我转换成汇编程序。我收到了以下的输出:
; ...
mov eax, DWORD PTR [rbp-8]
cdqe
movzx eax, BYTE PTR [rbp-528+rax]
xor eax, 1
test al, al
je .L3
; ...
可能有人给我解释一下什么是这个代码CDQE
和MOVZX
指令的意义和目的是什么?我也不懂使用XOR
的指令是什么。
所述CDQE
指令在签到延伸的DWORD(32位值)EAX
在RAX
寄存器注册到QWORD(64位值)。
所述MOVZX
指令零扩展的源到目的地。在这种情况下,通过符号-延伸[rbp-528+rax]
从存储器加载到所述DWORD目的地寄存器,EAX
的字节。
该XOR eax, 1
指令只是翻转EAX
的最低位。如果当前设置的(1),则它变得清晰(0)。如果当前(0)清除,则它被置(1)。
什么是大局?那么,事实证明,这几乎是完全没有意义的代码,输出您从编译器得到未启用优化的那种。它提供很少的目的是尝试和分析。
但是,如果你想要,我们可以反正分析它。这里是整个组件输出C代码,如在-O0
由GCC 8.2产生,带有加注解的每个指令:
main():
push rbp ; \ standard function
mov rbp, rsp ; / prologue code
sub rsp, 408 ; allocate space for stack array
mov DWORD PTR [rbp-8], 0 ; i = 0
.L4:
cmp DWORD PTR [rbp-8], 99 ; is i <= 99?
jg .L2 ; jump to L2 if i > 99; otherwise fall through
mov eax, DWORD PTR [rbp-8] ; EAX = i
cdqe ; RAX = i
movzx eax, BYTE PTR [rbp-528+rax] ; EAX = visited[i]
xor eax, 1 ; flip low-order bit of EAX (EAX ^= 1)
test al, al ; test if low-order bit is set?
je .L3 ; jump to L3 if low-order bit is clear (== 0)
; (which means it was originally set (== 1),
; which means visited[i] != 0)
; otherwise (visited[i] == 0), fall through
add DWORD PTR [rbp-4], 1 ; counter += 1
.L3:
add DWORD PTR [rbp-8], 1 ; i += 1
jmp .L4 ; unconditionally jump to top of loop (L4)
.L2:
mov eax, 0 ; EAX = 0 (EAX is result of main function)
leave ; function epilogue
ret ; return
既不是组件程序员也不是优化编译器将产生该代码。它使极其低效利用寄存器(宁愿加载和存储到内存中,包括像i
和counter
值,这是在寄存器中存储的首要目标),并且它有很多无谓的指令。
当然,一个优化的编译器会真的对这个代码的数字,完全eliding它,因为它没有可观察到的副作用。输出也只是:
main():
xor eax, eax ; main will return 0
ret
这不是那么有趣的分析,但更高效。这就是为什么我们付出我们的C编译器一掷千金。
C代码也有这些行未定义的行为:
int counter;
/* ... */
counter=counter+1;
你永远不会初始化counter
,但你尝试从中读取数据。因为它是具有自动存储持续时间的变量,它的内容不会被自动初始化,并从一个未初始化的变量读取是未定义的行为。这证明C编译器发射其希望的任何汇编代码。
让我们假设counter
被初始化为0,而我们手工编写本汇编代码,忽略eliding全乱的可能性。我们希望得到的东西,如:
main():
mov edx, OFFSET visited ; EDX = &visited[0]
xor eax, eax ; EAX = 0
MainLoop:
cmp BYTE PTR [rdx], 1 ; \ EAX += (*RDX == 0) ? 1
adc eax, 0 ; / : 0
inc rdx ; RDX += 1
cmp rdx, OFFSET visited + 100 ; is *RDX == &visited[100]?
jne MainLoop ; if not, keep looping; otherwise, done
ret ; return, with result in EAX
发生了什么?那么,调用约定说,EAX
始终保存了返回值,所以我在counter
把EAX
和假定我们是从函数返回counter
。 RDX
是一个指针跟踪visited
阵列中的当前位置。它被由1整个MainLoop
(一个字节的大小)递增。考虑到这一点,在代码的其余部分应简洁明了,除了ADC
指令。
这是一个附加与进位指令,用于写循环branchlessly的内部条件if
。一个ADC
执行以下操作:
destination = (destination + source + CF)
其中CF
是进位标志。该CMP
指令它设置权前的进位标志,如果visited[i] == 0
,源是0
,所以它正是我评论的指令的权利:它增加了1至EAX
(counter
)如果*RDX == 0
(visited[i] == 0
);否则,它增加了0(这是一个无操作)。
如果你想写枝代码,你会怎么做:
main():
mov edx, OFFSET visited ; EDX = &visited[0]
xor eax, eax ; EAX = 0
MainLoop:
cmp BYTE PTR [rdx], 0 ; (*RDX == 0)?
jne Skip ; if not, branch to Skip; if so, fall through
inc eax ; EAX += 1
Skip:
inc rdx ; RDX += 1
cmp rdx, OFFSET visited + 100 ; is *RDX == &visited[100]?
jne MainLoop ; if not, keep looping; otherwise, done
ret ; return, with result in EAX
该作品一样好,但根据visited
阵列的值是如何预见的是,可能是slower due to branch prediction failure。