Команды ассемблера и машинные команды. Команды ассемблера и машинные команды Дальнейшее развитие теста по Ассемблер x86

Может рассматриваться как автокод (см. ниже), расширенный конструкциями . Является существенно платформо-зависимым. Языки ассемблера для различных аппаратных платформ несовместимы, хотя могут быть в целом подобны.

В русском языке может именоваться просто «ассемблером » (типичны выражения типа «писать программу на ассемблере»), что, строго говоря, неверно, так как ассемблером именуется утилита трансляции программы с языка ассемблера в машинный код компьютера.

Общее определение

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

Перевод программы на языке ассемблера в исполнимый машинный код (вычисление выражений, раскрытие макрокоманд, замена мнемоник собственно машинными кодами и символьных адресов на абсолютные или относительные адреса) производится ассемблером - программой-транслятором, которая и дала языку ассемблера его название.

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

Кроме того, язык ассемблера позволяет использовать символические метки вместо адресов ячеек памяти, которые при ассемблировании заменяются на вычисляемые ассемблером или компоновщиком абсолютные или относительные адреса , а также так называемые директивы (команды ассемблера, не переводимые в машинные команды процессора, а выполняемые самим ассемблером).

Директивы ассемблера позволяют, в частности, включать блоки данных, задать ассемблирование фрагмента программы по условию, задать значения меток, использовать макрокоманды с параметрами.

Каждая модель (или семейство) процессоров имеет свой набор - систему - команд и соответствующий ему язык ассемблера. Наиболее популярные синтаксисы языков ассемблера - Intel-синтаксис и AT&T-синтаксис .

Существуют компьютеры, реализующие в качестве машинного язык программирования высокого уровня (Форт , Лисп , Эль-76). Фактически, в таких компьютерах они выполняют роль языков ассемблера.

Возможности

Использование языка ассемблера предоставляет программисту ряд возможностей, как правило, недоступных при программировании на языках высокого уровня. Большинство из них связано с близостью языка к аппаратной платформе.

  • Возможность максимально полного использования всех особенностей аппаратной платформы позволяет, теоретически, писать самый быстрый и компактный код из возможных для данного процессора. Искусный программист, как правило, способен значительно оптимизировать программу по сравнению с транслятором с языка высокого уровня по одному или нескольким параметрам и создать код, близкий к оптимальному по Парето (как правило, быстродействие программы достигается за счёт удлинения кода и наоборот):
    • за счёт более рационального использования ресурсов процессора, например, максимально эффективного размещения всех исходных данных в регистрах, можно исключить излишние обращения к оперативной памяти;
    • за счёт ручной оптимизации вычислений, в том числе более эффективного использования промежуточных результатов, может быть сокращён объём кода и повышена скорость программы.
  • Возможность непосредственного доступа к аппаратуре, и, в частности, портам ввода-вывода , конкретным адресам памяти, регистрам процессора (впрочем, данная возможность существенно ограничивается тем, что во многих операционных системах прямое обращение из прикладных программ для записи в регистры периферийного оборудования блокировано для надёжности работы системы).

Использование ассемблера практически не имеет альтернативы при создании:

  • драйверов оборудования и ядра операционной системы (по крайней мере, машинозависимых подсистем ядра ОС), тогда, когда важно временно́е согласование работы периферийных устройств с центральным процессором;
  • программ, которые должны храниться в ПЗУ ограниченного объёма и/или выполняться на устройствах с ограниченной производительностью («прошивок » компьютеров и различных электронных устройств)
  • платформо-зависимых компонентов компиляторов и интерпретаторов языков высокого уровня , системных библиотек и кода, реализующего совместимость платформ .

Отдельно можно отметить, что с помощью программы-дизассемблера возможно преобразование откомпилированной программы в программу на языке ассемблера. В большинстве случаев это единственный (хотя и крайне трудоёмкий) способ обратного реконструирования алгоритмов программы, если не доступен её исходный код на языке высокого уровня.

Ограничения

Применение

Исторически, если первым поколением языков программирования считать машинные коды, то язык ассемблера можно рассматривать как второе поколение языков программирования. Недостатки языка ассемблера, сложность разработки на нём больших программных комплексов привели к появлению языков третьего поколения - языков программирования высокого уровня (таких как Фортран , Лисп , Кобол , Паскаль , Си и др.). Именно языки программирования высокого уровня и их наследники в основном используются в настоящее время в индустрии информационных технологий . Однако языки ассемблера сохраняют свою нишу, обусловленную их уникальными преимуществами в части эффективности и возможности полного использования специфических средств конкретной платформы.

На языке ассемблера пишут программы или их фрагменты в тех случаях, когда критически важны:

  • быстродействие (драйверы , игры);
  • объём используемой памяти (загрузочные секторы, встраиваемое (англ. embedded ) программное обеспечение, программы для микроконтроллеров и процессоров с ограниченными ресурсами, вирусы , программные защиты).

