Вы находитесь на странице: 1из 8

“Самый лучший способ изучить новый язык – это сразу начать писать на нем программы” -

Брайан Керниган, Деннис Ритчи

Вот и мы последуем этому совету и начнем изучние с простой программы, которая


ищет максимальный элемент в массиве.

Код на ассемблере: Код на С:

#max.s - Find the maximum value //max.c - Find the maximum value
.section .data #include <stdlib.h>
array: int main(void) {
.int 7, 9, 24, 15, 11, 13 int array[] = {7, 9, 24, 15, 11, 13};
size: int i, max, size = 6;
.int 6 max = array[0];
.section .text for (i = 1; i < size; i++)
.globl _start if (max < array[i])
_start: max = array[i];
movl array, %ebx exit(max);
movl $1, %eax }
for:
cmpl size, %eax
jb forcode
jmp exit
forcode:
cmpl %ebx, array(, %eax, 4)
cmova array(, %eax, 4), %ebx
incl %eax
jmp for
exit:
movl $1, %eax
int $0x80

Итак попробуем построчно разобрать программу.


Первая строка начинается с символа «#», который, как несложно догадаться,
означает, что последующий текст в строке комменатрий. Также есть и многострочные
комментарии, которые задаются также, как и в С — /* текст комментария */.
Далее в программе идет секция данных «.section .data». Как написано в
документации к GAS, секция это диапазон последовательных адресов, данные в которой
имеют специальное предназначение. Общий формат для объявления секции такой:

.section name

, где name это имя секции, которое и указывает предназначение секции. Основными
секциями являются:
• .data — секция инициализированных данных (например переменных или констант);
• .bss — секция неинициализированных данных;
• .text — секция кода.
Директиву «.section» в программе можно опускать и писать сразу название секции,
например вместо «.section .data» писать просто «.data». Также вы можете оставлять
секцию пустой, тогда она просто не включается в исполняемый файл и соответственно
места в памяти не занимает (рис.1).

Для нашей программы в секции данных мы объявляем массив целочисленных


значений array и размер этого массива size:

array: size:
.int 7, 9, 24, 15, 11, 13 .int 6
Рис.1. Секция данных не включена в исполняемый файл

array и size это всего лишь метки, которые предназначены для того, чтобы к данным
можно было обратиться по имени, другими словами метка это адрес первого элемента
(если элементов данных несколько, то они перечисляются через запятую), который за ней
следует. Сами метки не занимают места в памяти. Вы можете и не задавать имя метки, но
тогда для обращения к данным вам придется вычислять их адрес, который может
изменяться.
Теперь поговорим о типах данных. Основные типы данных представлены ниже:
• .ascii — текстовая строка;
• .asciz — текстовая строка с добавлением нулевого символа в конец строки;
• .byte — значение длиной в один байт;
• .short (.word) — значение длиной в два байта (слово);
• .int (.long) — значение длиной в четыре байта (двойное слово)
• .quad — значение длиной в восемь байт;
• .octa — значение длиной в шестнадцать байт;
• .float (.single) — вещественное число одинарной точности;
• .double — вещественное число двойной точности.
Для текстовых строк, заданных с помощью директивы «.ascii», следующие
обозначения эквивалентны:

str1: str2:
.ascii "a", "b", "c" .ascii "abc"

Но при использовании директивы «.asciz» они уже не будут эквивалентными, так как
добавляется признак конца строки, т.е. в первом случае он будет добавлен после каждого
символа, а во втором только после последнего символа:

Рис. 2. Использование директивы «.asciz»


Общий формат для объявления «переменной» такой:

value:
.data_type num [, num_2, num_3, .......]

Перед шестнадцатиричными числами ставится префикс «0x», перед восмиричными


«0», перед двоичными «0b», десятичные числа пишутся как есть, но не должны начинаться
с нуля.
Далее идет секция кода «.section .text». Основное отличие секции кода от секции
данных заключается в том, что она имеет право на исполнение, то есть данные в ней
интерпретируются, как инструкции. Вот небольшой пример:

.section .text
.globl _start
_start:
.byte 0xB8
.int 1
.byte 0xBB
.int 0
.byte 0xCD, 0x80

На первый взгляд нет ни одной инструкции, но на самом деле это не так, в чем
можно убедиться, взглянув на код в HT Editor:

Рис. 3. Интерпретация данных, как инструкций

В этом примере инструкции задаются в виде опкодов (ОпКод — Код Операции).

Про использование опкодов можно почитать в цикле статей Aquila «Заклинание кода»

