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)
asm
section .data
msg db 'Hello world!',0xA
msg_size equ $-msg
section .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
asm
org 100h
mov ah, 9h ; функция вывода строки DOS
mov dx, hello
int 21h ; вызов прерывания
ret

hello 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 + badd eax, 42 ; eax += 42
ADCСложение с переносом (Carry)используется в многобайтовых числах
INCУвеличение на 1inc ebx
SUBВычитание: a = a - bsub [ecx], 5
SBBВычитание с заёмом
DECУменьшение на 1dec 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. Преобразования типов

ИнструкцияСмысл
CBWConvert Byte to Word (AL → AX)
CWDEConvert Word to Double (AX → EAX)
CWDConvert Word to Double (AX → DX:AX)
CDQConvert Double to Quad (EAX → EDX:EAX)
MOVSXПересылка с расширением знака
MOVZXПересылка с обнулением старших битов

8. Управление системой и прерывания

ИнструкцияОписание
INTПрограммное прерывание (например, int 0x80 в Linux)
HLTОстановка процессора до следующего прерывания
CLI / STIСбросить / установить флаг прерываний
NOPНет операции (часто используется для выравнивания кода)

Интересные факты об ассемблере

  1. Самый маленький исполняемый файл в DOS — программа, которая просто возвращает управление, занимает всего 1 байт (инструкция C3ret).
  2. Демосцена — сообщество программистов, которые соревнуются в написании максимально эффектных анимаций и музыки в крошечном размере. Например, .com-файлы размером 64, 128, 256 байт, которые показывают 3D-графику.
  3. Почему ассемблер до сих пор используется?
  1. В 1980-х программисты на ассемблере под ZX Spectrum и Commodore 64 умещали целые игры в 16–48 КБ. Сегодня одна фотография с телефона занимает в 100 раз больше.

Заключение

Ассемблер — это не «мёртвый язык». Это основа, на которой держится всё остальное. Понимание ассемблера делает вас лучшим программистом на C/C++, Rust и даже Go: вы начинаете чувствовать, что происходит под капотом, и можете писать более эффективный код.

Если вас заинтересовало низкоуровневое программирование, советую попробовать:

Удачи в изучении!

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Chat on Telegram