С использованием программирования на языке ассемблера производятся:

  • Оптимизация критичных к скорости участков программ в программах на языках высокого уровня, таких как C++ или Pascal . Это особенно актуально для игровых приставок , имеющих фиксированную производительность, и для мультимедийных кодеков , которые стремятся делать менее ресурсоёмкими и более быстрыми.
  • Создание операционных систем (ОС) или их компонентов. В настоящее время подавляющее большинство ОС пишут на более высокоуровневых языках (в основном на Си - языке высокого уровня, который специально был создан для написания одной из первых версий UNIX). Аппаратно зависимые участки кода, такие как загрузчик ОС , уровень абстрагирования от аппаратного обеспечения (hardware abstraction layer) и ядро, часто пишутся на языке ассемблера. Фактически, ассемблерного кода в ядрах Windows или Linux совсем немного, поскольку авторы стремятся обеспечить переносимость и надёжность , но тем не менее он там присутствует. Некоторые любительские ОС, такие как MenuetOS и KolibriOS , целиком написаны на языке ассемблера. При этом MenuetOS и KolibriOS помещаются на дискету и содержат графический многооконный интерфейс.
  • Программирование микроконтроллеров (МК) и других встраиваемых процессоров. По мнению профессора Таненбаума , развитие МК повторяет историческое развитие компьютеров новейшего времени . Сейчас (2013 г.) для программирования МК весьма часто применяют язык ассемблера (хотя и в этой области широкое распространение получают языки вроде Си). В МК приходится перемещать отдельные байты и биты между различными ячейками памяти . Программирование МК весьма важно, так как, по мнению Таненбаума, в автомобиле и квартире современного цивилизованного человека в среднем содержится 50 микроконтроллеров.
  • Создание драйверов . Драйверы (или их некоторые программные модули) программируют на языке ассемблера. Хотя в настоящее время драйверы также стремятся писать на языках высокого уровня (на высокоуровневом языке много проще написать надёжный драйвер) в связи с повышенными требованиями к надёжности и достаточной производительностью современных процессоров (быстродействие обеспечивает временно́е согласование процессов в устройстве и процессоре) и достаточным совершенством компиляторов с языков высокого уровня (отсутствие ненужных пересылок данных в сгенерированном коде), подавляющая часть современных драйверов пишется на языке ассемблера. Надёжность для драйверов играет особую роль, поскольку в Windows NT и UNIX (в том числе в Linux) драйверы работают в режиме ядра системы. Одна тонкая ошибка в драйвере может привести к краху всей системы.
  • Создание антивирусов и других защитных программ.
  • Написание кода низкоуровневых библиотек трансляторов языков программирования.

Связывание программ на разных языках

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

  • На этапе компиляции - вставка в исходный код программы на языке высокого уровня ассемблерных фрагментов (англ. inline assembler ) с помощью специальных директив языка. Способ удобен для несложных преобразований данных, но полноценного ассемблерного кода с данными и подпрограммами, включая подпрограммы со множеством входов и выходов, не поддерживаемых языком высокого уровня, с его помощью сделать невозможно.
  • На этапе компоновки при раздельной компиляции . Для взаимодействия компонуемых модулей достаточно, чтобы импортируемые функции (определённые в одних модулях и используемые в других) поддерживали определённое соглашение о вызове (англ. calling conventions ). Написаны же отдельные модули могут быть на любых языках, в том числе и на языке ассемблера.

Синтаксис

Синтаксис языка ассемблера определяется системой команд конкретного процессора.

Набор команд

Типичными командами языка ассемблера являются (большинство примеров даны для Intel-синтаксиса архитектуры x86):

  • Команды пересылки данных (mov и др.)
  • Арифметические команды (add , sub , imul и др.)
  • Логические и побитовые операции (or , and , xor , shr и др.)
  • Команды управления ходом выполнения программы (jmp , loop , ret и др.)
  • Команды вызова прерываний (иногда относят к командам управления): int
  • Команды ввода-вывода в порты (in , out)
  • Для микроконтроллеров и микрокомпьютеров характерны также команды, выполняющие проверку и переход по условию, например:
  • cjne - перейти, если не равно
  • djnz - декрементировать, и если результат ненулевой, то перейти
  • cfsneq - сравнить, и если не равно, пропустить следующую команду

Инструкции

Типичный формат записи команд

[метка:] [ [префикс] мнемокод [операнд {, операнд}] ] [ ;комментарий]

где мнемокод - непосредственно мнемоника инструкции процессору. К ней могут быть добавлены префиксы (повторения, изменения типа адресации и пр.).

Используемые мнемоники обычно одинаковы для всех процессоров одной архитектуры или семейства архитектур (среди широко известных - мнемоники процессоров и контроллеров x86 , ARM , SPARC , PowerPC , M68k). Они описываются в спецификации процессоров. Возможные исключения:

  • если ассемблер использует кроссплатформенный AT&T-синтаксис (оригинальные мнемоники приводятся к синтаксису AT&T);
  • если изначально существовало два стандарта записи мнемоник (система команд была наследована от процессора другого производителя).

Например, процессор Zilog Z80 наследовал систему команд Intel 8080 , расширил её и поменял мнемоники (и обозначения регистров) на свой лад. Процессоры Motorola Fireball наследовали систему команд Z80, несколько её урезав. Вместе с тем, Motorola официально вернулась к мнемоникам Intel и в данный момент половина ассемблеров для Fireball работает с мнемониками Intel, а половина - с мнемониками Zilog.

Директивы

Программа на языке ассемблера может содержать директивы : инструкции, не переводящиеся непосредственно в машинные команды, а управляющие работой компилятора. Набор и синтаксис их значительно разнятся и зависят не от аппаратной платформы, а от используемого транслятора (порождая диалекты языков в пределах одного семейства архитектур). В качестве «джентльменского набора» директив можно выделить следующие:

  • определение данных (констант и переменных),
  • управление организацией программы в памяти и параметрами выходного файла,
  • задание режима работы компилятора,
  • всевозможные абстракции (то есть элементы языков высокого уровня) - от оформления процедур и функций (для упрощения реализации парадигмы процедурного программирования) до условных конструкций и циклов (для парадигмы структурного программирования),

Пример программы

Примеры программы Hello, world! для разных платформ и разных диалектов:

SECTION .data msg: db " Hello , world " , 10 len: equ $-msg SECTION .text global _start _start: mov edx , len mov ecx , msg mov ebx , 1 ; stdout mov eax , 4 ; write(2) int 0x80 mov ebx , 0 mov eax , 1 ; exit(2) int 0x80

SECTION .data msg: db " Hello , world " , 10 len: equ $-msg SECTION .text global _start syscall: int 0x80 ret _start: push len push msg push 1 ; stdout mov eax , 4 ; write(2) call syscall add esp , 3 * 4 push 0 mov eax , 1 ; exit(2) call syscall

386 .model flat , stdcall option casemap : none include \ masm32 \ include \ windows.inc include \ masm32 \ include \ kernel32.inc includelib \ masm32 \ lib \ kernel32.lib .data msg db " Hello , world " , 13 , 10 len equ $-msg .data ? written dd ? .code start: push - 11 call GetStdHandle push 0 push OFFSET written push len push OFFSET msg push eax call WriteFile push 0 call ExitProcess end start

