Ассемблер js: 🤷🏽‍♀️ Page not found | MDN Web Docs

Содержание

что и как / Хабр

Эта статья основана на моём выступлении на конференции ITSubbotnik, прошедшем 2 ноября 2019 года в Москве.

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

Проблема

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

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

  • Zero configuration — это должно быть решение «из коробки», ничего кроме браузера.
  • Безопасно — новая технология не должна создавать новых угроз, у нас и так есть чем заняться по части безопасности.
  • Кросс-платформенно — браузеры работают на всех основных процессорах, включая мобильные платформы.
  • Удобно для разработчиков — то есть для нас с вами.

История до Wasm

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

Победители: это, безусловно, JavaScript; движок V8, сделавший JS таким быстрым; а также HTML5.

Проигравшие: ActiveX — если вы помните, эта технология позволяла делать с машиной вообще всё что угодно, т.е. с безопасностью было очень плохо; Flash — сейчас мы наблюдаем эпоху заката Flash, хотя внутри него работает ActionScript, по сути, тот же JavaScript; Silverlight — наверное, он появился слишком поздно, чтобы занять серьёзную нишу.

В итоге, проиграли все плагины, в том числе из-за проблем с безопасностью.

Были и другие попытки решения проблемы, уже в браузере:

  • NaCl — Native Client, предложенный Google; родной код прямо в браузере, что конечно ударяет по кросс-платформенности; Mozilla не поддержала эту инициативу, в итоге реализация была только в Chrome.
  • PNaCl — Portable Native Client — в качестве переносимого кода использовалось подмножество LLVM IR; также не было поддержано в Mozilla, опять же только Chrome. В итоге, Google отказалась от поддержки PNaCl в мае 2017 года.

asm.js

asm.js — ещё одна интересная инициатива, уже от Mozilla Foundation, которая подводит нас вплотную к теме WebAssembly. Появилась она в 2010 году, а в 2013 стала публично доступна.

asm.js это подмножество JavaScript, в него можно компилировать код из C и C++ с помощью компилятора Emscripten.

Поскольку это тоже JavaScript, то такой код будет исполняться в любом браузере. Кроме того, основные современные браузеры уже давно умеют быстро распознавать asm.js и эффективно компилировать его в родной код процессора. В сравнении с родным кодом, полученным непосредственно из C/C++, код полученный из asm.js медленнее всего в 1,5-2 раза (50-67 %).

Слева здесь код простейшей функции на C/C++, справа показано во что она превращается после компиляции в asm.js. Во-первых, мы здесь видим строку 'use asm', это маркер, дающий понять, что дальше идёт код на asm.js. И в этом коде мы видим конструкции вида |0, это побитовая операция ИЛИ с нулевым значением. Согласно спецификации, результатом этой операции является 32-разрядное целое со знаком. Но такого типа даже нет в JavaScript. Тем не менее, такой тип возникает в результате этой операции, т.е. это по сути это приведение значения к заданному типу.

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

Что же такое WebAssembly

WebAssembly (или Wasm) — это бинарный формат, запускаемый в браузере, виртуальная машина, и результат компиляции с языка высокого уровня.

Wasm это не язык программирования, подобно тому как байт-код Java это не язык программирования, а результат компиляции и запускаемый блок кода.

Кто-то очень умный сказал, что название web assembly (то есть «ассемблер для веба») полностью неправильное, потому что это не ассемблер (не язык программирования) и он никак не связан с вебом (потому что это просто виртуальная машина).

Создатели WebAssembly руководствовались следующими целями и ограничениями — см. видео Evolving Wasm into a proper misnomer: Andreas Rossberg.

По сути, они сводятся к трём вещам — кросс-платформенность, компактность, скорость. Но было ещё одно важное требование, это «продаваемость» — инициативу должны были воспринять и подхватить разработчики основных браузеров. В итоге это удалось «продать», в разработке спецификации прияли участие представители Google, Mozilla, Microsoft и Apple.

Посмотрим, что представляет из себя Wasm как виртуальная машина.

Это такой «выдуманный процессор», но без регистров, всё делается через стек. Всего четыре типа данных: два целых, два плавающих. Относительно простой набор операций — см. спецификацию и интерактивную таблицу.

Плоская модель памяти: под память выделяется единый блок, размер которого кратен 64 КБ. Здесь находится код, данные, константы, глобальные переменные, стек растущий вниз, куча растущая вверх. Можно сделать так, чтобы куча автоматически увеличивалась при необходимости, при этом блок памяти расширяется на размер кратный 64 КБ.

Указатели не используется (это сделано для безопасности), вместо этого используется индекс. Индекс 32-разрядный, поэтому адресуется до 4 ГБ памяти.

Вся память WebAssembly полностью доступна из JavaScript, причём как на чтение, так и на запись.

Посмотрим на модель исполнения WebAssembly. Wasm всегда загружается и вызывается ТОЛЬКО из JavaScript. Более того, JS и Wasm работают в одной и той же «песочнице», и исполняются одним и тем же движком.

Заметим, что из Wasm также можно вызывать JS. Это может быть вызов функции с передачей аргументов и возвратом значения, либо это может быть просто выполнение произвольной строки как JS-кода.

Пробуем WebAssembly

Для того чтобы освоиться с WebAssembly, я рекомендую воспользоваться сайтом WasmFiddle или WebAssembly Studio — это простой и наглядный способ понять для себя, что такое Wasm на самом деле.

Здесь слева вверху исходный код на C/C++ (в данном случае, рекурсивная функция расчёта числа Фибоначчи), справа вверху код на JS для загрузки Wasm, инстанциирования Wasm-модуля и вызова из него функции fib(). Когда мы нажимаем кнопку Build, то получаем блок кода Wasm (.wasm файл), который мы всегда можем развернуть в текстовое представление (.wat файл).

