Эту идею мы вынашивали долго. Наверное, несколько лет мы штурмовали ее со всех сторон, и всякий раз нам что-нибудь мешало. С одной стороны, ассемблер — это круто настолько, насколько вообще может быть круто для нашего читателя-хакера (крякера, реверсера) умение общаться с компьютером на его языке. С другой стороны — актуальных руководств по асму, в том числе издания этого века, достаточно, а времена нынче либеральные, веб-хакеры и любители JS могут нас не понять и не одобрить. 🙂 Точку в споре физиков, лириков, старообрядцев, никониан, веб-хакеров и тру-крякеров поставил успех цикла статей по реверсу малвари. Оказалось, что сейчас, в XXI веке, тру-крякеры все еще не сдали своих позиций и нашим читателям это интересно!

Но что такое программирование само по себе по своей сути, вне зависимости от какого-либо языка? Разнообразие ответов поражает. Наиболее часто можно услышать такое определение: программирование — это составление инструкций или команд для последовательного исполнения их машиной с целью решить ту или иную задачу. Такой ответ вполне справедлив, но, на мой взгляд, не отражает всей полноты, как если бы мы назвали литературу составлением из слов предложений для последовательного прочтения их читателем. Я склонен полагать, что программирование ближе к творчеству, к искусству. Как любой вид искусства — выражение творческой мысли, идеи, программирование представляет собой отражение человеческой мысли. Мысль же бывает и гениальная, и совершенно посредственная.

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

В последнее время ассемблер незаслуженно находится в тени других языков. Обусловлено это глобальной коммерциализацией, направленной на то, чтобы в максимально короткие сроки получить как можно большую прибыль от продукта. Иными словами, массовость взяла верх над элитарностью. А ассемблер, по моему мнению, ближе к последнему. Гораздо выгоднее в сравнительно небольшие сроки поднатаскать ученика в таких, например, языках, как С++, С#, PHP, Java, JavaScript, Python, чтобы он был более-менее способен создавать ширпотребный софт, не задаваясь вопросами, зачем и почему он так делает, чем выпустить хорошего специалиста по ассемблеру. Примером тому служит обширнейший рынок всевозможных курсов по программированию на любом языке, за исключением ассемблера. Та же тенденция прослеживается как в преподавании в вузах, так и в учебной литературе. В обоих случаях вплоть до сегодняшнего дня большая часть материала базируется на ранних процессорах серии 8086, на так называемом «реальном» 16-битном режиме работы, операционной среде MS-DOS! Возможно, что одна из причин в том, что, с одной стороны, с появлением компьютеров IBM PC преподавателям пришлось перейти именно на эту платформу из-за недоступности других. А с другой стороны, по мере развития линейки 80х86 возможность запуска программ в режиме DOS сохранялась, что позволяло сэкономить деньги на приобретение новых учебных компьютеров и составление учебников для изучения архитектуры новых процессоров. Однако сейчас такой выбор платформы для изучения совершенно неприемлем. MS-DOS как среда выполнения программ безнадежно устарела уже к середине девяностых годов, а с переходом к 32-битным процессорам, начиная с процессора 80386, сама система команд стала намного более логичной. Так что бессмысленно тратить время на изучение и объяснение странностей архитектуры реального режима, которые заведомо никогда уже не появятся ни на одном процессоре.

Что касается выбора операционной среды для изучения ассемблера, то, если говорить о 32-битной системе команд, выбор сравнительно невелик. Это либо операционные системы Windows, либо представители семейства UNIX.

Также следует сказать несколько слов о том, какой именно ассемблер выбрать для той или другой операционной среды. Как известно, для работы с процессорами х86 используются два типа синтаксиса ассемблера — это синтаксис AT&T и синтаксис Intel. Эти синтаксисы представляют одни и те же команды совершенно по-разному. Например, команда в синтаксисе Intel выглядит так:

mov eax,ebx

В синтаксисе же AT&T уже будет иной вид:

movl %eax,%ebx