Возвращаясь к нашей программе, далее по коду «.globl _start». Директива «.globl»


объявляет метку, которая будет доступна из внешних приложений. Для того, чтобы
линковщик знал откуда начинается программа необходимо указать точку входа (entry
point). По умолчанию этой точкой служит метка «_start» (при использовании GCC - «main»).
Если линковщик не может найти точку входа, то он выведет следующее предупреждение:
«ld: warning: cannot find entry symbol _start; defaulting to 0000000008048074».
Вот мы и добрались непосредственно до самого кода. Для начала следует упомянуть
о способах адресации. Выделяют следующие способы (говорят еще режимы) адресации:

• непосредственная адресация — значение (константа) напрямую указывается в


инструкции и не может быть изменено во время выполнения программы, например:

movl $1, %eax #поместить 1 в регистр %eax


addl $8, %esp #добавить 8 к регистру %esp

• регистровая адресация — при регистровой адресации источником служит регистр,


например:

movl %eax, %ebx #поместить содержимое регистра %eax в регистр %ebx


xchg %eax, %ebx #обменять содержимое регистров %eax и %ebx

• прямая (абсолютная) адресация — при прямой адресации адрес операнда задается в


виде именованного адреса (метки), например:

value:
.int 1
...
movl value, %eax #поместить 1 в регистр eax

Если нужно поместить адрес элемента, то перед «value» нужно поставить знак долара «$»

• индексная адресация — при индексной адресации используется смещение,


индексный регистр и множитель, который может принимать значения 1, 2 или 4
(байт, слово или двойное слово). Общая формула для расчета итогового адреса
такова:

базовый_адрес(смещение, индекс, множитель)


итоговый_адрес = базовый_адрес + смещение + индекс * множитель

Любое из значений может быть опущено, если опущены все значения, то остается
только базовый_адрес и, таким образом, получается прямая адресация.

• косвенная адресация — при косвенной адресации берется значение по адресу,


указанному в регистре, например:

value:
.int 1
...
movl $value, %eax #поместить адрес value в регистр %eax
movl (%eax), %ebx #поместить значение по адресу в %eax в регистр %ebx

• базовая адресация — базовая адресация аналогична косвенной, за исключением


лишь того, что перед (%register) указывается константа, которая прибавляется к
этому адресу, т.е. const(%register) = (%register + const).

Итак с адресацией вроде бы разобрались. Теперь снова вернемся к программе. Мы


устанавливаем максимальный размер элемента «max» равным первому элементу массива
«array[0]». На ассемблере это делается с помощью инструкции «movl array, %ebx»
(прямая адресация). Тут мы встречаемся с нашей первой инструкцией — MOV. Инструкция
MOV (от английского Move) это инструкция пересылки данных из первого операнда
(источник) во второй (приемник). Следует отметить, что мы используем синтаксис AT&T,
при использовании синтаксиса Intel первый операнд служит приемником, а второй
источником, это касается не только инструкции MOV, но и многих других. Формат
инструкции следующий:

movx источник, приемник или, чтобы было проще запомнить movx что, куда

«x» служит модификатором и указывает на размер пересылаемого значения. Он


может принимать следующие значения:

• b — значение размером в 1 байт;


• w — значение размером в 2 байта;
• l — значение размером в 4 байта;
• q — значение размером в 8 байт.

В качестве источника может выступать непосредственное значение (константа),


ячейка в памяти или регистр, в качестве приемника ячейка в памяти или регистр.
Действительно, инструкция вроде «movl $1, $2» (значение — значение) не имеет смысла.
Также не нужно забывать о том, что размеры источника и приемника должны быть
эквивалентными (вы же не покупаете обувь на несколько размеров меньше, нога ведь не
влезет).
Итак, с тем, что первый элемент массива помещается в регистр... стоп, а что такое
регистр? Wikipedia говорит - «Регистр процессора — сверхбыстрая память внутри
процессора, предназначенная прежде всего для хранения промежуточных результатов
вычисления (регистр общего назначения) или содержащая данные, необходимые для
работы процессора — смещения базовых таблиц, уровни доступа и т.д. (специальные
регистры)». Итак, регистры принято считать быстрой памятью. Регистры делятся на:

• регистры общего назначения (РОН) — EAX (Accumulator), EBX (Base Register), ECX
(Counter Register), EDX (Data Register);
• регистры указателей — EIP (Instruction Pointer), ESP (Stack Pointer), EBP (Base Pointer);
• регистры индексов — ESI (Source Index), EDI (Destination Index).
• регистр флагов — EFLAGS;
• сегментные регистры — CS, DS, SS, FS, ES, GS;

