stringtranslate.com

язык ассемблера x86

Язык ассемблера x86 — название семейства языков ассемблера , которые обеспечивают определенный уровень обратной совместимости с процессорами вплоть до микропроцессора Intel 8008 , выпущенного в апреле 1972 года. [1] [2] Он используется для создания объектного кода для класса процессоров x86 .

Рассматриваемый как язык программирования , ассемблер является машинно-специфичным и низкоуровневым . Как и все языки ассемблера, ассемблер x86 использует мнемонику для представления основных инструкций процессора или машинного кода . [3] Языки ассемблера чаще всего используются для подробных и критичных по времени приложений, таких как небольшие встраиваемые системы реального времени , ядра операционных систем и драйверы устройств , но могут также использоваться для других приложений. Компилятор иногда создает ассемблерный код в качестве промежуточного шага при переводе высокоуровневой программы в машинный код.

Ключевое слово

Зарезервированные ключевые слова языка ассемблера x86 [4] [5]

  • ааа
  • аад
  • аам
  • аас
  • АЦП
  • добавлять
  • и
  • арпл
  • граница
  • бсф
  • бср
  • bswap
  • бт
  • биткойн
  • бтр
  • бтс
  • вызов
  • cbtw
  • клк
  • клд
  • кли
  • cltd
  • clts
  • КМЦ
  • смк
  • cmps
  • cmpxchg
  • cwtd
  • cwtl
  • даа
  • дас
  • дек
  • див
  • входить
  • ф2хм1
  • фабрики
  • причуда
  • фаддп
  • фблд
  • фбстп
  • фчс
  • fflex
  • fcom
  • fcomp
  • fcompp
  • fcos
  • fdecstp
  • fdiv
  • fdivp
  • fdivr
  • fdivrp
  • ffree
  • фиадд
  • фиком
  • фикомп
  • фидив
  • фидивр
  • поле
  • фимул
  • финкстп
  • конечный
  • кулак
  • кулак
  • фисубр
  • fisubrp
  • флд
  • флд
  • fldcw
  • fldenv
  • fldl2e
  • fldl2t
  • fldlg2
  • fldln2
  • fldpi
  • флдз
  • фмул
  • fmulp
  • fnclex
  • fnint
  • фноп
  • fnsave
  • fnstenv
  • fnstew
  • fnstsw
  • фпатан
  • фпрем
  • фпрем
  • фптан
  • фрндинт
  • frstor
  • fsave
  • fscale
  • фсин
  • fsincos
  • fsqrt
  • фст
  • fstenv
  • fstew
  • fstp
  • fstsw
  • фсуб
  • fsubp
  • fsubr
  • fsubrp
  • фтст
  • фуком
  • фукомп
  • фукомпп
  • fwait
  • fxam
  • FXCH
  • fxtract
  • fyl2x
  • fyl2xp1
  • хлт
  • идив
  • имуль
  • в
  • вкл.
  • инс
  • инт
  • в
  • инвд
  • invlpg
  • ирет
  • jcxz
  • жмп
  • лахф
  • лар
  • lcall
  • лдкс
  • лея
  • оставлять
  • ле
  • ЛФС
  • лгдт
  • лгс
  • лидт
  • ljmp
  • ллдт
  • лмсв
  • замок
  • лодс
  • петля
  • лупнз
  • лупз
  • лрет
  • лсл
  • лсс
  • лтр
  • мов
  • movs
  • movsx
  • movw
  • movzb
  • муль
  • отрицательный
  • нет
  • нет
  • или
  • вне
  • ауты
  • поп
  • попа
  • попф
  • толкать
  • пуша
  • пушф
  • ркл
  • ркр
  • представитель
  • репнз
  • представитель
  • в отставке
  • рол
  • рор
  • сахф
  • сал
  • сар
  • сбб
  • скас
  • сеткс
  • сергей
  • шл
  • шлд
  • шр
  • клочок
  • сидт
  • слдт
  • смсв
  • стс
  • стандарт
  • сти
  • стос
  • ул
  • суб
  • тест
  • верр
  • верв
  • ждать
  • wbinvd
  • xadd
  • xchg
  • xlat
  • xor

Мнемоника и коды операций

Каждая инструкция ассемблера 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:

mov ax , 1234h ; копирует значение 1234hex (4660d) в регистр AX   
mov bx , ax ; копирует значение регистра AX в регистр BX   

