Расшифровка эквивалентного ассемблера кода C - PullRequest
2 голосов
/ 07 июня 2010

Желая увидеть выходные данные компилятора (в сборке) для некоторого кода C, я написал простую программу на C и сгенерировал ее файл сборки, используя gcc.

Код такой:

#include <stdio.h>  

int main()  
{  
    int i = 0;

    if ( i == 0 )
    {
        printf("testing\n");
    }

    return 0;  
}  

Здесь сгенерирована сборка для нее (только основная функция):

_main:  
pushl   %ebpz  
movl    %esp, %ebp  
subl    $24, %esp  
andl    $-16, %esp  
movl    $0, %eax  
addl    $15, %eax  
addl    $15, %eax  
shrl    $4, %eax  
sall    $4, %eax  
movl    %eax, -8(%ebp)  
movl    -8(%ebp), %eax  
call    __alloca  
call    ___main  
movl    $0, -4(%ebp)  
cmpl    $0, -4(%ebp)  
jne L2  
movl    $LC0, (%esp)  
call    _printf  
L2:  
movl    $0, %eax  
leave  
ret  

Я в абсолютной растерянности, чтобы соотнести код C и код сборки.Все, что нужно сделать коду - это сохранить 0 в регистре, сравнить его с константой 0 и предпринять соответствующие действия.Но что происходит в сборке?

Ответы [ 6 ]

6 голосов
/ 07 июня 2010

Поскольку main является особенным, вы часто можете получить лучшие результаты, выполняя подобные вещи в другой функции (предпочтительно в своем собственном файле без main). Например:

void foo(int x) {
    if (x == 0) {
       printf("testing\n");
    }
}

, вероятно, будет гораздо более понятным, чем сборка. Это также позволит вам скомпилировать с оптимизацией и при этом соблюдать условное поведение. Если бы вы собирали исходную программу с любым уровнем оптимизации выше 0, это, вероятно, покончило бы с сравнением, поскольку компилятор мог бы продолжить и рассчитать результат этого. С этим кодом часть сравнения скрыта от компилятора (в параметре x), поэтому компилятор не может выполнить эту оптимизацию.

Что такое дополнительный материал на самом деле

_main:  
pushl   %ebpz  
movl    %esp, %ebp  
subl    $24, %esp  
andl    $-16, %esp

Это настройка стекового фрейма для текущей функции. В x86 кадр стека - это область между значением указателя стека (SP, ESP или RSP для 16, 32 или 64 бит) и значением базового указателя (BP, EBP или RBP). Это предположительно, где локальные переменные живут, но не совсем, и явные кадры стека в большинстве случаев являются необязательными Однако использование массивов alloca и / или переменной длины потребует их использования.

Эта конкретная конструкция фрейма стека отличается от структуры, отличной от main, поскольку она также обеспечивает выравнивание стека на 16 байт. Вычитание из ESP увеличивает размер стека более чем достаточно для хранения локальных переменных, а andl эффективно вычитает от 0 до 15 из него, выравнивая его на 16 байт. Это выравнивание кажется чрезмерным, за исключением того, что оно заставит стек также начинать выравнивание кэша и выравнивание слов.

movl    $0, %eax  
addl    $15, %eax  
addl    $15, %eax  
shrl    $4, %eax  
sall    $4, %eax  
movl    %eax, -8(%ebp)  
movl    -8(%ebp), %eax  
call    __alloca  
call    ___main 

Я не знаю, что все это делает. alloca увеличивает размер кадра стека, изменяя значение указателя стека.

movl    $0, -4(%ebp)  
cmpl    $0, -4(%ebp)  
jne L2  
movl    $LC0, (%esp)  
call    _printf  
L2:  
movl    $0, %eax  

Я думаю, вы знаете, что это делает. Если нет, то movl просто перед тем, как call перемещает адрес вашей строки в верхнее расположение стека, чтобы его можно было извлечь с помощью printf. Он должен быть передан в стек, чтобы printf мог использовать свой адрес для вывода адресов других аргументов printf (если таковые имеются, которых нет в данном случае).

leave  

Эта инструкция удаляет кадр стека, о котором говорилось ранее. По существу это movl %ebp, %esp, за которым следует popl %ebp. Существует также инструкция enter, которую можно использовать для создания стековых фреймов, но gcc ее не использовал. Когда стековые фреймы явно не используются, EBP может использоваться как универсальный регистр, и вместо leave компилятор просто добавляет размер фрейма стека к указателю стека, что уменьшит размер стека на размер фрейма. ,

ret

Мне не нужно объяснять это.

Когда вы компилируете с оптимизацией

Я уверен, что вы перекомпилируете все это с разными уровнями оптимизации, поэтому я укажу на то, что может произойти, что вы, вероятно, найдете странным. Я наблюдал gcc замену printf и fprintf на puts и fputs, соответственно, когда строка формата не содержала % и не было передано никаких дополнительных параметров. Это связано с тем, что (по многим причинам) гораздо дешевле звонить на номера puts и fputs, и в итоге вы все равно получаете то, что хотели напечатать.

3 голосов
/ 07 июня 2010

Не беспокойтесь о преамбуле / постамбле - интересующая вас часть:

movl    $0, -4(%ebp)  
cmpl    $0, -4(%ebp)  
jne L2  
movl    $LC0, (%esp)  
call    _printf  
L2: 

Должно быть довольно очевидно, как это соотносится с исходным кодом C.

2 голосов
/ 07 июня 2010

Ну, во многом это накладные расходы, связанные с функцией. main () - это просто функция, как и любая другая, поэтому она должна сохранять адрес возврата в стеке в начале, устанавливать возвращаемое значение в конце и т. д.

Я бы порекомендовал использовать GCC для генерации смешанного исходного кода и ассемблера, который покажет вам сгенерированный ассемблер для каждой исходной строки.

Если вы хотите увидеть код C вместе со сборкой, в которую он был преобразован, используйте командную строку, например:

gcc -c -g -Wa,-a,-ad [other GCC options] foo.c > foo.lst

См. http://www.delorie.com/djgpp/v2faq/faq8_20.html

В Linux просто используйте gcc. На Windows загрузите Cygwin http://www.cygwin.com/


Изменить - см. Также этот вопрос Использование GCC для создания читаемой сборки?

и http://oprofile.sourceforge.net/doc/opannotate.html

2 голосов
/ 07 июня 2010

Первая часть - это некоторый код инициализации, который не имеет никакого смысла в случае вашего простого примера.Этот код будет удален с флагом оптимизации.

Последняя часть может быть сопоставлена ​​с кодом C:

movl    $0, -4(%ebp)    // put 0 into variable i (located at -4(%ebp))
cmpl    $0, -4(%ebp)    // compare variable i with value 0
jne L2                  // if they are not equal, skip to after the printf call
movl    $LC0, (%esp)    // put the address of "testing\n" at the top of the stack
call    _printf         // do call printf
L2:  
movl    $0, %eax        // return 0 (calling convention: %eax has the return code)
1 голос
/ 07 июня 2010

См. здесь больше информации.Для лучшего понимания вы можете сгенерировать ассемблерный код с комментариями на языке C.

gcc -g -Wa,-adhls your_c_file.c > you_asm_file.s

.

1 голос
/ 07 июня 2010

Вам нужны некоторые знания о языке ассемблера, чтобы понимать ассемблер, собранный компилятором C.

Этот учебник может быть полезен

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...