Простая программа на Си
#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