format PE console entry start include " include \ win32a.inc " section " .data " data readable writeable message db " Hello , world ! " , 0 section " .code " code readable executable start: ; CINVOKE макрос в составе FASM. ; Позволяет вызывать CDECL-функции. cinvoke printf , message cinvoke getch ; INVOKE аналогичный макрос для STDCALL-функций. invoke ExitProcess , 0 section " .idata " import data readable library kernel , " kernel32.dll " , \ msvcrt , " msvcrt.dll " import kernel , \ ExitProcess , " ExitProcess " import msvcrt , \ printf , " printf " , \ getch , " _getch "

;yasm-1.0.0-win32.exe -f win64 HelloWorld_Yasm.asm ;setenv /Release /x64 /xp ;link HelloWorld_Yasm.obj Kernel32.lib User32.lib /entry:main /subsystem:windows /LARGEADDRESSAWARE:NO bits 64 global main extern MessageBoxA extern ExitProcess section .data mytit db " The 64 - bit world of Windows & assembler... " , 0 mymsg db " Hello World ! " , 0 section .text main: mov r9d , 0 ; uType = MB_OK mov r8 , mytit ; LPCSTR lpCaption mov rdx , mymsg ; LPCSTR lpText mov rcx , 0 ; hWnd = HWND_DESKTOP call MessageBoxA mov ecx , eax ; uExitCode = MessageBox(...) call ExitProcess ret

Section ".data" hello: .asciz "Hello World!\n" .section ".text" .align 4 .global main main: save %sp , - 96 , %sp ! выделяем память mov 4 , %g1 ! 4 = WRITE (системный вызов ) mov 1 , %o0 ! 1 = STDOUT set hello , %o1 mov 14 , %o2 ! количество символов ta 8 ! вызов системы ! выход из программы mov 1 , %g1 ! move 1 (exit () syscall ) into %g1 mov 0 , %o0 ! move 0 (return address ) into %o0 ta 8 ! вызов системы

org 7 C00h use16 jmp code nop db " hellowrd " SectSize dw 00200 h ClustSize db 001 h ResSecs dw 00001 h FatCnt db 002 h RootSiz dw 000 E0h TotSecs dw 00 B40h Media db 0 F0h FatSize dw 00009 h TrkSecs dw 00012 h HeadCnt dw 00002 h HidnSec dw 00000 h code: cli mov ax , cs mov ds , ax mov ss , ax mov sp , 7 c00h sti mov ax , 0 b800h mov es , ax mov di , 200 mov ah , 2 mov bx , MessStr msg_print: mov al ,[ cs : bx ] mov [ es : di ], ax inc bx add di , 2 cmp bx , MessEnd jnz msg_print loo: jmp loo MessStr equ $ Message db " Hello , World ! " MessEnd equ $

История и терминология

Данный тип языков получил своё название от названия транслятора (компилятора) с этих языков - ассемблера (англ. assembler - сборщик). Название обусловлено тем, что программа «автоматически собиралась», а не вводилась вручную покомандно непосредственно в кодах . При этом наблюдается путаница терминов: ассемблером нередко называют не только транслятор, но и соответствующий язык программирования («программа на ассемблере»).

Пишу программу на ассемблере(х86) для проектора. Суть в том, что там есть автоматический режим переключения слайдов. В нем требуется делать задержку показа слайдов с шагом в 10 секунд. Мне вот помогли сделать такую задержку. Ниже приведены две процедуры (создания задержки и декремента задержки)

MakeDelay Proc Near mov al,Timer ;значение задержки в диапазоне от 10 до 90 единиц shr al,4 mov ah,al xor al,al ror ax,2 mov word ptr Delay+1,ax mov byte ptr Delay,0 mov byte ptr Delay + 3,0 ret MakeDelay Endp

mov ax,word ptr Delay or ax,word ptr Delay + 2 cmp ax,0 ;Задежка кончилась? je nxtslide ;Да - переход к выбору слайда sub word ptr Delay,1 ;Нет - переход к уменьшению sbb word ptr Delay + 2,0 ;Задержки jmp ext5 ;Выход из подпрограммы

В первой процедуре непонятно как вообще пришли к такому алгоритму задержки. А в процедуре декремента задержки непонятно зачем делать дизьюнкцию. Проблема в том, что выполнение задержки очень сильно зависит от операционной системы. И, например выставляя значение 60 в Виртуальной Windows xp получается задержка 65 секунд, а в Windows 7 50сек. Помогите, пожалуйста, избавиться от такого разброса.

Код к задаче: «Ассемблер x86»

Textual

Листинг программы

ACP proc near ;процедура формирования правильного значения с АЦП push ax ;сохранение регистров, используемых в этой процедуре push dx ; mov al,01h ;включаем "Start" out 03h,al ; Waiting: in al,02h ;ждем, пока загорится "Rdy" test al,01h ; jz Waiting ; mov al,0 ;сбрасываем "Start" out 03h,al ; in al,01h ;читаем значение на регистре ввода mov ah,10d ; mov dl,0FFh ;загружаем в dl максимально число, которое можно подать на регистр ввода mul ah ;умножаем число с регистра ввода на верхний предел АЦП (макс. напряжение) div dl ;делим результат на максимально значение на регистре ввода и получаем число из диапазона 1..10 mov skor_ACP,al pop dx pop ax ret ACP endp

Первоначально эта информация была помещена на странице Пояснения к основным таблицам . Но затем было решено, что эти длинные общие рассуждения лучше вынести на отдельную страницу. Однако, после такого переноса эти рассуждения еще немножко выросли. Теперь, пожалуй, они годятся только для раздела "Разные заметки" ...

Команды ассемблера и машинные команды

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

Команда ассемблера это некоторое мнемоническое имя. Для процессоров семейства x86 это имя записывается на английском языке. Например, у команды сложения имя ADD , а у команды вычитания имя SUB .

Команда показано имя команды на языке ассемблера.

Основой машинной команды является код операции, который является просто некоторым числом. Для процессоров x86 (впрочем, и для других процессоров тоже) принято использовать шестнадцатиричные числа. (Попутно заметим, что для советских ЭЦВМ были приняты восьмеричные числа, с ними было меньше путаницы, поскольку такие числа состоят только из цифр и не содержат букв).

