Эпиграф
“The registers are the CPU”s own workspace. They are not memory, and they are not variables. They are the places where the processor keeps the data it is working on right now — like a craftsman”s bench, where only the tools and materials for the current task are kept.”
“The stack is not just a pile of return addresses. It is the memory of the program”s decisions. Every
call
is a promise to return; everypush
is a thought set aside for later. In real mode, the stack pointer (SP) is the program”s short‑term memory.”“Segments are not an arbitrary limitation. They are a reflection of the physical world — of address lines, of memory chips, of the way data flows from one place to another. The segment:offset model is not a hack; it is a bridge between 16-bit registers and 20-bit addresses.”
— Рэй Дункан (Ray Duncan): »Programming in the MS‑DOS Environment», »Advanced MS‑DOS Programming»
"Registers are the fastest storage locations in the computer. But their speed is not what makes them special. What makes them special is that they are the only locations the CPU can operate on directly. You don”t compute with memory — you compute with registers, and memory is just a place to keep data between uses.”
“The stack is the foundation of structured programming. Without it, there would be no functions, no recursion, no local variables. The
call
andret
instructions are the verbs of program flow, and the stack is their grammar.”“Segmentation is often misunderstood as a flaw, but in the context of the 8086, it was a brilliant solution. It allowed a 16-bit processor to address 1 MB of memory — a compromise between cost, complexity, and capability. The segment registers (CS, DS, SS, ES) are not overhead — they are the keys to the kingdom.”
— Рэндэлл Хайд (Randall Hyde): »The Art of Assembly Language»
“Charm can fool you.”
— Ральф Броун (Ralf Brown): «Ralf Brown»s Interrupt» List» (часть 1, часть 2, часть 3)
Прежде чем погрузиться в технические детали, я хотел бы объяснить, почему эта часть статьи начинается не с кода и не с определений, а со слов тех, кто всё это создавал и понимал глубже всех.
Цитаты Рэя Дункана, Рэндэлла Хайды и Ральфа Броуна — это не просто отрывки из книг. Это фрагменты интеллектуального наследия, на котором выросло поколение программистов, реверс‑инженеров и создателей игр.
Рэй Дункан в своих книгах, «Programming in the MS‑DOS Environment» и «Advanced MS‑DOS Programming», не просто объяснял API DOS. Он учил мыслить в рамках реального режима, понимать, как железо, память и прерывания работают вместе. Его работы были библией для тех, кто писал на C и ассемблере под DOS. Они до сих пор остаются одними из самых точных описаний архитектуры 16-битной системы;
Рэндэлл Хайд с «The Art of Assembly Language» сделал невозможное — он превратил ассемблер из «языка машин» в предмет изучения, поддающийся логике и структуре. Его книга — это не справочник, а философия программирования, где каждая инструкция рассматривается как элемент мышления. Для многих, и меня в том числе, она стала первым шагом к пониманию, как работает компьютер «изнутри»;
Ральф Броун и его «Interrupt List» — это архив знаний, собранный вручную, без доступа к закрытым спецификациям. Его список прерываний — не просто справочник, а результат кропотливой работы, где каждый байт, каждая функция, каждый селектор были найдены, проверены и систематизированы. RBIL использовался при создании DOSBox, QEMU, FreeDOS и стал основой для всех, кто пытается понять, как работали старые программы
Изучение этих работ с последующим их осмыслением — первый шаг к настоящему реверс‑инжинирингу. Именно поэтому, я с них и начал.
Регистры
На Хабре, да и в других технических сообществах, я не раз видел дискуссии о том, что такое регистры. Один говорит: «Регистры — это переменные в CPU». Другой: «Это ячейки памяти». Третий: «Это состояния, а не данные». И все они, в какой‑то мере, правы. Но все они — часть истины. Я не люблю говорить: «Читайте документацию». Когда меня спрашивают, стараюсь объяснить понятно, по‑человечески, без сухих цитат. Но с регистрами решил поступить иначе. Не хочу навязывать своё определение, не хочу устраивать холивар, не хочу чтобы через год кто‑то написал: «Автор ошибся! Регистр — это не то, что он сказал». Я дам вам то, на что опираются все определения — ссылки на официальные документы Intel, от самого первого 4004 до современных x86–64. Это не побег от ответа, а приглашение встать на твёрдую почву. То, что я не могу объяснить без искажений, вы сможете прочитать в том, где это было впервые записано.
Процессор |
Год |
Документ |
Ссылка |
Intel 4004 |
1971 |
MCS-4 User’s Manual |
|
Intel 8008 |
1972 |
8008 User's Manual |
|
Intel 8080 |
1974 |
8080 User's Manual |
|
Intel 8086 |
1978 |
8086 Family Manual |
|
Intel 80286 |
1982 |
80286/80287 PRM |
|
Intel 80386 |
1985 |
80386 PPR |
|
Intel 80486 |
1989 |
80486 PRM |
|
IA-32 / x86-64 |
наше время |
Intel SDM |
Вывод: Регистры — это программно доступные ячейки хранения, определённые архитектурой, предназначенные для временного хранения операндов, адресов, состояния выполнения и управляющих данных. Они являются основным интерфейсом между программой и процессором, используются для выполнения арифметических, логических, адресных и управляющих операций.
— На основе: Intel® 386™ Programmer»s Reference Manual (1986), Volume 1, Section 3.2 «Registers», и Intel 80 486™ Microprocessor Family Programmer»s Reference Manual (1992), Chapter 3 «Architecture Overview»
Примечание: Некоторые разделы на сайте компании Intel могут быть недоступны для пользователей из России и Белоруссии. Я знаю что такое недоразумение не остановит ИТ‑умников и ИТ‑умниц, они смогут найти всю интересующую их информацию.
Категории регистров в 80386/80486
Регистры общего назначения (General-Purpose Registers, GPR).
Размер: 32 бита (в защищённом режиме) и 16 бит (в реальном режиме).
Назначение: Выполнение арифметических, логических операций, хранение адресов, параметров и временных данных.
32 бита |
16 бит |
8-битные части |
Короткое описание |
EAX |
AX |
AH, AL |
Аккумулятор (Accumulator) |
EBX |
BX |
BH, BL |
Базовый регистр (Base Register) |
ECX |
CX |
CH, CL |
Счётчик (Count Register) |
EDX |
DX |
DH, DL |
Регистр данных (Data Register) |
ESI |
SI |
- |
Индекс источника (Source Index) |
EDI |
DI |
- |
Индекс назначения (Destination Index) |
ESP |
SP |
- |
Указатель стека (Stack Pointer) |
EBP |
BP |
- |
Базовый указатель (Base Pointer) |
Подробное описание каждого регистра
EAX / AX / AH / AL — Аккумулятор (Accumulator).
Примечание: Термин «аккумулятор» уходит своими корнями в самые ранние процессоры 8080 и 8086
Назначение: основной регистр для арифметических операций, логических и операций ввода-вывода.
Особенности:
Используется по умолчанию в операциях
mul
,div
,in
,out
;Возвращает результат функций (аналог
return
в C);Часто используется в прерываниях DOS/BIOS (например, int 21h).
Примеры:
/* Turbo C 2.1 */
#include <dos.h>
void main(void)
{
_AX = 0x13; // Это то же самое, что и `mov ax, 13h`
geninterrupt(0x10); // Это то же самое, что и `int 10h`
}
; байты: b8 13 00
mov ax, 13h ; установка видеорежима BIOS в VGA 300x200, 256 цветов
; код mov ax, 13h равноценен такому коду из двух строк:
;
; mov ah, 00h
; mov al, 13h
; байты: cd 10
int 10h
/* Turbo C 2.1 */
#include <dos.h>
#include <stdio.h>
void main(void)
{
int res;
res = 5;
res = res + 10;
}
; байты: be 05 00
mov si, 0x5 ; SI = 5
; байты: 8b c6
mov ax, si ; переносим значение из источника в аккумулятор AX = SI = 5
; байты: 05 0a 00
add ax, 0xa ; AX = 15, SI = 5
EBX / BX / BH / BL — Базовый регистр (Base Register)
Примечание: Почему «базовый»? EBX/BX часто служит основой для вычисления адреса
Назначение: хранение адреса, от которого начинается доступ к данным в памяти.
Особенности:
Используется в режимах адресации вида [ebx + esi], [bx + di] и т.д.;
Часто указывает на начало структуры данных, таблицы или массива.
Пример:
mov ebx, [0x1234] ; адрес в памяти, в котором, предположительно,
; находится структура player
mov eax, [ebx + 4] ; получить координату Y
ECX / CX / CH / CL — Счётчик (Count Register)
Назначение: хранение счётчика итераций.
Особенности:
Часто используется в инструкциях
loop
,loope
,loopne
;Используется в строковых операциях с префиксом
rep
(rep movsb
,rep stosb
);В инструкциях сдвига (
shl
,shr
)CL
указывает количество бит для сдвига.
Пример:
mov cx, 0x64 ; 100 итераций
dec ax
loop 0x2345 ; адрес цикла
EDX / DX / DH / DL - Регистр данных (Data Register)
Назначение: часто используются в определенных инструкциях, таких как: ввод-вывод, умножение, деление.
Особенности:
В паре с EAX образует 32-битное значение (в 16-битном режиме) для
mul
иdiv
;Используется для передачи параметров в DOS API (например, DS:DX - адрес строки).
Примеры:
mov ax, 50000
mov dx, 0
div bx ; AX = частное, DX = остаток
mov dx, [0x3456] ; по этому адресу хранится имя файла
mov ah, 3Dh ; функция открытия файла
int 21h ; открыть файл
ESI / SI — Индекс источника (Source Index)
Назначение: указывает на источник данных в операциях со строками и массивами.
Особенности:
Используется с префиксом
rep
вmovsb
,movsw
,movsd
;Автоматически инкрементируется или декрементируется в зависимости от флага
DF
.
Пример:
mov esi, [0x1234]
mov edi, [0x5678]
mov ecx, 100
rep movsb ; скопировать 100 байт
EDI / DI — Индекс назначения (Destination Index)
Назначение: указывает на источник данных в операциях со строками и массивами.
Особенности:
Используется в
movsb
,stosb
,cmpsd
и других инструкциях;Часто указывает на видеопамять (0xA000h) или буфер вывода;
В паре с ESI используется для копирования и сравнения блоков памяти.
Пример:
mov edi, 0xA000h ; видеопамять
mov al, 15 ; белый пиксель
mov ecx, 64000 ; 320x200
rep stosb ; заполнить экран
ESP / SP — Указатель стека (Stack Pointer)
Назначение: указывает на вершину стека.
Особенности:
Автоматически изменяется командами
push
,pop
,call
,ret
;Используется в паре с
SS
(сегмент стека):SS:ESP
.
Пример:
push ax ; ESP -= 2
pop bx ; BX = [ESP], ESP += 2
Примечание: Не рекомендуется изменять напрямую
EBP / BP — Базовый указатель (Base Pointer)
Назначение: указывает на начало кадра стека текущей функции.
Особенности:
Используется для доступа к параметрам (
[ebp+8]
) и локальным переменным ([ebp-4]
);Сохраняется в начале функции: push bp ; mov bp, sp ;
Позволяет строить стек-трейсы.
Пример:
push bp
mov bp, sp
mov ax, [bp+4] ; первый параметр
mov bx, [bp-2] ; локальная переменная
pop bp
ret
Примечание: EBP (BP) — ключевой регистр для реверс-инжиниринга, так как позволяет восстановить сигнатуры функций.
Регистры общего назначения — это основной инструмент процессора для работы с данными. Понимание их назначения — ключ к анализу машинного кода, дизассемблированию и реверс-инжинирингу программ под DOS и ранние 32-битные системы.
Сегментные регистры (Segment Registers)
Размер: 16 бит.
Назначение: Хранение селекторов сегментов, используемых для адресов в памяти.
Регистр |
Описание |
Назначение |
CS |
Code Segment |
Сегмент кода — откуда процессор читает инструкции ( |
DS |
Data Segment |
Сегмент данных — используется по умолчанию при доступе к данным. |
SS |
Stack Segment |
Сегмент стека — используется с |
ES |
Extra Segment |
Экстра сегмент — часто используется в операциях со строками ( |
FS, GS |
80 386+ |
Frame Segment и Global Segment — дополнительные сегменты, используются в 64-битной архитектуре |
Зачем они нужны в реверсе?
При анализе.COM или.EXE файлов под DOS сегментные регистры критически важны:
Программа одновременно может работать с несколькими сегментами;
Смещения могут быть одинаковыми, но указывать на разные области памяти;
Непонимание сегментов ведёт к неверному толкованию данных и кода.
Что нужно знать:
CS:IP
— всегда текущая инструкция.CS
— сегмент кода,IP
— указатель команд. При запуске программы DOS загружает её в сегмент, значение которого неизвестно на этапе анализа. Ghidra и IDA по умолчанию используют относительные адреса (например, 0×100), но в реальности этоCS:0x100
, гдеCS
— динамическое значение. Если вы видите call 0×150, это не абсолютный адрес. Это относительный вызов внутри сегмента. Физический адрес: (CS << 4) + 0×150. Процессор автоматически обновляет EIP (IP) после каждой инструкции;DS
— сегмент данных: где хранятся строки, массивы, структуры. В DOS‑программах DS часто инициализируется какCS
(особенно в.COM файлах), потому что код и данные находятся в одном сегменте. Если вы видите mov ax, cs; mov ds, ax — значит, данные и код в одном сегменте;
mov ax, cs
mov ds, ax
DS:DX
— передача строк в DOS API и оканчивающаяся символом $.DX
— смещение,DS
— сегмент. ЕслиDS
не установлен, строка не будет найдена и возможно, игра использует односегментную модель (код = данные);
mov ah, 09h ; функция: вывод строки
mov dx, [0x1234] ; смещение строки
mov ax, cs
mov ds, ax ; DS = CS
int 21h
SS:SP
— стек, где хранятся параметры и возвраты.SS
— сегмент стека,SP
— указатель стека. В DOS-программахSS
иSP
часто инициализируются автоматически, но иногда устанавливаются вручную.
mov ax, stack_seg
mov ss, ax
mov sp, 0xFFFE
ES
— дополнительный сегмент: часто используется сDI
. В операциях со строкамиES
используется как сегмент назначения. Например,movsb
копирует байт изDS:SI
вES:DI
.
mov ax, 0xA000
mov es, ax
mov di, 0 ; начало видеопамяти
mov al, 15 ; белый пиксель
mov cx, 64000 ; 320x200
rep stosb ; заполнить экран
Регистр флагов EFLAGS
EFLAGS (FLAGS) — это 32-битный (16-битный) регистр состояния процессора, который хранит результаты выполнения инструкций и управляет поведением программы. Он не используется для хранения данных, но определяет, как будут работать условные переходы, циклы и отладка.
— На основе: Intel® 386™ Programmer’s Reference Manual, Volume 1, Section 3.2.3 "EFLAGS Register"
В контексте дизассемблирования, EFLAGS (FLAGS) — это «невидимый» аргумент условных операций. Вы не видите его напрямую в mov
или add
, но он полностью контролирует, куда пойдёт выполнение после cmp
, test
, sub
.
Бит |
Сокращение |
Название |
16 бит |
32 бита |
Описание |
0 |
CF |
Carry Flag |
+ |
+ |
Флаг переноса — устанавливается при переполнении арифметической операции |
1 |
Reserved |
||||
2 |
PF |
Parity Flag |
+ |
+ |
Чётность — устанавливается, если количество единиц в младших 8 битах чётное |
3 |
Reserved |
||||
4 |
AF |
Auxiliary Carry Flag |
+ |
+ |
Вспомогательный флаг переноса для BCD (Binary Coded Decimal) |
5 |
Reserved |
||||
6 |
ZF |
Zero Flag |
+ |
+ |
Флаг нуля — устанавливается, если результат равен нулю |
7 |
SF |
Sign Flag |
+ |
+ |
Флаг знака результата — устанавливается, если результат отрицательный |
8 |
TF |
Trap Flag |
+ |
+ |
Флаг трассировки — устанавливается в «пошаговый» режим (для отладки) |
9 |
IF |
Interrupt Flag |
+ |
+ |
Флаг прерываний — для управления прерываниями |
10 |
DF |
Direction Flag |
+ |
+ |
Флаг направления — для направления операций со строками |
11 |
OF |
Overflow Flag |
+ |
+ |
Флаг переполнения — установливается, если происходит переполнение со знаком, а именно когда переполняется бит, следующий за старшим знаковым битом |
12 |
IOPL |
I/O Privilege Level |
+ |
Уровень привилегий ввода‑вывода — уровень привилегий текущего выполняемого потока. IOPL 0 — это режим ядра, а 3 — пользовательский режим. |
|
13 |
IOPL |
I/O Privilege Level |
+ |
||
14 |
NT |
Nested Task Flag |
+ |
Флаг вложенной задачи — управляет цепочкой прерываний |
|
15 |
Reserved |
||||
16 |
RF |
Resume Flag |
+ |
Флаг возобновления — используется для обработки исключений во время отладки |
|
17 |
VM |
Virtual 8086 Mode Flag |
+ |
Флаг режима виртуальной машины 8086 — устанавливается для режима совместимости с 8086 |
|
18 |
AC |
Alignment Check Flag |
+ |
Флаг проверки выравнивания — устананавливается для проверки выравнивания памяти |
|
19 |
VIF |
Virtual Interrupt Flag |
+ |
Флаг виртуального прерывания — виртуальная версия флага IF в виртуальном режиме 8086 |
|
20 |
VIP |
Virtual Interrupt Pending Flag |
+ |
Флаг ожидания виртуального прерывания — устанавливается, когда прерывание находится в состоянии ожидания в виртуальном режиме 8086 |
|
21 |
ID |
Identification Flag |
+ |
Флаг ID — если этот флаг установлен, то поддерживается инструкция cpuid |
Основные флаги в реверсе:
CF (Carry Flag) (бит 0) — перенос и заём
Устанавливается при переносе из старшего бита (add, adc) или заеме (sub, sbb);
Используется в jc (jump if carry), jnc, а также в операциях с двойной точностью
Пример:
add ax, bx ; если переполнение, то CF = 1
jc overflow ; перейти, если переполнение
ZF (Zero Flag) (бит 6) — основа условных переходов:
Устанавливается, если результат операции равен нулю;
Используется в jz (jump if zero), jnz (jump if not zero).
Пример:
cmp ax, 10 ; условие: если AX == 10, то ZF = 1
jz label_done ; если ZF = 1, то перейти по метке
SF (Sign Flag) (бит 7) и OF (Overflow Flag) (бит 11) — проверка знака и переполнения:
SF = 1, если результат отрицательный (старший бит = 1);
OF = 1, если произошло переполнение со знаком (например, 7FFFh + 1 → 8000h)
Пример:
cmp eax, ebx
jl less_than ; jl (less), jg (greater) — не проверяют ZF, а используют SF и OF.
TF (Trap Flag) (бит 8) — «пошаговый» режим:
TF = 1 — после каждой инструкции вызывается исключение (Single Step);
Используется отладчиками (например: debug, SoftICE и др.)
Пример (управляемый код):
pushf
pop ax
or ax, 0100h ; установить TF
push ax
popf ; TF = 1
Анти-реверс (один из множества примеров):
pushf
pop ax
test ax, 0100h
jnz 0x1234 ; будьте уверены, вас здесь послали... на другую логическую ветку
; на анализ которой, вы потратите драгоценное время
; и это в лучшем случае...
IF (Interrupt Flag) (бит 9) — управление прерываниями:
IF = 1 — маскируемые прерывания (IRQ) разрешены;
IF = 0 — запрещены (критическая секция);
sti
— set interrupt flag (IF
= 1);cli
— clear interrupt flag (IF
= 0). Часто используется: перед изменением векторов прерываний, в критических секциях, в драйверах.
DF (Direction Flag) (бит 10) — направление строковых операций:
DF = 0 — строковые операции (movsb, lods, stos) идут вперёд (инкремент SI/DI);
DF = 1 — назад (декремент)
cld
— clear direction flag (DF
= 0)std
— set direction flag (DF
= 1)
Пример:
cld ; всегда надо проверять наличие. Если cld отсутствует перед rep,
; то возможно DF = 1 и копирование пойдёт назад
mov si, [0x1234] ; источник
mov di, [0x5678] ; назначение
mov cx, 100
rep movsb ; копировать 100 байт вперёд
Важно помнить:
Флаги устанавливаются не только
cmp
. Многие инструкции неявно изменяютEFLAGS
:add
,sub
,and
,or
,xor
,test
,dec
,inc
,shl
,shr
и др. test ax, ax - частый паттерн для проверки, равен ли регистр нулю
test ax, ax ; установит ZF, если AX == 0
jz zero_value
inc
иdec
не изменяютCF
. inc eax — увеличиваетEAX
, не затрагиваяCF
. add eax, 1 — увеличиваетEAX
, устанавливаетCF
при переполнении. Это используется в оптимизации и анти-реверсе
inc ax
jc overflow ; никогда не сработает!
Некоторые инструкции используют комбинацию флагов:
jo
/jno
— Overflow,js
/jns
— Sign,jp
/jnp
— Parity,ja
/jb
— Above/Below (на основе CF и ZF)
cmp al, 0Ah
jb is_digit ; если AL < 10 (unsigned)
Понимание EFLAGS (FLAGS) — ключ к расшифровке условных переходов, циклов, сравнений и защиты от отладки
Прерывания DOS: тайна, скрытая в коде
Представьте: вы запускаете DOS‑игру, то на экране отображаются ваши любимые герои, игра реагирует на ваши нажатия, записывает сохранки на диск... Но как это вообще возможно? Как эта самая программа может «общаться» с компьютером, не зная ни одного из его внутренних механизмов? Ответ кроется в прерываниях, «тайном языке», который знает каждый компьютер, но из людей мало кто его понимает. Прерывания — это как секретный код, скрытый в самом сердце DOS. Когда программа вызывает прерывание 10h, она «говорит»: «Пожалуйста, выведи текст на экран». Когда вызывается 16h — «Считай нажатие клавиши». Когда 21h — «Открой файл и прочитай из него данные».
Именно эти прерывания создают то самое волшебство, которое делало DOS-программы такими мощными, а игры такими увлекательными. Они позволяли программам использовать возможности компьютера, как будто они были частью системы.
Именно теперь вы готовы открыть дверь, за которой скрывается истина о том, как работают DOS-программы. Впереди вас ждут не просто цифры и коды, а настоящая археология программного обеспечения. Мы не будем изучать весь RBIL, но познакомимся с несколькими важными прерываниями, которые вы всегда встретите в DOS-играх.
Почему прерывания отправная точка для реверса DOS-игр?
Прерывания - это маяки и они:
Используют одну и ту же инструкцию в формате -
int N
;Выполняют конкретные, понятные действия: вывод на экран, чтение клавиш, открытие файлов и т. д.;
Находятся в начале и/или в конце функций, в циклах;
Перед вызовом
int
программа подготавливает регистры, чтобы передать параметры.
int 21h — Самое часто используемое прерывание в API DOS: файлы, ввод, выход и т .д.
Код функции |
AH |
Описание функции |
09h |
AH = 09h |
Вывести строку с терминатором ( |
3Dh |
AH = 3Dh |
Открыть файл |
3Fh |
AH = 3Fh |
Прочитать из файла |
40h |
AH = 40h |
Записать в файл |
4Ch |
AH = 4Ch |
Завершить программу |
Пример (загрузка файла):
mov ah, 3Dh ; функция: открыть файл
mov al, 0 ; доп. аргумент для функции: режим - только чтение
mov dx, [0x1234] ; DS:DX - строка с именем файла
int 21h ; инициализация прерывания
mov bx, ax ; сохраним дескриптор файла
Пример (выход из приложения):
mov ah, 4Ch ; функция: завершить программу
mov al, 0 ; доп. аргумент для функции: код возврата — 0
int 21h
int 10h — BIOS: видео и графика
Код функции |
AH |
Описание функции |
00h |
AH = 00h |
Установить видеорежим (список режимов) |
02h |
AH = 02h |
Установить курсор |
0Ch |
AH = 0Ch |
Нарисовать пиксель |
Пример (инициализация графического режима):
mov ax, 13h ; ah = 00h - функция: установить видеорежим
; доп. аргумент для функции:
; al = 13h - 320x200, 256/256K, VGA,MCGA,ATI VIP, text: 40x25
int 10h ; вызов BIOS
Пример (отрисовка пикселя):
mov ah, 0Ch ; функция: нарисовать пиксель
mov al, 15 ; доп. аргумент для функции: цвет - белый
mov cx, 100 ; X
mov dx, 50 ; Y
int 10h
int 16h - BIOS: ввод с клавиатуры
Код функции |
AH |
Описание функции |
00h |
AH = 00h |
Ждать нажатия (блокирующий) |
01h |
AH = 01h |
Проверить, нажата ли клавиша (неблокирующий) |
Пример (ожидание ввода с «зависанием» игры (блокировкой)):
mov ah, 00h ; функция: ждать нажатия
int 16h ; результат: AL = ASCII, AH = скан-код
int 33h — мышь (если поддерживается)
Код функции |
AH |
AL |
Описание функции |
00h |
AH = 00h |
AL = 00h |
Инициализировать мышь (в начале игры) |
00h |
AH = 00h |
AL = 03h |
Получить позицию мыши (в цикле) |
Пример:
mov ax, 0000h ; функция: инициализировать мышь
int 33h ; если AX != 0000h — мышь есть
Таймеры:
int 08h — таймер: используется анимаций, физики.
int 1Ch — пользовательский таймер. Можно установить свой обработчик для фоновых задач
Важно знать: int 20h — «старый» способ выхода (Terminate), используется в .COM файлах. Не использует код возврата
Работаем с Ghidra:
Вы можете помечать функции на основе тех прерываний, которые в них указаны (примеры):
FUN_00401000 → init_video() (если в ней вызывается int 10h);
FUN_00401200 → read_key() (если в ней вызывается int 16h);
FUN_00401500 → load_file() (int 21h, AH=3Dh)
Мы начали с цитат тех людей, котрые внесли максимаьный вклад в программирование под DOS. С Рэя Доббса, чьи книги «Programming in the MS‑DOS Environment» и «Advanced MS‑DOS Programming» научили нас мыслить в рамках реального режима, понимать, как код, данные и прерывания работают вместе. С Рэндэлла Хайды, чья «The Art of Assembly Language» превратила ассемблер из «языка машин» в предмет мышления, где каждая инструкция часть системы. И с Ральфа Броуна, чей Interrupt List стал для нас картой мира, в котором каждое int 10h
или int 21h
— это сигнал.
Именно их труды позволили нам перестать бояться .COM
и.EXE
‑файлов. Мы начать читать то, что казалось непознаваемым: регистры, стек, сегменты, флаги, прерывания. Мы разобрали, как AX
используется для передачи номера функции в int 10h
, как CX
служит счётчиком в циклах, как DS:DX
указывает на строку в int 21h
, как EFLAGS
управляют переходами после cmp
. Мы увидели, как регистры и сегменты работают вместе, и как прерывания становятся точками соприкосновения с системой. Мы увидели примеры кода на ассемблере и C, чтобы показать, как сильно они связаны.
Но это только продолжение того сложного пути, начатого в первой части. Теперь мы подошли к следующему рубежу — воссозданию кода. Впереди: разбор операндов, режимов адресации, структур данных. Попытаемся взять настоящие функции из DOS‑игры, «расшифровать» их логику и впервые написать на C код, который делает то же самое. Потому что реверс‑инжиниринг — это не просто анализ, а движение, от байтов к смыслообразующему коду.
P.S.: Все исходники и анализ кода, для этой статьи, будут опубликованы на GitHub через некоторое время. На данный момент готовы исходники только для Turbo C 2.1 и Turbo C++ 3.1. Частично готовы для TASM 4. Дополнительно готовятся на Watcom C 8.0 и Watcom С 10.5. Каждый компилятор по своему работает с оптимизацией кода, отличается работа с регистрами
Комментарии (6)
NightBlade74
14.08.2025 14:28Frame Segment и Global Segment - дополнительные сегменты, используются в 64-битной архитектуре
Ну, тут какая-то ерунда написана, i80386 появился за 18 лет до появления x64 (у Intel), так что два дополнительных сегментных регистра решали проблему дефицита сегментных регистров общего назначения (CS и SS, все же, специализированные). Особенно этот дефицит был заметен в защищенном режиме, т.к. влиял на производительность, поскольку тут в регистры записывался не адрес сегмента, а дескриптор, загрузка которого требовала еще множества телодвижений с таблицей дескрипторов и страниц виртуальной памяти.
Регистр SP я бы не относил к регистрам общего назначения, все ж таки он специальный. У самого Intel в документации регистры разделены на Data Group и Pointer and Index Group. Неплохо было бы указать, что стек растет со старших адресов к младшим, это важно.
IP вообще отдельным разделом обделен, что совершенно несправедливо, хоть он и не доступен программисту напрямую.
Может быть я пропустил, но не вижу, чтобы где-то указывалось, с какими сегментами по умолчанию работают разные указательные/индексные регистры.
drWhy
14.08.2025 14:28IP ... не доступен программисту напрямую.
ЕМНИП разрешено, хотя могу путать
POP IP
NeriaLab Автор
14.08.2025 14:28Да, FS и GS появились раньше, но они не используются в 32 битах так, как они используются в 64 битах, там куча ограничений и именно из-за них, не стал зацикливать на них внимание. Я много DOS игр "развернул" и с ними не сталкивался, хотя о них знаю;
ESP (SP) вот сами прочтите здесь (386™ SX MICROPROCESSOR PROGRAMMER'S REFERENCE MANUAL, "2.3.1 General Registers", p.2-7). Как я писал ранее - не хочу и не буду устраивать холивар;
-
Спасибо за Вашу критику и внимание, но тема несколько иная - здесь про реверс, а не изучение тонкостей ЯП Assembler. Именно для изучения Assembler я и дал ссылку на "библию" - "The Art of Assembly Language".
Зачем я даю ссылки на официальную документацию, на книги? Для того, чтобы люди сами всё прочитали, узнали что-то для себя. Если бы я в первой части учил бы работать с теми инструментами, которые я описал, то только бы через год, а может и два, приступил ко второй части. И еще через пару-тройку лет, к третьей.
P.S.: Небольшое интервью Хайда - "The rebirth of assembly language programming" (October 13, 2003)
JordanCpp
Спасибо за статью. Буду перечитывать.
NeriaLab Автор
Самое интересное будет в третьей части - практика. Первая часть подготовка инструментов и предварительный анализ, вторая часть - работа с информацией: RBIL, Art, мануалы. К третьей части нельзя подходить без первых двух.