Опубликован: 01.03.2016 | Доступ: свободный | Студентов: 463 / 20 | Длительность: 03:55:00
Лекция 3:

Синтаксис ассемблера

< Лекция 2 || Лекция 3: 12 || Лекция 4 >

Отдельных объяснений требует колонка "Выравнивание". Выравнивание задано у каждого фундаментального типа данных (типа данных, которым процессор может оперировать непосредственно). Например, выравнивание word - 4 байта. Это значит, что данные типа word должны располагаться по адресу, кратному 4 (например, 0x00000100, 0x03284478). Архитектура рекомендует, но не требует выравнивания: доступ к невыровненным данным может быть медленнее, но принципиальной разницы нет и ошибки это не вызовет.

Для соблюдения выравнивания в распоряжении программиста есть директива .p2align.

 .p2align степень_двойки, заполнитель, максимум

Директива .p2align выравнивает текущий адрес до заданной границы. Граница выравнивания задаётся как степень числа 2: например, если вы указали .p2align 3 - следующее значение будет выровнено по 8-байтной границе. Для выравнивания размещается необходимое количество байт-заполнителей со значением заполнитель. Если для выравнивания требуется разместить более чем максимум байт-заполнителей, то выравнивание не выполняется.

Второй и третий аргумент являются необязательными.

Примеры:

.data
        .string "Hello, world\n"    /* мы вряд ли захотим считать,
                                       сколько символов занимает эта 
                                       строка, и является ли следующий
                                       адрес выровненным            */
        .p2align 2                  /* выравниваем по границе 4 байта 
                                       для следующего .long         */
        .long 123456

Метки и прочие символы

Вы, наверно, заметили, что мы не присвоили имён нашим данным. Как же к ним обращаться? Очень просто: нужно поставить метку. Метка - это просто константа, значение которой - адрес.

hello_str:
        .string "Hello, world!\n"

Сама метка, в отличие от данных, места в памяти программы не занимает. Когда компилятор встречает в исходном коде метку, он запоминает текущий адрес и читает код дальше. В результате компилятор помнит все метки и адреса, на которые они указывают. Программист может ссылаться на метки в своём коде. Существует специальная псевдометка, указывающая на текущий адрес. Это метка . (точка).

Значение метки как константы - это всегда адрес. А если вам нужна константа с каким-то другим значением? Тогда мы приходим к более общему понятию "символ". Символ - это просто некоторая константа. Причём он может быть определён в одном файле, а использован в других.

Возьмём hello.s и скомпилируем его так:

[user@host:~]$ gcc -c hello.s
[user@host:~]$ 

Обратите внимание на параметр -c. Мы компилируем исходный код не в исполняемый файл, а лишь только в отдельный объектный файл hello.o. Теперь воспользуемся программой nm(1):

[user@host:~]$ nm hello.o
00000000 d hello_str
0000000e a hello_str_length
00000000 T main

nm(1) выводит список символов в объектном файле. В первой колонке выводится значение символа, во второй - его тип, в третьей - имя. Посмотрим на символ hello_str_length. Это длина строки Hello, world!\n. Значение символа чётко определено и равно 0xe, об этом говорит тип a - absolute value. А вот символ hello_str имеет тип d - значит, он находится в секции данных (data). Символ main находится в секции кода (text section, тип T). А почему a представлено строчной буквой, а T - прописной? Если тип символа обозначен строчной буквой, значит это локальный символ, который видно только в пределах данного файла. Заглавная буква говорит о том, что символ глобальный и доступен другим модулям. Символ main мы сделали глобальным при помощи директивы .global main.

Для создания нового символа используется директива .set. Синтаксис:

 .set    символ, выражение

Например, определим символ foo = 42:

.set    foo, 42

Ещё пример из hello.s:

hello_str:                              
        .string "Hello, world!\n"                    /* наша строка  */
        .set    hello_str_length, . - hello_str - 1  /* длина строки */

Сначала определяется символ hello_str, который содержит адрес строки. После этого мы определяем символ hello_str_length, который, судя по названию, содержит длину строки. Директива .set позволяет в качестве значения символа использовать арифметические выражения. Мы из значения текущего адреса (метка "точка") вычитаем адрес начала строки - получаем длину строки в байтах. Потом мы вычитаем ещё единицу, потому что директива .string добавляет в конце строки нулевой байт (а на экран мы его выводить не хотим).

Неинициализированные данные

Часто требуется просто зарезервировать место в памяти для данных, без инициализации какими-то значениями. Например, у вас есть переменная, значение которой определяется параметрами командной строки. Действительно, вы вряд ли сможете дать ей какое-то осмысленное начальное значение, разве что 0. Такие данные называются неинциализированными, и для них выделена специальная секция под названием .bss. В скомпилированной программе эта секция места не занимает. При загрузке программы в память секция неинициализированых данных будет заполнена нулевыми байтами.

Хорошо, но известные нам директивы размещения данных требуют указания инициализирующего значения. Поэтому для неинициализированных данных используются специальные директивы:

 .space  количество_байт
 .space  количество_байт, заполнитель

Директива .space резервирует количество_байт байт.

Также эту директиву можно использовать для размещения инициализированных данных, для этого существует параметр заполнитель - этим значением будет инициализирована память.

Например:

.bss
long_var_1:                   /* по размеру как .long                */
        .space 4
 
buffer:                       /* какой-то буфер в 1024 байта         */
        .space 1024
 
struct:                       /* какая-то структура размером 20 байт */
        .space 20
< Лекция 2 || Лекция 3: 12 || Лекция 4 >
Алексей Силенок
Алексей Силенок
Россия, С-Пб, Высшая инженерная школа СПбПУ Петра Великого, 2020