В таблицах настоящего справочника в колонке Код показан код операции машинной команды, а в колонке Формат показан формат машинной команды.

Можно считать, что количество разных машинных команд у данного процессора равно количеству возможных кодов операции. По формату можно узнать, из каких составных частей состоит данная машинная команда. Разные машинные команды могут иметь разный формат. Код операции машинной команды полностью определяет ее формат.

Часто одна ассемблерная команда имеет несколько разных вариантов машинных команд. Причем форматы этих машинных команд могут быть разными для разных вариантов.

Например, ассемблерная команда ADD имеет десять вариантов машинных команд с разными кодами операций. Но разных форматов оказывается меньше, только три. И каждый из этих трех форматов требует свои типы операндов при записи команды на языке ассемблера.

Здесь важно обратить внимание, что все эти десять машинных команд выполняют одну и ту же элементарную операцию, которая на языке ассемблера называется ADD.

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

Общий список всех команд процессора можно построить разными способами, выбирая разный порядок расположения команд. Основные два способа такие.

Способ (1). Взять за основу команды языка ассемблера и расположить команды по алфавиту. Тогда могут получиться вот такие таблицы. Все команды по алфавиту (кратко)

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

Конечно, возможен еще и третий способ, который обычно применяется в учебниках. Разделить все команды на группы по смыслу и изучать их по группам, начиная с более простых.

Основной байт кода операции

В системе команд x86 одного байта (256 разных комбинаций) оказалось недостаточно для кодирования всех команд. Поэтому код операции в машинной команде занимает либо один байт, либо два байта.

Если первый байт содержит код 0F , то код операции состоит из двух байтов.

Если в машинной команде код операции состоит из одного байта, то этот единственный байт и является основным байтом кода операции. И содержимое этого байта определяет, что это за операция.

Если в машинной команде код операции состоит из двух байт, то уже не первый, а второй байт будет основным и определяющим в коде операции.

В таблицах справочника, в которых показано кодирование машинных команд, основной байт кода операции обычно бывает показан дважды, сначала в колонке "Код" в виде шестнадцатиричного числа, а затем в колонке "Формат" в виде условных восьми черточек, на которых отмечены особые биты, если таковые есть в основном байте кода операции.

Основные страницы справочника

Справочник по командам процессоров x86 - главная страница (здесь карта всех страниц справочника)

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

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

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

Целевая аудитория теста Ассемблер x86

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

Структура теста по Ассемблер x86

Условно можно выделить такие тематики:

  • Общие вопросы
  • Режимы работы процессора (реальный, защищенный)
  • Инструкции процессора

Дальнейшее развитие теста по Ассемблер x86

В дальнейшем планируется добавить вопросы по не рассмотренным тематикам (FPU, работа с устройствами/портами). Также, в разработке находиться тест по среднему уровню, который вскоре будет доступен для прохождения.

Простите, у вас не найдется минутки поговорить о спасителе нашем, ассемблере? В прошлой статье мы написали наше первое hello world приложение на асме , научились его компилировать и отлаживать, а также узнали, как делать системные вызовы в Linux. Сегодня же мы познакомимся непосредственно с ассемблерными инструкциями, понятием регистров, стека и вот этого всего. Ассемблеры для архитектур x86 (a.k.a i386) и x64 (a.k.a amd64) очень похожи, в связи с чем нет смысла рассматривать их в отдельных статьях. Притом акцент я постараюсь делать на x64, попутно отмечая отличия от x86, если они есть. Далее предполагается, что вы уже знаете, например, чем стек отличается от кучи, и объяснять такие вещи не требуется.

Регистры общего назначения

Регистр — это небольшой (обычно 4 или 8 байт) кусочек памяти в процессоре с чрезвычайно большой скоростью доступа. Регистры делятся на регистры специального назначения и регистры общего назначения. Нас сейчас интересуют регистры общего назначения. Как можно догадаться по названию, программа может использовать эти регистры под свои нужды, как ей вздумается.

На x86 доступно восемь 32-х битных регистров общего назначения — eax, ebx, ecx, edx, esp, ebp, esi и edi. Регистры не имеют заданного наперед типа, то есть, они могут трактоваться как знаковые или беззнаковые целые числа, указатели, булевы значения, ASCII-коды символов, и так далее. Несмотря на то, что в теории эти регистры можно использовать как угодно, на практике обычно каждый регистр используется определенным образом. Так, esp указывает на вершину стека, ecx играет роль счетчика, а в eax записывается результат выполнения операции или процедуры. Существуют 16-и битные регистры ax, bx, cx, dx, sp, bp, si и di, представляющие собой 16 младших бит соответствующих 32-х битных регистров. Также доступны и 8-и битовые регистры ah, al, bh, bl, ch, cl, dh и dl, которые представляют собой старшие и младшие байты регистров ax, bx, cx и dx соответственно.

Рассмотрим пример. Допустим, выполняются следующие три инструкции:

(gdb) x/3i $pc
=> 0x8048074: mov $0xaabbccdd,%eax
0x8048079: mov $0xee,%al
0x804807b: mov $0x1234,%ax

Значения регистров после записи в eax значения 0 x AABBCCDD:

(gdb) p/x $eax
$1 = 0xaabbccdd
(gdb) p/x $ax
$2 = 0xccdd
(gdb) p/x $ah
$3 = 0xcc
(gdb) p/x $al
$4 = 0xdd

Значения после записи в регистр al значения 0 x EE:

(gdb) p/x $eax
$5 = 0xaabbccee
(gdb) p/x $ax
$6 = 0xccee
(gdb) p/x $ah
$7 = 0xcc
(gdb) p/x $al
$8 = 0xee

Значения регистров после записи в ax числа 0 x 1234:

(gdb) p/x $eax
$9 = 0xaabb1234
(gdb) p/x $ax
$10 = 0x1234
(gdb) p/x $ah
$11 = 0x12
(gdb) p/x $al
$12 = 0x34

Как видите, ничего сложного.