В среде ОС UNIX более популярен синтаксис типа AT&T, однако учебных пособий по нему нет, он описывается исключительно в справочной и технической литературе. Поэтому логично выбрать ассемблер на основе синтаксиса Intel. Для UNIX-систем есть два основных ассемблера — это NASM (Netwide Assembler) и FASM (Flat Assembler). Для линейки Windows популярностью пользуются FASM и MASM (Macro Assembler) от фирмы Microsoft, и также существовал еще TASM (Turbo Assembler) фирмы Borland, которая уже довольно давно отказалась от поддержки собственного детища.

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

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

 

Что такое ассемблер?

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

На заре компьютерной эры первые ЭВМ занимали целые комнаты и весили не одну тонну, имея объем памяти с воробьиный мозг, а то и того меньше. Единственным способом программирования в те времена было вбивать программу в память компьютера непосредственно в цифровом виде, переключая тумблеры, проводки и кнопочки. Число таких переключений могло достигать нескольких сотен и росло по мере усложнения программ. Встал вопрос об экономии времени и денег. Поэтому следующим шагом в развитии стало появление в конце сороковых годов прошлого века первого транслятора-ассемблера, позволяющего удобно и просто писать машинные команды на человеческом языке и в результате автоматизировать весь процесс программирования, упростить, ускорить разработку программ и их отладку. Затем появились языки высокого уровня и компиляторы (более интеллектуальные генераторы кода с более понятного человеку языка) и интерпретаторы (исполнители написанной человеком программы на лету). Они совершенствовались, совершенствовались — и, наконец, дошло до того, что можно просто программировать мышкой.

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

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

 

Синтаксис

Общепринятого стандарта для синтаксиса языков ассемблера не существует. Однако большинство разработчиков языков ассемблера придерживаются общих традиционных подходов. Основные такие стандарты — Intel-синтаксис и AT&T-синтаксис.

Общий формат записи инструкций одинаков для обоих стандартов:

[метка:] опкод [операнды] [;комментарий]

Опкод — это и есть собственно ассемблерная команда, мнемоника инструкции процессору. К ней могут быть добавлены префиксы (например, повторения, изменения типа адресации). В качестве операндов могут выступать константы, названия регистров, адреса в оперативной памяти и так далее. Различия между стандартами Intel и AT&T касаются в основном порядка перечисления операндов и их синтаксиса при разных методах адресации.

Используемые команды обычно одинаковы для всех процессоров одной архитектуры или семейства архитектур (среди широко известных — команды процессоров и контроллеров Motorola, ARM, x86). Они описываются в спецификации процессоров.

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

 

Директивы

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

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

Достоинства и недостатки

К достоинствам можно отнести следующее:

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

За недостатки можно принять:

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

Почему следует изучать язык ассемблера?

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

Так зачем же тратить время на его изучение? По ряду веских причин, и вот одна из них: ассемблер — это краеугольный камень, на котором покоится все бесконечное пространство программирования, начиная от рождения первого процессора. Каждый физик мечтает разгадать тайну строения вселенной, найти эти загадочные первичные неделимые (низкоуровневые) элементы, из которых она состоит, не удовлетворяясь лишь смутным о том представлением квантовой теории. Ассемблер же и есть та первичная материя, из которой состоит вселенная процессора. Он — тот инструмент, который дает человеку способность мыслить в терминах машинных команд. А подобное умение просто необходимо любому профессиональному программисту, даже если никогда в жизни он не напишет ни единой ассемблерной строчки. Нельзя отрицать того, что невозможно стать математиком, совершенно не имея понятия об элементарной арифметике. На каком бы языке вы ни писали программы, необходимо хотя бы в общих чертах понимать, что конкретно будет делать процессор, исполняя ваше высочайшее повеление. Если такого понимания нет, программист начинает бездумно применять все доступные операции, совершенно не ведая, что на самом деле он творит.

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

Иными словами, до тех пор пока существуют процессоры, ассемблер будет необходим.

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

 

