我有以下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
指令的用途。
答案 0 :(得分:2)
CDQE
指令将EAX
寄存器中的DWORD(32位值)符号扩展为RAX
寄存器中的QWORD(64位值)。
MOVZX
指令将源零扩展到目标。在这种情况下,它会将在[rbp-528+rax]
处从内存加载的BYTE符号扩展到DWORD目标寄存器EAX
。
XOR eax, 1
指令仅翻转EAX
的最低位。如果当前设置为(1),则将其清除(0)。如果当前清除状态为(0),则将其设置为(1)。
总体情况如何?好吧,事实证明,这几乎是毫无意义的代码,是您在未启用优化的情况下从编译器获得的输出。尝试分析它的目的很少。
但是,如果您愿意,我们仍然可以对其进行分析。这是GCC 8.2在-O0
处生成的C代码的整个程序集输出,其中每条指令都带有注释:
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
之类的值,它们是存储在寄存器中的主要目标),并且有很多毫无意义的说明。
当然,一个优化的编译器确实会对这个代码进行大量处理,因为它没有明显的副作用,因此完全将其消除了。输出将是:
main():
xor eax, eax ; main will return 0
ret
分析起来没那么有趣,但是效率更高。这就是为什么我们向C编译器支付巨额费用的原因。
C代码在以下几行中也具有未定义的行为:
int counter;
/* ... */
counter=counter+1;
您永远不会初始化counter
,但是您尝试从中读取。由于它是具有自动存储持续时间的变量,因此不会自动初始化其内容,并且从未初始化的变量读取是未定义的行为。这证明C编译器可以发出所需的任何汇编代码。
让我们假设counter
初始化为0,我们将手工编写此汇编代码,而忽略了消除整个混乱的可能性。我们会得到类似的东西:
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
数组中的当前位置。在整个MainLoop
中,它递增1(BYTE的大小)。考虑到这一点,除ADC
指令外,其余代码应简单明了。
这是一个带进位加法指令,用于将条件if
无条件地写入循环内部。 ADC
执行以下操作:
destination = (destination + source + CF)
其中CF
是进位标志。 CMP
指令位于visited[i] == 0
之前,它设置进位标志,而源代码是0
,因此它的作用与我在指令右边的注释相同:它将1加到{如果EAX
(counter
)为{1}}(*RDX == 0
);否则,它将加0(这是一个空操作)。
如果要编写分支代码,请执行以下操作:
visited[i] == 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
数组的值的可预测性,可能是slower due to branch prediction failure。