Примечание: Синтаксис GAS позволяет явно указывать размеры операндов путем использования суффиксов b (байт), w (слово, 2 байта), l (длинное слово, 4 байта), q (четверное слово, 8 байт) и некоторых других . Например, вместо команды mov $0xEE , % al можно написать movb $0xEE , % al , вместо mov $0x1234 , % ax movw $0x1234 , % ax , и так далее. В современном GAS эти суффиксы являются опциональными и я лично их не использую. Но не пугайтесь, если увидите их в чужом коде.

На x64 размер регистров был увеличен до 64-х бит. Соответствующие регистры получили название rax, rbx, и так далее. Кроме того, регистров общего назначения стало шестнадцать вместо восьми. Дополнительные регистры получили названия r8, r9, …, r15. Соответствующие им регистры, которые представляют младшие 32, 16 и 8 бит, получили название r8d, r8w, r8b, и по аналогии для регистров r9-r15. Кроме того, появились регистры, представляющие собой младшие 8 бит регистров rsi, rdi, rbp и rsp — sil, dil, bpl и spl соответственно.

Про адресацию

Как уже отмечалось, регистры могут трактоваться, как указатели на данные в памяти. Для разыменования таких указателей используется специальный синтаксис:

mov (% rsp ) , % rax

Эта запись означает «прочитай 8 байт по адресу, записанному в регистре rsp, и сохрани их в регистр rax». При запуске программы rsp указывает на вершину стека, где хранится число аргументов, переданных программе (argc), указатели на эти аргументы, а также переменные окружения и кое-какая другая информация. Таким образом, в результате выполнения приведенной выше инструкции (разумеется, при условии, что перед ней не выполнялось каких-либо других инструкций) в rax будет записано количество аргументов, с которыми была запущена программа.

В одной команде можно указывать адрес и смешение (как положительное, так и отрицательное) относительно него:

mov 8 (% rsp ) , % rax

Эта запись означает «возьми rsp, прибавь к нему 8, прочитай 8 байт по получившемуся адресу и положи их в rax». Таким образом, в rax будет записан адрес строки, представляющей собой первый аргумент программы, то есть, имя исполняемого файла.

При работе с массивами бывает удобно обращаться к элементу с определенным индексом. Соответствующий синтаксис:

# инструкция xchg меняет значения местами
xchg 16 (% rsp ,% rcx , 8 ) , % rax

Читается так: «посчитай rcx*8 + rsp + 16, и поменяй местами 8 байт (размер регистра) по получившемуся адресу и значение регистра rax». Другими словами, rsp и 16 все так же играют роль смещения, rcx играет роль индекса в массиве, а 8 — это размер элемента массива. При использовании данного синтаксиса допустимыми размерами элемента являются только 1, 2, 4 и 8. Если требуется какой-то другой размер, можно использовать инструкции умножения, бинарного сдвига и прочие, которые мы рассмотрим далее.

Наконец, следующий код тоже валиден:

Data
msg:
. ascii "Hello, world!\n"
. text

Globl _start
_start:
# обнуление rcx
xor % rcx , % rcx
mov msg(,% rcx , 8 ) , % al
mov msg, % ah

В смысле, что можно не указывать регистр со смещением или вообще какие-либо регистры. В результате выполнения этого кода в регистры al и ah будет записан ASCII-код буквы H, или 0 x 48.

В этом контексте хотелось бы упомянуть еще одну полезную ассемблерную инструкцию:

# rax:= rcx*8 + rax + 123
lea 123 (% rax ,% rcx , 8 ) , % rax

Инструкция lea очень удобна, так как позволяет сразу выполнить умножение и несколько сложений.

Fun fact! На x64 в байткоде инструкций никогда не используются 64-х битовые смещения. В отличие от x86, инструкции часто оперируют не абсолютными адресами, а адресами относительно адреса самой инструкции, что позволяет обращаться к ближайшим +/- 2 Гб оперативной памяти. Соответствующий синтаксис:

movb msg(% rip) , % al

Сравним длины опкодов «обычного» и «относительного» mov (objdump -d ):

4000b0: 8a 0c 25 e8 00 60 00 mov 0x6000e8,%cl
4000b7: 8a 05 2b 00 20 00 mov 0x20002b(%rip),%al # 0x6000e8

Как видите, «относительный» mov еще и на один байт короче! Что это за регистр такой rip мы узнаем чуть ниже.

Для записи же полного 64-х битового значения в регистр предусмотрена специальная инструкция:

movabs $0x1122334455667788 , % rax

Другими словами, процессоры x64 так же экономно кодируют инструкции, как и процессоры x86, и в наше время нет особо смысла использовать процессоры x86 в системах, имеющих пару гигабайт оперативной памяти или меньше (мобильные устройства, холодильники, микроволновки, и так далее). Скорее всего, процессоры x64 будут даже более эффективны за счет большего числа доступных регистров и большего размера этих регистров.

Арифметические операции

Рассмотрим основные арифметические операции:

# инциализируем значения регистров
mov $123 , % rax
mov $456 , % rcx

# инкремент: rax = rax + 1 = 124
inc % rax

# декремент: rax = rax - 1 = 123
dec % rax

# сложение: rax = rax + rcx = 579
add % rcx , % rax

# вычитание: rax = rax - rcx = 123
sub % rcx , % rax

# изменение знака: rcx = - rcx = -456
neg % rcx

Здесь и далее операндами могут быть не только регистры, но и участки памяти или константы. Но оба операнда не могут быть участками памяти. Это правило применимо ко всем инструкциям ассемблера x86/x64, по крайней мере, из рассмотренных в данной статье.

Пример умножения:

mov $100 , % al
mov $3 , % cl
mul % cl

В данном примере инструкция mul умножает al на cl, и сохраняет результат умножения в пару регистров al и ah. Таким образом, ax примет значение 0 x 12C или 300 в десятичной нотации. В худшем случае для сохранения результата перемножения двух N-байтовых значений может потребоваться до 2*N байт. В зависимости от размера операнда результат сохраняется в al:ah, ax:dx, eax:edx или rax:rdx. Притом в качестве множителей всегда используется первый из этих регистров и переданный инструкции аргумент.