Это текстовое представление с кучей скобочек — по сути, абстрактное синтаксическое дерево (AST). Ну и собственно скобочной записью это напоминает язык Lisp. По идее, мы можем редактировать код в виде текстового представления, и затем свернуть его вновь в бинарный формат — это напоминает программирование на языке ассемблера. Но обычно мы получаем бинарный Wasm в результате компиляции с языка высокого уровня.

2017 год: Production Ready

В ноябре 2017 года WebAssembly был объявлен «готовым к использованию в продакшене». Спецификация на все основные части Wasm была подготовлена, вышла реализация Wasm во всех основных браузерах. Тем самым, для WebAssembly был «выпущен» MVP — Minimum Viable Product, версия 1.0, с которой мы и имеем дело сейчас.

Поддержка в браузерах

В конце 2017 года были выпущены релизы всех основных браузеров с поддержкой WebAssembly:

Исключение — IE11, для него поддержки нет, и по всей видимости, уже не будет. Предполагалось, что для старых браузеров будет polyfill — возможность преобразования Wasm в asm.js; такие прототипы есть, но насколько я видел, эти проекты заброшены, видимо, сообществу не до них.

Сейчас среди всех установленных браузеров, ~88% поддерживают Wasm.

Поддержка языков

Для того, чтобы ваш любимый язык компилировался в Wasm, нужно чтобы компилятор обеспечивал такую цель компиляции. Сейчас уже довольно много языков поддерживают Wasm, и их становится всё больше с каждым месяцем. См. appcypher/awesome-wasm-langs.

  • C/C++ — через Emscripten, очень хорошая поддержка.
  • Rust — поддержка Wasm появилась довольно давно, экосистема вокруг Wasm строится во многом на основе Rust.
  • Java — через TeaVM, JWebAssembly, Bytecoder — на экспериментальном уровне.
  • Kotlin — есть поддержка в Kotlin/Native через LLVM backend — экспериментальная.
  • Go
  • C# — через Blazor (mono) и в будущем на Uno Platform.
  • TypeScript — через AssemblyScript.

Не так давно появилась новость, что LLVM теперь поддерживает Wasm как цель компиляции. Это облегчает работу для разработчиков языков программирования, мы получим ещё больше языков, компилирующих в Wasm.

Сценарии использования

Наверное, это один из главных вопросов — «А как я могу использовать Wasm?»

Уже сейчас WebAssembly активно применяется:

  • Игры, игровые движки, движки физики, VR/AR — например, Godot, Doom 3
  • Эмуляторы, виртуальные машины — например, DOSBox
  • Графические/3D редакторы — Figma, AutoCAD
  • Веб-клиенты для финансовых торговых площадок — я видел презентации про два таких клиента
  • Кодеки и фильтры аудио/видео — например, ffmpeg
  • Базы данных — например, sqlite

Другие возможные сценарии:

  • Progressive web applications (PWA)
  • Распознавание натренированной нейронной сетью

Производительность

Производительность Wasm была одним из его главных «продающих» факторов, но что с ней происходит на самом деле?

В сравнении с JavaScript, получается, что в среднем Wasm быстрее, но в каждом частном случае нужно делать сравнение JS/Wasm, потому что может получиться и во много раз лучше, и в несколько раз хуже. Также это может сильно зависеть от используемого браузера.

На самом деле, пиковая производительность JS и Wasm одинакова, поскольку оба в итоге превращаются в родной код процессора. Но JS гораздо легче теряет в производительности, а Wasm обеспечивает более «ровный» подход.

Как правило, Wasm хорошо показывает себя на объёмных вычислениях. Там где много операций с памятью, Wasm проигрывает. Ну и основная проблема в реальных применениях — это медленный интероп JS <-> Wasm. См. например, бенчмарк.

В июле 2019 года вышла научная статья «Not So Fast: Analyzing the Performance of WebAssembly vs. Native Code». Авторы реализовали возможность запуска под WebAssembly консольных утилит Linux, для запуска бенчмарков, и использовали бенчмарки SPEC для оценки производительности Wasm по сравнению с теми же тестами на asm.js и на родном коде.

Результаты такие:

  • Wasm на 30% быстрее, чем JavaScript (в среднем, на этих тестах)
  • Wasm на 50% медленнее, чем родной код (в среднем, на этих тестах)

Авторы статьи также дали анализ причин, на чём именно Wasm «подтормаживает»:

  • примерно вдвое больше операций загрузки/сохранения данных, по сравнению с родным кодом;
  • больше ветвлений — вызвано необходимостью дополнительных проверок при обращении к памяти;
  • больше «промахов» мимо кеша L1.

В общем, на самом деле, с производительностью всё не так плохо. К тому же, этот анализ позволит разработчикам браузеров сделать Wasm ещё быстрее.

В будущем нас ожидает ускорение Wasm не только за счёт лучшей оптимизации в браузерах, но и за счёт новых фич, таких как: блоковые операции над памятью, поддержка SIMD-инструкций, поддержка threads.

Что будет дальше?

Как развивается WebAssembly?

Во-первых, группа, работающая над спецификациями для Wasm, продолжает свою работу. Спецификации находятся на разных этапах (фазах), есть определённая «дорожная карта» этой работы.

