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

Bar*_*ekS 3 64-bit x86 assembly x86-64

我有以下 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;
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

我将其转换为汇编程序。我收到以下输出:

   ; ...

    mov     eax, DWORD PTR [rbp-8]
    cdqe
    movzx   eax, BYTE PTR [rbp-528+rax]
    xor     eax, 1
    test    al, al
    je      .L3

    ; ...
Run Code Online (Sandbox Code Playgroud)

任何人都可以向我解释这段代码中CDQEMOVZX指令的含义和目的是什么?我也不明白XOR指令的用途是什么。

Cod*_*ray 10

CDQE指令将EAX寄存器中的 DWORD(32 位值)符号扩展为寄存器中的 QWORD(64 位值)RAX

MOVZX指令将源零扩展到目标。在这种情况下,它将从内存加载的字节符号扩展[rbp-528+rax]到 DWORD 目标寄存器,EAX

XOR eax, 1指令只是翻转 的最低位EAX。如果当前设置为 (1),则清除 (0)。如果当前清零 (0),则它变为设置 (1)。

什么是大局?好吧,事实证明,这几乎是完全没有意义的代码,您从未启用优化的编译器中获得的那种输出。尝试和分析它没有什么意义。

但是,如果您愿意,我们无论如何都可以对其进行分析。这是您的 C 代码的整个汇编输出,由 GCC 8.2 at 生成-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
Run Code Online (Sandbox Code Playgroud)

汇编程序员和优化编译器都不会生成此代码。它使寄存器的使用极其低效(更喜欢加载和存储到内存,包括i和 之类的值counter,它们是存储在寄存器中的主要目标),并且它有很多毫无意义的指令。

当然,优化编译器真的会对这段代码做一些处理,完全省略它,因为它没有可观察到的副作用。输出只是:

main():
        xor     eax, eax    ; main will return 0
        ret
Run Code Online (Sandbox Code Playgroud)

分析起来并不那么有趣,但效率更高。这就是我们为 C 编译器支付大笔费用的原因。

C 代码在这些行中也有未定义的行为:

int counter;
/* ... */
counter=counter+1;
Run Code Online (Sandbox Code Playgroud)

您从不 initialize 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
Run Code Online (Sandbox Code Playgroud)

发生了什么?好吧,调用约定说EAX总是保存返回值,所以我已经counter输入EAX并假设我们正在counter从函数返回。RDX是跟踪visited数组中当前位置的指针。它在整个MainLoop. 考虑到这一点,除了ADC指令之外,其余的代码应该很简单。

这是一条带进位的加法指令,用于if无分支地在循环内部写入条件。AnADC执行以下操作:

destination = (destination + source + CF)
Run Code Online (Sandbox Code Playgroud)

CF进位标志在哪里。CMP就在它设置进位标志 if 之前的指令visited[i] == 0,并且源是0,因此它执行我在指令右侧注释的内容:它将EAX( counter) if *RDX == 0( visited[i] == 0) 加 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
Run Code Online (Sandbox Code Playgroud)

这同样有效,但取决于visited数组值的可预测性,可能由于分支预测失败变慢