awesome-everything EN
↑ Обратно к восхождению

Базовый CS с нуля

Идея ассемблера

Суть Язык ассемблера даёт каждой машинной инструкции короткую человекочитаемую мнемонику. Ассемблер — это программа, которая переводит текст ассемблера в двоичный машинный код для CPU — одна мнемоника на инструкцию, по сути один к одному.
◷ 18 min

В предыдущем разделе ты узнал, что CPU выполняет машинный код: необработанные битовые паттерны, хранящиеся в памяти. Ты даже вручную декодировал 16-битную инструкцию — разбивая её на биты опкода, биты регистра и непосредственное значение. Это сработало, но было медленно и мучительно. Представь, что нужно написать программу из тысяч инструкций, каждая из которых — строка нулей и единиц, без меток, без имён, без указаний на то, что делает каждая последовательность. Один неверный бит незаметно даёт неправильный опкод. Завтра это не прочесть.

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

Цель

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

1

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

МнемоникаЧто делает
LOADЧитает значение из памяти в регистр
STOREЗаписывает значение регистра в память
ADDСкладывает значения двух регистров
SUBВычитает значение одного регистра из другого
JUMPУстанавливает счётчик команд на новый адрес
HALTОстанавливает CPU

Ты пишешь мнемонику в исходном файле ассемблера как текст. Ассемблер находит её в таблице и заменяет соответствующим битовым паттерном. Больше ничего не меняется — битовый паттерн это именно тот опкод, который CPU в любом случае потребовал бы. Мнемоника — просто человекочитаемое имя для числа.

2

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

Это принципиальное отличие ассемблера от всех других языков, с которыми ты встретишься. Когда ты пишешь x = a + b на Python или TypeScript, среда выполнения или компилятор может породить десятки машинных инструкций для этой одной строки. Когда ты пишешь ADD R0, R1 на ассемблере, ты получаешь ровно одну машинную инструкцию — опкод ADD и закодированные номера регистров.

Ассемблер — это просто машинный код, написанный читаемыми именами вместо сырых битов.

3

Что ассемблер на самом деле делает. Ассемблер — это сама по себе программа. Исторически одна из первых когда-либо написанных программ: как только появился один ассемблер, его можно было использовать для написания следующего. Его задача — прочитать исходный файл на ассемблере и создать бинарный файл с машинным кодом. Процесс состоит из двух частей:

Часть 1 — Трансляция. Для каждой инструкции ассемблера программа-ассемблер:

  1. Читает мнемонику (например, LOAD).
  2. Находит её в таблице и получает биты опкода (например, 00 для LOAD в нашем игрушечном CPU).
  3. Читает каждый операнд (имя регистра или константу) и преобразует его в битовое представление.
  4. Объединяет эти битовые поля в полную двоичную инструкцию.
  5. Записывает двоичные байты в выходной файл.

Часть 2 — Разрешение символов. Практические программы на ассемблере используют метки — именованные маркеры адресов памяти. Вместо JUMP 84 ты пишешь JUMP loop_start, где loop_start — метка, размещённая перед инструкцией, к которой нужно перейти. Ассемблер записывает адрес каждой метки в таблицу символов, а затем заменяет каждую ссылку на метку соответствующим числовым адресом. Тебе не нужно считать адреса вручную — ассемблер считает их за тебя.

Обе части выполняются за один или два прохода по исходному файлу. Результат — бинарный файл, готовый для выполнения на CPU.

Почему это работает

Почему ассемблер появился так рано, раньше любого другого инструмента высокого уровня? Потому что сам ассемблер — очень маленькая и прямолинейная программа. Трансляция чисто механическая: заменить этот текст тем битовым паттерном. Никакого сложного анализа, никакой оптимизации. Это означало, что программисты в конце 1940-х и начале 1950-х годов могли написать первый ассемблер вручную в машинном коде (мучительно, но один раз), а затем сразу использовать его для написания всего остального на ассемблере. Это был тот самый бутстрап, который сделал возможным создание всех дальнейших инструментов.

4

Ассемблер зависит от конкретного CPU. Поскольку мнемоники ассемблера — просто имена для опкодов конкретного CPU, каждое семейство CPU имеет свой собственный язык ассемблера. Мнемоники для процессоров x86-64 (используемых в большинстве настольных компьютеров и ноутбуков) отличаются от мнемоник для процессоров ARM (используемых в телефонах и Mac с Apple Silicon), потому что у этих CPU разные системы команд с разными битовыми кодировками.

Программу на ассемблере, написанную для x86-64, нельзя транслировать и запустить на CPU ARM без переписывания — общего словаря не существует. Ассемблер находится ровно на один тонкий уровень выше железа, и этот уровень так же специфичен для CPU, как и сам машинный код.

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