Ассемблер — программирование или искусство?

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

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

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

 

Ассемблер и терминатор

Не так давно Джеймс Кэмерон выпустил в свет 3D-версию второго «Терминатора», и в качестве интересного исторического факта можно отметить один любопытный момент из жизни киборга-убийцы...

Кадр из фильма «Терминатор»
Кадр из фильма «Терминатор»

Здесь мы видим «зрение» терминатора, а слева на нем отображается ассемблерный листинг. Судя по нему, знаменитый Уничтожитель работал на процессоре MOS Technology 6502 либо на MOS Technology 6510. Этот процессор впервые был разработан в 1975 году, использовался на компьютерах Apple и, помимо всего прочего, на знаменитых игровых приставках того времени Atari 2600 и Nintendo Entertainment System (у нас более известной как Dendy). Имел лишь три 8-разрядных регистра: А-аккумулятор и два индексных регистра X и Y. Такое малое их количество компенсировалось тем, что первые 256 байт оперативной памяти (так называемая нулевая страница) могли адресоваться специальным образом и фактически использовались в качестве 8-разрядных или 16-разрядных регистров. У данного процессора было 13 режимов адресации на всего 53 команды. У терминатора идет цепочка инструкций LDA-STA-LDA-STA... В семействе 6502 программы состояли чуть менее чем полностью из LDA/LDY/LDX/STA/STX/STY:

LDA — загрузить в аккумулятор
LDY — загрузить в регистр Y
LDX — загрузить в регистр X
STA — сохранить из аккумулятора
STX — сохранить из регистра X
STY — сохранить из регистра Y

Чтение и запись в порты ввода-вывода также выполнялись этими командами, и программа терминатора имеет вполне осмысленный вид, а не представляет собой бестолковую фантазию сценариста: MOS Technology 6502 / Система команд.

 

Отрасли практического применения

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

  • Разработка встроенного программного обеспечения. Это небольшие программы, не требующие значительного объема памяти на таких устройствах, как, например, телефоны, автомобильные системы зажигания, системы безопасности, видео- и звуковые карты, модемы и принтеры. Ассемблер для этого идеальный инструмент.
  • В компьютерных игровых консолях для оптимизации и уменьшения объема кода и для быстродействия.
  • Для использования в программе новых команд, доступных на новых процессорах. Компилятор высокого уровня хоть и оптимизирует код при компиляции, но практически никогда не способен генерировать инструкции из расширенных наборов команд типа AVX, CTV, XOP. Потому что команды в процессоры добавляют быстрее, чем в компиляторах появляется логика для генерации этих команд.
  • Большая доля программ для графического процессора GPU пишется на ассемблере, наряду с языками высокого уровня HLSL или GLSL.
  • Для написания кода, создание которого невозможно или затруднено на языках высокого уровня, например получение дампа памяти/стека. Даже когда аналог на языке высокого уровня возможен, преимущество языка ассемблера может быть значительным. Например, реализация подсчета среднего арифметического двух чисел с учетом переполнения для x86 процессоров занимает всего две команды (сложение с выставлением флага переноса и сдвиг с займом этого флага). Аналог на языке высокого уровня ((long) x + y) >> 1 либо может не работать в принципе, ведь sizeof(long) == sizeof(int), либо при компиляции конвертируется в огромнейшее количество команд процессора.
  • Написание вирусов и антивирусников. Единственный язык программирования для создания достойных инфекторов — CIH, Sality, Sinowal.
  • И конечно же, нельзя не упомянуть оборотную сторону медали: взлом, крэкинг и более легальный вариант — reverse engineering. Знание ассемблера — это мощнейший инструмент в руках реверсера. Ни дизассемблирование, ни отладка программ без знаний о нем невозможны.
 

Вместо заключения

Мы продолжим погружаться в ассемблер в следующих статьях цикла. Темы этого цикла мы в целом определили, но если у тебя есть какие-нибудь идеи или пожелания — смело пиши в комменты, все учтем. 🙂

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    52 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии