assembly C编译器输出的代码中的MOVZX、CDQE指令的含义/用途是什么?

z5btuh9x  于 2023-06-06  发布在  其他
关注(0)|答案(1)|浏览(352)

下面是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

    ; ...

有谁能解释一下这段代码中CDQEMOVZX指令的含义和用途吗?我也不明白XOR指令有什么用。

xienkqul

xienkqul1#

CDQE指令将EAX寄存器中的DWORD(32位值)符号扩展为RAX寄存器中的QWORD(64位值)。
MOVZX指令将源零扩展到目标。在这种情况下,它将从[rbp-528+rax]处的内存加载的BYTE零扩展到DWORD目的寄存器EAX
XOR eax, 1指令只是翻转EAX的最低位。如果当前设置为(1),则变为清除(0)。如果当前为清除(0),则变为设置(1)。
什么是大局观?好吧,事实证明,这几乎是完全无意义的代码,你从编译器中得到的那种输出没有启用优化。尝试和分析它没有什么意义。
但如果你愿意,我们可以分析它。下面是C代码的完整汇编输出,由GCC 8.2在-O0上生成,每个指令都有注解:

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

汇编程序员和优化编译器都不会产生这种代码。它对寄存器的使用效率极低(更喜欢加载和存储到 memory,包括像icounter这样的值,这些值是存储在寄存器中的主要目标),并且它有很多无意义的指令。
当然,优化编译器实际上会对这段代码做一些处理,完全省略它,因为它没有可观察到的副作用。输出将是:

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中,并假设我们从函数返回counterRDX是一个指针,用于跟踪visited数组中的当前位置。它在整个MainLoop中递增1(字节大小)。考虑到这一点,除了ADC指令之外,其余代码应该很简单。
这是一个带进位的加法指令,用于无分支地将条件if写入循环内部。ADC执行以下操作:

destination = (destination + source + CF)

其中CF是进位标志。在它之前的CMP指令设置进位标志if visited[i] == 0,源代码是0,所以它执行了我在指令右侧注解的操作:如果*RDX == 0visited[i] == 0),则EAXcounter)加1;否则,它添加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数组的值的可预测性,由于分支预测失败,可能会更慢。

相关问题