Граничные случаи

Псевдоинструкции и директивы ассемблера. Некоторые ассемблеры добавляют небольшой слой удобства поверх обычной трансляции: псевдоинструкции — мнемоники ассемблера, которые не отображаются на одну реальную инструкцию, а раскрываются в короткую последовательность реальных инструкций. Например, в MIPS-ассемблере MOVE Rd, Rs — псевдоинструкция, раскрывающаяся в ADDU Rd, Rs, R0 (прибавление нуля для копирования значения; используется ADDU, а не ADD, чтобы избежать ловушки переполнения). Директивы ассемблера — команды для самой программы-ассемблера (а не для CPU): .data отмечает начало секции данных, .byte 42 резервирует один байт со значением 42, .global main экспортирует метку для компоновщика. Директивы не производят машинный код напрямую; они управляют тем, как ассемблер компонует бинарный файл.

LOAD
текст
R0
рег.
200
адрес
00
опкод
0
рег.
11001000
200
Ассемблер переводит одну инструкцию ассемблера (слева, выделено синим) в одну машинную инструкцию (справа, выделено зелёным). Мнемоника LOAD становится битами опкода 00 (биты 7–6); регистр R0 — битом 5 = 0; десятичный адрес 200 — его 8-битной двоичной формой 11001000 в байте операнда. Никаких других инструкций не создаётся.
Разбор примера

Пошаговая трансляция ассемблером программы из четырёх инструкций.

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

Исходный код на ассемблере:

        LOAD  R0, 200    ; загрузить первое число в R0
        LOAD  R1, 201    ; загрузить второе число в R1
        ADD   R0, R1     ; R0 = R0 + R1
        STORE 202, R0    ; сохранить результат по адресу 202

Ассемблер обрабатывает каждую строку:

СтрокаМнемоникаБиты опкода (7–6)Бит рег. (5)Байт операндаДвоичная инструкция
1LOAD R0, 200000 (R0)11001000 (=200)00000000 11001000
2LOAD R1, 201001 (R1)11001001 (=201)00100000 11001001
3ADD R0, R110000000000 (не используется)10000000 00000000
4STORE 202, R0010 (R0)11001010 (=202)01000000 11001010

Ассемблер записывает 8 байтов в выходной бинарный файл: 0x00 0xC8 0x20 0xC9 0x80 0x00 0x40 0xCA

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

Обрати внимание: четыре строки ассемблера породили четыре машинные инструкции — один к одному.

Практика 0 / 5

Язык ассемблера имеет соответствие один к одному с машинным кодом. Сколько машинных инструкций производит одна инструкция ассемблера? Введи число.

Ассемблер преобразует текст мнемоники ADD в соответствующий битовый паттерн опкода. Видит ли CPU когда-либо текст 'ADD'? Введи 1 (да) или 0 (нет).

Метка в ассемблере — это именованный маркер адреса в памяти. Что ассемблер хранит в таблице символов для каждой метки? Введи 1 (адрес помеченного места) или 2 (текст мнемоники инструкции по этому адресу).

Можно ли транслировать программу на ассемблере x86-64 и запустить её напрямую на CPU ARM без каких-либо изменений? Введи 1 (да) или 0 (нет).

В разобранном примере программа на ассемблере из четырёх инструкций породила сколько байтов в выходном бинарном файле? (Каждая инструкция занимает 2 байта.)

Проверь себя
Викторина

Какова связь между мнемоникой языка ассемблера и машинной инструкцией?

Итог

Язык ассемблера — это текстовое представление машинного кода, в котором каждая инструкция CPU записывается как короткая человекочитаемая мнемоника (например, LOAD, ADD или JUMP), а не как сырые биты. Связь — один к одному: каждая инструкция ассемблера переводится ровно в одну машинную инструкцию, и наоборот. Программа, выполняющая эту трансляцию, называется ассемблером: она читает исходный текст ассемблера, находит в таблице соответствующие биты опкода для каждой мнемоники, преобразует имена регистров и числовые константы в их двоичные формы, разрешает метки (именованные маркеры адресов) через таблицу символов и записывает полученные байты в выходной бинарный файл. Ассемблер зависит от конкретного CPU — каждое семейство CPU имеет свой язык ассемблера со своими мнемониками, — потому что мнемоники — просто имена для битовых паттернов опкодов этого CPU. Ассемблер был первым инструментом, сделавшим программирование терпимо продуктивным, и по-прежнему остаётся тончайшим возможным слоем над голым железом.

Продолжить восхождение ↑Зачем нужны языки высокого уровня
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.