В частности, в ближайшее время мы ожидаем дальше увидеть такие фичи:

  • Non-trapping float-to-int conversions — сейчас конвертация из плавающего значения в целое в некоторых условиях может вызвать исключение; для программиста это довольно неожиданно, поэтому все такие конвертации приходится оборачивать, теряя на этом производительность. Эта фича решает проблему.
  • Multi-value — возможность возврата больше одного значения из функции, возможность создания новых инструкций, возвращающих более одного значения — например, результат деления и остаток от деления, или результат сложения и бит переноса.
  • Reference Types — введение типа anyref, обозначающего «ссылка на что-то в куче JS», первый шаг к тому чтобы ускорить взаимодействие с JS.

Во-вторых, разработчики браузеров, со своей стороны, реализуют эти спецификации, т.е. постепенно к Wasm добавляются новые фичи, сначала скрытые «под флагом» в настройках, а затем и включенные по умолчанию.

Вот, например, список фич Chrome, относящихся к WebAssembly.

Для Firefox подобный список можно найти здесь.

WebAssembly вне браузера

Как было сказано выше, Wasm по сути никак не связан с вебом, это просто виртуальная машина. А значит, его вполне можно использовать и вне веба.

Сейчас видны несколько сценариев использования Wasm вне браузера:

  • Node.js — в основе Node.js лежит движок V8, который поддерживает Wasm.
  • Отдельное консольное приложение, код приложения исполняется в Wasm VM — примеры таких runtime: wasmtime, wasmer.
  • Wasm VM используется как библиотека из других языков — например, wasmer позволяет вызывать себя из десятка различных языков.

Для Wasm работающего вне браузера, уже не нужны ограничения «песочницы», напротив, необходим доступ к функциям системы — файловая система и файлы, консольный ввод/вывод и т.д. Это привело к созданию WebAssembly System Interface (WASI) — спецификации кросс-платформенного API, подобного POSIX. См. WebAssembly/WASI и wasi.dev.

Следующим шагом стало создание менеджера пакетов — Wasm Package Manager (WAPM) — wapm.io. Здесь вы можете взять готовый .wasm-файл и использовать его в своём приложении. Обычно тут речь идёт о Wasm-версиях каких-то известных библиотек. Часть пакетов помечено тэгом «WASI», что означает — их можно использовать только в сценариях работы вне браузера.

Заключение

Итак, WebAssembly вполне можно использовать, он уже два года как «production ready».

Применение Wasm вполне может дать некоторое ускорение, по сравнению с аналогичным кодом на JavaScript, но всегда нужно проверять, получился ли прирост скорости.

Поддержка Wasm со стороны языков программирования постоянно развивается.

Ну и самое главное, WebAssembly несколько «изменил ландшафт» веба — предоставил нам новые сценарии использования, которые мы можем реализовать в своих приложениях.

Ссылки

Awesome списки:

Видео:

Ассемблер для Веб. Яваскрипт в отставку?

Вот уже неделю западная айтишная пресса и форумы не могут успокоиться, обсуждая занятную новинку под названием WebAssembly. И «гудят» не столько из-за перспектив, сколько из-за невозможности сойтись во мнении — что же это на самом деле такое и как должно быть построено. Масла в огонь подливает состав участников разработки, включающий Mozilla, Google, Microsoft и Apple. Так что сомнений в том, что штука получится мощная, нет. Сложности оттого, что Сеть такой технологии ещё не знала. Были попытки реализовать нечто подобное, но все они или заброшены или ограничились узкими нишами, то есть универсальными не стали. А универсальность тут совершенно необходима. Впрочем, довольно загадок, давайте разберёмся что же это за зверь.

Если окинуть мысленным взором историю браузеров как класса, станет очевидна простая вещь: это не столько хаотичный взрыв технологий, сколько попытка уподобить веб-обозреватель классической вычислительной машине. Сначала интуитивно, позже осознанно, разработчики браузеров учили свои детища исполнять приложения и работать с информацией так же, как это делает любая офлайновая компьютерная программа. Отсюда потребность в языке программирования, который стал бы фундаментом для приложений Веб. Java, Adobe Flash, Silverlight, Javascript, NaCl — всё это, в общем, одного поля ягоды. Но никто из них задачу в полном объёме не решил.

Недостатки известны. Что-то требует подключения плагинов и исполняет веб-приложение вне периметра безопасности браузера. Что-то несвободно, то есть по определению ограничено рамками одной операционной системы. Что-то требует внесения серьёзных изменений в браузерную архитектуру. На этом фоне, конечно, выделяется Javascript — максимально приблизившийся к идеальному решению. Он всеми признан и всеми поддерживается. Но и он не без греха.

Дело в том, что требования к браузерному языку со временем изменились: уже в нулевые веб-дизайнерам стало недостаточно только лишь иметь свой язык программирования, им понадобился язык-посредник, на который можно «переводить» более сложные приложения, написанные на классических языках вроде C++. Javascript по мере сил и возможностей роль такого посредника выполняет, его даже модифицировали, чтобы у него это получалось лучше (см. конструктор asm.js). Однако изначально он для такого посредничества не предназначался — и это проявляется в работе: парсинг (чтение, расшифровка JS-программы) на мобильных устройствах отнимает немало времени и энергии.

А представьте, как здорово было бы заиметь абсолютно универсальный язык-посредник для веб-приложений! Требования к нему, впрочем, кажутся невыполнимыми. Во-первых, это должен быть не просто ещё один язык программирования, а такой, на который легко переводить программы с любых других языков — хоть с C, хоть с Python, хоть с того же Javascript. Во-вторых, предназначаться в первую очередь он должен не для человека, а для машины, а потому записываться не текстом, а байт-кодом (компактней, быстрее читается компьютером и легче транслируется в машинный код для исполнения). Наконец, в-третьих, он должен пониматься любым браузером на любой платформе. Иначе говоря, необходимо заставить разработчиков всех браузеров трудиться сообща — и это кажется едва не самым сложным!

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

