dementiy 10.07.2010 02:13
Coding — Ассемблер в Linux. Часть первая
“Самый лучший способ изучить новый язык – это сразу начать писать на нем программы” - Брайан Керниган, Деннис РитчиВот и мы последуем этому совету и начнем изучние с простой программы, которая ищет максимальный элемент в массиве.
Итак попробуем построчно разобрать программу.
Первая строка начинается с символа «#», который, как несложно догадаться, означает, что последующий текст в строке комменатрий. Также есть и многострочные комментарии, которые задаются также, как и в С — /* текст комментария */.
Далее в программе идет секция данных «.section .data». Как написано в документации к GAS, секция это диапазон последовательных адресов, данные в которой имеют специальное предназначение. Общий формат для объявления секции такой:
.section name
где name это имя секции, которое и указывает предназначение секции. Основными секциями являются:
.data — секция инициализированных данных (например переменных или констант);
.bss — секция неинициализированных данных;
.text — секция кода.Директиву «.section» в программе можно опускать и писать сразу название секции, например вместо «.section .data» писать просто «.data». Также вы можете оставлять секцию пустой, тогда она просто не включается в исполняемый файл и соответственно места в памяти не занимает (рис.1).
Рис.1. Секция данных не включена в исполняемый файл
Для нашей программы в секции данных мы объявляем массив целочисленных значений array и размер этого массива size:
1 |
|
array и size это всего лишь метки, которые предназначены для того, чтобы к данным можно было обратиться по имени, другими словами метка это адрес первого элемента (если элементов данных несколько, то они перечисляются через запятую), который за ней следует. Сами метки не занимают места в памяти. Вы можете и не задавать имя метки, но тогда для обращения к данным вам придется вычислять их адрес, который может изменяться.
Теперь поговорим о типах данных. Основные типы данных представлены ниже:
.ascii — текстовая строка;
.asciz — текстовая строка с добавлением нулевого символа в конец строки;
.byte — значение длиной в один байт;
.short (.word) — значение длиной в два байта (слово);
.int (.long) — значение длиной в четыре байта (двойное слово)
.quad — значение длиной в восемь байт;
.octa — значение длиной в шестнадцать байт;
.float (.single) — вещественное число одинарной точности;
.double — вещественное число двойной точности.Для текстовых строк, заданных с помощью директивы «.ascii», следующие обозначения эквивалентны:
1 |
|
Но при использовании директивы «.asciz» они уже не будут эквивалентными, так как добавляется признак конца строки, т.е. в первом случае он будет добавлен после каждого символа, а во втором только после последнего символа:
Рис. 2. Использование директивы «.asciz»
Общий формат для объявления «переменной» такой:
1 |
|
Перед шестнадцатиричными числами ставится префикс «0x», перед восмиричными «0», перед двоичными «0b», десятичные числа пишутся как есть, но не должны начинаться с нуля.
Далее идет секция кода «.section .text». Основное отличие секции кода от секции данных заключается в том, что она имеет право на исполнение, то есть данные в ней интерпретируются, как инструкции. Вот небольшой пример:
1 |
|
На первый взгляд нет ни одной инструкции, но на самом деле это не так, в чем можно убедиться, взглянув на код в HT Editor:
Рис. 3. Интерпретация данных, как инструкций
В этом примере инструкции задаются в виде опкодов (ОпКод — Код Операции).
Про использование опкодов можно почитать в цикле статей Aquila - Заклинание кода
Возвращаясь к нашей программе, далее по коду «.globl _start». Директива «.globl» объявляет метку, которая будет доступна из внешних приложений. Для того, чтобы линковщик знал откуда начинается программа необходимо указать точку входа (entry point). По умолчанию этой точкой служит метка «_start» (при использовании GCC - «main»). Если линковщик не может найти точку входа, то он выведет следующее предупреждение: «ld: warning: cannot find entry symbol _start; defaulting to 0000000008048074».
Вот мы и добрались непосредственно до самого кода. Для начала следует упомянуть о способах адресации. Выделяют следующие способы (говорят еще режимы) адресации:
непосредственная адресация — значение (константа) напрямую указывается в инструкции и не может быть изменено во время выполнения программы, например:
1 |
|
регистровая адресация — при регистровой адресации источником служит регистр, например:
1 |
movl %eax, %ebx #поместить содержимое регистра %eax в регистр %ebx
|
прямая (абсолютная) адресация — при прямой адресации адрес операнда задается в виде именованного адреса (метки), например:
1 |
|
Если нужно поместить адрес элемента, то перед «value» нужно поставить знак долара «$»
индексная адресация — при индексной адресации используется смещение, индексный регистр и множитель, который может принимать значения 1, 2 или 4 (байт, слово или двойное слово). Общая формула для расчета итогового адреса такова:
базовый_адрес(смещение, индекс, множитель)
итоговый_адрес = базовый_адрес + смещение + индекс * множитель
Любое из значений может быть опущено, если опущены все значения, то остается только базовый_адрес и, таким образом, получается прямая адресация.
косвенная адресация — при косвенной адресации берется значение по адресу, указанному в регистре, например:
1 |
value:
|
базовая адресация — базовая адресация аналогична косвенной, за исключением лишь того, что перед (%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) следующая:
Такая структура РОН и отличает их от остальных регистров, т.е. можно обращаться не только ко всему регистру, но и к отдельным его частям, например:
1 |
movb $0xff, %al #поместить FFh в %al
|
Регистр 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» выступает в роли счетчика, в него помещается значение равное количеству итераций, которое необходимо совершить, например:
1 |
|
После выполнения всех инструкций регистр «%eax» будет содержать «50». Каждое выполнение инструкции «loop» уменьшает значение регистра «%ecx» на единицу и сравнивает его с нулем (именно в таком порядке: уменьшить — сравнить), если значение «%ecx» равно нулю, то продолжить выполнение следующей за «loop» инструкции, иначе перейти на метку.
Остальные регистры будут описаны по мере их использования.
Снова вернемся к программе. Регистр «%eax» выступает в роли счетчика цикла и предварительно устанавливается равным единице. Далее идет сам цикл. Для того чтобы лучше понять работу цикла «for» представим его в виде цикла «while» на С:
1 |
|
Что же здесь происходит? Сначала мы устанавливаем счетчик цикла, затем начинается цикл с проверки условия выхода из него, если оно истино, то выполняются инструкции в теле цикла и увеличивается счетчик, если оно ложно, то происходит выход из цикла. Итак, счетчик цикла мы установили «movl $1, %eax». Далее устанавливается метка «for» (можно дать ей любое название), которая является началом цикла. Теперь нам надо сравнить счетчик с размером массива, чтобы знать следует ли продолжать выполнение цикла. За сравнение значений отвечает инструкция CMP (Compare). Формат инструкции:
cmp операнд_2, операнд_1
Реально происходит вычитание второго операнда (операнд_2) из первого (операнд_1) и, в зависимости от результата, устанавливаются флаги в регистре EFLAGS (CF, OF, SF, ZF, AF, и PF). Как же воспользоваться этим результатом? А для этого специально предназначены инструкции условных переходов. Формат этих инструкций такой:
jxx адрес
где «xx» — от 1 до 3 кодовых символа перехода (таблица 1), адрес — место в программе куда следует перейти и обычно задается меткой.
Таблица 1. Инструкция JXX
Есть два типа условных переходов:
short jumps — короткие переходы используют 8-битное смещение;
near jumps — ближние переходы используют либо 16-битное смещение, либо 32-битное смещение.Инструкция «jxx» не поддерживает дальние переходы (far jumps), поэтому в документации Intel можно найти такое решение данной проблемы:
1 |
|
Мы используем инструкцию «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». Вот замечательный ресурс, где можно посмотреть какие значения должны находиться в регистрах для каждого системного вызова. На этом пока все.
P.S. Получилось не так, как хотелось, довольно скомкано и возможно запутано. В дальнейшем надеюсь будет более ясная мысль и лучшее изложение текста (если конечно эта тема еще интересует). Если Вы найдете в тексте какие-то недочеты или грубые ошибки, то обязательно напишите в комментариях, будем исправлять вместе =). Ну и как всегда pdf'ка с текстом статьи.
s2h 10.07.2010 03:12 #
+ 1 -
без комментариев
Огого. Вилинуксую =)
Спасибо за статью, может начну асм изучать в свободное время.
Спасибо за статью, может начну асм изучать в свободное время.
статья как-раз когда я уже сдал весь асм в универе))
вот буквально на пол года раньше, ваще круто былобы)
спасибо, полезная статья
вот буквально на пол года раньше, ваще круто былобы)
спасибо, полезная статья
Питон один из тех языков, которые доставляют удовольствие, а не мучения :)
Во всяком случае для меня.
Во всяком случае для меня.
Товарищ автор, а можно Вас еще помучить вопросами? Разбирали пример с х32 регистром, можете показать что-нибудь х64-специфичное?
Или коротенькая заметка о взаимодействии с пользователем - std{in,out}, м? ;)
Или коротенькая заметка о взаимодействии с пользователем - std{in,out}, м? ;)
Извините пожалуйста, что заставил ждать (лето, хорошая погода, баскетбол и чемпионат мира дали о себе знать). К сожалению с 64 разрядными регистрами я не работал, но если хотите могу покапать эту тему. На счет stdin и stdout. Я обязательно включу работу с ними (и не только) в следующую статью, а также о работе с числами (мы, как Вы миогли заметить, рассматривали только целые беззнаковые числа), функциями, может быть еще макросы, в общем как пойдет =). А вообще, если интересно и Вы умете писать хотя бы самые простые программы на С, то у GCC есть опция "-S" (полностью команда будет "gcc -S file.c"), которая выдает исходный код на ассемблере (правда используются не системные вызовы, а библиотека С).
hello world
Я в шоке ;) Потому и спрашиваю о std{in,out}, ибо догадываюсь, что руками код короче и проще....
.file "1.c"
.section .rodata
.LC0:
.string "Hello, world!"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
movq %rsp, %rbp
.cfi_offset 6, -16
.cfi_def_cfa_register 6
movl $.LC0, %edi
call puts
movl $0, %eax
leave
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Gentoo 4.4.2 p1.0) 4.4.2"
.section .note.GNU-stack,"",@progbits
Я в шоке ;) Потому и спрашиваю о std{in,out}, ибо догадываюсь, что руками код короче и проще....
Например с использованием библиотеки С:
И без использование библиотеки, через системный вызов write:
.section .data
output:
.asciz "Hello world\n"
.section .text
.globl _start
_start:
pushl $output #помещаем адрес строки в стек
call printf #вызываем printf
addl $8, %esp #выравниваем стек
pushl $0
call exit
И без использование библиотеки, через системный вызов write:
.section .data
output:
.asciz "Hello world\n"
.equ length, . - output - 1
.section .text
.globl _start
_start:
movl $4, %eax #номер системного вызова write (4) в %eax
movl $1, %ebx #номер файлового дескриптора (stdout - 1) в %ebx
movl $length, %ecx #длина строки в %ecx
movl $output, %edx #адрес строки в %edx
int $0x80
movl $1, %eax
movl $0, %ebx
int $0x80
чьерт, чувствую себя дураком, даже собрать не могу
Честно говоря я занимался ассемблером последний раз года 3 назад, когда сидел на винде и трассировал в SoftIce (sic!) shareware для небезызвестных целей.. :)
Посему никакого продуктивного опыта...
razum2um@localhost /tmp $ cat h.as
.section .data
output:
.asciz "Hello world\n"
.section .text
.globl _start
_start:
pushl $output #помещаем адрес строки в стек
call printf #вызываем printf
addl $8, %esp #выравниваем стек
pushl $0
call exit
.section .data
output:
.asciz "Hello world\n"
.section .text
.globl _start
_start:
pushl $output #помещаем адрес строки в стек
call printf #вызываем printf
addl $8, %esp #выравниваем стек
pushl $0
call exit
razum2um@localhost /tmp $ as h.as
h.as: Assembler messages:
h.as:7: Error: suffix or operands invalid for `push'
h.as:10: Error: suffix or operands invalid for `push'
h.as: Assembler messages:
h.as:7: Error: suffix or operands invalid for `push'
h.as:10: Error: suffix or operands invalid for `push'
Честно говоря я занимался ассемблером последний раз года 3 назад, когда сидел на винде и трассировал в SoftIce (sic!) shareware для небезызвестных целей.. :)
Посему никакого продуктивного опыта...
пардон.. второй компилится, но
razum2um@localhost /tmp $ cat h.as
.section .data
output:
.asciz "Hello world\n"
.equ length, . - output - 1
.section .text
.globl _start
_start:
movl $4, %eax
movl $1, %ebx
movl $length, %ecx
movl $output, %edx
int $0x80
movl $1, %eax
movl $0, %ebx
int $0x80
razum2um@localhost /tmp $ as h.as
razum2um@localhost /tmp $ chmod a+x a.out
razum2um@localhost /tmp $ ./a.out
bash: ./a.out: не могу запустить бинарный файл
.section .data
output:
.asciz "Hello world\n"
.equ length, . - output - 1
.section .text
.globl _start
_start:
movl $4, %eax
movl $1, %ebx
movl $length, %ecx
movl $output, %edx
int $0x80
movl $1, %eax
movl $0, %ebx
int $0x80
razum2um@localhost /tmp $ as h.as
razum2um@localhost /tmp $ chmod a+x a.out
razum2um@localhost /tmp $ ./a.out
bash: ./a.out: не могу запустить бинарный файл
Хм... Все должно работать. Два вопроса: Работает ли у Вас пример из статьи? и У Вас 64 разрядная система (просто у Вас используются 64 разрядные регистры)? Попробуйте указать опцию у as "--32" (as --32 h.as).
гента х64, пример сначала не проверял, только читал все, вспоминал
поддержка х32 вкомпилена, модуль ia32_aout загружен
CONFIG_IA32_EMULATION=y
CONFIG_IA32_AOUT=m
bash: ./a.out: не могу запустить бинарный файл - теперь везде (и с --32 и без, на всех трех)
поддержка х32 вкомпилена, модуль ia32_aout загружен
CONFIG_IA32_EMULATION=y
CONFIG_IA32_AOUT=m
bash: ./a.out: не могу запустить бинарный файл - теперь везде (и с --32 и без, на всех трех)
гента х64, пример сначала не проверял, только читал все, вспоминал
поддержка х32 вкомпилена, модуль ia32_aout загружен
CONFIG_IA32_EMULATION=y
CONFIG_IA32_AOUT=m
bash: ./a.out: не могу запустить бинарный файл - теперь везде (и с --32 и без, на всех трех)
поддержка х32 вкомпилена, модуль ia32_aout загружен
CONFIG_IA32_EMULATION=y
CONFIG_IA32_AOUT=m
bash: ./a.out: не могу запустить бинарный файл - теперь везде (и с --32 и без, на всех трех)
Такую статью (ещё лучше организовать цикл статей) публиковать на Хабрахабре. Там ей самое место. А так всё очень интересно, подробно и не скучно написно.
Отлично! Люто, бешено Плюсую!
Так держать и пожалуйста - не забрасывайте цикл, продолжайте!
Так держать и пожалуйста - не забрасывайте цикл, продолжайте!
FILO (First Input Last Output — первым вошел, последним вышел)
Общепринятым обозначением таки является LIFO - Last In First Out... Смысл один и тот же, но лучше использовать устоявшиеся термины.
А вообще - так держать, и не останавливайтесь - пишите есчо, отлично пишете!
Вы правы, LIFO общепринятое обозначение, но у меня в голове вертелось русское употребление "Первым вошел, последним вышел" и пример тарелок (можно взять только верхнюю) =) Продолжать обязательно буду, вот только не знаю, как по времени получится (совсем скоро останусь на две недели без своего ноутбука, а это на данный момент единственная железка, которая у меня есть). И спасибо всем кто оставил комментарии =)