Рассмотрим регистры общего назначения на примере регистра EAX. Структура


регистра EAX (EBX, ECX, EDX) следующая:

31 15 7 0
%EAX
%AX
%AH %AL

Мы рассматриваем только 8-16-32 битные регистры, также есть и 64 разрядные регистры

Такая структура РОН и отличает их от остальных регистров, т.е. можно обращаться


не только ко всему регистру, но и к отдельным его частям, например:

movb $0xff, %al #поместить FFh в %al


movb $0xff, %ah #поместить FFh в %ah
movw $0xffff, %ax #поместить FFFFh в %ax
movl $0xffffffff, %eax #поместить FFFFFFFFh в %eax

Регистр EIP содержит адрес следующей инструкции, которая подлежит выполнению.


Регистр ESP указывает на вершину стека. Стек — это область памяти, работа с
которой организована по принципу FILO (First Input Last Output — первым вошел, последним
вышел). В основном стек используется для передачи аргументов функциям.
Регистр EFLAGS это 32-битный регистр, в котором 17 бит являются флагами. Если
флаг установлен, то бит принимает значение 1 и наоборот. В большинстве случаев
проверяются следующие флаги:

• ZF (Zero flag) — устанавливается, если результатом арифметической или логической


операции был нуль;
• OF (Overflow flag) — предназначен для работы со знаковыми числами и
устанавливается, если результат операции выходит за пределы допустимого
значения;
• PF (Parity flag) — устанавливается, если в резульатет оперции проверяемое значение
содержит четное число бит со значением «1»;
• SF (Sign flag) — используется при работе со знаковыми числами и указывает на
изменение знака числа;
• CF (Carry flag) — предназначен для работы с беззнаковыми числами и
устанавливается, если в результате математической операции произошло
переполнение;
Некоторые инструкции связаны с определенными регистрами, например инструкция
LOOP связана с регистром «%ecx». Формат инструкции LOOP:

loop адрес

, где адрес это место в программе куда следует перейти. Регистр «%ecx» выступает
в роли счетчика, в него помещается значение равное количеству итераций, которое
необходимо совершить, например:

movl $0, %eax


movl $10, %ecx
label:
addl $5, %eax
loop label
...

После выполнения всех инструкций регистр «%eax» будет содержать «50». Каждое
выполнение инструкции «loop» уменьшает значение регистра «%ecx» на единицу и
сравнивает его с нулем (именно в таком порядке: уменьшить — сравнить), если значение
«%ecx» равно нулю, то продолжить выполнение следующей за «loop» инструкции, иначе
перейти на метку.
Остальные регистры будут описаны по мере их использования.
Снова вернемся к программе. Регистр «%eax» выступает в роли счетчика цикла и
предварительно устанавливается равным единице. Далее идет сам цикл. Для того чтобы
лучше понять работу цикла «for» представим его в виде цикла «while» на С:

i = 1;
while (i < size) {
//тело цикла
i++;
}

Что же здесь происходит? Сначала мы устанавливаем счетчик цикла, затем


начинается цикл с проверки условия выхода из него, если оно истино, то выполняются
инструкции в теле цикла и увеличивается счетчик, если оно ложно, то происходит выход из
цикла. Итак, счетчик цикла мы установили «movl $1, %eax». Далее устанавливается метка
«for» (можно дать ей любое название), которая является началом цикла. Теперь нам надо
сравнить счетчик с размером массива, чтобы знать следует ли продолжать выполнение
цикла. За сравнение значений отвечает инструкция CMP (Compare). Формат инструкции:

cmp операнд_2, операнд_1

Реально происходит вычитание второго операнда (операнд_2) из первого (операнд_1)


и, в зависимости от результата, устанавливаются флаги в регистре EFLAGS (CF, OF, SF, ZF,
AF, и PF). Как же воспользоваться этим результатом? А для этого специально
предназначены инструкции условных переходов. Формат этих инструкций такой:

jxx адрес

, где «xx» — от 1 до 3 кодовых символа перехода (таблица 1), адрес — место в


программе куда следует перейти и обычно задается меткой.
Есть два типа условных переходов:

• short jumps — короткие переходы используют 8-битное смещение;


• near jumps — ближние переходы используют либо 16-битное смещение, либо 32-
битное смещение.

Инструкция «jxx» не поддерживает дальние переходы (far jumps), поэтому в


