Подтвердить что ты не робот

В чем смысл каждой строки сборки сборки C-hello world?

Я запустил gcc -S над этим:

int main()
{
printf ("Hello world!");
}

и я получил этот код сборки:

        .file   "test.c"
        .section        .rodata
.LC0:
        .string "Hello world!"
        .text
.globl main
        .type   main, @function
main:
        leal    4(%esp), %ecx
        andl    $-16, %esp
        pushl   -4(%ecx)
        pushl   %ebp
        movl    %esp, %ebp
        pushl   %ecx
        subl    $20, %esp
        movl    $.LC0, (%esp)
        call    printf
        addl    $20, %esp
        popl    %ecx
        popl    %ebp
        leal    -4(%ecx), %esp
        ret
        .size   main, .-main
        .ident  "GCC: (GNU) 4.3.0 20080428 (Red Hat 4.3.0-8)"
        .section        .note.GNU-stack,"",@progbits

Мне любопытно понять этот результат. Может ли кто-то поделиться некоторыми указателями в понимании этого вывода, или если кто-то может отметить комментарии к каждой из этих строк/групп строк, объясняя, что он делает, было бы здорово.

4b9b3361

Ответ 1

Вот как это делается:

        .file   "test.c"

Исходное имя исходного файла (используется отладчиками).

        .section        .rodata
.LC0:
        .string "Hello world!"

Строка с нулевым завершением включена в раздел ".rodata" ( "ro" означает "только для чтения": приложение сможет считывать данные, но любая попытка записи в него вызовет исключение).

        .text

Теперь мы пишем вещи в раздел ".text", где находится код.

.globl main
        .type   main, @function
main:

Мы определяем функцию, называемую "main" и глобально видимую (другие объектные файлы могут ее вызывать).

        leal    4(%esp), %ecx

Мы сохраняем в регистре %ecx значение 4+%esp (%esp - указатель стека).

        andl    $-16, %esp

%esp немного изменен, так что он становится кратным 16. Для некоторых типов данных (формат с плавающей запятой, соответствующий C double и long double), производительность лучше, когда обращения к памяти находятся по адресам которые являются кратными 16. Здесь это действительно не нужно, но при использовании без флага оптимизации (-O2...) компилятор имеет тенденцию создавать довольно много общего бесполезного кода (то есть кода, который может быть полезен в некоторых случаев, но не здесь).

        pushl   -4(%ecx)

Это немного странно: в этот момент слово по адресу -4(%ecx) является словом, которое находилось поверх стека до andl. Код извлекает это слово (которое, кстати, должно быть обратным адресом) и снова выталкивает его. Этот тип эмулирует то, что было бы получено при вызове функции, которая имела выровненный по 16 байт стек. Я предполагаю, что этот push является остатком последовательности копирования аргументов. Поскольку функция скорректировала указатель стека, она должна скопировать аргументы функции, которые были доступны через старое значение указателя стека. Здесь нет аргументов, кроме адреса возврата функции. Обратите внимание, что это слово не будет использоваться (опять же, это код без оптимизации).

        pushl   %ebp
        movl    %esp, %ebp

Это стандартная функция пролог: мы сохраняем %ebp (так как мы собираемся ее модифицировать), затем установите %ebp, чтобы указать на стек стека. После этого %ebp будет использоваться для доступа к аргументам функции, после чего %esp освободится. (Да, аргументов нет, поэтому для этой функции это бесполезно.)

        pushl   %ecx

Мы сохраняем %ecx (нам понадобится его при выходе из функции, чтобы восстановить %esp по значению, имевшемуся до andl).

        subl    $20, %esp

Мы оставляем 32 байта в стеке (помните, что стек растет "вниз" ). Это пространство будет использоваться для хранения аргументов printf() (которые переполняют, поскольку существует один аргумент, который будет использовать 4 байта [это указатель]).

        movl    $.LC0, (%esp)
        call    printf

Мы "нажимаем" аргумент на printf() (т.е. убедитесь, что %esp указывает на слово, которое содержит аргумент, здесь $.LC0, который является адресом постоянной строки в разделе rodata). Тогда будем называть printf().

        addl    $20, %esp

Когда printf() возвращается, мы удаляем пространство, выделенное для аргументов. Этот addl отменяет то, что сделал выше subl.

        popl    %ecx

Мы восстанавливаем %ecx (нажата выше); printf() может изменить его (условные обозначения вызовов описывают, какой регистр может изменять функцию, не восстанавливая их при выходе, %ecx - один из таких регистров).

        popl    %ebp

Эпилог функции: это восстанавливает %ebp (соответствует pushl %ebp выше).

        leal    -4(%ecx), %esp

Мы восстанавливаем %esp до его начального значения. Эффект этого кода операции заключается в сохранении в %esp значения %ecx-4. %ecx был установлен в первом опционном коде функции. Это отменяет любое изменение до %esp, включая andl.

        ret

Выход функции.

        .size   main, .-main

Здесь задается размер функции main(): в любой момент сборки "." является псевдонимом для "адреса, в котором мы сейчас добавляем вещи". Если здесь добавлена ​​другая инструкция, она будет идти по адресу, указанному ".". Таким образом, ".-main" здесь является точным размером кода функции main(). Директива .size указывает ассемблеру записать эту информацию в объектный файл.

        .ident  "GCC: (GNU) 4.3.0 20080428 (Red Hat 4.3.0-8)"

GCC просто любит оставлять следы своего действия. Эта строка заканчивается как комментарий в объектном файле. Линкера удалит его.

        .section        .note.GNU-stack,"",@progbits

Специальный раздел, в котором GCC пишет, что код может содержать неиспользуемый стек. Это нормальный случай. Исполняемые стеки необходимы для некоторых специальных применений (не стандартных C). На современных процессорах ядро ​​может создавать неиспользуемый стек (стек, который запускает исключение, если кто-то пытается выполнить как код некоторые данные, находящиеся в стеке); это рассматривается некоторыми людьми как "функция безопасности", потому что помещение кода в стек является распространенным способом использования переполнения буфера. В этом разделе исполняемый файл будет помечен как "совместимый с неиспользуемым стеком", который ядро ​​с удовольствием предоставит как таковое.

Ответ 2

Вот дополнение к ответу @Thomas Pornin.

  • .LC0 локальная константа, например строковый литерал.
  • .LFB0 начало локальной функции,
  • .LFE0 завершение локальной функции,

Суффикс этой метки является числом и начинается с 0.

Это соглашение о сборщике gcc.

Ответ 3

    leal    4(%esp), %ecx
    andl    $-16, %esp
    pushl   -4(%ecx)
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %ecx
    subl    $20, %esp

эти инструкции не сравниваются в вашей программе c, они всегда выполняются в начале каждой функции (но это зависит от компилятора/платформы)

    movl    $.LC0, (%esp)
    call    printf

этот блок соответствует вашему вызову printf(). первая инструкция помещает в стек его аргумент (указатель на "привет мир" ), затем вызывает функцию.

    addl    $20, %esp
    popl    %ecx
    popl    %ebp
    leal    -4(%ecx), %esp
    ret

эти инструкции противоположны первому блоку, они являются своего рода файлами манипуляции стеками. всегда выполняется тоже