Уговаривать вендоров не понадобилось: они сами пришли к пониманию необходимости совместного труда — движимые нуждой, ибо Javascript уже явно недостаточен. В настоящий момент wasm пересекается только с одним языком, а именно всё с тем же Javascript (это облегчит внедрение поддержки wasm на начальном этапе), но в перспективе, как задумывается, перевести на него можно будет программу с любого языка. Главное его преимущество перед яваскриптом — в скорости: wasm-программа быстрее передаётся по сети (ведь она короче: байт-код!), в десятки раз быстрее читается и преобразуется в машинный код, а кроме того, возможно, будет и быстрей исполняться (wasm ведь не ограничен устаревшими конструкциями, как Javascript; он ближе к ассемблеру, чем к языкам высокого уровня).

В то же время wasm призван не заменить Javascript, а избавить его от задач, для решения которых тот не предназначался. Отец яваскрипта Брендан Айк — несмотря на недавние памятные события (см. «Месть геев»), он каким-то чудом в разработке WebAssembly участвует — так вот Айк уверен, что яваскрипт не исчезнет. Просто каждый язык пойдёт своим путём. Javascript возьмёт на себя задачи, не требующие больших вычислительных затрат, а wasm станет именно посредником: писать на нём не будут, в него будут переводить сложные программы с других высокоуровневых языков. В результате станет практически возможным писать эффективные веб-приложения на любом языке программирования, а среднестатистическая производительность таких приложений вырастет.

Неунывающий Брендан Айк по-прежнему рекомендует в любой непонятной ситуации ставить на Javascript!

Работа над WebAssembly сконцентрирована сейчас в открытой группе при W3C, подключиться к которой может каждый желающий (кстати, приятный сюрприз: там много русских имён). Естественно, ни о какой стандартизации пока и речи нет, всё сшито на живую нитку (есть сырой FAQ). Но удастся ли вообще довести wasm до стадии стандарта? Ведь вендоры неизбежно станут тянуть одеяло каждый на себя, как делали это всегда. Впрочем, оглядываясь на успехи Javascript — который худо-бедно, не без оговорок, всё-таки универсален — можно надеяться, что и wasm достигнет по крайней мере того же уровня совместимости.

Как много времени это займёт? Айк считает, что через несколько лет все топовые браузеры обзаведутся поддержкой WebAssembly. И уже сейчас советует делать ставку не только на Javascript (его обычный совет последние лет пятнадцать), но и на wasm. Имхо, стоит прислушаться.

P.S. В статье использованы иллюстрации Charis Tsevis, Дмитрия Барановского, ModernWeb2015.

Управление ходом программы

Короткие условные переходы

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

Команды перехода, проверяющие одиночный флаг

Команда Описание Условие Обратная команда
JZ , JE Переход, если «равно» («нуль»). Т.е. если сравниваемые значения равны, то ZF = 1 и переход выполняется  ZF = 1 JNZ, JNE
JC , JB, JNAE Переход, если есть перенос («ниже», «не выше или равно»).  CF = 1 JNC, JNB, JAE
JS Переход по знаку.  SF = 1 JNS
JO Переход по переполнению.  OF = 1 JNO
JPE, JP Переход, если есть паритет или паритет четный.  PF = 1 JPO
JNZ , JNE Переход по «не равно» или по «не нуль»  ZF = 0 JZ, JE
JNC , JNB, JAE Переход, если нет переноса («выше или равно» или «не ниже»).  CF = 0 JC, JB, JNAE
JNS Переход, если нет знака.  SF = 0 JS
JNO Переход, если нет переполнения.  OF = 0 JO
JPO, JNP Переход, если нет паритета или паритет нечетный.  PF = 0 JPE, JP

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

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

Команда Описание Условие Обратная команда
JE , JZ Переход, если «равно» (=).
переход, если «ноль».
ZF = 1 JNE, JNZ
JNE , JNZ Переход, если «не равно» ().
Переход, если «не ноль».
ZF = 0 JE, JZ
JG , JNLE Переход, если «больше» (>).
Переход, если «не меньше или равно» (not <=).
ZF = 0
and
SF = OF
JNG, JLE
JL , JNGE Переход, если «меньше» ().
Переход, если «не больше или равно» (not >=).
SF <> OF JNL, JGE
JGE , JNL Переход, если «больше или равно» (>=).
Переход, если «не меньше» (not <).
SF = OF JNGE, JL
JLE , JNG Переход, если «меньше или равно» (<=).
Переход, если «не больше» (not >).
ZF = 1
or
SF OF
JNLE, JG

<> — этот знак означает «не равно».

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

Команда Описание Условие Обратная команда
JE , JZ Переход, если «равно» (=).
Переход, если «ноль».
ZF = 1 JNE, JNZ
JNE , JNZ Переход, если «не равно» ().
Переход, если «не ноль».
ZF = 0 JE, JZ
JA , JNBE Переход, если «выше» (>).
Переход, если «не ниже или равно» (not <=).
CF = 0
and
ZF = 0
JNA, JBE
JB , JNAE, JC Переход, если «ниже» (<).
Переход, если «не выше или равно» (not >=).
Переход по переносу.
CF = 1 JNB, JAE, JNC
JAE , JNB, JNC Переход, если «выше или равно» (>=).
Переход, если «не ниже» (not <).
Переход, если «нет переноса».
CF = 0 JNAE, JB
JBE , JNA Переход, если «ниже или равно» (<=).
Переход, если «не выше» (not >).
CF = 1
or
ZF = 1
JNBE, JA

