Язык ассемблера x86 — это название семейства ассемблерных языков , которые обеспечивают некоторый уровень обратной совместимости с процессорами вплоть до микропроцессора Intel 8008 , выпущенного в апреле 1972 года. [1] [2] Он используется для создания объектного кода для процессоры класса x86 .
Ассемблер считается языком программирования и является машинно-специфичным и низкоуровневым . Как и все языки ассемблера, ассемблер x86 использует мнемонику для представления основных инструкций ЦП или машинного кода . [3] Языки ассемблера чаще всего используются для детальных и критичных по времени приложений, таких как небольшие встроенные системы реального времени , ядра операционных систем и драйверы устройств , но также могут использоваться и для других приложений. Компилятор иногда создает ассемблерный код в качестве промежуточного шага при трансляции программы высокого уровня в машинный код.
Зарезервированные ключевые слова языка ассемблера x86 [4] [5]
Каждая инструкция ассемблера x86 представлена мнемоникой , которая, часто в сочетании с одним или несколькими операндами, преобразуется в один или несколько байтов, называемых кодом операции ; например, инструкция NOP преобразуется в 0x90, а инструкция HLT — в 0xF4. [3] Существуют потенциальные коды операций без документированной мнемоники, которые разные процессоры могут интерпретировать по-разному, из-за чего использующая их программа ведет себя непоследовательно или даже генерирует исключение на некоторых процессорах. Эти коды операций часто появляются на соревнованиях по написанию кода как способ сделать код меньше, быстрее, элегантнее или просто продемонстрировать мастерство автора.
Язык ассемблера x86 имеет две основные ветви синтаксиса : синтаксис Intel и синтаксис AT&T . [6] Синтаксис Intel доминирует в мире DOS и Windows , а синтаксис AT&T доминирует в мире Unix , поскольку Unix была создана в AT&T Bell Labs . [7] Вот краткое изложение основных различий между синтаксисом Intel и синтаксисом AT&T :
Многие ассемблеры x86 используют синтаксис Intel , включая FASM , MASM , NASM , TASM и YASM. GAS , который изначально использовал синтаксис AT&T , поддерживает оба синтаксиса, начиная с версии 2.10 посредством .intel_syntax
директивы. [6] [8] [9] Особенностью синтаксиса AT&T для x86 является то, что операнды x87 меняются местами, что является унаследованной ошибкой исходного ассемблера AT&T. [10]
Синтаксис AT&T почти универсален для всех других архитектур (сохраняя тот же mov
порядок); изначально это был синтаксис сборки PDP-11. Синтаксис Intel специфичен для архитектуры x86 и используется в документации платформы x86. Intel 8080 , который предшествовал x86, также использует порядок «сначала место назначения» для mov
. [11]
Процессоры x86 имеют набор регистров, которые можно использовать в качестве хранилищ двоичных данных. В совокупности регистры данных и адреса называются общими регистрами. Каждый регистр имеет особое назначение в дополнение к тому, что они могут делать: [3]
Помимо общих регистров дополнительно имеются:
Регистр IP указывает на смещение памяти следующей инструкции в сегменте кода (он указывает на первый байт инструкции). Программист не может получить прямой доступ к регистру IP.
Регистры x86 можно использовать с помощью инструкций MOV . Например, в синтаксисе Intel:
Мов топор , 1234h ; копирует значение 1234hex (4660d) в регистр AX
mov bx , топор ; копирует значение регистра AX в регистр BX
Архитектура x86 в реальном и виртуальном режиме 8086 использует для адресации памяти процесс, известный как сегментация , а не плоскую модель памяти, используемую во многих других средах. Сегментация предполагает составление адреса памяти из двух частей: сегмента и смещения ; сегмент указывает на начало группы адресов размером 64 КиБ (64×2 10 ), а смещение определяет, насколько далеко от этого начального адреса находится желаемый адрес. При сегментной адресации для полного адреса памяти требуются два регистра. Один для хранения сегмента, другой для смещения. Чтобы преобразовать обратно в плоский адрес, значение сегмента сдвигается на четыре бита влево (что эквивалентно умножению на 2, 4 или 16), а затем добавляется к смещению для формирования полного адреса, что позволяет преодолеть барьер 64 КБ за счет умного выбора адресов. , хотя это значительно усложняет программирование.
Например, только в реальном режиме /защищенном режиме, если DS содержит шестнадцатеричное число 0xDEAD , а DX содержит число 0xCAFE , они вместе будут указывать на адрес памяти . Таким образом, ЦП может адресовать до 1 048 576 байт (1 МБ) в реальном режиме. Комбинируя значения сегмента и смещения , мы находим 20-битный адрес.0xDEAD * 0x10 + 0xCAFE == 0xEB5CE
Первоначальный размер программ IBM PC ограничивался 640 КБ, но спецификация расширенной памяти использовалась для реализации схемы переключения банков, которая вышла из употребления, когда более поздние операционные системы, такие как Windows, использовали более широкие диапазоны адресов новых процессоров и реализовывали собственную виртуальную память. схемы.
Защищенный режим, начиная с Intel 80286, использовался OS/2 . Ряд недостатков, таких как невозможность доступа к BIOS и невозможность вернуться в реальный режим без перезагрузки процессора, препятствовали широкому использованию. [12] 80286 также по-прежнему был ограничен адресацией памяти 16-битными сегментами, то есть одновременно можно было получить доступ только к 216 байтам (64 килобайтам ). Чтобы получить доступ к расширенным функциям 80286, операционная система перевела процессор в защищенный режим, включив 24-битную адресацию и, следовательно, 2 24 байта памяти (16 мегабайт ).
В защищенном режиме селектор сегмента можно разбить на три части: 13-битный индекс, бит индикатора таблицы , определяющий, находится ли запись в GDT или LDT , и 2-битный запрошенный уровень привилегий ; см. сегментацию памяти x86 .
При обращении к адресу с сегментом и смещением используется обозначение сегмент : смещение , поэтому в приведенном выше примере плоский адрес 0xEB5CE может быть записан как 0xDEAD:0xCAFE или как пара регистров сегмента и смещения; ДС:DX.
Существуют некоторые специальные комбинации сегментных регистров и регистров общего назначения, которые указывают на важные адреса:
Intel 80386 имел три режима работы: реальный режим, защищенный режим и виртуальный режим. Защищенный режим , который дебютировал в 80286, был расширен, чтобы позволить 80386 адресовать до 4 ГБ памяти, а совершенно новый виртуальный режим 8086 ( VM86 ) позволил запускать одну или несколько программ реального режима в защищенной среде, которая в значительной степени имитировала реальный режим, хотя некоторые программы были несовместимы (обычно из-за уловок с адресацией памяти или использования неуказанных кодов операций).
Модель 32-битной плоской памяти в расширенном защищенном режиме 80386 , возможно, была самым важным изменением в семействе процессоров x86 до тех пор, пока AMD не выпустила x86-64 в 2003 году, поскольку она способствовала широкомасштабному внедрению Windows 3.1 (которая опиралась на защищенный режим), поскольку Windows теперь могла запускать множество приложений одновременно, включая приложения DOS, используя виртуальную память и простую многозадачность.
Процессоры x86 поддерживают пять режимов работы кода x86: реальный режим , защищенный режим , длинный режим , виртуальный режим 86 и режим управления системой , в которых одни инструкции доступны, а другие нет. 16-битное подмножество инструкций доступно на 16-битных процессорах x86, а именно 8086, 8088, 80186, 80188 и 80286. Эти инструкции доступны в реальном режиме на всех процессорах x86 и в 16-битном защищенном режиме. ( начиная с 80286 ), доступны дополнительные инструкции, относящиеся к защищенному режиму. В 80386 и более поздних версиях 32-битные инструкции (включая более поздние расширения) также доступны во всех режимах, включая реальный режим; на этих процессорах добавлен режим V86 и 32-битный защищенный режим, а в этих режимах предусмотрены дополнительные инструкции для управления их функциями. SMM с некоторыми собственными специальными инструкциями доступен на некоторых процессорах Intel i386SL, i486 и более поздних версиях. Наконец, в длинном режиме (начиная с AMD Opteron ) также доступны 64-битные инструкции и больше регистров. Набор команд в каждом режиме одинаков, но адресация памяти и размер слова различаются, что требует разных стратегий программирования.
Режимы, в которых может выполняться код x86:
Процессор работает в реальном режиме сразу после включения питания, поэтому ядро операционной системы или другая программа должны явно переключиться в другой режим, если они хотят работать в каком-либо другом режиме, кроме реального. Переключение режимов осуществляется путем изменения определенных битов регистров управления процессора после некоторой подготовки, и после переключения может потребоваться некоторая дополнительная настройка.
На компьютере с устаревшей версией BIOS BIOS и загрузчик работают в реальном режиме . Ядро 64-битной операционной системы проверяет и переключает ЦП в длинный режим, а затем запускает новые потоки режима ядра , выполняющие 64-битный код.
На компьютере под управлением UEFI прошивка UEFI (кроме CSM и устаревшего дополнительного ПЗУ ), загрузчик UEFI и ядро операционной системы UEFI работают в длинном режиме.
В целом особенности современного набора команд x86 таковы:
ebp
) регистров общего назначения в 32-битном режиме и все пятнадцать (счетных rbp
) регистров общего назначения в 64-битном режиме могут свободно использоваться в качестве аккумуляторов или для адресации, большинство из них также неявно используются определенными (более или менее меньше) специальные инструкции; Поэтому затронутые регистры должны быть временно сохранены (обычно сложены), если они активны во время таких последовательностей команд.xchg
, cmpxchg
/ cmpxchg8b
, xadd
и целочисленных инструкций, которые сочетаются с lock
префиксом).Архитектура x86 имеет аппаратную поддержку механизма стека выполнения . push
Такие инструкции , как pop
, call
и ret
используются с правильно настроенным стеком для передачи параметров, выделения места для локальных данных, а также для сохранения и восстановления точек возврата вызова. Инструкция ret
size очень полезна для реализации соглашений об эффективном (и быстром) вызове , когда вызываемый объект отвечает за освобождение пространства стека, занятого параметрами.
При настройке кадра стека для хранения локальных данных рекурсивной процедуры есть несколько вариантов; Инструкция высокого уровня enter
(введенная в 80186) принимает аргумент глубины вложенности процедуры , а также аргумент локального размера и может быть быстрее, чем более явное манипулирование регистрами (например, push bp
; mov bp, sp
; ). Будет ли это быстрее или медленнее, зависит от конкретной реализации процессора x86, а также от соглашения о вызовах, используемого компилятором, программистом или конкретным программным кодом; большая часть кода x86 предназначена для работы на процессорах x86 от нескольких производителей и на разных технологических поколениях процессоров, что подразумевает сильно различающиеся микроархитектуры и решения микрокода , а также различные варианты проектирования на уровне затворов и транзисторов .sub sp, size
Полный диапазон режимов адресации (включая немедленный и базовый+смещение ) даже для таких инструкций, как push
и pop
, упрощает прямое использование стека для целочисленных данных , данных с плавающей запятой и адресных данных, а также сохраняет относительно простые спецификации и механизмы ABI по сравнению с некоторые RISC-архитектуры (требуют более подробной информации о стеке вызовов).
Сборка x86 имеет стандартные математические операции: add
, sub
, и (для целых чисел со знаком), with и (для целых чисел без знака) neg
; логические операторы , , , ; битовый сдвиг арифметический и логический, / (для целых чисел со знаком), / (для целых чисел без знака); вращение с переносом и без него, / , / , дополнение арифметических инструкций BCD , , , и другие.imul
idiv
mul
div
and
or
xor
not
sal
sar
shl
shr
rcl
rcr
rol
ror
aaa
aad
daa
Язык ассемблера x86 включает инструкции для стекового модуля с плавающей запятой (FPU). FPU был дополнительным отдельным сопроцессором для моделей с 8086 по 80386, он был встроенной опцией для серии 80486 и является стандартной функцией каждого процессора Intel x86, начиная с 80486, начиная с Pentium. Инструкции FPU включают сложение, вычитание, отрицание, умножение, деление, остаток, квадратные корни, усечение целых чисел, усечение дробей и масштабирование по степени двойки. Операции также включают инструкции преобразования, которые могут загружать или сохранять значение из памяти в любом из следующих форматов: двоично-десятичный, 32-битное целое число, 64-битное целое число, 32-битное число с плавающей запятой, 64-битное число с плавающей запятой. или 80-битный формат с плавающей запятой (при загрузке значение преобразуется в текущий используемый режим с плавающей запятой). x86 также включает ряд трансцендентных функций, включая синус, косинус, тангенс, арктангенс, возведение в степень по основанию 2 и логарифмирование по основаниям 2, 10 или e .
Формат команд стекового регистра для стековых регистров обычно или , где эквивалентен , и является одним из 8 стековых регистров ( , , ..., ). Как и целые числа, первый операнд является одновременно первым исходным операндом и целевым операндом. и его следует выделить как первую замену исходных операндов перед выполнением вычитания или деления. Инструкции сложения, вычитания, умножения, деления, сохранения и сравнения включают режимы инструкций, которые поднимают вершину стека после завершения своей операции. Так, например, выполняет вычисление , затем удаляет с вершины стека, таким образом превращая то, что было в результате, в вершину стека в .fop st, st(n)
fop st(n), st
st
st(0)
st(n)
st(0)
st(1)
st(7)
fsubr
fdivr
faddp st(1), st
st(1) = st(1) + st(0)
st(0)
st(1)
st(0)
Современные процессоры x86 содержат инструкции SIMD , которые в основном выполняют одну и ту же операцию параллельно со многими значениями, закодированными в широком регистре SIMD. Различные технологии команд поддерживают разные операции с разными наборами регистров, но в целом (от MMX до SSE4.2 ) они включают общие вычисления по целочисленной арифметике или арифметике с плавающей запятой (сложение, вычитание, умножение, сдвиг, минимизация, максимизация, сравнение, деление или квадратный корень). Так, например, paddw mm0, mm1
выполняется 4 параллельных 16-битных (обозначенных w
) целых чисел, добавляет (обозначенных padd
) mm0
значений mm1
и сохраняет результат в mm0
. Потоковые расширения SIMD или SSE также включают режим с плавающей запятой, в котором фактически изменяется только самое первое значение регистров (расширяется в SSE2 ). Были добавлены некоторые другие необычные инструкции, включая сумму абсолютных разностей (используется для оценки движения при сжатии видео , например, в MPEG ) и 16-битную инструкцию умножения-накопления (полезную для программного альфа-смешивания и цифровой фильтрации ). . SSE (начиная с SSE3 ) и 3DNow! расширения включают инструкции сложения и вычитания для обработки парных значений с плавающей запятой как комплексных чисел.
Эти наборы команд также включают в себя множество фиксированных подсловных инструкций для перетасовки, вставки и извлечения значений внутри регистров. Кроме того, существуют инструкции для перемещения данных между целочисленными регистрами и регистрами XMM (используется в SSE)/FPU (используется в MMX).
Процессор x86 также включает сложные режимы адресации для адресации памяти с немедленным смещением, регистра, регистра со смещением, масштабируемого регистра со смещением или без него, а также регистра с необязательным смещением и еще одного масштабируемого регистра. Так, например, можно закодировать mov eax, [Table + ebx + esi*4]
в виде одной инструкции, которая загружает 32 бита данных по адресу, вычисленному как (Table + ebx + esi * 4)
смещение от селектора ds
, и сохраняет их в eax
регистр. В общем, процессоры x86 могут загружать и использовать память, соответствующую размеру любого регистра, с которым они работают. (Инструкции SIMD также включают инструкции половинной загрузки.)
Большинство инструкций x86 с двумя операндами, включая целочисленные инструкции ALU, используют стандартный « байт режима адресации » [13], часто называемый байтом MOD-REG-R/M . [14] [15] [16] Многие 32-битные инструкции x86 также имеют байт режима адресации SIB , который следует за байтом MOD-REG-R/M. [17] [18] [19] [20] [21]
В принципе, поскольку код операции инструкции отделен от байта режима адресации, эти инструкции ортогональны, поскольку любой из этих кодов операции можно смешивать и сопоставлять с любым режимом адресации. Однако набор команд x86 обычно считается неортогональным, поскольку многие другие коды операций имеют некоторый фиксированный режим адресации (у них нет байта режима адресации), и каждый регистр является особенным. [21] [22]
Набор инструкций x86 включает инструкции загрузки, сохранения, перемещения, сканирования и сравнения строк ( lods
, stos
, и ), которые выполняют каждую операцию до заданного размера ( для 8-битного байта, movs
для 16-битного слова, для 32-битного двойного слова). затем увеличивает/уменьшает (в зависимости от DF, флага направления) регистр неявного адреса ( for , for and , и оба for and ). Для операций загрузки, сохранения и сканирования неявный регистр назначения/источника/сравнения находится в регистре или ( в зависимости от размера). Используются неявные сегментные регистры for и for . Регистр или используется в качестве уменьшающего счетчика, и операция прекращается, когда счетчик достигает нуля или (для сканирования и сравнения) при обнаружении неравенства. К сожалению, с течением времени выполнением некоторых из этих инструкций стали пренебрегать, и в некоторых случаях теперь можно получить более быстрые результаты, написав алгоритмы самостоятельно. Однако Intel и AMD обновили некоторые инструкции, и некоторые из них теперь имеют очень приличную производительность, поэтому программисту рекомендуется прочитать последние авторитетные статьи по тестированию производительности, прежде чем использовать конкретную инструкцию из этой группы.scas
cmps
b
w
d
si
lods
di
stos
scas
movs
cmps
al
ax
eax
ds
si
es
di
cx
ecx
Стек — это область памяти и связанный с ней «указатель стека», указывающий на дно стека. Указатель стека уменьшается при добавлении элементов («push») и увеличивается после их удаления («pop»). В 16-битном режиме этот неявный указатель стека адресуется как SS:[SP], в 32-битном режиме — SS:[ESP], а в 64-битном режиме — [RSP]. Указатель стека фактически указывает на последнее сохраненное значение, при условии, что его размер будет соответствовать рабочему режиму процессора (т. е. 16, 32 или 64 бита), чтобы соответствовать ширине по умолчанию инструкций push
/ pop
/ call
/ ret
. Также включены инструкции enter
, leave
которые резервируют и удаляют данные из вершины стека при настройке указателя кадра стека в bp
/ ebp
/ rbp
. Однако прямая установка или сложение и вычитание в регистре sp
/ esp
/ rsp
также поддерживается, поэтому инструкции enter
/ leave
обычно не нужны.
Этот код является началом функции, типичной для языка высокого уровня, когда оптимизация компилятора отключена для удобства отладки:
нажать РБП ; Сохраняем указатель кадра стека вызывающей функции (регистр rbp) mov rbp , rsp ; Создать новый кадр стека ниже стека нашего вызывающего абонента sub rsp , 32 ; Зарезервируйте 32 байта стекового пространства для локальных переменных этой функции. ; Локальные переменные будут находиться ниже rbp, и на них можно будет ссылаться относительно rbp, ; опять же, лучше всего для простоты отладки, но для лучшей производительности rbp не будет ; вообще можно использовать, а на локальные переменные будут ссылаться относительно rsp ; потому что, если не считать сохранения кода, rbp бесплатен для других целей. … … ; Однако если здесь изменить rbp, его значение должно сохраниться для вызывающей стороны. mov [ rbp - 8 ], rdx ; Пример доступа к локальной переменной из ячейки памяти в регистр rdx
... функционально эквивалентно просто:
введите 32 , 0
Другие инструкции для управления стеком включают pushfd
(32-битные)/ pushfq
(64-битные) и popfd/popfq
для хранения и извлечения регистра EFLAGS (32-битные)/RFLAGS (64-битные).
Предполагается, что значения для загрузки или сохранения SIMD упакованы в соседние позиции для регистра SIMD и выравнивают их в последовательном порядке с прямым порядком байтов. Для правильной работы некоторых инструкций загрузки и сохранения SSE требуется выравнивание по 16 байтам. Наборы инструкций SIMD также включают инструкции «предварительной выборки», которые выполняют загрузку, но не нацелены на какой-либо регистр, используемый для загрузки кэша. Наборы инструкций SSE также включают инструкции вневременного сохранения, которые будут выполнять операции сохранения непосредственно в памяти без выполнения выделения кэша, если место назначения еще не кэшировано (в противном случае оно будет вести себя как обычное сохранение).
Большинство общих целочисленных инструкций и инструкций с плавающей запятой (но не SIMD) могут использовать один параметр в качестве комплексного адреса в качестве второго исходного параметра. Целочисленные инструкции также могут принимать один параметр памяти в качестве операнда-адресата.
Сборка x86 имеет операцию безусловного перехода, jmpкоторая может принимать в качестве параметра непосредственный адрес, регистр или косвенный адрес (обратите внимание, что большинство RISC-процессоров поддерживают только регистр связи или короткое немедленное смещение для перехода).
Также поддерживаются несколько условных переходов, в том числе jz
(переход на ноль), jnz
(переход на ненулевое значение), jg
(переход на большее, со знаком), jl
(переход на меньшее, со знаком), ja
(переход на выше/больше, без знака) , jb
(перейти ниже/меньше, без знака). Эти условные операции основаны на состоянии определенных битов в регистре (E)FLAGS . Многие арифметические и логические операции устанавливают, очищают или дополняют эти флаги в зависимости от их результата. Инструкции сравнения cmp
(сравнения) и testинструкции устанавливают флаги так, как если бы они выполнили вычитание или побитовую операцию И соответственно, без изменения значений операндов. Существуют также такие инструкции, как clc
(очистить флаг переноса) и cmc
(дополнить флаг переноса), которые работают непосредственно с флагами. Сравнения с плавающей запятой выполняются с помощью инструкций fcom
или ficom
, которые в конечном итоге необходимо преобразовать в целочисленные флаги.
Каждая операция перехода имеет три различные формы, в зависимости от размера операнда. Короткий переход использует 8-битный знаковый операнд, который является относительным смещением относительно текущей инструкции. Ближайший переход аналогичен короткому переходу, но использует 16-битный знаковый операнд (в реальном или защищенном режиме) или 32-битный знаковый операнд (только в 32-битном защищенном режиме) . Дальний переход — это переход, в котором в качестве абсолютного адреса используется полное значение сегмента base:offset. Существуют также косвенные и индексированные формы каждого из них.
Помимо простых операций перехода, существуют инструкции call
(вызов подпрограммы) и ret
(возврат из подпрограммы). Перед передачей управления подпрограмме помещает в стек call
адрес смещения сегмента инструкции, следующей за ; извлекает это значение из стека и переходит к нему, эффективно возвращая поток управления этой части программы. В случае a база сегмента перемещается после смещения; выталкивает смещение, а затем базу сегмента для возврата.call
ret
far call
far ret
Есть также две аналогичные инструкции int( прерывание ), которые сохраняют текущее значение регистра (E)FLAGS в стеке, а затем выполняют операцию, far call
за исключением того, что вместо адреса используется вектор прерывания , индекс в таблице обработчиков прерываний. адреса. Обычно обработчик прерываний сохраняет все остальные регистры ЦП, которые он использует, если только они не используются для возврата результата операции вызывающей программе (в программном обеспечении, называемом прерываниями). Соответствующей командой возврата из прерывания является iret
, которая восстанавливает флаги после возврата. Мягкие прерывания описанного выше типа используются некоторыми операционными системами для системных вызовов , а также могут использоваться при отладке обработчиков жестких прерываний. Аппаратные прерывания запускаются внешними аппаратными событиями и должны сохранять все значения регистров, поскольку состояние выполняющейся в данный момент программы неизвестно. В защищенном режиме прерывания могут быть настроены операционной системой для запуска переключения задачи, что автоматически сохранит все регистры активной задачи.
В следующих примерах используется так называемый вариант синтаксиса Intel , используемый ассемблерами Microsoft MASM, NASM и многими другими. (Примечание: существует также альтернативный вариант синтаксиса AT&T , в котором, помимо многих других отличий, меняется порядок операндов источника и назначения.) [23]
Использование инструкции программного прерывания 21h для вызова операционной системы MS-DOS для вывода на дисплей — в других примерах для записи в стандартный вывод используется процедура printf() библиотеки C libc . Обратите внимание, что первый пример — это пример 30-летней давности, использующий 16-битный режим, как на Intel 8086. Второй пример — это код Intel 386 в 32-битном режиме. Современный код будет в 64-битном режиме. [24]
.модель маленькая .стек 100h .data msg db 'Привет, мир!$'Начало .кода : mov ax , @DATA ; Инициализирует сегмент данных mov ds , ax mov ah , 09h ; Устанавливает 8-битный регистр «ah», старший байт регистра ax, в 9, в ; выберите номер подфункции подпрограммы MS-DOS, вызываемой ниже ; через программное прерывание int 21h для отображения сообщения lea dx , msg ; Берет адрес сообщения и сохраняет его в 16-битном регистре dx int 21h ; Различные процедуры MS-DOS могут быть вызваны программным прерыванием 21h ; Наша требуемая подфункция была установлена в регистре выше. Мов топор , 4C00h ; Устанавливает в регистре ax номер подфункции для программного обеспечения MS-DOS ; прерывание int 21h для службы «завершить программу». интервал 21ч ; Вызов этой службы MS-DOS никогда не возвращает результат, так как завершает программу. конец начало
; требуется переключатель /coff в версии 6.15 и более ранних версиях .386 .model small , c .stack 1000h .data msg db "Привет, мир!" , 0 .code includelib libcmt.lib includelib libvcruntime.lib includelib libucrt.lib includelib Legacy_stdio_definitions.lib extrn printf : рядом extrn выход : рядом public main main proc push offset msg call printf push 0 вызов выхода main endp конец
; База изображения = 0x00400000 %define RVA(x) (x-0x00400000) раздел .text push dword hello call dword [ printf ] push byte + 0 call dword [ выход ] ret раздел .data hello db "Привет, мир!" раздел .idata dd RVA ( msvcrt_LookupTable ) dd - 1 dd 0 dd RVA ( msvcrt_string ) dd RVA ( msvcrt_imports ) раз 5 dd 0 ; завершает таблицу дескрипторов msvcrt_string dd "msvcrt.dll" , 0 msvcrt_LookupTable: dd RVA ( msvcrt_printf ) dd RVA ( msvcrt_exit ) dd 0 msvcrt_imports: printf dd RVA ( msvcrt_printf ) выход dd RVA ( msvcrt_exit ) dd 0 msvcrt_printf: dw 1 dw "printf" , 0 msvcrt_exit: dw 2 dw "exit" , 0 дд 0
.данные ; раздел для инициализированных данных str: .ascii "Hello, world!\n" ; определить строку текста, содержащую «Привет, мир!» а затем новая строка. стр_лен = . - ул ; получить длину str, вычитая его адрес .текст ; раздел программных функций .globl _start ; экспортируйте функцию _start, чтобы ее можно было запустить _start: ; начать функцию _start movl $4 , %eax ; укажите инструкцию для 'sys_write' movl $1 , %ebx ; указать вывод на стандартный вывод, 'stdout' movl $str , %ecx ; укажите выводимый текст в нашей определенной строке movl $str_len , %edx ; укажите количество символов для записи как длину нашей определенной строки. интервал $0x80 ; вызвать системное прерывание, чтобы инициировать созданный нами системный вызов. movl $1 , %eax ; укажите инструкцию для 'sys_exit' movl $0 , %ebx ; укажите код выхода 0, что означает успех int $0x80 ; вызвать другое системное прерывание, чтобы завершить программу
; ; Эта программа работает в 32-битном защищенном режиме. ; сборка: nasm -f elf -F stabs name.asm ; ссылка: ld -o name name.o ; ; В 64-битном длинном режиме вы можете использовать 64-битные регистры (например, rax вместо eax, rbx вместо ebx и т. д.) ; Также замените «-f elf» на «-f elf64» в команде сборки. ; раздел .данные ; раздел для инициализированных данных str: db 'Hello world!' , 0Ач ; строка сообщения с символом новой строки в конце (10 десятичных знаков) str_len: equ $ - str ; вычисляет длину строки (в байтах) путем вычитания начального адреса строки ; от «здесь, этот адрес» (символ «$» означает «здесь») раздел .текст ; это участок кода (текст программы) в глобальной памяти _start ; _start является точкой входа и требует глобальной области видимости, чтобы ; linker --эквивалент функции main() в C/C++ _start: ; определение процедуры _start начинается здесь move eax , 4 ; указать код функции sys_write (из таблицы векторов ОС) mov ebx , 1 ; указать файловый дескриптор stdout --в gnu/linux все рассматривается как файл, ; даже аппаратные устройства mov ecx , str ; переместить начальный _адрес_ строкового сообщения в регистр ecx mov edx , str_len ; переместить длину сообщения (в байтах) int 80h ; прерывание ядра для выполнения только что настроенного системного вызова - ; в gnu/linux сервисы запрашиваются через ядро mov eax , 1 ; указать код функции sys_exit (из таблицы векторов ОС) mov ebx , 0 ; указать код возврата для ОС (ноль означает, что ОС все прошло нормально) int 80h ; прерывание ядра для выполнения системного вызова (для выхода)
Для 64-битного длинного режима адресом сообщения будет «lea rcx, str», обратите внимание на 64-битный регистр rcx.
; ; Эта программа работает в 32-битном защищенном режиме. ; gcc по умолчанию связывает стандартную библиотеку C; сборка: nasm -f elf -F stabs name.asm ; ссылка: gcc -o name name.o ; ; В 64-битном длинном режиме вы можете использовать 64-битные регистры (например, rax вместо eax, rbx вместо ebx и т. д.) ; Также замените «-f elf» на «-f elf64» в команде сборки. ; глобальный главный ; 'main' должен быть определен во время компиляции ; против стандартной библиотеки C extern printf ; объявляет использование внешнего символа, например printf ; printf объявлен в другом объектном модуле. ; Компоновщик разрешает этот символ позже. сегмент.данные ; _ раздел для инициализированной строки данных db 'Hello world!' , 0Ач , 0 ; строка сообщения, заканчивающаяся символом новой строки (10 ; десятичное число) и нулевым байтом терминатора 'NUL' ; «строка» теперь относится к начальному адресу ; в котором хранится «Hello, World». сегмент .text main: push string ; Поместите адрес «строки» в стек. ; Это уменьшает esp на 4 байта перед сохранением ; 4-байтовая адресная «строка» в память по адресу ; новый esp, новое дно стека. ; Это будет аргументом для вызова printf() printf ; вызывает функцию C printf(). добавить ЕСП , 4 ; Увеличивает указатель стека на 4, чтобы вернуть его обратно ; туда, где это было до «толчка», который ; уменьшил его на 4 байта. РЭТ ; Вернитесь к нашему абоненту.
Этот пример находится в современном 64-битном режиме.
; сборка: nasm -f elf64 -F dwarf hello.asm ; ссылка: ld -o привет привет.оОТНОС ПО УМОЛЧАНИЮ ; по умолчанию используйте режимы адресации относительно RIP, поэтому [foo] = [rel foo] РАЗДЕЛ .rodata ; данные только для чтения должны находиться в разделе .rodata в GNU/Linux, как .rdata в Windows Hello: db "Hello world!" , 10 ; Заканчивается байтом 10 = новая строка (ASCII LF) len_Hello: equ $ - Hello ; Заставьте NASM вычислить длину как константу времени сборки ; символ «$» означает «здесь». write() принимает такую длину, что ; строка в стиле C с нулевым завершением не требуется. ; Это было бы для C puts() РАЗДЕЛ .rodata ; данные только для чтения могут находиться в разделе .rodata в GNU/Linux, как .rdata в Windows Hello: db "Hello world!" , 10 ; 10 = `\n`. len_Hello: equ $ — Привет ; заставить NASM вычислить длину как константу времени сборки ;; write() принимает длину, поэтому строка в стиле C с нулевым завершением не требуется. Это было бы для путов РАЗДЕЛ .текст глобальный _start _start: mov eax , 1 ; __NR_write номер системного вызова из Linux asm/unistd_64.h (x86_64) mov edi , 1 ; int fd = STDOUT_FILENO lea rsi , [ rel Hello ] ; x86-64 использует LEA, относящийся к RIP, для помещения статических адресов в регистры mov rdx , len_Hello ; size_t count = len_Hello системный вызов ; написать (1, Привет, len_Hello); вызов ядра для фактического выполнения системного вызова ;; возвращаемое значение в RAX. RCX и R11 также перезаписываются системным вызовом. мов eax , 60 ; __NR_exit номер вызова (x86_64) хранится в регистре eax. хор Эди , Эди ; Это нули edi, а также rdi. ; Этот трюк xor-self является предпочтительной общей идиомой для обнуления ; регистр, и это всегда самый быстрый метод. ; Когда 32-битное значение сохраняется, например, в edx, старшие биты 63:32 равны ; автоматически обнуляется в каждом случае. Это избавит вас от необходимости устанавливать ; биты с дополнительной инструкцией, как это бывает очень часто ; необходимо, чтобы весь 64-битный регистр был заполнен 32-битным значением. ; Это устанавливает статус выхода нашей процедуры = 0 (обычный выход) syscall ; _exit(0)
Запуск его под straceуправлением проверяет, что в процессе не выполняется никаких дополнительных системных вызовов. Версия printf будет выполнять гораздо больше системных вызовов для инициализации libc и динамического связывания . Но это статический исполняемый файл, поскольку мы компонуем его с помощью ld без -pie или каких-либо общих библиотек; единственные инструкции, которые выполняются в пользовательском пространстве, — это те, которые вы предоставляете.
$ strace ./hello > /dev/null # без перенаправления стандартный вывод вашей программы смешивается с журналированием strace на stderr. Обычно это нормально execve("./hello", ["./hello"], 0x7ffc8b0b3570 /* 51 vars */) = 0 write(1, "Hello world!\n", 13) = 13 exit(0) "=" +++ завершился с 0 +++
Флаги широко используются для сравнений в архитектуре x86. Когда выполняется сравнение двух данных, ЦП устанавливает соответствующий флаг или флаги. После этого можно использовать инструкции условного перехода для проверки флагов и перехода к коду, который должен выполняться, например:
cmp eax , ebx jne do_something ; ... сделай что-нибудь: ; сделай что-нибудь здесь
Помимо инструкций сравнения, существует множество арифметических и других инструкций, которые устанавливают биты в регистре флагов. Другими примерами являются инструкции sub, test и add, а также многие другие. Общие комбинации, такие как cmp + условный переход, внутренне «слиты» (« макрослияние ») в одну микроинструкцию (μ-op) и выполняются быстро, если процессор может угадать, в каком направлении пойдет условный переход: прыжок или продолжение.
Регистр флагов также используется в архитектуре x86 для включения и выключения определенных функций или режимов выполнения. Например, чтобы отключить все маскируемые прерывания, можно использовать инструкцию:
Кли
К регистру флагов также можно получить прямой доступ. Младшие 8 бит регистра флагов можно загрузить ah
с помощью этой lahf
инструкции. Весь регистр флагов также можно перемещать в стек и из него с помощью инструкций pushfd/pushfq
, popfd/popfq
, int
(включая into
) и iret
.
Математическая подсистема с плавающей запятой x87 также имеет собственный независимый регистр типа «флаги» для слова состояния fp. В 1990-х годах процедура доступа к битам флагов в этом регистре была неудобной и медленной, но на современных процессорах есть инструкции «сравнить два значения с плавающей запятой», которые можно использовать с обычными инструкциями условного перехода/ветви напрямую, без каких-либо промежуточных шагов. .
Указатель инструкции вызывается в ip
16-битном режиме, eip
в 32-битном режиме и rip
в 64-битном режиме. Регистр указателя команд указывает на адрес следующей инструкции, которую процессор попытается выполнить. К нему нельзя получить прямой доступ в 16-битном или 32-битном режиме, но можно записать последовательность, подобную следующей, для помещения адреса next_line
в eax
(32-битный код):
вызвать next_line next_line: pop eax
Запись в указатель инструкции проста — jmp
инструкция сохраняет заданный целевой адрес в указателе инструкции, поэтому, например, последовательность, подобная следующей, поместит содержимое rax
в rip
(64-битный код):
JMP Ракс
В 64-битном режиме инструкции могут ссылаться на данные относительно указателя инструкции, поэтому нет необходимости копировать значение указателя инструкции в другой регистр.