документации Intel можно найти такое решение данной проблемы:
...
jxx beyond
jmp farlabel
beyond:
...
Таблица 1. Инструкция JXX
Инструкция Описание Проверка флагов
JA Перейти, если больше CF =0 и ZF =0
JAE Перейти, если или равно CF =0
JB Перейти, если меньше CF = 1
JBE Перейти, если меньше или равно CF = 1 и ZF = 1
JC Перейти, если установлен carry flag CF = 1
JCXZ Перейти, если регистр %cx равен 0
JECXZ Перейти, если регистр %ecx равен 0
JE Перейти, если равно ZF = 1
JG Перейти, если больше ZF = 0 и SF = OF
JGE Перейти, если или равно SF = OF
JL Перейти, если меньше SF <> OF
JLE Перейти, если меньше или равно ZF = 1 и SF <> OF
JNA Перейти, если не больше CF = 1 и ZF = 1
JNAE Перейти, если не больше или равно CF = 1
JNB Перейти, если не меньше CF = 0
JNBE Перейти, если не меньше или равно CF = 0 и ZF = 0
JNC Перейти, если сброшен carry flag СF = 0
JNE Перейти, если не равно ZF = 0
JNG Перейти, если не больше ZF = 1 или SF <> OF
JNGE Перейти, если не больше или равно SF <> OF
JNL Перейти, если не меньше SF = OF
JNLE Перейти, если не меньше или равно ZF = 0 и SF = OF
JNO Перейти, если сброшен overflow flag OF = 0
JNP Перейти, если сброшен parity flag PF = 0
JNS Перейти, если сброшен sign flag SF = 0
JNZ Перейти, если не нуль ZF = 0
JO Перейти, если установлен overflow flag OF = 1
JP Перейти, если установлен parity flag PF = 1
JPE Перейти, если установлен parity flag PF = 1
JPO Перейти, если сброшен parity flag PF = 0
JS Перейти, если установлен sign flag SF = 1
JZ Перейти, если нуль ZF = 1

А (Above) и B (Below) используются для беззнаковых, G (Greater) и L (Less) для знаковых


Мы используем инструкцию «jb» (jump if below) для перехода к телу цикла (метке
«forcode»), если значение в регистре %eax все еще меньше size, в противном случае
выполняется следующая за «jl» инструкция — JMP (Jump). JMP это инструкция безусловного
перехода, то есть переход происходит всегда. Инструкция JMP является аналогом GOTO в
языках высокого уровня. Формат инструкции такой:

jmp адрес

, где адрес также, как и в инструкции «jxx» место в программе (адрес в памяти) куда
следует перейти.
И в который раз возращаясь к программе. Итак мы перешли в тело цикла, где
должно происходить сравнение текущего максимального элемента (который храниться в
регистре «%ebx») с элементом массива (к элементам массива мы обращаемся используя
индексную адресацию). Это делает инструкция «cmpl %ebx, array(, %eax, 4)». Теперь,
если значение в регистре «%ebx» меньше, чем значение элемента массива, то «%ebx»
присваивается новый максимальный элемент. Можно было бы воспользоваться уже
описанными инструкциями, но есть специально предназначенная инструкция для условного
перемещения — CMOV (Conditional Move). Формат инструкции такой:

cmovxx источник, приемник

, где «xx» - от 1 до 3 кодовых символа, которые определяют условие перемещения


значения из источника в приемник (модификаторы принимают те же значения, что и для
инструкции «jxx», см. таблицу 1).
После того как мы переместили или не переместили значение в регистр «%ebx», мы
наращиваем счетчик и прыгаем на начало цикла, где все повторяется заново. Счетчик
наращивается с помощью инструкции инкремента — INC (Increment). Формат инструкции:

inc регистр или incx значение_в_памяти

Для регистра модификатор (модификатор принимает те же значения, что и для


инструкции «mov») указывать необязательно, но для значения в памяти нужно. Есть и
обратная инструкция, которая уменьшает значение на единицу — DEC (Decrement). Формат
инструкции такой же, как и у INC:

dec регистр или decx значение_в_памяти

Вот мы и подошли к развязке. В конце концов цикл завершает свое выполнение и


осуществляется переход на метку «exit». Нам остается только завершить выполнение
программы, для этого используется системный вызов «exit». В регистр «%eax» помещается
номер системного вызова, в «%ebx» возвращаемое значение, а затем выполняется
прерывание с номером «80h». Номера системных вызовов можно найти в исходных текстах
ядра, а именно в файле «arch/x86/kernel/syscall_table_32.S». Вот замечательный ресурс,
где можно посмотреть какие значения должны находиться в регистрах для каждого
системного вызова — http://syscalls.kernelgrok.com/.