Каково значение / использование инструкций MOVZX, CDQE в этом коде, выводимом компилятором C? - PullRequest
0 голосов
/ 10 февраля 2019

У меня есть следующий фрагмент кода 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.

1 Ответ

0 голосов
/ 11 февраля 2019

Инструкция CDQE расширяет DWORD (32-битное значение) в регистре EAX до QWORD (64-битное значение) в регистре RAX.

MOVZX инструкция обнуляет источник до пункта назначения.В этом случае он расширяет знак BYTE, загруженный из памяти в [rbp-528+rax], в регистр назначения 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

Ни программист, ни оптимизирующий компилятор не будут производить этот код.Это крайне неэффективно использует регистры (предпочитая загружать и хранить в память , включая такие значения, как 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.Он увеличивается на 1 (размер байта) на протяжении MainLoop.Помня об этом, остальная часть кода должна быть простой, за исключением инструкции ADC.

Это инструкция сложения с переносом, используемая для записи условного if внутри циклаbranchlessly.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, может быть медленнее из-за ошибки прогнозирования ветвления .

...