Пример разбора листинга простой программы на Си

 

Простая программа на Си

 

#include<stdio.h>

float power(float x,int n)

{ int i;

  float s;

  s=1;

  for (i=0;i<n;i++) s=s*x;

  return s;

}

 

int main()

{ float y;

  y=power(2.125,10);

  printf("Result: %f\n",y);

  return 0;

}

 

Ассемблерный листинг, сгенерированный командой: gcc -O0 -S test.c

 

 

     .file     "test.c"

     .version  "01.01"

gcc2_compiled.:

.text

     .align 4

.globl power

     .type     power,@function

power:                       // Начало функции power

// Параметры функции передаются через стек (специально отведенная под это область памяти),

// адрес вершины которого находится в регистре esp. Всякие локальные переменные тоже будем

// размещать в стеке. Для этого сформируем кадр - часть стека, отведенная под локальные

// переменные. Указывать на кадр будет регистр ebp - через него будем обращаться к локальным

// переменным. Через него же будем обращаться и к параметрам подпрограммы.

// Кстати, стек растет в сторону уменьшения адреса. Т.е. уменьшаем esp, значит добавляем в стек.

     pushl %ebp              // Сохраняем указатель кадра вызвавшей программы

                                                                        // (помещаем в стек значение в регистре ebp)

     movl %esp,%ebp          // -ормируем указатель нашего кадра:

// пишем в регистр ebp значение регистра esp

// (это будет начало кадра для локальных переменных)

     subl $24,%esp      // Вычесть из esp значение 24

// (смещаем вершину стека на 24 байта вперед - резервируем

// место под локальные переменные).

     movl $1065353216,-8(%ebp)// Записать по адресу -8(%ebp) значение $1065353216

                                                // Видимо, это s=1 в исходной программе. Т.е. переменная s находится

// по адресу -8(%ebp), а $1065353216 - это вещественная единица.

     movl $0,-4(%ebp)        // Записать по адресу -4(%ebp) значение $0

                                                                        // В программе на Си - это операция i=0.

     .p2align 4,,7

.L3:                         // Просто такая метка в программе, сюда можно перейти

     movl -4(%ebp),%eax      // Записать в ax значение из памяти (переменная i)

     cmpl 12(%ebp),%eax      // Сравнить значение из памяти и ax

                                    // Видимо, это сравнение i<n, т.е. 12(%ebp) - адрес переменной-параметра n

                                    // Заметили? Локальные переменные лежат по адресам (%ebp - x),

                                    // а параметры функции - по адресам (%ebp + x), т.к. они уже были в стеке

// до вызова функции.

     jl .L6                  // Переход, если больше (иначе нет перехода)

     jmp .L4                 // Безусловный переход на метку .L4 (там конец функции)

     .p2align 4,,7

.L6:

     flds -8(%ebp)           // Загрузка в st(0) значения из памяти (переменная s).

     fmuls 8(%ebp)           // Умножение st(0) = st(0) * х

     fstps -8(%ebp)          // Выталкивание значения из st(0) в память (в s)

.L5:

     incl -4(%ebp)           // Увеличение значения в памяти на 1 (i++)

     jmp .L3                 // Безусловный переход на метку .L3 (след. итерация)

     .p2align 4,,7

.L4:

     flds -8(%ebp)           // Загрузка в st(0) значения из памяти (переменная s)

// (его мы вернем в качестве результата функции)

     jmp .L2                 // Безусловный переход на .L2

     .p2align 4,,7

.L2:

     leave     // Это эквивалентно movl %ebp, %esp; popl %ebp

                                    // Так мы восстанавливаем состояние стека и кадра, которые были до вызова

     ret                     // Выход из подпрограммы (power)

.Lfe1:

     .size     power,.Lfe1-power

.section  .rodata            // Тут хранятся всякие строки, константы и т.п.

.LC1:

     .string   "Result: %f\n"

     .align 4

.LC0:

     .long 0x40080000

.text

     .align 4

.globl main

     .type     main,@function

main:                                                            // Начало функции main

     pushl %ebp              // Сохраняем указатель кадра вызвавшей программы

     movl %esp,%ebp          // -ормируем указатель нашего кадра

     subl $24,%esp           // Выделяем место в стеке под кадр

     addl $-8,%esp           // И еще немного┘ не мог сразу вычесть 30?

                                                                        // Вот что значит без оптимизации!

     pushl $10               // Поместить в стек число 10

     flds .LC0               // Поместить в st(0) значение по адресу .LC0

     subl $4,%esp            // Вычесть из esp число 4

                                                                        // Это мы выделили 4 байта в стеке под параметр

     fstps (%esp)            // Поместить в память по адресу в esp число из st(0)

                                                                        // Вот этот самый параметр и записали в стек

     call power              // Вызов функции power

                                                                        // Тут мы знаем, что результат вернулся в st(0)

                                                // (кстати, если результат целочисленный, он возвращается в eax)

     addl $16,%esp           // Чистим место в стеке (увеличиваем указатель стека)

     fstps -4(%ebp)           // Записываем в память результат функции

// из вершины стека сопроцессора st(0)

// (видимо, в переменную y)

     addl $-4,%esp           // Выделяем место (4 байта) в стеке

     flds -4(%ebp)           // Помещаем в стек сопроцессора, т.е. st(0), переменную y

     subl $8,%esp            // Выделяем еще место (8 байт) в стеке

     fstpl (%esp)            // Записываем в выделенное место значение из st(0)

     pushl $.LC1             // Помещаем в стек адрес .LC1

// Теперь в стеке есть параметры - адрес строки и

// значение переменной y (а можно было проще┘)

     call printf             // Вызов функции printf

     addl $16,%esp           // Очистка стека от параметров

     xorl %eax,%eax          // Обнуление регистра ax

     jmp .L7                 // Скачем к выходу

     .p2align 4,,7

.L7:

     leave                   // Восстанавливаем указатели стека и кадра (esp и ebp)

     ret                     // Возврат из функции main

.Lfe2:

     .size     main,.Lfe2-main

     .ident    "GCC: (GNU) 2.95.4 20011002 (Debian prerelease)"

 

 

Замечания по синтаксису ассемблера AT&T

-        К названиям команд, имеющим операнды, добавляются суффиксы, отражающие размер операндов:

b - байт

w - слово

l - двойное слово

q - учетверенное слово

s - 32-битное число с плавающей точкой

l - 64-битное число с плавающей точкой

t - 80-битное число с плавающей точкой

-        Если команда имеет несколько операндов, операнд-источник записывается первым, а операнд-приемник - последним.

-        Способы адресации:

Регистровый операнд всегда начинается с символа "%":

xorl      %eax, %eax                             // обнулить регистр eax

            Непосредственный операнд всегда начинается с символа "$":

                        movl     $variable, %edx                        // записать в edx адрес переменной variable (variable - переменная в памяти)

            Косвенная адресация использует немодифицированное имя переменной:

                        pushl    variable                                    // записать значение переменной variable в стек (variable - переменная в памяти, стек также занимает область памяти)

            Примеры более сложных способов адресации памяти:

                        addr(%ebx,%edi,4)      -          адрес: addr + ebx + edi * 4

                         (%ebx,%eax,4)           -          адрес: ebx + eax * 4

                        -2(%ebp)                     -          адрес: -2 + ebp

                         (,%edi,2)                     -          адрес: edi * 2

                        (%ebx)                         -          адрес: ebx