Знаковое умножение производится точно так же при помощи инструкции imul. Кроме того, существуют варианты imul с двумя и тремя аргументами:

mov $123 , % rax
mov $456 , % rcx

# rax = rax * rcx = 56088
imul % rcx , % rax

# rcx = rax * 10 = 560880
imul $10 , % rax , % rcx

Инструкции div и idiv производят действия, обратные mul и imul. Например:

mov $0 , % rdx
mov $456 , % rax
mov $123 , % rcx

# rax = rdx:rax / rcx = 3
# rdx = rdx:rax % rcx = 87
div % rcx

Как видите, был получен результат целочисленного деления, а также остаток от деления.

Это далеко не все арифметические инструкции. Например, есть еще adc (сложение с учетом флага переноса), sbb (вычитание с учетом займа), а также соответствующие им инструкции, выставляющие и очищающие соответствующие флаги (ctc, clc), и многие другие. Но они распространены намного меньше, и потому в рамках данной статьи не рассматриваются.

Логические и битовые операции

Как уже отмечалось, особой типизации в ассемблере x86/x64 не предусмотрено. Поэтому не стоит удивляться, что в нем нет отдельных инструкций для выполнения булевых операций и отдельных для выполнения битовых операций. Вместо этого есть один набор инструкций, работающих с битами, а уж как интерпретировать результат — решает конкретная программа.

Так, например, выглядит вычисление простейшего логического выражения:

mov $0 , % rax # a = false
mov $1 , % rbx # b = true
mov $0 , % rcx # c = false

# rdx:= a || !(b && c)
mov % rcx , % rdx # rdx = c
and % rbx , % rdx # rdx &= b
not % rdx # rdx = ~ rdx
or % rax , % rdx # rdx |= a
and $1 , % rdx # rdx &= 1

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

Еще одна полезная инструкция — это xor (исключающее или). В логических выражениях xor используется нечасто, однако с его помощью часто происходит обнуление регистров. Если посмотреть на опкоды инструкций, то становится понятно, почему:

4000b3: 48 31 db xor %rbx,%rbx
4000b6: 48 ff c3 inc %rbx
4000b9: 48 c7 c3 01 00 00 00 mov $0x1,%rbx

Как видите, инструкции xor и inc кодируются всего лишь тремя байтами каждая, в то время, как делающая то же самое инструкция mov занимает целых семь байт. Каждый отдельный случай, конечно, лучше бенчмаркать отдельно, но общее эвристическое правило такое — чем короче код, тем больше его помещается в кэши процессора, тем быстрее он работает.

В данном контексте также следует вспомнить инструкции побитового сдвига, тестирования битов (bit test) и сканирования битов (bit scan):

# положим что-нибудь в регистр
movabs $0xc0de1c0ffee2beef , % rax

# сдвиг влево на 3 бита
# rax = 0x0de1c0ffee2beef0
shl $4 , % rax

# сдвиг вправо на 7 бит
# rax = 0x001bc381ffdc57dd
shr $7 , % rax

# циклический сдвиг вправо на 5 бит
# rax = 0xe800de1c0ffee2be
ror $5 , % rax

# циклический сдвиг влево на 5 бит
# rax = 0x001bc381ffdc57dd
rol $5 , % rax

# то же самое + установить бит (bit test and set)

bts $13 , % rax

# то же самое + сбросить бит (bit test and reset)
# rax = 0x001bc381ffdc57dd, CF = 1
btr $13 , % rax

# то же самое + инвертировать бит (bit test and complement)
# rax = 0x001bc381ffdc77dd, CF = 0
btc $13 , % rax

# найти самый младший ненулевой байт (bit scan forward)
# rcx = 0, ZF = 0
bsf % rax , % rcx

# найти самый старший ненулевой байт (bit scan reverse)
# rdx = 52, ZF = 0
bsr % rax , % rdx

# если все биты нулевые, ZF = 1, значение rdx неопределено
xor % rax , % rax
bsf % rax , % rdx

Еще есть битовые сдвиги со знаком (sal, sar), циклические сдвиги с флагом переноса (rcl, rcr), а также сдвиги двойной точности (shld, shrd). Но используются они не так уж часто, да и утомишься перечислять вообще все инструкции. Поэтому их изучение я оставляю вам в качестве домашнего задания.

Условные выражения и циклы

Выше несколько раз упоминались какие-то там флаги, например, флаг переноса. Под флагами понимаются биты специального регистра eflags / rflags (название на x86 и x64 соответственно). Напрямую обращаться к этому регистру при помощи инструкций mov, add и подобных нельзя, но он изменяется и используется различными инструкциями косвенно. Например, уже упомянутый флаг переноса (carry flag, CF) хранится в нулевом бите eflags / rflags и используется, например, в той же инструкции bt. Еще из часто используемых флагов можно назвать zero flag (ZF, 6-ой бит), sign flag (SF, 7-ой бит), direction flag (DF, 10-ый бит) и overflow flag (OF, 11-ый бит).

Еще из таких неявных регистров следует назвать eip / rip, хранящий адрес текущей инструкции. К нему также нельзя обращаться напрямую, но он виден в GDB вместе с eflags / rflags, если сказать info registers , и косвенно изменяется всеми инструкциям. Большинство инструкций просто увеличивают eip / rip на длину этой инструкции, но есть и исключения из этого правила. Например, инструкция jmp просто осуществляет переход по заданному адресу:

# обнуляем rax
xor % rax , % rax
jmp next
# эта инструкция будет пропущена
inc % rax
next:
inc % rax

В результате значение rax будет равно единице, так как первая инструкция inс будет пропущена. Заметьте, что адрес перехода также может быть записан в регистре:

xor % rax , % rax
mov $next, % rcx
jmp *% rcx
inc % rax
next:
inc % rax

Впрочем, на практике такого кода лучше избегать, так как он ломает предсказание переходов и потому менее эффективен.

Примечание: GAS позволяет давать меткам цифирные имена типа 1: , 2: , и так далее, и переходить к ближайшей предыдущей или следующей метке с заданным номером инструкциями вроде jmp 1b и jmp 1f . Это довольно удобно, так как иногда бывает трудно придумать меткам осмысленные имена. Подробности можно найти .