Сегментированная адресация

Архитектура x86 в реальном и виртуальном режиме 8086 использует процесс, известный как сегментация, для адресации памяти, а не плоскую модель памяти, используемую во многих других средах. Сегментация включает в себя составление адреса памяти из двух частей, сегмента и смещения ; сегмент указывает на начало группы адресов размером 64 КБ (64×2 10 ), а смещение определяет, насколько далеко от этого начального адреса находится желаемый адрес. При сегментированной адресации для полного адреса памяти требуются два регистра. Один для хранения сегмента, другой для хранения смещения. Чтобы перевести обратно в плоский адрес, значение сегмента сдвигается на четыре бита влево (эквивалентно умножению на 2 4 или 16), затем добавляется к смещению для формирования полного адреса, что позволяет преодолеть барьер в 64 КБ за счет разумного выбора адресов, хотя это значительно усложняет программирование.

В реальном режиме /protected только, например, если 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-битных сегментах, что означало, что за один раз можно было получить доступ только к 2 16 байтам (64 килобайтам ). Чтобы получить доступ к расширенным функциональным возможностям 80286, операционная система переводила процессор в защищенный режим, включив 24-битную адресацию и, таким образом, 2 24 байтам памяти (16 мегабайтам ).

В защищенном режиме селектор сегмента можно разбить на три части: 13-битный индекс, бит индикатора таблицы , который определяет, находится ли запись в GDT или LDT , и 2-битный запрошенный уровень привилегий ; см. сегментацию памяти x86 .

При ссылке на адрес с сегментом и смещением используется запись сегмент:смещение , поэтому в приведенном выше примере плоский адрес 0xEB5CE можно записать как 0xDEAD:0xCAFE или как пару регистров сегмента и смещения; DS:DX.

Существуют некоторые специальные комбинации сегментных регистров и общих регистров, которые указывают на важные адреса:

Intel 80386 имел три режима работы: реальный режим, защищенный режим и виртуальный режим. Защищенный режим , дебютировавший в 80286, был расширен, чтобы позволить 80386 адресовать до 4 ГБ памяти, совершенно новый виртуальный режим 8086 ( VM86 ) позволял запускать одну или несколько программ реального режима в защищенной среде, которая в значительной степени эмулировала реальный режим, хотя некоторые программы были несовместимы (обычно из-за трюков с адресацией памяти или использования неуказанных кодов операций).

32-битная плоская модель памяти расширенного защищенного режима 80386 , возможно, была самым важным изменением функций для семейства процессоров x86 до выпуска AMD x86-64 в 2003 году, поскольку она способствовала массовому внедрению Windows 3.1 (которая полагалась на защищенный режим), поскольку Windows теперь могла запускать множество приложений одновременно, включая приложения DOS, используя виртуальную память и простую многозадачность.

Режимы выполнения

Процессоры x86 поддерживают пять режимов работы для кода x86: Real Mode , Protected Mode , Long Mode , Virtual 86 Mode и System Management Mode , в которых некоторые инструкции доступны, а другие — нет. 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-разрядной операционной системы проверяет и переключает ЦП в режим Long, а затем запускает новые потоки режима ядра, выполняющие 64-разрядный код.

На компьютере с UEFI прошивка UEFI (за исключением CSM и устаревшего дополнительного ПЗУ ), загрузчик UEFI и ядро ​​операционной системы UEFI работают в режиме Long.

Типы инструкций

В целом, особенности современного набора инструкций x86 таковы:

Инструкции по стекированию

Архитектура x86 имеет аппаратную поддержку для механизма стека выполнения . Такие инструкции push, как pop, callи retиспользуются с правильно настроенным стеком для передачи параметров, выделения места для локальных данных и сохранения и восстановления точек возврата вызова. Инструкция ret размера очень полезна для реализации соглашений о вызовах, эффективных с точки зрения пространства (и быстрых) , где вызываемый объект отвечает за освобождение пространства стека, занятого параметрами.

При настройке стекового кадра для хранения локальных данных рекурсивной процедуры есть несколько вариантов; enterинструкция высокого уровня (введенная в 80186) принимает аргумент глубины вложенности процедуры, а также аргумент локального размера и может быть быстрее, чем более явная манипуляция регистрами (например push bp , ; mov bp, sp ; ). То, быстрее это или медленнее, зависит от конкретной реализации процессора x86, а также от соглашения о вызовах, используемого компилятором, программистом или конкретным программным кодом; большая часть кода x86 предназначена для работы на процессорах x86 от нескольких производителей и на разных технологических поколениях процессоров, что подразумевает сильно различающиеся микроархитектуры и решения микрокода , а также различные варианты дизайна на уровне затвора и транзистора .sub sp, size

Полный спектр режимов адресации (включая немедленную и базовую+смещение ) даже для таких инструкций, как pushи pop, упрощает прямое использование стека для целочисленных , плавающих и адресных данных, а также сохраняет спецификации и механизмы ABI относительно простыми по сравнению с некоторыми архитектурами RISC (требуют более явных подробностей стека вызовов).

Целочисленные инструкции ALU

В ассемблере x86 имеются стандартные математические операции, add, sub, negи imul( idivдля целых чисел со знаком), с mulи div(для целых чисел без знака); логические операторы and , or, xor, not; арифметика сдвига битов и логические операции, sal/ sar(для целых чисел со знаком), shl/ shr(для целых чисел без знака); поворот с переносом и без него, rcl/ rcr, rol/ ror, дополнение к арифметическим инструкциям BCD , 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), ststst(0)st(n)st(0)st(1)st(7)fsubrfdivrfaddp st(1), stst(1) = st(1) + st(0)st(0)st(1)st(0)

SIMD-инструкции

Современные процессоры 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 также включают инструкции половинной загрузки.)

Большинство 2-операндных инструкций 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, movs, scasи cmps), которые выполняют каждую операцию до указанного размера ( bдля 8-битного байта, wдля 16-битного слова, dдля 32-битного двойного слова), а затем увеличивают/уменьшают (в зависимости от DF, флага направления) неявный адресный регистр ( siдля lods, diдля stosи scas, и оба для movsи cmps). Для операций загрузки, сохранения и сканирования неявный целевой/исходный/сравнительный регистр находится в регистре al, axили eax(в зависимости от размера). Используемые неявные сегментные регистры — dsдля siи esдля di. Регистр cxили ecxиспользуется как уменьшающийся счетчик, и операция останавливается, когда счетчик достигает нуля или (для сканирований и сравнений) при обнаружении неравенства. К сожалению, со временем производительность некоторых из этих инструкций стала игнорироваться, и в некоторых случаях теперь можно получить более быстрые результаты, самостоятельно написав алгоритмы. Однако Intel и AMD обновили некоторые инструкции, и некоторые из них теперь имеют весьма достойную производительность, поэтому программистам рекомендуется прочитать последние авторитетные статьи о тестах производительности, прежде чем выбирать конкретную инструкцию из этой группы.

Стек — это область памяти и связанный с ней «указатель стека», который указывает на дно стека. Указатель стека уменьшается при добавлении элементов («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обычно не нужны.

Этот код представляет собой начало функции, типичной для языка высокого уровня, когда оптимизация компилятора отключена для удобства отладки:

 push rbp ; Сохраняем указатель стекового кадра вызывающей функции (регистр 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 и будут выравнивать их в последовательном порядке little-endian. Некоторые инструкции загрузки и сохранения SSE требуют 16-байтового выравнивания для правильной работы. Наборы инструкций SIMD также включают инструкции "prefetch", которые выполняют загрузку, но не нацелены ни на один регистр, используемый для загрузки кэша. Наборы инструкций SSE также включают инструкции non-temporal store, которые выполняют сохранение прямо в память без выполнения выделения кэша, если место назначения еще не кэшировано (в противном случае оно будет вести себя как обычное сохранение).

Большинство общих целочисленных и плавающих (но не 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помещает адрес смещения сегмента инструкции, следующей за , callв стек; retизвлекает это значение из стека и переходит к нему, фактически возвращая поток управления в эту часть программы. В случае far call, база сегмента помещается после смещения; far retизвлекает смещение, а затем базу сегмента для возврата.

Также есть две похожие инструкции int( interrupt ), которая сохраняет текущее значение регистра (E)FLAGS в стеке, затем выполняет far call, за исключением того, что вместо адреса он использует вектор прерывания , индекс в таблице адресов обработчика прерываний. Обычно обработчик прерываний сохраняет все другие регистры ЦП, которые он использует, если только они не используются для возврата результата операции вызывающей программе (в программном обеспечении это называется прерываниями). Соответствующая инструкция возврата из прерывания — iret, которая восстанавливает флаги после возврата. Программные прерывания описанного выше типа используются некоторыми операционными системами для системных вызовов , а также могут использоваться при отладке обработчиков жестких прерываний. Жесткие прерывания запускаются внешними аппаратными событиями и должны сохранять все значения регистров, поскольку состояние текущей выполняемой программы неизвестно. В защищенном режиме прерывания могут быть настроены ОС для запуска переключения задачи, которое автоматически сохранит все регистры активной задачи.

Примеры

В следующих примерах используется так называемый вариант синтаксиса Intel , который используется ассемблерами Microsoft MASM, NASM и многими другими. (Примечание: существует также альтернативный вариант синтаксиса AT&T , в котором порядок исходных и целевых операндов меняется местами, среди многих других отличий.) [23]

Программа «Hello world!» для MS-DOS на языке ассемблера MASM

Использование инструкции программного прерывания 21h для вызова операционной системы MS-DOS для вывода на дисплей – другие примеры используют процедуру C printf() из libc для записи в stdout . Обратите внимание, что первый пример – это пример 30-летней давности, использующий 16-битный режим, как на Intel 8086. Второй пример – это код Intel 386 в 32-битном режиме. Современный код будет в 64-битном режиме. [24]

.модель маленькая .стек 100h  .data msg db 'Привет, мир!$'.code start: mov ax , @ DATA ; Инициализирует сегмент данных mov ds , ax mov ah , 09h ; Устанавливает 8-битный регистр 'ah', старший байт регистра ax, в 9, в ; выбирает номер подфункции процедуры MS-DOS, вызываемой ниже ; через программное прерывание int 21h для отображения сообщения lea dx , msg ; Берет адрес msg, сохраняет адрес в 16-битном регистре dx int 21h ; Различные процедуры MS-DOS могут вызываться программным прерыванием 21h ; Наша требуемая подфункция была установлена ​​в регистре ah выше               mov ax , 4C00h ; Устанавливает регистр ax в номер подфункции для программного обеспечения MS-DOS ; прерывание int 21h для службы «завершить программу». int 21h ; Вызов этой службы MS-DOS никогда не возвращает управление, поскольку завершает программу.    конец начало 

Программа "Hello world!" для Windows на ассемблере в стиле MASM

; требуется переключатель /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 exit : рядом  публичный главный основной процесс push смещение msg вызов printf push 0 вызов выход главный endp            конец

Программа «Hello world!» для Windows на ассемблере в стиле NASM

; База изображения = 0x00400000 %define RVA(x) (x-0x00400000) section .text push dword hello call dword [ printf ] push byte + 0 call dword [ exit ] ret         раздел .data hello db "Привет, мир!"   section .idata dd RVA ( msvcrt_LookupTable ) dd - 1 dd 0 dd RVA ( msvcrt_string ) dd RVA ( msvcrt_imports ) times 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 dd 0       

.data ; раздел для инициализированных данных str: .ascii "Hello, world!\n" ; определение строки текста, содержащей "Hello, world!" и затем новую строку. str_len = . - str ; получение длины str путем вычитания ее адреса         .text ; раздел для функций программы .globl _start ; экспорт функции _start для ее запуска _start: ; начало функции _start movl $4 , %eax ; указание инструкции для 'sys_write' movl $1 , %ebx ; указание вывода на стандартный вывод, 'stdout' movl $str , %ecx ; указание выводимого текста в нашей определенной строке movl $str_len , %edx ; указание количества символов для записи в качестве длины нашей определенной строки. int $0x80 ; вызов системного прерывания для инициирования созданного нами системного вызова.                        movl $1 , %eax ; указать команду 'sys_exit' movl $0 , %ebx ; указать код выхода 0, что означает успех int $0x80 ; вызвать другое системное прерывание для завершения программы          

Программа «Hello world!» для Linux на ассемблере в стиле NASM

; ; Эта программа работает в 32-битном защищенном режиме. ; сборка: nasm -f elf -F stabs name.asm ; ссылка: ld -o name name.o ; ; В 64-битном длинном режиме вы можете использовать 64-битные регистры (например, rax вместо eax, rbx вместо ebx и т. д.) ; Также измените "-f elf " на "-f elf64" в команде сборки. ; раздел .data ; раздел для инициализированных данных str: db 'Hello world!' , 0Ah ; строка сообщения с символом новой строки в конце (10 десятичных) str_len: equ $ - str ; вычисляет длину строки (байты) путем вычитания начального адреса str ; из 'здесь, этот адрес' (символ '$' означает 'здесь')            section .text ; это раздел кода (текст программы) в памяти global _start ; _start является точкой входа и требует глобальной области видимости, чтобы быть «видимым» ; компоновщиком --эквивалентно main() в C/C++ _start: ; определение процедуры _start начинается здесь mov 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.

Программа «Hello world!» для Linux в стиле ассемблера NASM с использованием стандартной библиотеки C

; ; Эта программа работает в 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 ; 'main' должен быть определен, так как он компилируется ; в соответствии со стандартной библиотекой C extern printf ; объявляет использование внешнего символа, как printf ; printf объявлен в другом объектном модуле. ; Компоновщик разрешит этот символ позже.         сегмент .data ; раздел для инициализированных данных string db 'Hello world!' , 0Ah , 0 ; строка сообщения, заканчивающаяся символом новой строки (10 ; десятичная) и нулевым байтом 'NUL' terminator ; 'string' теперь ссылается на начальный адрес ; по которому хранится 'Hello, World'.          сегмент .текст main: push string ; Помещает адрес 'string' в стек. ; Это уменьшает esp на 4 байта перед сохранением ; 4-байтового адреса 'string' в памяти в ; новом esp, новом дне стека.        ; Это будет аргументом для printf() call printf ; вызывает функцию C printf(). add esp , 4 ; Увеличивает указатель стека на 4, чтобы вернуть его ; туда, где он был до 'push', что ; уменьшило его на 4 байта. ret ; Возвращаемся к нашему вызывающему.           

Программа «Hello world!» для 64-битного режима Linux в ассемблере в стиле NASM

Этот пример выполнен в современном 64-битном режиме.

; сборка: nasm -f elf64 -F dwarf hello.asm ; ссылка: ld -o hello hello.oDEFAULT REL ; по умолчанию используются режимы адресации относительно 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 $ - Hello ; заставить NASM вычислить длину как константу времени сборки ;; write() принимает длину, поэтому строка в стиле C с нулевым завершением не нужна. Она подойдет для puts     РАЗДЕЛ .текст global _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 использует RIP-относительный LEA для помещения статических адресов в регистры mov rdx , len_Hello ; size_t count = len_Hello системный вызов ; write(1, Hello, len_Hello); вызов в ядре для фактического выполнения системного вызова ;; возвращаемое значение в RAX. RCX и R11 также перезаписываются системным вызовом           mov eax , 60 ; номер вызова __NR_exit (x86_64) хранится в регистре eax. xor edi , edi ; Это обнуляет edi и rdi. ; Этот трюк с xor-self является предпочтительной общей идиомой для обнуления ; регистра и всегда является самым быстрым методом. ; Когда 32-битное значение сохраняется, например, в edx, старшие биты 63:32 ; автоматически обнуляются в каждом случае. Это избавляет вас от необходимости устанавливать ; биты с помощью дополнительной инструкции, поскольку это очень часто ; требуется для заполнения всего 64-битного регистра 32-битным значением. ; Это устанавливает статус выхода нашей подпрограммы = 0 (нормальный выход) syscall ; _exit(0)            

Запуск его под straceпроверяет, что в процессе не выполняется никаких дополнительных системных вызовов. Версия printf сделала бы гораздо больше системных вызовов для инициализации libc и динамического связывания . Но это статический исполняемый файл, поскольку мы связались с помощью ld без -pie или каких-либо общих библиотек; единственные инструкции, которые выполняются в пользовательском пространстве, — это те, которые вы предоставляете.

$ strace  ./hello  >  /dev/null # без перенаправления stdout вашей программы смешивается с журналированием 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 ; ... do_something: ; сделать что-то здесь 

Помимо инструкций сравнения, существует множество арифметических и других инструкций, которые устанавливают биты в регистре флагов. Другими примерами являются инструкции sub, test и add, а также множество других. Распространенные комбинации, такие как cmp + условный переход, внутренне «слиты» (« макрослияние ») в одну микроинструкцию (μ-op) и работают быстро, если процессор может угадать, в каком направлении пойдет условный переход, jump или continue.

Регистр флагов также используется в архитектуре x86 для включения и выключения определенных функций или режимов выполнения. Например, чтобы отключить все маскируемые прерывания, можно использовать инструкцию:

кли

Регистр флагов также может быть доступен напрямую. Нижние 8 бит регистра флагов могут быть загружены ahс помощью lahfинструкции. Весь регистр флагов также может быть перемещен в стек и из него с помощью инструкций pushfd/pushfq, popfd/popfq, int(включая into) и iret.

Подсистема вычислений с плавающей точкой x87 также имеет свой собственный независимый регистр типа «флаги» — слово состояния fp. В 1990-х годах доступ к битам флагов в этом регистре был неудобной и медленной процедурой, но на современных процессорах есть инструкции «сравнить два значения с плавающей точкой», которые можно использовать с обычными инструкциями условного перехода/перехода напрямую, без каких-либо промежуточных шагов.

Использование регистра указателя инструкций

Указатель инструкций вызывается в ip16-битном режиме, eipв 32-битном режиме и ripв 64-битном режиме. Регистр указателя инструкций указывает на адрес следующей инструкции, которую процессор попытается выполнить. К нему нельзя получить прямой доступ в 16-битном или 32-битном режиме, но можно написать последовательность, подобную следующей, чтобы поместить адрес next_lineв eax(32-битный код):

вызов next_line next_line: поп eax

Запись в указатель инструкций проста — jmpинструкция сохраняет заданный целевой адрес в указатель инструкций, так что, например, последовательность, подобная следующей, поместит содержимое raxв rip(64-битный код):

jmp раx

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

Смотрите также

Ссылки

  1. ^ "Семейство микропроцессоров Intel 8008 (i8008)". www.cpu-world.com . Получено 2021-03-25 .
  2. ^ "Intel 8008". МУЗЕЙ ЦП - МУЗЕЙ МИКРОПРОЦЕССОРОВ И ФОТОГРАФИИ МИКРОПРОЦЕССОРОВ . Получено 25.03.2021 .
  3. ^ abc "Intel 8008 OPCODES". www.pastraiser.com . Получено 2021-03-25 .
  4. ^ "Справочник по языку ассемблера". www.ibm.com . Получено 28.11.2022 .
  5. ^ "Справочное руководство по языку ассемблера x86" (PDF) .
  6. ^ abcde Нараям, Рам (2007-10-17). "Ассемблерные программы Linux: сравнение GAS и NASM". IBM . Архивировано из оригинала 3 октября 2013 г. Получено 2008-07-02 .
  7. ^ "Создание Unix". Архивировано из оригинала 2 апреля 2014 года.
  8. ^ Хайд, Рэндалл. «Какой ассемблер лучший?» . Получено 18.05.2008 .
  9. ^ "GNU Assembler News, v2.1 поддерживает синтаксис Intel". 2008-04-04 . Получено 2008-07-02 .
  10. ^ "i386-Bugs (Using as)". Документация Binutils . Получено 15 января 2020 г.
  11. ^ "Руководство по программированию на языке ассемблера Intel 8080" (PDF) . Получено 12 мая 2023 г.
  12. ^ Мюллер, Скотт (24 марта 2006 г.). "P2 (286) Процессоры второго поколения". Модернизация и ремонт ПК, 17-е издание (книга) (17-е изд.). Que. ISBN  0-7897-3404-4. Получено 2017-12-06 .
  13. ^ Кертис Медоу. «Кодирование инструкций 8086».
  14. ^ Игорь Холодов. «6. Кодирование операндов инструкций x86, байт MOD-REG-R/M».
  15. ^ «Кодирование инструкций x86».
  16. ^ Майкл Абраш. "Дзен языка ассемблера: Том I, Знание". "Глава 7: Адресация памяти". Раздел "Адресация mod-reg-rm".
  17. ^ Справочник программиста Intel 80386. "17.2.1 ModR/M и байты SIB"
  18. ^ "Кодирование инструкций X86-64: байты ModR/M и SIB"
  19. ^ "Рисунок 2-1. Формат инструкций архитектур Intel 64 и IA-32".
  20. ^ «Адресация x86 изнутри».
  21. ^ Стивен Маккамант. «Ручной и автоматизированный обратный инжиниринг двоичных кодов».
  22. ^ «Список пожеланий по инструкции X86».
  23. ^ Питер Кордес (18 декабря 2011 г.). «NASM (Intel) против синтаксиса AT&T: в чем преимущества?». Stack Overflow .
  24. ^ "Я только что начал Assembly". daniweb.com . 2008.

Дальнейшее чтение

Руководства

Книги