Синтаксис ассемблера
Команды
Команды ассемблера - это те инструкции, которые будет исполнять процессор. По сути, это самый низкий уровень программирования процессора. Каждая команда состоит из операции (что делать?) и операндов (аргументов). Операции мы будем рассматривать отдельно. А операнды у всех операций задаются в одном и том же формате. Операндов может быть от 0 (то есть нет вообще) до 3. В роли операнда могут выступать:
- Конкретное значение, известное на этапе компиляции, - например, числовая константа или символ. Записываются при помощи знака $, например: $0xf1, $10, $hello_str. Эти операнды называются непосредственными.
- Регистр. Перед именем регистра ставится знак %, например: %eax, %bx, %cl.
- Указатель на ячейку в памяти (как он формируется и какой имеет синтаксис записи - далее в этом разделе).
- Неявный операнд. Эти операнды не записываются непосредственно в исходном коде, а подразумеваются. Нет, конечно, компьютер не читает ваши мысли. Просто некоторые команды всегда обращаются к определённым регистрам без явного указания, так как это входит в логику их работы. Такое поведение всегда описывается в документации.
Внимание! Если вы забудете знак $, когда записываете непосредственное числовое значение, компилятор будет интерпретировать число как абсолютный адрес. Это не вызовет ошибок компиляции, но, скорее всего, приведёт к ошибке сегментации (segmentation fault) при выполнении. |
Почти у каждой команды можно определить операнд-источник (из него команда читает данные) и операнд-назначение (в него команда записывает результат). Общий синтаксис команды ассемблера такой:
Операция Источник, Назначение
Для того, чтобы привести пример команды, я, немного забегая наперед, расскажу об одной операции. Команда mov источник, назначение производит копирование источника в назначение. Возьмем строку из hello.s:
movl $4, %eax /* поместить номер системного вызова write = 4 в регистр %eax */
Как видим, источник - это непосредственное значение 4, а назначение - регистр %eax. Суффикс l в имени команды указывает на то, что ей следует работать с операндами длиной в 4 байта. Все суффиксы:
- b (от англ. byte) - 1 байт,
- w (от англ. word) - 2 байта,
- l (от англ. long) - 4 байта,
- q (от англ. quad) - 8 байт.
Таким образом, чтобы записать $42 в регистр %al (а он имеет размер 1 байт):
movb $42, %al
Важной особенностью всех команд является то, что они не могут работать с двумя операндами, находящимися в памяти. Хотя бы один из них следует сначала загрузить в регистр, а затем выполнять необходимую операцию.
Как формируется указатель на ячейку памяти? Синтаксис:
смещение(база, индекс, множитель)
Вычисленный адрес будет равен база + индекс ? множитель + смещение. Множитель может принимать значения 1, 2, 4 или 8. Например:
- (%ecx) адрес операнда находится в регистре %ecx. Этим способом удобно адресовать отдельные элементы в памяти, например, указатель на строку или указатель на int;
- 4(%ecx) адрес операнда равен %ecx + 4. Удобно адресовать отдельные поля структур. Например, в %ecx адрес некоторой структуры, второй элемент которой находится "на расстоянии" 4 байта от её начала (говорят "по смещению 4 байта");
- -4(%ecx) адрес операнда равен %ecx ? 4;
- foo(,%ecx,4) адрес операнда равен foo + %ecx ? 4, где foo - некоторый адрес. Удобно обращаться к элементам массива. Если foo - указатель на массив, элементы которого имеют размер 4 байта, то мы можем заносить в %ecx номер элемента и таким образом обращаться к самому элементу.
Ещё один важный нюанс: команды нужно помещать в секцию кода. Для этого перед командами нужно указать директиву .text. Вот так:
.text movl $42, %eax ...
Данные
Существуют директивы ассемблера, которые размещают в памяти данные, определенные программистом. Аргументы этих директив - список выражений, разделенных запятыми.
- .byte - размещает каждое выражение как 1 байт;
- .short - 2 байта;
- .long - 4 байта;
- .quad - 8 байт.
Например:
.byte 0x10, 0xf5, 0x42, 0x55 .long 0xaabbaabb .short -123, 456
Также существуют директивы для размещения в памяти строковых литералов:
- .ascii "STR" размещает строку STR. Нулевых байтов не добавляет.
- .string "STR" размещает строку STR, после которой следует нулевой байт (как в языке Си).
- У директивы .string есть синоним .asciz (z от англ. zero - ноль, указывает на добавление нулевого байта).
Строка-аргумент этих директив может содержать стандартные escape-последовательности, которые вы использовали в Си, например, \n, \r, \t, \\, \" и так далее.
Данные нужно помещать в секцию данных. Для этого перед данными нужно поместить директиву .data. Вот так:
.data .string "Hello, world\n" ...
Если некоторые данные не предполагается изменять в ходе выполнения программы, их можно поместить в специальную секцию данных только для чтения при помощи директивы .section .rodata:
.section .rodata .string "program version 0.314"
Приведём небольшую таблицу, в которой сопоставляются типы данных в Си на IA-32 и в ассемблере. Нужно заметить, что размер этих типов в языке Си на других архитектурах (или даже компиляторах) может отличаться.
Тип данных в Си | Размер (sizeof), байт | Выравнивание, байт | Название |
---|---|---|---|
Char signed char | 1 | 1 | signed byte (байт со знаком) |
Unsigned char | 1 | 1 | unsigned byte (байт без знака) |
Short signed short | 2 | 2 | signed halfword (полуслово со знаком) |
Unsigned short | 2 | 2 | unsigned halfword (полуслово без знака) |
Int signed int long signed long enum | 4 | 4 | signed word (слово со знаком) |
unsigned int unsigned long | 4 | 4 | unsigned word (слово без знака) |