Почему Ассемблер — это круто, но сложно

Есть высо­ко­уров­не­вые язы­ки — это те, где вы гово­ри­те if — else, print, echo, function и так далее. «Высо­кий уро­вень» озна­ча­ет, что вы гово­ри­те с ком­пью­те­ром более-менее чело­ве­че­ским язы­ком. Дру­гой чело­век может не понять, что имен­но у вас напи­са­но в коде, но он хотя бы смо­жет про­чи­тать сло­ва.

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

Ассем­блер — это соби­ра­тель­ное назва­ние язы­ков низ­ко­го уров­ня: код всё ещё пишет чело­век, но он уже гораз­до бли­же к прин­ци­пам рабо­ты ком­пью­те­ра, чем к прин­ци­пам мыш­ле­ния чело­ве­ка.

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

Как мыслит процессор

Что­бы понять, как рабо­та­ет Ассем­блер и поче­му он рабо­та­ет имен­но так, нам нуж­но немно­го разо­брать­ся с внут­рен­ним устрой­ством про­цес­со­ра.

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

Реги­стры быва­ют раз­но­го вида и назна­че­ния: одни слу­жат, что­бы хра­нить инфор­ма­цию; дру­гие сооб­ща­ют о состо­я­нии про­цес­со­ра; тре­тьи исполь­зу­ют­ся как нави­га­то­ры, что­бы про­цес­сор знал, куда идти даль­ше, и так далее. Подроб­нее — в рас­хло­пе ↓

Какими бывают регистры

Обще­го назна­че­ния. Это 8 реги­стров, каж­дый из кото­рых может хра­нить все­го 4 бай­та инфор­ма­ции. Такой регистр мож­но раз­де­лить на 2 или 4 части и рабо­тать с ними как с отдель­ны­ми ячей­ка­ми.

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

Регистр фла­гов. Флаг — какое-то свой­ство про­цес­со­ра. Напри­мер, если уста­нов­лен флаг пере­пол­не­ния, зна­чит про­цес­сор полу­чил в ито­ге такое чис­ло, кото­рое не поме­ща­ет­ся в нуж­ную ячей­ку памя­ти. Он туда кла­дёт то, что поме­ща­ет­ся, и ста­вит в этот флаг циф­ру 1. Она — сиг­нал про­грам­ми­сту, что что-то пошло не так.

Фла­гов в про­цес­со­ре мно­го, какие-то мож­но менять вруч­ную, и они будут вли­ять на вычис­ле­ния, а какие-то мож­но про­сто смот­реть и делать выво­ды. Фла­ги — как сиг­наль­ные лам­пы на пане­ли при­бо­ров в само­лё­те. Они что-то озна­ча­ют, но толь­ко само­лёт и пилот зна­ют, что имен­но.

Сег­мент­ные реги­стры. Нуж­ны были для того, что­бы рабо­тать с опе­ра­тив­ной памя­тью и полу­чать доступ к любой ячей­ке. Сей­час такие реги­стры име­ют по 32 бита, и это­го доста­точ­но, что­бы полу­чить 4 гига­бай­та опе­ра­тив­ки. Для про­грам­мы на Ассем­бле­ре это­го обыч­но хва­та­ет.

Так вот: всё, с чем рабо­та­ет Ассем­блер, — это коман­ды про­цес­со­ра, пере­мен­ные и реги­стры.

Здесь нет при­выч­ных типов дан­ных — у нас есть толь­ко бай­ты памя­ти, в кото­рых мож­но хра­нить что угод­но. Даже если вы поме­сти­те в ячей­ку какой-то сим­вол, а потом захо­ти­те рабо­тать с ним как с чис­лом — у вас полу­чит­ся. А вме­сто при­выч­ных цик­лов мож­но про­сто прыг­нуть в нуж­ное место кода.

Команды Ассемблера

Каж­дая коман­да Ассем­бле­ра — это коман­да для про­цес­со­ра. Не опе­ра­ци­он­ной систе­ме, не фай­ло­вой систе­ме, а имен­но про­цес­со­ру — то есть в самый низ­кий уро­вень, до кото­ро­го может дотя­нуть­ся про­грам­мист.

Любая коман­да на этом язы­ке выгля­дит так:

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

Мет­ка — это имя для фраг­мен­та кода. Напри­мер, вы хоти­те отдель­но поме­тить место, где начи­на­ет­ся рабо­та с жёст­ким дис­ком, что­бы было лег­че читать код. Ещё мет­ка нуж­на, что­бы в дру­гом участ­ке про­грам­мы мож­но было напи­сать её имя и сра­зу пере­прыг­нуть к нуж­но­му кус­ку кода.