Условные переходы обычно осуществляются при помощи инструкции cmp, которая сравнивает два своих операнда и выставляет соответствующие флаги, за которой следует инструкция из семейства je, jg и подобных:

cmp % rax , % rcx

je 1f # перейти, если равны (equal)
jl 1f # перейти, если знаково меньше (less)
jb 1f # перейти, если беззнаково меньше (below)
jg 1f # перейти, если знаково больше (greater)
ja 1f # перейти, если беззнаково больше (above)

Существует также инструкции jne (перейти, если не равны), jle (перейти, если знаково меньше или равны), jna (перейти, если беззнаково не больше) и подобные. Принцип их именования, надеюсь, очевиден. Вместо je / jne часто пишут jz / jnz, так как инструкции je / jne просто проверяют значение ZF. Также есть инструкции, проверяющие другие флаги — js, jo и jp, но на практике они используются редко. Все эти инструкции вместе взятые обычно называют jcc. То есть, вместо конкретных условий пишутся две буквы «c», от «condition». можно найти хорошую сводную таблицу по всем инструкциям jcc и тому, какие флаги они проверяют.

Помимо cmp также часто используют инструкцию test:

test % rax , % rax
jz 1f # перейти, если rax == 0
js 2f # перейти, если rax < 0
1 :
# какой-то код
2 :
# какой-то еще код

Fun fact! Интересно, что cmp и test в душе являются теми же sub и and, только не изменяют своих операндов. Это знание можно использовать для одновременного выполнения sub или and и условного перехода, без дополнительных инструкций cmp или test.

Еще из инструкций, связанных с условными переходами, можно отметить следующие.

jrcxz 1f
# какой-то код
1 :

Инструкция jrcxz осуществляет переход только в том случае, если значение регистра rcx равно нулю.

cmovge % rcx , % rax

Инструкции семейства cmovcc (conditional move) работают как mov, но только при выполнении заданного условия, по аналогии с jcc.

setnz % al

Инструкции setcc присваивают однобайтовому регистру или байту в памяти значение 1, если заданное условие выполняется, и 0 иначе.

cmpxchg % rcx , (% rdx )

Сравнить rax с заданным куском памяти. Если равны, выставить ZF и сохранить по указанному адресу значение указанного регистра, в данном примере rcx. Иначе очистить ZF и загрузить значение из памяти в rax. Также оба операнда могут быть регистрами.

cmpxchg8b (% rsi )
cmpxchg16b (% rsi )

Инструкция cmpxchg8b главным образом нужна в x86. Она работает аналогично cmpxchg, только производит compare and swap сразу 8-и байт. Регистры edx:eax используются для сравнения, а регистры ecx:ebx хранят то, что мы хотим записать. Инструкция cmpxchg16b по тому же принципу производит compare and swap сразу 16-и байт на x64.

Важно! Примите во внимание, что без префикса lock все эти compare and swap инструкции не атомарны.

mov $10 , % rcx
1 :
# какой-то код
loop 1b
# loopz 1b
# loopnz 1b

Инструкция loop уменьшает значение регистра rcx на единицу, и если после этого rcx != 0 , осуществляет переход на заданную метку. Инструкции loopz и loopnz работают аналогично, только условия более сложные — (rcx != 0) && (ZF == 1) и (rcx != 0) && (ZF == 0) соответственно.

Не нужно быть семи пядей во лбу, чтобы изобразить при помощи этих инструкций конструкцию if-then-else или циклы for / while, поэтому двигаемся дальше.

«Строковые» операции

Рассмотрим следующий кусок кода:

mov $str1, % rsi
mov $str2, % edi
cld
cmpsb

В регистры rsi и rdi кладутся адреса двух строк. Командой cld очищается флаг направления (DF). Инструкция, выполняющая обратное действие, называется std. Затем в дело вступает инструкция cmpsb. Она сравнивает байты (%rsi) и (%rdi) и выставляет флаги в соответствии с результатом сравнения. Затем, если DF = 0, rsi и rdi увеличиваются на единицу (количество байт в том, что мы сравнивали), иначе — уменьшаются. Аналогичные инструкции cmpsw, cmpsl и cmpsq сравнивают слова, длинные слова и четверные слова соответственно.

Инструкции cmps интересны тем, что могут использоваться с префиксом rep, repe (repz) и repne (repnz). Например:

mov $str1, % rsi
mov $str2, % edi
mov $len, % rcx
cld
repe cmpsb
jne not_equal

Префикс rep повторяет инструкцию заданное в регистре rcx количество раз. Префиксы repz и repnz делают то же самое, но только после каждого выполнения инструкции дополнительно проверяется ZF. Цикл прерывается, если ZF = 0 в случае c repz и если ZF = 1 в случае с repnz. Таким образом, приведенный выше код проверяет равенство двух буферов одинакового размера.

Аналогичные инструкции movs перекладывает данные из буфера, адрес которого указан в rsi, в буфер, адрес которого указан в rdi (легко запомнить — rsi значит source, rdi значит destination). Инструкции stos заполняет буфер по адресу из регистра rdi байтами из регистра rax (или eax, или ax, или al, в зависимости от конкретной инструкции). Инструкции lods делают обратное действие — копируют байты по указанному в rsi адресу в регистр rax. Наконец, инструкции scas ищут байты из регистра rax (или соответствующих регистров меньшего размера) в буфере, адрес которого указан в rdi. Как и cmps, все эти инструкции работают с префиксами rep, repz и repnz.

На базе этих инструкций легко реализуются процедуры memcmp, memcpy, strcmp и подобные. Интересно, что, например, для обнуления памяти инженеры Intel рекомендуют использовать на современных процессорах rep stosb , то есть, обнулять побайтово, а не, скажем, четверными словами.

Работа со стеком и процедуры

Со стеком все очень просто. Инструкция push кладет свой аргумент на стек, а инструкция pop извлекает значение со стека. Например, если временно забыть про инструкцию xchg, то поменять местами значение двух регистров можно так:

push % rax
mov % rcx , % rax
pop % rcx

Существуют инструкции, помещающие на стек и извлекающие с него регистр rflags / eflags:

pushf
# делаем что-то, что меняет флаги
popf
# флаги восстановлены, самое время сделать jcc

А так, к примеру, можно получить значение флага CF:

pushf
pop % rax
and $1 , % rax

На x86 также существуют инструкции pusha и popa, сохраняющие на стеке и восстанавливающие с него значения всех регистров. В x64 этих инструкций больше нет. Видимо, потому что регистров стало больше и сами регистры теперь длиннее — сохранять и восстанавливать их все стало сильно дороже.

Процедуры, как правило, «создаются» при помощи инструкций call и ret. Инструкция call кладет на стек адрес следующей инструкции и передает управление по указанному в аргументе адресу. Инструкция ret читает со стека адрес возврата и передает по нему управление. Например:

someproc:
# типичный пролог процедуры
# для примера выделяем 0x10 байт на стеке под локальные переменные
# rbp - указатель на фрейм стека
push % rbp
mov % rsp , % rbp
sub $0x10 , % rsp

# тут типа какие-то вычисления...
mov $1 , % rax

# типичный эпилог процедуры
add $0x10 , % rsp
pop % rbp

# выход из процедуры
ret

Start:
# как и в случае с jmp, адрес перехода может быть в регистре
call someproc
test % rax , % rax
jnz error

Примечание: Аналогичный пролог и эпилог можно написать при помощи инструкций enter $0x10 , $0 и leave . Но в наше время эти инструкции используются редко, так как они выполняются медленнее из-за дополнительной поддержки вложенных процедур.

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

I] ;
}
return hash;
}

Дизассемблерный листинг (при компиляции с -O0 , комментарии мои):

# типичный пролог процедуры
# регистр rsp не изменяется, так как процедура не вызывает никаких
# других процедур
400950: 55 push %rbp
400951: 48 89 e5 mov %rsp,%rbp

# инициализация локальных переменных:
# -0x08(%rbp) - const unsigned char *data (8 байт)
# -0x10(%rbp) - const size_t data_len (8 байт)
# -0x14(%rbp) - unsigned int hash (4 байта)
# -0x18(%rbp) - int i (4 байта)
400954: 48 89 7d f8 mov %rdi,-0x8(%rbp)
400958: 48 89 75 f0 mov %rsi,-0x10(%rbp)
40095c: c7 45 ec 4b 43 41 48 movl $0x4841434b,-0x14(%rbp)
400963: c7 45 e8 00 00 00 00 movl $0x0,-0x18(%rbp)

# rax:= i. если достигли data_len, выходим из цикла
40096a: 48 63 45 e8 movslq -0x18(%rbp),%rax
40096e: 48 3b 45 f0 cmp -0x10(%rbp),%rax
400972: 0f 83 28 00 00 00 jae 4009a0

# eax:= (hash << 5) + hash
400978: 8b 45 ec mov -0x14(%rbp),%eax
40097b: c1 e0 05 shl $0x5,%eax
40097e: 03 45 ec add -0x14(%rbp),%eax

# eax += data[i]
400981: 48 63 4d e8 movslq -0x18(%rbp),%rcx
400985: 48 8b 55 f8 mov -0x8(%rbp),%rdx
400989: 0f b6 34 0a movzbl (%rdx,%rcx,1),%esi
40098d: 01 f0 add %esi,%eax

# hash:= eax
40098f: 89 45 ec mov %eax,-0x14(%rbp)

# i++ и перейти к началу цикла
400992: 8b 45 e8 mov -0x18(%rbp),%eax
400995: 83 c0 01 add $0x1,%eax
400998: 89 45 e8 mov %eax,-0x18(%rbp)
40099b: e9 ca ff ff ff jmpq 40096a

# возвращаемое значение (hash) кладется в регистр eax
4009a0: 8b 45 ec mov -0x14(%rbp),%eax

# типичный эпилог
4009a3: 5d pop %rbp
4009a4: c3 retq

Здесь мы встретили две новые инструкции — movs и movz. Они работают точно так же, как mov, только расширяют один операнд до размера второго, знаково и беззнаково соответственно. Например, инструкция movzbl (%rdx,%rcx,1),%esi читайт байт (b) по адресу (%rdx,%rcx,1) , расширяет его в длинное слово (l) путем добавления в начало нулей (z) и кладет результат в регистр esi.

Как видите, два аргумента были переданы процедуре через регистры rdi и rsi. По всей видимости, используется конвенция под названием System V AMD64 ABI . Утверждается, что это стандарт де-факто под x64 на *nix системах. Я не вижу смысла пересказывать описание этой конвенции здесь, заинтересованные читатели могут ознакомиться с полным описанием по приведенной ссылке.

Заключение

Само собой разумеется, в рамках одной статьи, описать весь ассемблер x86/x64 не представляется возможным (более того, я не уверен, что сам знаю его прямо таки весь ). Как минимум, за кадром остались такие темы, как операции над числами с плавающей точкой, MMX-, SSE- и AVX-инструкции, а также всякие экзотические инструкции вроде lidt, lgdt, bswap , rdtsc, cpuid, movbe, xlatb, или prefetch. Я постараюсь осветить их в следующих статьях, но ничего не обещаю. Следует также отметить, что в выводе objdump -d для большинства реальных программ вы очень редко увидите что-то помимо описанного выше.

Еще интересный топик, оставшийся за кадром — это атомарные операции, барьеры памяти, спинлоки и вот это все. Например, compare and swap часто реализуется просто как инструкция cmpxchg с префиксом lock . По аналогии реализуется атомарный инкремент, декремент, и прочее. Увы, все это тянет на тему для отдельной статьи.

В качестве источников дополнительной информации можно рекомендовать книгу Modern X86 Assembly Language Programming , и, конечно же, мануалы от Intel . Также довольно неплоха книга x86 Assembly на wikibooks.org.

Из онлайн-справочников по ассемблерным инструкциям стоит обратить внимание на следующие:

А знаете ли вы ассемблер, и если да, то находите ли это знание полезным?