Обычно, если требуется сравнить два числовых значения, то
используют команду CMP (она делает то же самое, что и команда
SUB (вычитание), но не сохраняет результат, а влияет
только на флаги.

Логика очень простая, например:

требуется сравнить числа
5 и 2,

5 — 2 = 3

результат — НЕ НОЛЬ (Флаг Нуля — Zero Flag (ZF) установлен в 0).

Другой пример:

требуется сравнить 7 и
7,

7 — 7 = 0

результат — НОЛЬ! (Флаг Нуля — Zero Flag (ZF) установлен в
1 и команды JZ или JE выполнят переход).

Ниже приведен пример команды CMP и условного перехода:

include emu8086.inc

ORG    100h

MOV    AL, 25     ; записать в AL число 25.
MOV    BL, 10     ; записать в BL число 10.

CMP    AL, BL     ; сравнить AL с BL.

JE     equal      ; если AL = BL (ZF = 1), то перейти к метке equal.

PUTC   'N'        ; иначе, если AL <> BL, то продолжить выполнение
JMP    stop       ; программы - напечатать 'N' и перейти к метке stop.

equal:            ; если программа на этой метке,
PUTC   'Y'        ; то AL = BL, поэтому выводим на экран 'Y'.

stop:

RET               ; сюда приходим в любом случае

END

Попробуйте вышеописанный пример с различными числами в AL и BL,
откройте флаги, щелкнув по кнопке [FLAGS]. Используйте
[Single Step — пошаговый режим] и наблюдайте за происходящим.
Не забывайте перекомпилировать и перезагружать вашу программу после
кажого сделанного вами в ней изменения (используйте клавишу F5).


Все условные переходы имеют одно серьезное ограничение — в отличие от
команды JMP, они могут выполнять переход только на 127
байтов вперед или на 128 байтов назад (учтите, что большие
команды ассемблируются в 3 и более байтов).

Мы можем легко преодолеть это ограничение, используя следующий метод:

  • Взять обратную команду из приведенной выше таблицы
    и выполнить переход к метке label_x.
  • Использовать команду JMP для перехода
    к нужному участку программы.
  • Определить метку label_x: только после
    команды JMP.

label_x: — может быть любым именем.

Пример:

include emu8086.inc

ORG    100h

MOV    AL, 25     ; записать в AL число 25.
MOV    BL, 10     ; записать в BL число 10.

CMP    AL, BL     ; сравнить AL с BL.



JNE    not_equal  ; переход, если AL <> BL (ZF = 0).
JMP    equal
not_equal:


; представим, что здесь у нас
; размещается код, который ассмеблируется
; более чем в 127 байтов...


PUTC   'N'        ; если мы оказались здесь, то AL <> BL,
JMP    stop       ; тогда печатаем 'N', и переходим к метке stop.

equal:            ; если мы оказались здесь,
PUTC   'Y'        ; то AL = BL, тогда печатаем 'Y'.

stop:

RET               ; сюда приходим в любом случае.

END

Инструкция JB







ТВ онлайн: более 100 каналов в HD-качестве

1) Большинство каналов бесплатные. 2) Можно смотреть как на сайте, так и в приложениях для iOS, Android и Smart TV. 3) Множество телесериалов и фильмов бесплатно или за 1 рубль. 4) Несколько платных подписок, в том числе 18+ и телеканал «Дождь». 5) А также записи телепередач, новости, ТВ-шоу и др.
Подробнее…




Инструкция JB выполняет короткий переход, если первый операнд МЕНЬШЕ
второго операнда при выполнении операции сравнения с помощью
команды CMP.


Синтаксис команды JB:


JB МЕТКА


О метках я рассказывал в статье о
команде JMP. Повторяться не буду.


Инструкция JB проверяет флаг CF. Если этот флаг равен 1, то выполняется переход к МЕТКЕ.


Сама же инструкция JB при работе никакие флаги не изменяет.


Условный переход в Ассемблере


В упомянутой выше статье о команде JMP рассказано о безусловном переходе. Условный переход в Ассемблере можно выполнить с помощью нескольких инструкций, в том числе с помощью команды JB.


Но прежде чем перейти к примерам, для новичков расскажу о том, что такое условный переход.


Итак, условный переход в Ассемблере выполняется примерно по такому алгоритму:

  • Если УСЛОВИЕ ВЫПОЛНЯЕТСЯ, то перейти к МЕТКЕ
  • Если УСЛОВИЕ НЕ ВЫПОЛНЯЕТСЯ, то выполнить следующую инструкцию


Если говорить непосредственно об инструкции JB, то условием перехода к метке является CF=1 (то есть когда первый операнд меньше второго), а алгоритм её использования может быть таким:

  • Сравнить ЧИСЛО1 с ЧИСЛОМ2 с помощью команды CMP
  • УСЛОВИЕ: ЧИСЛО1
  • Выполнить инструкцию JB


Пример использования команды JB приведён ниже:


	.model	tiny
	.code
	ORG 	100h
	
start:	

	MOV AL, 5      ; AL = 5
	MOV AH, 5      ; AH = 5
	CMP AL, AH     ; AL = AH, ZF = 1
	JB  lblLess
	;Так как AL = AH, то УСЛОВИЕ ПЕРЕХОДА НЕ ВЫПОЛНЯЕТСЯ
	;Поэтому будут выполнены следующие инструкции
	MOV AL, 5      ; AL = 5
	MOV AH, 6      ; AH = 6
	CMP AL, AH     ; AL JB  lblLess
	;Так как AL lblLess:	
	MOV AH, 9      ; AH = 9

	END	start


В комментариях всё подробно расписано, поэтому что-то ещё добавлять нет смысла.


В конце попробую предположить, почему эта команда ассемблера называется JB.
Честно говоря, точно не знаю. Первая буква — это первая буква английского слова JUMP,
которое переводится как “прыжок, переход”. А вот что означает вторая буква — это
вопрос. Можно только предположить, что это тоже первая буква какого-то английского
слова, например, BOTTOM (низкий). То есть название этой команды можно вольно перевести как “переход, если ниже (меньше)”.





Первые шаги в программирование

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


WebAssembly: Google, Microsoft и Mozilla запустят новый бинарный формат для веб

Google, Microsoft, Mozilla и инженеры проекта WebKit 17 июня анонсировали новый совместный проект по запуску нового бинарного формата для компилирования веб-приложений WebAssembly, пишет «Хабр» со ссылкой на TechCrunch.

Читать далее

Веб развивается благодаря стандартам, и, плохо это или хорошо, JavaScript один из них. Однако на протяжении многих лет мы видели много попыток обойти ограничения языка, например, создание компиляторов, которые транслируют код из других языков в JavaScript. Некоторые из этих проектов фокусируются на добавлении новых возможностей в язык (например, TypeScript от Microsoft) или ускорении JavaScript (например, Mozilla asm.js). Сейчас многие из этих проектов объединяются в том или ином виде в WebAssembly.

Новый формат позволяет программистам компилировать их код для браузера (на текущий момент разработчики сфокусировались на C/C++, другие языки будут добавлены позже). Этот скомплированный код в дальнейшем исполняется внутри движка JavaScript. Вместо того, чтобы парсить исходный код, что все-таки часто занимает длительное время (особенно на мобильных устройствах), WebAssembly может быть декодирован значительно быстрее.

Идея состоит в том, что WebAssembly предоставит разработчикам единый способ компиляции, который в конечном счёте станет веб-стандартом, реализованным во всех браузерах.

Файлы JavaScript — это простые текстовые файлы, которые скачиваются с сервера и затем парсятся и компилируются движком JavaScript в браузере. Команда WebAssembly решила использовать бинарный формат потому, что код может быть сжат лучше, чем стандартный текстовый JavaScript файл, и потому, что для движка намного быстрее декодировать бинарный формат (до 23 раз быстрее в текущей реализации), чем, например, декодировать asm.js код.

Проект asm.js от Mozilla имеет долгосрочную цель привнести скорости, близкие к нативным в веб. Проект Native Client от Google для запуска нативного кода в браузере имеет похожую цель, но получил относительно небольшое распространение. Похоже на то, что сейчас WebAssembly имеет возможность привнести лучшее от этих проектов в браузеры.

В качестве первого шага, команда WebAssembly ставит цель достичь той же функциональности, что и asm.js (и разработчики смогут использовать ту же утилиту Emscripten для WebAssembly, что они используют для компиляции asm.js кода сейчас).

На этой ранней стадии команда также планирует запустить библиотеку polyfill, которая будет транслировать код WebAssembly в JavaScript таким образом, что он может быть выполнен любым браузером — даже без наличия встроенной поддержки WebAssembly (что, очевидно, абсурдно, потому что необходимость в этой библиотеке отпадет, как только браузеры получат встроенную поддержку WebAssembly). Со временем команда разработает больше утилит (компиляторы, дебаггеры и прочие) и добавит поддержку других языков (например, Rust, Go и C#).

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

Мы нечасто можем увидеть, что все значимые разработчики браузеров работают вместе над таким проектом, так что что-то определенно стоящее ожидает нас впереди.

Глава 11 — команды сравнения и передачи управления

Команда сравнения CMP сравнивает два числа, вычитая второе из первого, также как и команда SUB. Отличие команд CMP и SUB состоит в том, что инструкция CMP не сохраняет результат, а лишь устанавливает в соответствии с результатом флаги состояния. Основное назначение команды CMP – это организация ветвлений (условных переходов) в ассемблерных программах.

 

Безусловный переход – это переход, который передает управление без сохранения информации возврата всякий раз, когда выполняется. Ему соответствует команда JMP. Эта команда может осуществлять переход вплоть до 32768 байт. Если заранее известно, что переход вперед делается на место, лежащее в диапазоне 128 байт от текущего места, можно использовать команду JMP SHORT LABEL. Атрибут SHORT заставляет Ассемблер сформировать короткую форму команды перехода, даже если он еще не встретил метку LABEL.

Условный переход проверяет текущее состояние машины (флагов или регистров), чтобы определить, передать управление или нет. Команды переходов по условию делятся на две группы: 

1) проверяющие результаты предыдущей арифметической или логической операции Jcc;

2) управляющие итерациями фрагмента программы (организация циклов) LOOPcc. 

Все условные переходы имеют однобайтовое смещение, то есть метка, на которую происходит переход должна находится в том же кодовом сегменте и на расстоянии, не превышающем –128 +127 байт от первого байта следующей команды. Если условный переход осуществляется на место, находящееся дальше 128 байт, то вместо недопустимой команды
JZ ZERO
необходимо использовать специальные конструкции типа:
JNZ CONTINUE
JMP ZERO
CONTINUE:

Первая группа команд Jcc (кроме JCXZ/JECXZ) проверяет текущее состояние регистра флагов (не изменяя его) и в случае соблюдения условия осуществляет переход на смещение, указанное в качестве операнда. Флаги, проверяемые командой, кодируются в ее мнемонике, например: JC – переход, если установлен CF. Сокращения «L» (less – меньше) и «G» (greater – больше) применяются для целых со знаком, а «A» (above – над) и «B» (below – под) для целых без знака. Ниже в таблице показаны команды условного перехода и проверяемые ими флаги. 

 

Буква Х в любой позиции означает, что команда не проверяет флаг. Цифра 0 означает, что флаг должен быть сброшен, а цифра 1 означает, что флаг должен быть установлен, чтобы условие было выполнено (переход произошел).

Команды условного перехода можно разделить на три подгруппы:

1) Непосредственно проверяющие один из флагов на равенство 0 или 1.

2) Арифметические сравнения со знаком. Существуют 4 условия, которые могут быть проверены: меньше (JL), меньше или равно (JLE), больше (JG), больше или равно (JGE). Эти команды проверяют одновременно три флага: знака, переполнения и нуля.

3) Арифметические без знака. Здесь также существует 4 возможных соотношения между операндами. Учитываются только два флага. Флаг переноса показывает какое из двух чисел больше. Флаг нуля определяет равенство.

Ниже приведен фрагмент программы, иллюстрирующий использование команд сравнения и перехода.
MOV BH,X ;Загрузка в BH значения Х
MOV BL,Y ;Загрузка в BL значения Y 
CMP BH,BL ;Сравнение BH и BL
JE MET1 ;Если BH=BL, то переход на MET1
JMP MET2 ;Иначе переход на MET2
MET1:

JMP MET3
MET2:

MET3:
MOV AH,4Ch
INT 21H

Есть еще команда JCXZ, она отличается от других команд условного перехода тем, что она проверяет содержимое регистра CX, а не флагов. Эту команду лучше всего применять в начале условного цикла, чтобы предотвратить вхождение в цикл, если CX=0.

Вторая группа команд условного перехода — LOOPcc служит для организации циклов в программах. Все команды цикла используют регистр CX в качестве счетчика цикла. Простейшая из них – команда LOOP. Она уменьшает содержимое CX на 1 и передает управление на указанную метку, если содержимое CX не равно 0. Если вычитание 1 из CX привело к нулевому результату, выполняется команда, следующая за LOOP. 

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

BUF DB “0123406789”
…………………………….. 
MOV BX,OFFSET BUF;В BX – начало буферов
MOV CX,10 ;В CX – длинна буфера 
MOV SI,0 
M1: MOV DL,[BX+SI] ;В DL – символ из буфера
MOV AH,2 ;в AH номер функции-вывода (для int)
INT 21H ;Вывод на экран — функция OS
INC SI ;Увеличение индекса на 1
LOOP M1 ;Оператор цикла

Шпаргалка по основным инструкциям ассемблера x86/x64

В прошлой статье мы написали наше первое 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 значения 0xAABBCCDD:

(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 значения 0xEE:

(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 числа 0x1234:

(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, %axmovw $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 соответственно.

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

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

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

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

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

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

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

Сравним длины опкодов «обычного» и «относительного» 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 примет значение 0x12C или 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

# положить в CF (см далее) значение 13-го бита
# CF = !!(0x1bc381ffdc57dd & (1 << 13)) = 0
bt  $13, %rax

# то же самое + установить бит (bit test and set)
# rax = 0x001bc381ffdc77dd, CF = 0
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)

1:

Существует также инструкции 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 равно нулю.

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

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

Сравнить 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, если это ООП язык, и так далее. В общем случае для произвольно взятой архитектуры, компилятора и языка программирования соглашение о вызовах может быть вообще каким угодно.

Для примера рассмотрим ассемблерный код, сгенерированный CLang 3.8 для простой программки на языке C под x64. Так выглядит одна из процедур:

unsigned int
hash(const unsigned char *data, const size_t data_len) {
  unsigned int hash = 0x4841434B;
  for(int i = 0; i < data_len; i++) {
    hash = ((hash << 5) + hash) + data[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 <hash+0x50>

# 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+0x1a>

# возвращаемое значение (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.

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

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

Метки: Ассемблер.

javascript — Как сборка (asm.js) работает в браузере?

Зачем компилировать в JavaScript?

JavaScript — единственный язык, который работает во всех веб-браузерах. Хотя в браузере будет работать только JavaScript, вы все равно можете писать на других языках и по-прежнему компилировать в JavaScript, что позволяет ему также работать в браузере. Это стало возможным благодаря технологии, известной как emscripten.

Emscripten — это проект на основе LLVM, который компилирует C и C ++ в высокопроизводительный JavaScript в asm.js формат. Вкратце: близкие к родным скоростям, используя C и C ++, внутри браузера. Более того, emscripten преобразует OpenGL, графический API для настольных компьютеров, в WebGL, который является веб-вариантом этого API.

Как asm.js вписывается в картинку?

Asm.js, сокращение от Assembly JavaScript, является подмножеством JavaScript. Программа asm.js будет вести себя одинаково, независимо от того, запущена она в существующем механизме JavaScript или в механизме предварительной компиляции (AOT), который распознает и оптимизирует asm.js — кроме скорости, конечно!

С точки зрения скорости трудно предложить точное измерение того, как он сравнивается с нативным кодом, но предварительные тесты программ на C, скомпилированных в asm.js, обычно в два раза медленнее по сравнению с нативной компиляцией с помощью clang, интерфейса компилятора для Языки программирования C, C ++ и Obj-C. Важно отметить, что это «лучший» случай для однопоточных программ. Подробнее об этом ограничении языка JavaScript ниже.

На внутреннем уровне Clang использует LLVM, которая представляет собой библиотеку для создания, оптимизации и создания промежуточного и / или двоичного машинного кода (снова эти 0 и 1).LLVM может использоваться как каркас компилятора, где вы предоставляете «внешний интерфейс» (синтаксический анализатор и лексер, такой как Clang) и «серверную часть» (код, преобразующий представление LLVM в реальный машинный код)

Дополнительная литература: у Алона Закая из Mozilla есть фантастическая колода слайдов, в которой подробно рассказывается о том, как все это работает.

Так насколько же классный asm.js? Ну, у него есть собственная учетная запись в Твиттере, @asmjs. Хотя сайт asm немного скуден, он охватывает спецификацию W3C, а также содержит подробный FAQ.Более того, Mozilla координировала Humble Mozilla Bundle в 2014 году, что позволило вам купить набор игр, использующих asm.js.

Почему бы просто не превратить ваш код JavaScript в asm.js?

JavaScript не может быть скомпилирован в asm.js и предлагает много преимуществ из-за его динамической природы. Это та же проблема, что и при попытке скомпилировать его на C или даже на собственный код — виртуальная машина с ним должна позаботиться об этих нестатических аспектах.Однако вы можете написать asm.js вручную.

Если бы уже можно было переводить стандартный Javascript полностью статическим образом, не было бы необходимости в asm.js. Asm.js существует для того, чтобы обещать, что Javascript станет быстрее без каких-либо усилий со стороны разработчика. JIT было бы очень сложно понять динамический язык так же, как статический компилятор.

Чтобы лучше понять это, важно понять, почему asm.js вообще обеспечивает повышение производительности; или почему языки со статической типизацией работают лучше, чем языки с динамической типизацией.Одна из причин заключается в том, что «проверка типов во время выполнения требует времени», и более продуманный ответ будет включать расширенные возможности оптимизации статически типизированного кода. Последним преимуществом перехода от статически типизированного языка, такого как C, является тот факт, что компилятор знает тип каждого объекта при его компиляции.

Asm.js — это ограниченное подмножество JS, которое можно легко преобразовать в байт-код. Первый необходимый шаг потребует разбить все расширенные функции JS на это подмножество, чтобы получить это преимущество, что немного сложно.Но движки JavaScript оптимизированы и предназначены для преобразования всех этих расширенных функций непосредственно в байт-код, поэтому промежуточный шаг, такой как asm.js, не дает особых преимуществ.

Я подробно расскажу о фотографиях в этом посте.

javascript — В чем разница между asm.js и WebAssembly?

Код asm.js компилируется и запускается вовремя? Скомпилировано во что?

Различные браузеры по-разному компилируют код asm.js.По состоянию на август 2015 года:

  • Firefox компилирует asm.js в машинный код (и кэширует машинный код для будущих загрузок того же asm.js) [1].
  • В Windows 10 в качестве экспериментального флага Edge также будет выполнять некоторую предварительную проверку и компиляцию asm.js [2].
  • Chrome специально распознает директиву «use asm» в начале asm.js для более тщательного анализа и анализа кода и настройки эвристики компиляции.
  • Safari не выполняет специальной обработки asm.js.

Кроме того, что asm.js является текстом, а wasm (веб-сборка) является двоичным, в чем разница между двумя?

asm.js — это просто JavaScript и, следовательно, должен вести себя в точном соответствии со спецификацией JavaScript. В качестве нового стандарта WebAssembly может исправить некоторые критические случаи, когда поведение JavaScript не является идеальным (с точки зрения производительности или компиляции) [3]. В будущем [4] WebAssembly сможет добавлять функции, которые иначе было бы трудно выразить в JavaScript.

Что это означает для других языков сценариев, работающих в
браузер? Возьмем, к примеру, python, это будет

  • код Python, скомпилированный в wasm? или
  • Интерпретатор Python

  • (Cpython), скомпилированный в wasm и интерпретирующий Python?

В версии 1 самый простой способ запустить Python в браузере — это скомпилировать интерпретатор Python в wasm, как вы сказали. Это означает, например, что сборщик мусора Python работает в коде wasm и вручную управляет линейной памятью wasm.Уже были экспериментальные проекты по добавлению бэкэнда asm.js в PyPy [5] (который мог бы работать так же хорошо для wasm). В настоящее время он сталкивается с ограничениями asm.js, которые могут быть устранены с помощью будущей функции динамического связывания wasm. Идя дальше, wasm стремится обеспечить как интеграцию сборщика мусора, так и поддержку JIT-компиляции, что позволит обеспечить более эффективную и естественную интеграцию с веб-платформой.

Assembly.css

Assembly — это атомарный CSS-фреймворк с отличными стилями элементов формы по умолчанию.Используйте его, и вашей команде больше никогда не придется писать CSS.

Начало работы

Включите в заголовок вашего HTML таблицу стилей сборки.

    

Включите JavaScript сборки в любом месте вашего HTML. Можно безопасно использовать атрибуты async и defer . См. Подробности в Javascript API.

  <скрипт async defer src = "https: // api.mapbox.com/mapbox-assembly/v1.2.1/assembly.js ">   

HTML-шаблон

Начните прямо сейчас с этого простого HTML-шаблона.

  


   
  
  
  
  
  


  

  

Не знаете, что еще вам нужно в ? Ознакомьтесь с подробным руководством MDN.

Философия

Комплексная

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

Гибкость

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

Практичный

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

Настраиваемая

Assembly предоставляет процесс сборки в общедоступном API. Настроить цвета, шрифты и медиа-запросы; добавлять свои собственные таблицы стилей; и укажите варианты цвета, чтобы уменьшить размер файла.

Обзор

Классы и классы-модификаторы

Двойной дефис в имени класса (например, граница - синий ) указывает, что класс является классом-модификатором. Класс-модификатор расширяет класс, имя которого стоит перед двойным дефисом (например, граница - синий изменяет границу ; flex-child - grow изменяет flex-child ). И классы-модификаторы должны использоваться только в сочетании с классом, который они изменяют: классы-модификаторы не будут работать сами по себе.

6-пиксельная сетка базовой линии

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

Нет стиля по умолчанию для семантических элементов

Вы должны использовать

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