Коман­да — слу­жеб­ное сло­во для про­цес­со­ра, кото­рое он дол­жен выпол­нить. Спе­ци­аль­ные ком­пи­ля­то­ры пере­во­дят такие коман­ды в машин­ный код. Это сде­ла­но для того, что­бы не запо­ми­нать сами машин­ные коман­ды, а исполь­зо­вать вме­сто них какие-то бук­вен­ные обо­зна­че­ния, кото­рые про­ще запом­нить. В этом, соб­ствен­но, и выра­жа­ет­ся чело­веч­ность Ассем­бле­ра: коман­ды в нём хотя бы отда­лён­но напо­ми­на­ют чело­ве­че­ские сло­ва.

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

Ком­мен­та­рий — это про­сто пояс­не­ние к коду. Его мож­но писать на любом язы­ке, и на выпол­не­ние про­грам­мы он не вли­я­ет. При­ме­ры команд:

mov eax, ebx ; Пересылаем значение регистра EBX в регистр EAX

mov x, 0 ; Записываем в переменную x значение 0

add eax, х ; Складываем значение регистра ЕАХ и переменной х, результат отправится в регистр ЕАХ

Здесь нет меток, пер­вы­ми идут коман­ды (mov или add), а за ними — опе­ран­ды и ком­мен­та­рии.

Пример: возвести число в куб

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

Пер­вый вари­ант

mov al, x ; Пересылаем x в регистр AL

imul al ; Умножаем регистр AL на себя, AX = x * x

movsx bx, x ; Пересылаем x в регистр BX со знаковым расширением

imul bx ; Умножаем AX на BX. Результат разместится в DX:AX

Вто­рой вари­ант

mov al, x ; Пересылаем x в регистр AL

imul al ; Умножаем регистр AL на себя, AX = x * x

cwde ; Расширяем AX до EAX

movsx ebx, x ; Пересылаем x в регистр EBX со знаковым расширением

imul ebx ; Умножаем EAX на EBX. Поскольку x – 1-байтовая переменная, результат благополучно помещается в EAX

На любом высо­ко­уров­не­вом язы­ке воз­ве­сти чис­ло в куб мож­но одной стро­кой. Напри­мер:

x = Math.pow(x,3);
x := exp(ln(x) * 3);
на худой конец x = x*x*x.

Хит­рость в том, что когда каж­дая из этих строк будет све­де­на к машин­но­му коду, это­го кода может быть и 5 команд, и 10, и 50, и даже 100. Чего сто­ит вызов объ­ек­та Math и его мето­да pow: толь­ко на эту слу­жеб­ную опе­ра­цию (ещё до само­го воз­ве­де­ния в куб) может уйти несколь­ко сотен и даже тысяч машин­ных команд.

А на Ассем­бле­ре это гаран­ти­ро­ван­но пять команд. Ну, или как реа­ли­зу­е­те.

Почему это круто

Ассем­блер поз­во­ля­ет рабо­тать с про­цес­со­ром и памя­тью напря­мую — и делать это очень быст­ро. Дело в том, что в Ассем­бле­ре почти не тра­тит­ся зря про­цес­сор­ное вре­мя. Если про­цес­сор рабо­та­ет на часто­те 3 гига­гер­ца — а это при­мер­но 3 мил­ли­ар­да про­цес­сор­ных команд в секун­ду, — то очень хоро­ший код на Ассем­бле­ре будет выпол­нять при­мер­но 2,5 мил­ли­ар­да команд в секун­ду. Для срав­не­ния, JavaScript или Python выпол­нят в тыся­чу раз мень­ше команд за то же вре­мя.

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

Спра­вед­ли­во­сти ради отме­тим, что совре­мен­ные ком­пи­ля­то­ры С++ дают машин­ный код, близ­кий по быст­ро­дей­ствию к Ассем­бле­ру, но всё рав­но немно­го усту­па­ют ему.

Почему это сложно

Для того, что­бы писать про­грам­мы на Ассем­бле­ре, нуж­но очень любить крем­ний:

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

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

Для чего всё это

Ассем­блер неза­ме­ним в таких вещах:

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

На самом деле на Ассем­бле­ре мож­но даже запи­лить свой сайт с фору­мом, если у про­грам­ми­ста хва­та­ет ква­ли­фи­ка­ции. Но чаще все­го Ассем­блер исполь­зу­ют там, где даже ско­ро­сти и воз­мож­но­стей C++ недо­ста­точ­но.