Команды сравнения и условные переходы. Безусловный переход
Команда loop неявно сравнивает регистр %ecx с нулём. Это довольно удобно для организации циклов, но часто циклы бывают намного сложнее, чем те, что можно записать при помощи loop. К тому же нужен эквивалент конструкции if(){}. Вот команды, позволяющие выполнять произвольные сравнения операндов:
cmp операнд_2, операнд_1
Команда cmp выполняет вычитание операнд_1 - операнд_2 и устанавливает флаги. Результат вычитания нигде не запоминается.
Внимание! Обратите внимание на порядок операндов в записи команды: сначала второй, потом первый. |
Сравнили, установили флаги, - и что дальше? А у нас есть целое семейство jump-команд, которые передают управление другим командам. Эти команды называются командами условного перехода. Каждой из них поставлено в соответствие условие, которое она проверяет. Синтаксис:
jcc метка
Команды jcc не существует, вместо cc нужно подставить мнемоническое обозначение условия.
Мнемоника | Английское слово | Смысл | Тип операндов |
E | equal | равенство | любые |
N | not | инверсия условия | любые |
G | greater | больше | со знаком |
L | less | меньше | со знаком |
A | above | больше | без знака |
B | below | меньше | без знака |
Таким образом, je проверяет равенство операндов команды сравнения, jl проверяет условие операнд_1 < операнд_2 и так далее. У каждой команды есть противоположная: просто добавляем букву n:
· je - jne: равно - не равно;
· jg - jng: больше - не больше.
Теперь пример использования этих команд:
.text
/* Тут пропущен код, который получает некоторое значение в %eax.
Пусть нас интересует случай, когда %eax = 15 */
cmpl $15, %eax /* сравнение */
jne not_equal /* если операнды не равны, перейти на
метку not_equal */
/* сюда управление перейдёт только в случае, когда переход не
сработал, а значит, %eax = 15 */
not_equal:
/* а сюда управление перейдёт в любом случае */
Сравните с кодом на Си:
if(eax == 15)
{
/* сюда управление перейдёт только в случае, когда переход не сработал,
а значит, %eax = 15 */
}
/* а сюда управление перейдёт в любом случае */
Кроме команд условного перехода, область применения которых ясна сразу, также существует команда безусловного перехода. Эта команда чем-то похожа на оператор goto языка Си. Синтаксис:
jmp адрес
Эта команда передаёт управление на адрес, не проверяя никаких условий. Заметьте, что адрес может быть задан в виде непосредственного значения (метки), регистра или обращения к памяти.
Произвольные циклы
Все инструкции для написания произвольных циклов мы уже рассмотрели, осталось лишь собрать всё воедино. Лучше сначала посмотрите код программы, а потом объяснение к ней. Прочитайте её код и комментарии и попытайтесь разобраться, что она делает. Если сразу что-то непонятно - не страшно, сразу после исходного кода находится более подробное объяснение.
Программа: поиск наибольшего элемента в массиве
.data
printf_format:
.string "%d\n "
array:
.long -10, -15, -148, 12, -151, -3, -72
array_end:
.text
.globl main
main:
movl array, %eax /* в %eax будет храниться результат;
в начале наибольшее значение - array[0]*/
movl $array+4, %ebx /* в %ebx находится адрес текущего
элемента массива */
jmp ch_bound /* проверить границы массива */
loop_start: /* начало цикла */
cmpl %eax, (%ebx) /* сравнить текущий элемент массива с
текущим наибольшим значением из %eax
*/
jle less /* если текущий элемент массива меньше
или равен наибольшему, пропустить
следующий код */
movl (%ebx), %eax /* а вот если элемент массива
превосходит наибольший, значит, его
значение и есть новый максимум */
less:
addl $4, %ebx /* увеличить %ebx на размер одного
элемента массива, 4 байта */
ch_bound:
cmpl $array_end, %ebx /* сравнить адрес текущего элемента и
адрес конца массива */
jne loop_start /* если они не равны, повторить цикл снова*
/*
* следующий код выводит число из %eax на экран и завершает программу
*/
pushl %eax
pushl $printf_format
call printf
addl $8, %esp
movl $0, %eax
ret
Сначала мы заносим в регистр %eax число array[0]. После этого мы сравниваем каждый элемент массива, начиная со следующего (нам незачем сранивать нулевой элемент с самим собой), с текущим наибольшим значением из %eax, и, если этот элемент больше, он становится текущим наибольшим. После просмотра всего массива в %eax находится наибольший элемент. Отметим, что если массив состоит из 1 элемента, то следующий после нулевого элемента будет находиться за границей массива, поэтому перед циклом стоит безусловный переход на проверку границы.
Этот код соответствует приблизительно следующему на Си:
#include <stdio.h >
int main()
{
static int array[] = { -10, -15, -148, 12, -151, -3, -72 };
static int *array_end = &array[sizeof(array) / sizeof(int)];
int max = array[0];
int *p = array+1;
while (p != array_end)
{
if(*p > max)
{
max = *p;
}
p++;
}
printf( "%d\n ", max);
return 0;
}
Возможно, такой способ обхода массива не очень привычен для вас. В Си принято использовать переменную с номером текущего элемента, а не указатель на него. Никто не запрещает пойти этим же путём и на ассемблере:
.data
printf_format:
.string "%d\n "
array:
.long 10, 15, 148, -3, 151, 3, 72
array_size:
.long (. - array)/4 /* количество элементов массива */
.text
.globl main
main:
movl array, %eax /* в %eax будет храниться результат;
в начале наибольшее значение - array[0] */
movl $1, %ecx /* начать просмотр с первого элемента
*/
jmp ch_bound
loop_start: /* начало цикла */
cmpl %eax, array(,%ecx,4) /* сравнить текущий элемент
массива с текущим наибольшим
значением из %eax */
jle less /* если текущий элемент массива меньше
или равен наибольшему, пропустить
следующий код */
movl array(,%ecx,4), %eax /* а вот если элемент массива
превосходит наибольший, значит, его
значение и есть новый максимум */
less:
incl %ecx /* увеличить на 1 номер текущего
элемента */
ch_bound:
cmpl array_size, %ecx /* сравнить номер текущего элемента с
общим числом элементов */
jne loop_start /* если они не равны, повторить цикл снова */
/*
* следующий код выводит число в %eax на экран и завершает программу
*/
pushl %eax
pushl $printf_format
call printf
addl $8, %esp
movl $0, %eax
ret
Рассматривая код этой программы, вы, наверно, уже поняли, как создавать произвольные циклы с постусловием на ассемблере, наподобие do{} while(); в Си. Ещё раз повторю эту конструкцию, выкинув весь код, не относящийся к циклу:
loop_start: /* начало цикла */
/* вот тут находится тело цикла */
cmpl ... /* что-то с чем-то сравнить для
принятия решения о выходе из цикла */
jne loop_start /* подобрать соответствующую команду
условного перехода для повторения цикла */
В Си есть ещё один вид цикла, с проверкой условия перед входом в тело цикла (цикл с предусловием): while(){}. Немного изменив предыдущий код, получаем следующее:
jmp check
loop_start: /* начало цикла */
/* вот тут находится тело цикла */
check:
cmpl ... /* что-то с чем-то сравнить для
принятия решения о выходе из цикла */
jne loop_start /* подобрать соответствующую команду
условного перехода для повторения цикла */
Кто-то скажет: а ещё есть цикл for()! Но цикл
for(init; cond; incr)
{
body;
}
эквивалентен такой конструкции:
init;
while(cond)
{
body;
incr;
}
Таким образом, нам достаточно и уже рассмотренных двух видов циклов.
Логическая арифметика
Кроме выполнения обычных арифметических вычислений, можно проводить и логические, то есть битовые.
and источник, приёмник
or источник, приёмник
xor источник, приёмник
not операнд
test операнд_1, операнд_2
Команды and, or и xor ведут себя так же, как и операторы языка Си &, |, ^. Эти команды устанавливают флаги согласно результату.
Команда not инвертирует каждый бит операнда (изменяет на противоположный), так же как и оператор языка Си .
Команда test выполняет побитовое И над операндами, как и команда and, но, в отличие от неё, операнды не изменяет, а только устанавливает флаги. Её также называют командой логического сравнения, потому что с её помощью удобно проверять, установлены ли определённые биты. Например, так:
testb $0b00001000, %al /* установлен ли 3-й (с нуля) бит? */
je not_set
/* нужные биты установлены */
not_set:
/* биты не установлены */
Обратите внимание на запись константы в двоичной системе счисления: используется префикс 0b.
Команду test можно применять для сравнения значения регистра с нулём:
testl %eax, %eax
je is_zero
/* %eax != 0 */
is_zero:
/* %eax == 0 */
Intel Optimization Manual рекомендует использовать test вместо cmp для сравнения регистра с нулём3.
Ещё следует упомянуть об одном трюке с xor. Как вы знаете, a XOR a = 0. Пользуясь этой особенностью, xor часто применяют для обнуления регистров:
xorl %eax, %eax
/* теперь %eax == 0 */
Почему применяют xor вместо mov? Команда xor короче, а значит, занимает меньше места в процессорном кэше, меньше времени тратится на декодирование, и программа выполняется быстрее. Но эта команда устанавливает флаги. Поэтому, если вам нужно сохранить состояние флагов, применяйте mov4.
Иногда для обнуления регистра применяют команду sub. Помните, она тоже устанавливает флаги.
subl %eax, %eax
/* теперь %eax == 0 */
К логическим командам также можно отнести команды сдвигов:
/* Shift Arithmetic Left/SHift logical Left */
sal/shl количество_сдвигов, назначение
/* SHift logical Right */
shr количество_сдвигов, назначение
/* Shift Arithmetic Right */
sar количество_сдвигов, назначение
количество_сдвигов может быть задано непосредственным значением или находиться в регистре %cl. Учитываются только младшие 5 бит регистра %cl, так что количество сдвигов может варьироваться в пределах от 0 до 31.
Принцип работы команды shl:
До сдвига:
+---+ +----------------------------------+
| ? | | 10001000100010001000100010001011 |
+---+ +----------------------------------+
Флаг CF Операнд
Сдвиг влево на 1 бит:
+---+ +----------------------------------+
| 1 | <-- | 00010001000100010001000100010110 | <-- 0
+---+ +----------------------------------+
Флаг CF Операнд
Сдвиг влево на 3 бита:
+----+ +---+ +----------------------------------+
| 10 | | 0 | <-- | 01000100010001000100010001011000 | <-- 000
+----+ +---+ +----------------------------------+
Улетели Флаг CF Операнд
в никуда
Принцип работы команды shr:
До сдвига:
+----------------------------------+ +---+
| 10001000100010001000100010001011 | | ? |
+----------------------------------+ +---+
Операнд Флаг CF
Логический сдвиг вправо на 1 бит:
+----------------------------------+ +---+
0 -- > | 01000100010001000100010001000101 | -- > | 1 |
+----------------------------------+ +---+
Операнд Флаг CF
Логический сдвиг вправо на 3 бита:
+----------------------------------+ +---+ +----+
000 -- > | 00010001000100010001000100010001 | -- > | 0 | | 11 |
+----------------------------------+ +---+ +----+
Операнд Флаг CF Улетели
в никуда
Эти две команды называются командами логического сдвига, потому что они работают с операндом как с массивом бит. Каждый "выдвигаемый " бит попадает в флаг cf, причём с другой стороны операнда "вдвигается " бит 0. Таким образом, в флаге cfоказывается самый последний "выдвинутый " бит. Такое поведение вполне допустимо для работы с беззнаковыми числами, но числа со знаком будут обработаны неверно из-за того, что знаковый бит может быть потерян.
Для работы с числами со знаком существуют команды арифметического сдвига. Команды shl и sal выполняют полностью идентичные действия, так как при сдвиге влево знаковый бит не теряется (расширение знакового бита влево становится новым знаковым битом). Для сдвига вправо применяется команда sar. Она "вдвигает " слева знаковый бит исходного значения, таким образом сохраняя знак числа:
До сдвига:
+----------------------------------+ +---+
| 10001000100010001000100010001011 | | ? |
+----------------------------------+ +---+
Операнд Флаг CF
старший бит равен 1 == >
== > значение отрицательное == >
== > "вдвинуть " бит 1 ---+
|
+-------------------------------+
|
V Арифметический сдвиг вправо на 1 бит:
+----------------------------------+ +---+
1 -- > | 11000100010001000100010001000101 | -- > | 1 |
+----------------------------------+ +---+
Операнд Флаг CF
Арифметический сдвиг вправо на 3 бита:
+----------------------------------+ +---+ +----+
111 -- > | 11110001000100010001000100010001 | -- > | 0 | | 11 |
+----------------------------------+ +---+ +----+
Операнд Флаг CF Улетели
в никуда
Многие программисты Си знают об умножении и делении на степени двойки (2, 4, 8…) при помощи сдвигов. Этот трюк отлично работает и в ассемблере, используйте его для оптимизации.
Кроме сдвигов обычных, существуют циклические сдвиги:
/* ROtate Right */
ror количество_сдвигов, назначение
/* ROtate Left */
rol количество_сдвигов, назначение
Объясню на примере циклического сдвига влево на три бита: три старших ( "левых ") бита "выдвигаются " из регистра влево и "вдвигаются " в него справа. При этом в флаг cfзаписывается самый последний "выдвинутый " бит.
Принцип работы команды rol:
До сдвига:
+---+ +----------------------------------+
| ? | | 10001000100010001000100010001011 |
+---+ +----------------------------------+
Флаг CF Операнд
Циклический сдвиг влево на 1 бит:
+---+ 1 1 +----------------------------------+
| 1 | <--+--- | 00010001000100010001000100010111 | ---+
+---+ | +----------------------------------+ |
Флаг CF V Операнд ^
| |
+------------------- >--- >--- >----------------+
Циклический сдвиг влево на 3 бита:
+---+ 0 100 +----------------------------------+
| 0 | <--+--- | 01000100010001000100010001011100 | ---+
+---+ | +----------------------------------+ |
Флаг CF V Операнд ^
| |
+------------------- >--- >--- >----------------+
Принцип работы команды ror:
До сдвига:
+----------------------------------+ +---+
| 10001000100010001000100010001011 | | ? |
+----------------------------------+ +---+
Операнд Флаг CF
Циклический сдвиг вправо на 1 бит:
+----------------------------------+ 1 1 +---+
+--- | 11000100010001000100010001000101 | ---+-- > | 1 |
| +----------------------------------+ | +---+
^ Операнд V Флаг CF
| |
+------------------- <--- <--- <----------------+
Циклический сдвиг вправо на 3 бита:
+----------------------------------+ 011 0 +---+
+--- | 01110001000100010001000100010001 | ---+-- > | 0 |
| +----------------------------------+ | +---+
^ Операнд V Флаг CF
| |
+------------------- <--- <--- <----------------+
Существует ещё один вид сдвигов - циклический сдвиг через флаг cf. Эти команды рассматривают флаг cf как продолжение операнда.
/* Rotate through Carry Right */
rcr количество_сдвигов, назначение
/* Rotate through Carry Left */
rcl количество_сдвигов, назначение
Принцип работы команды rcl:
До сдвига:
+---+ +----------------------------------+
| X | | 10001000100010001000100010001011 |
+---+ +----------------------------------+
Флаг CF Операнд
Циклический сдвиг влево через CF на 1 бит:
X +---+ +----------------------------------+
+- <- | 1 | <--- | 0001000100010001000100010001011X | ---+
| +---+ +----------------------------------+ |
V Флаг CF Операнд ^
| |
+------------------------------ >--- >--- >----------------+
Циклический сдвиг влево через CF на 3 бита:
X10 +---+ +----------------------------------+
+- <- | 0 | <--- | 01000100010001000100010001011X10 | ---+
| +---+ +----------------------------------+ |
V Флаг CF Операнд ^
| |
+------------------------------ >--- >--- >----------------+
Принцип работы команды rcr:
До сдвига:
+----------------------------------+ +---+
| 10001000100010001000100010001011 | | X |
+----------------------------------+ +---+
Операнд Флаг CF
Циклический сдвиг вправо через CF на 1 бит:
+----------------------------------+ +---+ X
+--- | X1000100010001000100010001000101 | --- > | 1 | - >-+
| +----------------------------------+ +---+ |
^ Операнд Флаг CF V
| |
+------------------- <--- <--- <---------------------------+
Циклический сдвиг вправо через CF на 3 бита:
+----------------------------------+ +---+ 11X
+--- | 11X10001000100010001000100010001 | --- > | 0 | - >-+
| +----------------------------------+ +---+ |
^ Операнд Флаг CF V
| |
+------------------- <--- <--- <---------------------------+
Эти сложные циклические сдвиги вам редко понадобятся в реальной работе, но уже сейчас нужно знать, что такие инструкции существуют, чтобы не изобретать велосипед потом. Ведь в языке Си циклический сдвиг производится приблизительно так:
int main()
{
int a = 0x11223344;
int shift_count = 8;
a = (a < < shift_count) | (a > > (32 - shift_count));
printf( "%x\n ", a);
return 0;
}
Подпрограммы
Термином "подпрограмма " будем называть и функции, которые возвращают значение, и функции, не возвращающие значение (void proc(…)). Подпрограммы нужны для достижения одной простой цели - избежать дублирования кода. В ассемблере есть две команды для организации работы подпрограмм.
call метка
Используется для вызова подпрограммы, код которой находится по адресу метка. Принцип работы:
· Поместить в стек адрес следующей за call команды. Этот адрес называется адресом возврата.
· Передать управление на метку.
Для возврата из подпрограммы используется команда ret.
ret
ret число
Принцип работы:
· Извлечь из стека новое значение регистра %eip (то есть передать управление на команду, расположенную по адресу из стека).
· Если команде передан операнд число, %esp увеличивается на это число. Это необходимо для того, чтобы подпрограмма могла убрать из стека свои параметры.
Существует несколько способов передачи аргументов в подпрограмму.
· При помощи регистров. Перед вызовом подпрограммы вызывающий код помещает необходимые данные в регистры. У этого способа есть явный недостаток: число регистров ограничено, соответственно, ограничено и максимальное число передаваемых параметров. Также, если передать параметры почти во всех регистрах, подпрограмма будет вынуждена сохранять их в стек или память, так как ей может не хватить регистров для собственной работы. Несомненно, у этого способа есть и преимущество: доступ к регистрам очень быстрый.
· При помощи общей области памяти. Это похоже на глобальные переменные в Си. Современные рекомендации написания кода (а часто и стандарты написания кода в больших проектах) запрещают этот метод. Он не поддерживает многопоточное выполнение кода. Он использует глобальные переменные неявным образом - смотря на определение функции типа void func(void) невозможно сказать, какие глобальные переменные она изменяет и где ожидает свои параметры. Вряд ли у этого метода есть преимущества. Не используйте его без крайней необходимости.
· При помощи стека. Это самый популярный способ. Вызывающий код помещает аргументы в стек, а затем вызывает подпрограмму.
· Рассмотрим передачу аргументов через стек подробнее. Предположим, нам нужно написать подпрограмму, принимающую три аргумента типа long (4 байта). Код:
sub:
pushl %ebp /* запоминаем текущее значение
регистра %ebp, при этом %esp -= 4 */
movl %esp, %ebp /* записываем текущее положение
вершины стека в %ebp */
/* пролог закончен, можно начинать работу */
subl $8, %esp /* зарезервировать место для локальных
переменных */
movl 8(%ebp), %eax /* что-то cделать с параметрами */
movl 12(%ebp), %eax
movl 16(%ebp), %eax
/* эпилог */
movl %ebp, %esp /* возвращем вершину стека в исходное
положение */
popl %ebp /* восстанавливаем старое значение
%ebp, при этом %esp += 4 */
ret
main:
pushl $0x00000010 /* поместить параметры в стек */
pushl $0x00000020
pushl $0x00000030
call sub /* вызвать подпрограмму */
addl $12, %esp
С вызовом всё ясно: помещаем аргументы в стек и даём команду call. А вот как в подпрограмме удобно достать параметры из стека? Вспомним про регистр %ebp.
Мы сохраняем предыдущее значение регистра %ebp, а затем записываем в него указатель на текущую вершину стека. Теперь у нас есть указатель на стек в известном состоянии. Сверху в стек можно помещать сколько угодно данных, %esp поменяется, но у нас останется доступ к параметрам через %ebp. Часто эта последовательность команд в начале подпрограммы называется "прологом ".
. .
. .
. .
+----------------------+ 0x0000F040 <-- новое значение %ebp
| старое значение %ebp |
+----------------------+ 0x0000F044 <-- %ebp + 4
| адрес возврата |
+----------------------+ 0x0000F048 <-- %ebp + 8
| 0x00000030 |
+----------------------+ 0x0000F04C <-- %ebp + 12
| 0x00000020 |
+----------------------+ 0x0000F050 <-- %ebp + 16
| 0x00000010 |
+----------------------+ 0x0000F054
. .
. .
. .
Используя адрес из %ebp, мы можем ссылаться на параметры:
8(%ebp) = 0x00000030
12(%ebp) = 0x00000020
16(%ebp) = 0x00000010
Как видите, если идти от вершины стека в сторону аргументов, то мы будем встречать аргументы в обратном порядке по отношению к тому, как их туда поместили. Нужно сделать одно из двух: или помещать аргументы в обратном порядке (чтобы доставать их в прямом порядке), или учитывать обратный порядок аргументов в подпрограмме. В Си принято при вызове помещать аргументы в обратном порядке. Так как операционная система Linux и большинство библиотек для неё написаны именно на Си, для обеспечения переносимости и совместимости лучше использовать "сишный " способ передачи аргументов и в ваших ассемблерных программах.
Подпрограмме могут понадобится собственные локальные переменные. Их принято держать в стеке, так как в этом случае легко обеспечить необходимое время жизни локальных переменных: достаточно в конце подпрограммы вытолкнуть их из стека. Для того, чтобы зарезервировать для них место, мы просто уменьшим содержимое регистра %esp на размер наших переменных. Это действие эквивалентно использованию соответствующего количества команд push, только быстрее, так как не требует записи в память. Предположим, что нам нужно 2 переменные типа long (4 байта), итого 2 ? 4 = 8 байт. Таким образом, регистр %espнужно уменьшить на 8. Теперь стек выглядит так:
. .
. .
. .
+------------------------+ 0x0000F038 <-- %ebp - 8
| локальная переменная 2 |
+------------------------+ 0x0000F03C <-- %ebp - 4
| локальная переменная 1 |
+------------------------+ 0x0000F040 <-- %ebp
| старое значение %ebp |
+------------------------+ 0x0000F044 <-- %ebp + 4
| адрес возврата |
+------------------------+ 0x0000F048 <-- %ebp + 8
| 0x00000030 |
+------------------------+ 0x0000F04C <-- %ebp + 12
| 0x00000020 |
+------------------------+ 0x0000F050 <-- %ebp + 16
| 0x00000010 |
+------------------------+ 0x0000F054
. .
. .
. .
Вы не можете делать никаких предположений о содержимом локальных переменных. Никто их для вас не инициализировал нулём. Можете для себя считать, что там находятся случайные значения.
При возврате из процедуры мы восстанавливаем старое значение %ebp из стека, потому что после возврата вызывающая функция вряд ли будет рада найти в регистре %ebp неизвестно что (а если серьёзно, этого требует ABI). Для этого необходимо, чтобы старое значение %ebp было на вершине стека. Если подпрограмма что-то поместила в стек после старого %ebp, она должна это убрать. К счастью, мы не должны считать, сколько байт мы поместили, сколько достали и сколько ещё осталось. Мы можем просто поместить значение регистра %ebp в регистр %esp, и стек станет точно таким же, как и после сохранения старого %ebp в начале подпрограммы. После этого команда ret возвращает управление вызывающему коду. Эта последовательность команд часто называется "эпилогом " подпрограммы.
Внимание! Сразу после того, как вы восстановили значение %esp в эпилоге, вы должны считать, что локальные переменные уничтожены. Хотя они ещё не перезаписаны, они, несомненно, будут затёрты последующими командами push, поэтому вы не должны сохранять указатели на локальные переменные дальше эпилога своей функции. |
Остаётся одна маленькая проблема: в стеке всё ещё находятся аргументы для подпрограммы. Это можно решить одним из следующих способов:
· использовать команду ret с аргументом;
· использовать необходимое число раз команду pop и выбросить результат;
· увеличить %esp на размер всех помещенных в стек параметров.
В Си используется последний способ. Так как мы поместили в стек 3 значения типа long по 4 байта каждый, мы должны увеличить %esp на 12, что и делает команда addl сразу после call.
Заметьте, что не всегда обязательно выравнивать стек. Если вы вызываете несколько подпрограмм подряд (но не в цикле!), то можно разрешить аргументам "накопиться " в стеке, а потом убрать их всех одной командой. Если ваша подпрограмма не содержит вызовов других подпрограмм в цикле и вы уверены, что оставшиеся аргументы в стеке не вызовут проблем переполнения стека, то аргументы можно не убирать вообще. Всё равно это сделает команда эпилога, которая восстанавливает %esp из %ebp. С другой стороны, если не уверены - лучше уберите аргументы, от одной лишней команды программа медленнее не станет.
Строго говоря, все эти действия с %ebp не требуются. Вы можете использовать %ebp для хранения своих значений, никак не связанных со стеком, но тогда вам придётся обращаться к аргументам и локальным переменным через %esp или другие регистры, в которые вы поместите указатели. Трюк состоит в том, чтобы не изменять %esp после резервирования места для локальных переменных и до конца функции: так вы сможете использовать %esp на манер %ebp, как было показано выше. Не изменять %esp значит, что вы не сможете использовать push и pop (иначе все смещения переменных в стеке относительно %esp "поплывут "); вам понадобится создать необходимое число локальных переменных для хранения этих временных значений. С одной стороны, этот способ доступа к переменным немного сложнее, так как вы должны заранее просчитать, сколько места в стеке вам понадобится. С другой стороны, у вас появляется еще один свободный регистр %ebp. Так что если вы решите пойти этой дорогой, вы должны заранее продумать, сколько места для локальных переменных вам понадобится, и дальше обращаться к ним через смещения относительно %esp.
И последнее: если вы хотите использовать вашу подпрограмму за пределами данного файла, не забудьте сделать её глобальной с помощью директивы .globl.
Посмотрим на код, который выводил содержимое регистра %eax на экран, вызывая функцию стандартной библиотеки Сиprintf(3). Вы его уже видели в предыдущих программах, но там он был приведен без объяснений. Для справки привожу цитату из man:
PRINTF(3) Linux Programmer 's Manual PRINTF(3)
NAME
printf - formatted output conversion
SYNOPSIS
#include <stdio.h >
int printf(const char *format, ...);
.data
printf_format:
.string "%d\n "
.text
/* printf(printf_format, %eax); */
pushl %eax /* аргумент, подлежащий печати */
pushl $printf_format /* аргумент format */
call printf /* вызов printf() */
addl $8, %esp /* выровнять стек */
Обратите внимание на обратный порядок аргументов и очистку стека от аргументов.
Внимание! Значения регистров глобальны, вызывающая и вызываемая подпрограммы видят одни и те же регистры. Конечно же, подпрограмма может изменять значения любых пользовательских регистров, но она обязана при возврате восстановить значения регистров %ebp, %ebx, %esi, %edi и %esp. Сохранение остальных регистров перед вызовом подпрограммы - задача программиста. Даже если вы заметили, что подпрограмма не изменяет какой-то регистр, это не повод его не сохранять. Ведь неизвестно, как будут обстоять дела в следующей версии подпрограммы. Вы не должны делать каких-либо предположений о состоянии регистров на момент выхода из подпрограммы. Можете считать, что они содержат случайные значения. Также внимания требует флаг df. При вызове подпрограмм флаг должен быть равен 0. Подпрограмма при возврате также должна установить флаг в 0. Коротко: если вам вдруг нужно установить этот флаг для какой-то операции, сбросьте его сразу, как только надобность в нём исчезнет. |
До этого момента мы обходились общим термином "подпрограмма ". Но если подпрограмма - функция, она должна как-то передать возвращаемое значение. Это принято делать при помощи регистра %eax. Перед началом эпилога функция должна поместить в %eax возвращаемое значение.
Программа: печать таблицы умножения
Рассмотрим программу посложнее. Итак, программа для печати таблицы умножения. Размер таблицы умножения вводит пользователь. Нам понадобится вызвать функцию scanf(3) для ввода, printf(3) для вывода и организовать два вложенных цикла для вычислений.
.data
input_prompt:
.string "enter size (1-255): "
scanf_format:
.string "%d "
printf_format:
.string "%5d "
printf_newline:
.string "\n "
size:
.long 0
.text
.globl main
main:
/* запросить у пользователя размер таблицы */
pushl $input_prompt /* format */
call printf /* вызов printf */
/* считать размер таблицы в переменную size */
pushl $size /* указатель на переменную size */
pushl $scanf_format /* format */
call scanf /* вызов scanf */
addl $12, %esp /* выровнять стек одной командой сразу
после двух функций */
movl $0, %eax /* в регистре %ax команда mulb будет
выдавать результат, но мы печатаем
всё содержимое %eax, поэтому два
старших байта %eax должны быть
нулевыми */
movl $0, %ebx /* номер строки */
print_line:
incl %ebx /* увеличить номер строки на 1 */
cmpl size, %ebx
ja print_line_end /* если номер строки больше
запрошенного размера, завершить цикл
*/
movl $0, %ecx /* номер колонки */
print_num:
incl %ecx /* увеличить номер колонки на 1 */
cmpl size, %ecx
ja print_num_end /* если номер колонки больше
запрошенного размера, завершить цикл
*/
movb %bl, %al /* команда mulb ожидает второй
операнд в %al */
mulb %cl /* вычислить %ax = %cl * %al */
pushl %ebx /* сохранить используемые регистры
перед вызовом printf */
pushl %ecx
pushl %eax /* данные для печати */
pushl $printf_format /* format */
call printf /* вызов printf */
addl $8, %esp /* выровнять стек */
popl %ecx /* восстановить регистры */
popl %ebx
jmp print_num /* перейти в начало цикла */
print_num_end:
pushl %ebx /* сохранить регистр */
pushl $printf_newline /* напечатать символ новой строки */
call printf
addl $4, %esp
popl %ebx /* восстановить регистр */
jmp print_line /* перейти в начало цикла */
print_line_end:
movl $0, %eax /* завершить программу */
ret
Программа: вычисление факториала
Теперь напишем рекурсивную функцию для вычисления факториала. Она основана на следующей формуле:
.data
printf_format:
.string "%d\n "
.text
/* int factorial(int) */
factorial:
pushl %ebp
movl %esp, %ebp
/* извлечь аргумент в %eax */
movl 8(%ebp), %eax
/* факториал 0 равен 1 */
cmpl $0, %eax
jne not_zero
movl $1, %eax
jmp return
not_zero:
/* следующие 4 строки вычисляют выражение
%eax = factorial(%eax - 1) */
decl %eax
pushl %eax
call factorial
addl $4, %esp
/* извлечь в %ebx аргумент и вычислить %eax = %eax * %ebx */
movl 8(%ebp), %ebx
mull %ebx
/* результат в паре %edx:%eax, но старшие 32 бита нужно
отбросить, так как они не помещаются в int */
return:
movl %ebp, %esp
popl %ebp
ret
.globl main
main:
pushl %ebp
movl %esp, %ebp
pushl $5
call factorial
pushl %eax
pushl $printf_format
call printf
/* стек можно не выравнивать, это будет сделано
во время выполнения эпилога */
movl $0, %eax /* завершить программу */
movl %ebp, %esp
popl %ebp
ret
Любой программист знает, что если существует очевидное итеративное (реализуемое при помощи циклов) решение задачи, то именно ему следует отдавать предпочтение перед рекурсивным. Итеративный алгоритм нахождения факториала даже проще, чем рекурсивный; он следует из определения факториала:
Говоря проще, нужно перемножить все числа от 1 до .
Функция - на то и функция, что её можно заменить, при этом не изменяя вызывающий код. Для запуска следующего кода просто замените функцию из предыдущей программы вот этой новой версией:
factorial:
movl 4(%esp), %ecx
cmpl $0, %ecx
jne not_zero
movl $1, %eax
ret
not_zero:
movl $1, %eax
loop_start:
mull %ecx
loop loop_start
ret
Что же здесь изменено? Рекурсия переписана в виде цикла. Кадр стека больше не нужен, так как в стек ничего не перемещается и другие функции не вызываются. Пролог и эпилог поэтому убраны, при этом регистр %ebp не используется вообще. Но если бы он использовался, сначала нужно было бы сохранить его значение, а перед возвратом восстановить.
Автор увлёкся процессом и написал 64-битную версию этой функции. Она возвращает результат в паре %eax:%edx и может вычислить .
.data
printf_format:
.string "%llu\n "
.text
.type factorial, @function /* long long int factorial(int) */
factorial:
movl 4(%esp), %ecx
cmpl $0, %ecx
jne not_zero
movl $1, %eax
ret
not_zero:
movl $1, %esi /* младшие 32 бита */
movl $0, %edi /* старшие 32 бита */
loop_start:
movl %esi, %eax /* загрузить младшие биты для
умножения */
mull %ecx /* %eax:%edx = младшие биты * %ecx */
movl %eax, %esi /* записать младшие биты
обратно в %esi */
movl %edi, %eax /* загрузить старшие биты */
movl %edx, %edi /* записать в %edi старшие биты
предыдущего умножения; теперь
результат умножения младших битов
находится в %esi:%edi, а старшие
биты - в %eax для следующего
умножения */
mull %ecx /* %eax:%edx = старшие биты * %ecx */
addl %eax, %edi /* сложить полученный результат со
старшими битами предыдущего
умножения */
loop loop_start
movl %esi, %eax /* результат вернуть в паре */
movl %edi, %edx /* %eax:%edx */
ret
.size factorial, .-factorial
.globl main
main:
pushl %ebp
movl %esp, %ebp
pushl $20
call factorial
pushl %edx
pushl %eax
pushl $printf_format
call printf
/* стек можно не выравнивать, это будет сделано во время
выполнения эпилога */
movl $0, %eax /* завершить программу */
movl %ebp, %esp
popl %ebp
ret
Умножение 64-битного числа на 32-битное делается как при умножении "в столбик ":
%edi %esi
? %ecx
---------------------
%edi?%ecx %esi?%ecx
A |
/|\ |
+-- <-- <-- <--+
старшие 32 бита
Но произведение %esi ? %ecx не поместится в 32 бита, останутся ещё старшие 32 бита. Их мы должны прибавить к старшим 32-м битам результата. Приблизительно так вы это делаете на бумаге в десятичной системе:
2 5 25 ? 3 = 75
? 3
----
+ 6
----
7 5
Задание: напишите программу-считалочку. Есть числа от 0 до , которые располагаются по кругу. Счёт начинается с элемента 0. Каждый -й элемент удаляют. Счёт продолжается с элемента, следующего за удалённым. Напишите программу, выводящую список вычеркнутых элементов. Подсказка: используйте malloc(3) для получения байт памяти и занесите в каждый байт число 1 при помощи memset(3). Значение 1 означает, что элемент существует, значением 0 отмечайте удалённые элементы. При счете пропускайте удалённые элементы.
Системные вызовы
Программа, которая не взаимодействует с внешним миром, вряд ли может сделать что-то полезное. Вывести сообщение на экран, прочитать данные из файла, установить сетевое соединение - это всё примеры действий, которые программа не может совершить без помощи операционной системы. В Linux поль