ORG 100h
Дружок, недавно ты скомпилировал свой исходный код. Вероятнее всего, размер полученного файла оказался больше килобайта. Если у тебя компьютер i7/16ГБ DDR3/1ТБ HDD, ты не обратишь на это внимания. Но если у тебя 486/2МБ SIMM/512МБ HDD — ты гарантированно будешь негодовать от странной утечки дисковой памяти.
Почему «Hello World» на C весит почти 5 килобайт?
Возьмём классику: программа, выводящая «Hello World!».
На C можно написать очень кратко (в GCC даже без указания #include <stdio.h>):
main(){printf("Hello World!\n");}
Но после компиляции я получил 4483 байта! Почему так много? Всё, что нужно программе — вывести строку. Это делается через системный вызов или библиотечную функцию. Но код раздувается за счёт:
- Заголовков объектных файлов
- Секций с отладочной информацией
- Копирайтов и лицензионных строк (да, их тоже вшивают)
- Подключения рантайма (инициализация, очистка)
Как этого избежать? Писать на ассемблере!
Сравнение: «Hello World» на C и на ассемблере
| GNU/Linux (NASM) | DOS (TASM) |
|---|---|
asmsection .data msg db 'Hello world!',0xA msg_size equ $-msgsection .text global _start_start: mov eax, 4 ; sys_write mov ebx, 1 ; stdout mov ecx, msg mov edx, msg_size int 0x80 ; вызов ядра mov eax, 1 ; sys_exit xor ebx, ebx int 0x80 | asmorg 100hmov ah, 9h ; функция вывода строки DOSmov dx, helloint 21h ; вызов прерыванияrethello db 13,10,"Hello, World!$" |
| Размер: ~160 байт | Размер: 21 байт |
В DOS-версии — всего 21 байт! Никаких заголовков, никакой отладочной информации, чистый машинный код.
А что насчёт логики? Сравнение условного оператора
Код на C:
int a, b;
scanf("%d%d", &a, &b);
if (a > b)
printf(">\n");
else
printf("<\n");
Тот же смысл на ассемблере (Linux) занимает 80 байт против 4610 байт в скомпилированной C-программе (GCC без оптимизаций). C-код, конечно, можно сжать ключами -Os -s -ffreestanding, но суть понятна: ассемблер даёт полный контроль над каждым байтом.
Инструкции процессоров x86: краткий путеводитель
Читатели справедливо недоумевают — что за MOV, SUB, CMP, JMP? Это мнемоники — сокращённые обозначения инструкций, понятные человеку. Ассемблер переводит их в машинные коды.
Важно: Мнемоника зависит от архитектуры процессора. То, что работает на x86, не сработает на ARM, Z80 или PDP-11. Но основные идеи везде похожи.
Чтобы глубже понять, как работает процессор, советую прочитать статью о логике работы центрального процессора.
1. Математические и логические
| Инструкция | Описание | Пример |
|---|---|---|
ADD | Сложение: a = a + b | add eax, 42 ; eax += 42 |
ADC | Сложение с переносом (Carry) | используется в многобайтовых числах |
INC | Увеличение на 1 | inc ebx |
SUB | Вычитание: a = a - b | sub [ecx], 5 |
SBB | Вычитание с заёмом | |
DEC | Уменьшение на 1 | dec word [esi] |
NEG | Смена знака (из плюса в минус и наоборот) | |
XADD | Обмен и сложение (атомарно) |
Все эти инструкции изменяют флаги: ZF (ноль), SF (знак), OF (переполнение), CF (перенос).
| Инструкция | Описание |
|---|---|
MUL / IMUL | Беззнаковое / знаковое умножение |
DIV / IDIV | Деление |
NOT | Побитовая инверсия |
AND | Логическое И |
OR | Логическое ИЛИ |
XOR | Исключающее ИЛИ (XOR регистра с самим собой — быстрый способ обнулить) |
2. Битовые сдвиги
| Инструкция | Действие |
|---|---|
SHL / SHR | Логический сдвиг влево / вправо (освободившиеся биты — 0) |
SAL / SAR | Арифметический сдвиг (знак сохраняется) |
ROL / ROR | Циклический сдвиг (биты переходят на другой конец) |
3. Перемещение данных
| Инструкция | Описание |
|---|---|
MOV | Копирование: источник → назначение |
XCHG | Обмен значениями (регистр-регистр, регистр-память) |
MOV умеет пересылать между регистрами, памятью, сегментными регистрами, константами и даже управляющими регистрами (cr0, cr3). Все варианты не перечислить, но общая форма: MOV destination, source.
4. Безусловные переходы
JMP — прыгнуть на метку (или адрес). Аналог goto в C.
5. Сравнение и условные переходы
CMP — вычитает второй операнд из первого, но не сохраняет результат, а только изменяет флаги. Затем проверяем флаги условными переходами:
| Инструкция | Условие |
|---|---|
JE / JZ | равно (Zero) |
JNE / JNZ | не равно |
JG / JNLE | больше (для знаковых) |
JL / JNGE | меньше |
JA / JNBE | выше (беззнак.) |
JB / JNAE | ниже |
JC | есть перенос |
JNC | нет переноса |
Пример:
cmp eax, ebx
je equal_label ; если eax == ebx
6. Работа со стеком
| Инструкция | Действие |
|---|---|
PUSH | Поместить значение в стек (ESP уменьшается) |
POP | Извлечь из стека (ESP увеличивается) |
PUSHA / PUSHAD | Сохранить все регистры |
POPA / POPAD | Восстановить все регистры |
Для 16-битного режима:
pusha=pushaw,popa=popaw. Для 32-битного:pushad,popad.
7. Преобразования типов
| Инструкция | Смысл |
|---|---|
CBW | Convert Byte to Word (AL → AX) |
CWDE | Convert Word to Double (AX → EAX) |
CWD | Convert Word to Double (AX → DX:AX) |
CDQ | Convert Double to Quad (EAX → EDX:EAX) |
MOVSX | Пересылка с расширением знака |
MOVZX | Пересылка с обнулением старших битов |
8. Управление системой и прерывания
| Инструкция | Описание |
|---|---|
INT | Программное прерывание (например, int 0x80 в Linux) |
HLT | Остановка процессора до следующего прерывания |
CLI / STI | Сбросить / установить флаг прерываний |
NOP | Нет операции (часто используется для выравнивания кода) |
Интересные факты об ассемблере
- Самый маленький исполняемый файл в DOS — программа, которая просто возвращает управление, занимает всего 1 байт (инструкция
C3—ret). - Демосцена — сообщество программистов, которые соревнуются в написании максимально эффектных анимаций и музыки в крошечном размере. Например, .com-файлы размером 64, 128, 256 байт, которые показывают 3D-графику.
- Почему ассемблер до сих пор используется?
- Операционные системы (ядро Linux содержит вставки на ассемблере для работы с железом)
- Драйверы устройств
- Программирование микроконтроллеров
- Критичные по производительности участки (кодеки, шифрование)
- В 1980-х программисты на ассемблере под ZX Spectrum и Commodore 64 умещали целые игры в 16–48 КБ. Сегодня одна фотография с телефона занимает в 100 раз больше.
Заключение
Ассемблер — это не «мёртвый язык». Это основа, на которой держится всё остальное. Понимание ассемблера делает вас лучшим программистом на C/C++, Rust и даже Go: вы начинаете чувствовать, что происходит под капотом, и можете писать более эффективный код.
Если вас заинтересовало низкоуровневое программирование, советую попробовать:
- Написать «Hello World» под DOS в эмуляторе DOSBox
- Поэкспериментировать с отладчиком
gdbи посмотреть на ассемблерный код своей C-программы (gcc -S) - Изучить инструкцию от Intel по набору команд x86 (она доступна бесплатно)
Удачи в изучении!