Учебное пособие NASM
Учебное пособие NASM
Этот учебник покажет вам, как писать программы на языке ассемблера на архитектуре x86-64.
Вы будете писать как (1) самостоятельные программы, так и (2) программы, интегрированные с C.
Не волнуйтесь, мы не будем слишком фантазировать.
Ваша первая программа
Прежде чем изучать подробности, давайте убедимся, что вы можете набирать и запускать программы.
Убедитесь, что установлены nasm и gcc. Сохраните одну из следующих программ как hello.asm , в зависимости от платформы вашей машины. Затем запустите программу в соответствии с приведенными инструкциями.
Если вы работаете на ОС на базе Linux:
Если вы работаете на macOS:
Структура программы NASM
NASM основан на строках. Большинство программ состоит из строк, за которыми следует одна или несколько . Строки могут иметь необязательный . За большинством строк следует ноль или более .
Как правило, код помещается в раздел .text, а постоянные данные – в раздел .data.
Подробности
-
, что довольно хорошо!
Ваши первые несколько инструкций
Три вида операндов
Очень полезно знать эти понятия. Приготовьтесь запомнить их. Как вы можете запоминать? Ники Кейс поможет вам в этом!
Операнды регистров
В этом учебнике нас интересуют только регистры целых чисел, регистр флагов и регистры xmm. (Если вы знакомы с архитектурой x86, вы знаете, что это означает, что мы пропускаем регистры FP, MMX, YMM, сегментные, управляющие, отладочные, тестовые и регистры защищенного режима). Надеюсь, вы уже знакомы с архитектурой x86-84, в таком случае это будет краткий обзор. 16 целочисленных регистров имеют ширину 64 бита и называются:
(Обратите внимание, что 8 из регистров имеют альтернативные имена.) Вы можете рассматривать младшие 32 бита каждого регистра как сам регистр, но с использованием этих имен:
Младшие 16 бит каждого регистра можно рассматривать как регистр, но с использованием этих имен:
Младшие 8 бит каждого регистра можно рассматривать как регистр, но с использованием этих имен:
По историческим причинам биты с 15 по 8 из R0 … R3 названы:
И, наконец, есть 16 регистров XMM, каждый шириной 128 бит, с именами:
Изучите этот рисунок; надеюсь, он вам поможет:
Операторы памяти
- [ число ]
- [ reg ]
- [ reg + reg*scale ] масштаб – 1, 2, 4 или только 8
- [ reg + число ]
- [ reg + reg*scale + число ].
Число называется числом ; простой регистр называется регистром ; регистр с масштабом называется регистром .
Непосредственные операнды
Они могут быть записаны различными способами. Вот несколько примеров из официальной документации.
Инструкции с двумя операндами памяти встречаются крайне редко
- $mathtt;reg, reg$
- $mathtt;reg, mem$
- $mathtt;reg, imm$
- $mathtt;mem, reg$
- $mathtt;mem, imm$
Определение данных и резервирование пространства
Эти примеры взяты из главы 3 документации. Для размещения данных в памяти:
Существуют и другие формы; ознакомьтесь с документацией NASM. Позже.
Для резервирования места (без инициализации) можно использовать следующие псевдоинструкции. Их следует поместить в раздел
equ на самом деле не является реальной инструкцией. Она просто определяет аббревиатуру для использования самим ассемблером. (Это глубокая идея.)
Секция .bss предназначена для записываемых данных.
- Использование библиотеки Си
- Написание автономных программ с использованием только системных вызовов – это здорово, но редкость. Мы хотели бы использовать все лучшее, что есть в библиотеке Си.
- Помните, как в языке C выполнение “начинается” с функции main? Это потому, что библиотека C на самом деле имеет внутри себя метку _start! Код в _start выполняет некоторую инициализацию, затем вызывает main, затем выполняет некоторую очистку, затем выдает системный вызов для выхода. Таким образом, вам просто нужно реализовать main. Мы можем сделать это на ассемблере!
- Если у вас Linux, попробуйте это:
Под macOS это будет выглядеть немного иначе:
В стране macOS функции C (или любые функции, экспортируемые из одного модуля в другой) должны иметь префикс с подчеркиванием. Стек вызовов должен быть выровнен по 16-байтовой границе (подробнее об этом позже). А при обращении к именованным переменным требуется префикс rel.
Понимание соглашений о вызовах
Как мы узнали, что аргумент puts должен быть в RDI? Ответ: существует ряд соглашений, которые соблюдаются в отношении вызовов.
Слева направо передавайте столько параметров, сколько поместится в регистрах. Порядок, в котором выделяются регистры, следующий:
Для целых чисел и указателей: rdi , rsi , rdx , rcx , r8 , r9 .
Для плавающей точки (float, double), xmm0 , xmm1 , xmm2 , xmm3 , xmm4 , xmm5 , xmm6 , xmm7 .
Понятно? Нет? Нужно больше примеров и практики.
- Вот программа, которая иллюстрирует, как нужно сохранять и восстанавливать регистры:
- Смешивание языка Си и языка ассемблера
- Эта программа представляет собой простую функцию, которая принимает три целочисленных параметра и возвращает максимальное значение.
Вот программа на языке Си, которая вызывает функцию на языке ассемблера.
Условные инструкции
s (знак)
z (ноль)
c (перенос)
o (переполнение)
- Условные инструкции имеют три базовые формы: j для условного перехода, cmov для условного перемещения и set для условного набора. Суффикс инструкции имеет одну из 30 форм: s ns z nz c nc o no p np pe po e ne l nl le nle g ng ge nge a na ae nae b nb be nbe .
- Аргументы командной строки
- Вы знаете, что в Си main – это обычная старая функция, и у нее есть несколько собственных параметров:
- Итак, вы угадали, argc будет находиться в rdi , а argv (указатель) – в rsi . Вот программа, которая использует этот факт, чтобы просто повторять аргументы командной строки программы, по одному в строке:
Более длинный пример
Обратите внимание, что для библиотеки C аргументы командной строки всегда являются строками. Если вы хотите рассматривать их как целые числа, вызовите atoi . Вот изящная программа для вычисления $x^y$.
Инструкции с плавающей точкой
Аргументы с плавающей точкой поступают в регистры xmm. Вот простая функция для суммирования значений в двойном массиве:
Обратите внимание, что инструкции с плавающей точкой имеют суффикс sd; это самая распространенная инструкция, но позже мы увидим и другие. Вот программа на языке Си, которая ее вызывает:
Секции данных
В большинстве операционных систем текстовая секция доступна только для чтения, поэтому вам может понадобиться секция данных. В большинстве операционных систем раздел данных предназначен только для инициализированных данных, а для неинициализированных данных есть специальный раздел .bss. Вот программа, которая усредняет аргументы командной строки, ожидаемые как целые числа, и выводит результат в виде числа с плавающей точкой.
Рекурсия
Возможно, удивительно, но для реализации рекурсивных функций не требуется ничего необычного. Вам просто нужно быть осторожным, чтобы сохранить регистры, как обычно. Выталкивание и вталкивание вокруг рекурсивного вызова является типичной стратегией.
Пример вызывающей функции:
SIMD-параллелизм
Регистры XMM могут выполнять арифметические действия над значениями с плавающей точкой по одной операции за раз (скалярные) или по несколько операций за раз (упакованные). Операции имеют вид:
Вот функция, которая складывает четыре плавающих числа одновременно:
Насыщенная арифметика
Регистры XMM также могут выполнять арифметические действия над целыми числами. Инструкции имеют вид:
Вот пример. Он также иллюстрирует, как загружать регистры XMM. Вы не можете загружать мгновенные значения; для перемещения из памяти нужно использовать movaps. Есть и другие способы, но мы не будем рассматривать все в этом учебнике.
Графика
Любая программа на языке Си может быть “перенесена” на язык ассемблера. Это относится и к графическим программам.
Эта программа, скорее всего, не работает.
Последний раз я тестировал ее в 2003 году. Еще во времена старой школы OpenGL. Использовалась Win32. Во времена до GLSL. Использовался GLUT. У меня давно не было доступа к компьютеру с Windows, и я даже не уверен, что это будет работать. Это представлено здесь только для исторического интереса. Если вы сможете модифицировать его для работы под современным OpenGL, пожалуйста, дайте мне знать. Я обновлю программу и, конечно же, процитирую ваш вклад!
Локальные переменные и стековые рамки
Сначала, пожалуйста, прочитайте статью Эли Бендерски. Этот обзор более полный, чем мои краткие заметки.
При вызове функции вызывающая сторона сначала помещает параметры в нужные регистры, а затем выдает инструкцию вызова. Дополнительные параметры, выходящие за пределы регистров, будут помещены в стек до вызова. Инструкция вызова помещает адрес возврата на вершину стека. Итак, если у вас есть функция:
То при входе в функцию, $x$ будет в edi, $y$ будет в esi, а адрес возврата будет на вершине стека. Где мы можем разместить локальные переменные? Простой выбор – на самом стеке, но если у вас достаточно регистров, используйте их! Регистры в любом случае работают быстрее.
Если вы работаете на машине со стандартным ABI, вы можете оставить rsp на месте и обращаться к “дополнительным параметрам” и локальным переменным непосредственно из rsp, например:
nasmstructure.png
rdx.png