Руководство по ассемблеру MASM для архитектуры Intel x86-64

Тема в разделе "MASM", создана пользователем Mikl___, 28 окт 2026 в 08:20.

  1. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    4.364

    Работа с данными и памятью

    Преобразование данных


    Если мы попытаемся использовать в инструкции два несовместимых по размеру операнда, то мы получим ошибку. Например:
    Код (ASM):
    1. .data
    2.  num byte 17
    3. .code
    4. main proc
    5.  mov eax, num
    6.  ret
    7. main endp
    8. end
    В данном случае в 32-разрядный регистр EAX мы пытаемся поместить значение 8-разрядной переменной num. И в принципе то переменная num вполне может поместить в EAX, но ассемблер требует, чтобы оба операнда были одного размера. Соответственно программа не скомпилируется, и мы получим ошибку.
    Чтобы решить данную проблему, мы можем применять преобразование типов. Преобзование имеет следующую форму:
    Код (ASM):
    1. тип ptr значение
    Сначала указывается тип данных, в который мы преобразуем (byte, word, dword и т.д.). Далее идет оператор ptr, после которого указывается преобразуемое значение. Например, изменим предыдущую программу, применив преобразование:
    Код (ASM):
    1. .data
    2.  num byte 17
    3. .code
    4. main proc
    5.  mov eax, dword ptr num
    6.  ret
    7. main endp
    8. end
    Выражение dword ptr num означает, что значение num преобразуется к типу dword.
    Подобным образом можно преобразовывать константы
    Код (ASM):
    1. num = 19
    2. .code
    3. main proc
    4.  mov eax, dword ptr num
    5.  ret
    6. main endp
    7. end
    Надо учитывать, что преобразования от типов с большим размеров в тип меньшего размера могут сопроваждаться потерей точности. Например:
    Код (ASM):
    1. .data
    2.  num word 0FFFEh
    3. .code
    4. main proc
    5.  mov al, byte ptr num
    6.  ret
    7. main endp
    8. end
    Здесь 16-разрядную переменную num (равная 0FFFEh или 65534 в десятичной системе) преобразуем в 8-разрядный тип byte для помещения в регистр AL. При преобразовании будет из 16 бит в 8 бит в num будет отброшен старший бит и останется число FEh или 254.
    Если же значение преобразуемой переменной укладывается в диапазон значений требуемого типа, тогда проблем с преобразованием никаких нет.
     
    Последнее редактирование: 29 окт 2026 в 09:07
  2. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    4.364

    Работа с данными и памятью

    Символы

    Как и все данные, символы в памяти компьютера представляют набор битов - единиц и нулей, упорядоченных определенным образом. Большинство компьютеров для представления одного символа используют 1 или 2 байта. Windows, macOS, FreeBSD и Linux используют для символов кодировку ASCII или Unicode.
    Преобразование чисел в символы
    Таблица ASCII (American Standard Code for Information Interchange) представляет один из наиболее простых и популярных способов кодирования символов. ASCII представляет таблицу кодировки символов, в которой каждому символу соответствует определенное число. Например, латинской букве A соответствует число 65, а букве B - число 66. Более того цифровые символы имеет свои собственные числовые коды. Например, символу 0 соответствует число 48, символу 1 - число 49 и т.д. Каждый отдельный символ из таблицы ASCII представляет 1 байт.
    Таблица символов ASCII
    Первые 32 символа - управляющие последовательности.
    DECHEXBINСимволОписаниеDECHEXBINСимволОписание
    00000000000NULНоль/нулевой байт161000010000DLEКлюч связи данных
    10100000001SOHНачало заголовка171100010001DC1Управление устройством 1
    20200000010STXНачало текста181200010010DC2Управление устройством 2
    30300000011ETXКонец текста191300010011DC3Управление устройством 3
    40400000100EOTКонец передачи201400010100DC4Управление устройством 4
    50500000101ENQзапрос211500010101NAKОтрицательное подтверждение
    60600000110ACKПодтверждение221600000000SYNСинхронизация
    70700000111BELСигнал/звонок231700000000ETBКонец передаваемого блока
    80800001000BSШаг назад241800000000CANОтмена
    90900001001HTГоризонтальная табуляция251900000000EMКонец среды
    100A00001010LFПеревод строки261A00000000SUBЗамена
    110B00001011VTВертикальная табуляция271B00000000ESCВыход
    120C00001100FFНовая страница281C00000000FSРазделитель файлов
    130D00001101CRВозврат каретки291D00000000GSРазделитель групп
    140E00001110SOВыключить сдвиг301E00011110RSРазделитель записей
    150F00001111SIВключить сдвиг311F00011111USРазделитель модулей
    Печатные символы
    DECHEXBINСимволDECHEXBINСимволDECHEXBINСимволDECHEXBINСимвол
    32​
    20​
    00100000​
    пробел​
    56​
    38​
    00111000​
    8​
    80​
    50​
    01010000​
    P​
    104​
    68​
    01101000​
    h​
    33​
    21​
    00100001​
    !​
    57​
    39​
    00111001​
    9​
    81​
    51​
    01010001​
    Q​
    105​
    69​
    01101001​
    i​
    34​
    22​
    00100010​
    "​
    58​
    3A​
    00111010​
    :​
    82​
    52​
    01010010​
    R​
    106​
    6A​
    01101010​
    j​
    352300100011#593B00111011;835301010011S1076B01101011k
    362400100100$603C00111100<845401010100T1086C01101100l
    372500100101%613D00111101=855501010101U1096D01101101m
    382600100110&623E00111110>865601010110V1106E01101110n
    392700100111'633F00111111?875701010111W1116F01101111o
    402800101000(644001000000@885801011000X1127001110000p
    412900101001)654101000001A895901011001Y1137101110001q
    422A00101010*664201000010B905A01011010Z1147201110010r
    432B00101011+674301000011C915B01011011[1157301110011s
    442C00101100,684401000100D925C01011100\1167401110100t
    452D00101101-694501000101E935D01011101]1177501110101u
    462E00101110.704601000110F945E01011110^1187601110110v
    472F00101111/714701000111G955F01011111_1197701110111w
    4830001100000724801001000H966001100000`1207801111000x
    4931001100011734901001001I976101100001a1217901111001y
    5032001100102744A01001010J986201100010b1227A01111010z
    5133001100113754B01001011K996301100011c1237B01111011{
    5234001101004764C01001100L1006401100100d1247C01111100|
    5335001101015774D01001101M1016501100101e1257D01111101}
    5436001101106784E01001110N1026601100110f1267E01111110~
    5537001101117794F01001111O1036701100111g1277F01111111DEL
    Использование символов в ассемблере
    Для определения символа или строки в ассемблере применяются кавычки. Можно использовать как одинарные кавычки, так и двойные:
    Код (ASM):
    1. .code
    2. main proc
    3.  mov rax, "A"  ; в регистре RAX числовой код символа A - 65
    4.  mov r8, 'B'  ; в регистре R8 числовой код символа B - 66
    5.  ret
    6. main endp
    7. end
    Для определения переменных, которые представляют символы ASCII, применяется тип byte:
    Код (ASM):
    1. .data
    2.  char byte "D"  ; определяем один символ
    3. .code
    4. main proc
    5.  mov al, char  ; в регистре AL числовой код символа D - 68
    6.  ret
    7. main endp
    8. end
    Преобразование символов
    Если мы посмотрим на таблицу ASCII, то увидим, что данная кодировка упрощает в некоторых случаях манипуляции с данными. Так, строчные буквы отличаются от заглавным установленным 6-м битом:
    Буква A 01000001
    Буква a 01100001
    Соответственно, чтобы, например, перевести строчную букву в верхний регистр, достаточно снять шестой бит, а чтобы сделать заглавную букву строчной - наоборот, установить шестой бит. Для установки этого бита можно прибавить к числовому коду символа число 32, а для сброса этого бита - отнять от числового кода символа 32.
    Или например, из таблицы мы видим, что числовой код цифровых символов начинается с 48 (или с 30 в 16-й сиситеме). Поэтому для перехода от числа к его символьному представлению и обратно мы можем прибавлять/вычитать 48.
    Поскольку бинарный код чисел отличается от бинарного кода цифровых символов только старшим полубайтом, который у символов равен 3, то для перехода от символа к числу можно применить операцию AND, которая сбросит полубайт. А для перехода от числа к его символьному представлению можно установить полубайт с помощью операции OR. Например, преобразуем символ в число:
    Код (ASM):
    1. .code
    2. main proc
    3.  mov rax, "2"  ; числовой код цифры 2 равен 50 или 00110010
    4.  and rax, 0Fh  ; убираем старший полубайт - 0011 с помощью маски 00001111
    5.  ret
    6. main endp
    7. end
    Здесь преобразуем в число символ 2. В бинарной форме он равен 00110010. То есть нам надо сбросить первый полубайт 0011. Для этого применяем логическое умножение на число 0Fh, что в двоичной форме равно 00001111. То есть получим:
    00110010 * 00001111 = 00000010
    То есть получаем бинарное представление числа 2. Причем 0Fh - это маска, которая подходит для перехода от любого цифрового символа к числу для все числовых значений. Выполним обратный переход:
    Код (ASM):
    1. .code
    2. main proc
    3.  mov rax, 2  ; числовой код 2 равен 00000010
    4.  or rax, 30h  ; устанавливаем старший полубайт в 0011 с помощью маски 0011000
    5.  ret
    6. main endp
    7. end
    Здесь преобразуем число 2 в соответствующий символ. В бинарной форме число 2 равно 00000010. То есть нам надо установить старший полубайт в 0011. Для этого применяем логическое сложение с числом 30h, что в двоичной форме равно 00110000. То есть получим:
    00000010 + 00110000 = 00110010
    То есть получаем число 50 - числовой код символа "2".
    Подобным образом используя логического сложение с маской 20h можно получить из заглавной буквы строчную:
    Код (ASM):
    1. .code
    2. main proc
    3.  mov rax, "A"  ; числовой код символа A равен 65 или 01000001
    4.  or rax, 20h  ; 01000001 + 00100000 = 97 - код символа a
    5.  ret
    6. main endp
    7. end
    Используя логическое умножение на маску DFh или 11011111, можно перейти от строчной к заглавной букве:
    Код (ASM):
    1. .code
    2. main proc
    3.  mov rax, "a"  ; числовой код символа a равен 97 или 01100001
    4.  and rax, 0DFh  ; 01100001 * 11011111 = 01000001 = 65 - код символа A
    5.  ret
    6. main endp
    7. end
    Для той же цели можно прибавлять/вычитать 32 из кода символа:
    Код (ASM):
    1. .code
    2. main proc
    3.  mov rax, "A"  ; в RAX код 65
    4.  add rax, 32  ; 65 + 32 = 97 - код символа a
    5.  ret
    6. main endp
    7. end
     
    Последнее редактирование: 29 окт 2026 в 13:32
  3. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    4.364

    Работа с данными и памятью

    Режимы адресации. Косвенная адресация

    Режимы адресации памяти x86-64 обеспечивают гибкий доступ к памяти, позволяя легко обращаться к переменным, массивам, записям, указателям и другим сложным типам данных. Архитектура x86-64 предоставляет несколько режимов адресации:
    • Адресация регистров
    • Адресация памяти относительно счетчика команд (PC-relative memory addressing)
    • Косвенная адресация регистров ([reg64])
    • Косвенная адресация регистров со смещением ([reg64 + offset])
    • Косвенная адресация регистров со смещением и масштабированием ([reg64 + offset + reg64 * scale])
    Режим адресации регистров предоставляет доступ к регистрам общего назначения. Это стандартное обращение к регистрам процессора:
    Код (ASM):
    1. mov rax, rdx
    Регистры представляют самый быстрый тип памяти компьютера. Инструкции, которые используют регистры, короче и быстрее, чем те, которые обращаются к памяти. Поскольку для большинства вычислений требуется по крайней мере один регистр, режим адресации регистров популярен в ассемблере x86-64. Единственное ограничение - оба операнда должны иметь одну и ту же разрядность.
    Адресация памяти относительно счетчика команд
    Данный режим заключается в использовании имени переменной или константы, которая определена в одном из разделов - .data, .data?, .const, .code и т. д.
    Код (ASM):
    1. .data
    2.  i32 dword 33
    3. .code
    4. main proc
    5.  mov eax, i32
    6.  ret
    7. main endp
    8. end
    Для вычисления адреса переменных/констант применяется регистр RIP (Instruction Pointer), он же счетчик команд (Program Counter), отсюда и соответствующее название.
    Косвенная адресация регистров
    Косвенная адресация регистров (register-indirect addressing mode) означает, что регистр содержит адрес некоторого объекта. Используя данный адрес, извлекается данный объект. Чтобы указать, что мы хотим взять не значение из регистра, а значения из адреса, который хранится в регистре, название регистра помещается в квадратные скобки: [reg64] Где reg64 - это один из 64-разрядных регистров общего назначения: RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP, R8, R9, R10, R11, R12, R13, R14 и R15.
    Инструкция lea
    Для загрузки адреса некоторого объекта может применяться инструкция lea (load effective address). Данная инструкция имеет следующий синтаксис:
    Код (ASM):
    1. lea reg64, variable
    Первый операнд инструкции представляет 64-разрядный регистр общего назначения, куда загружается адрес переменной variable - второго операнда. Стоит отметить, что переменная variable может представлять любой размер, поскольку в регистр загружается именно ее адрес, а адрес в 64-разрядной архитектуре имеет длину 64 бита.
    Например, определим в секции данных переменную text и загрузим ее адрес в регистр rcx:
    Код (ASM):
    1. .data
    2. text byte "hello", 0
    3. .code
    4. main proc
    5.  lea rcx, text  ; загрузка в rcx адреса переменной text
    6.  ret
    7. main endp
    8. end
    Стоит отметить, что в C/C++ это было бы аналогично следующему коду:
    Код (ASM):
    1. char text[] = "hello";
    2. char *RCX = &text[0];
    Загрузка и сохранение значения по адресу
    Загруженный адрес затем можно использовать для получения данных при косвенной адресации:
    Код (ASM):
    1. .data
    2.  i32 dword 39
    3. .code
    4. main proc
    5.  lea rcx, i32  ; загружаем в RCX адрес переменной i32
    6.  mov eax, [rcx]  ; загружаем в EAX значение по адресу из RCX
    7.  ret
    8. main endp
    9. end
    Здесь в регистр RCX загружается адрес переменной i32. Затем в регистр EAX помещается значение, которое хранится по адресу из регистра RCX - то есть значение переменной i32.
    Также можно наоборот - сохранять значение по определенному адресу. Например:
    Код (ASM):
    1. .data
    2.  i32 dword ?
    3. .code
    4. main proc
    5.  lea rcx, i32  ; загружаем в регистр RCX адрес переменной i32
    6.  mov rdx, 12  ; в регистр RDX помещаем число 12
    7.  mov [rcx], rdx  ; в память по адресу [RCX] помещаем значение из RDX
    8.  mov eax, i32  ; в регистр EAX помещаем значение переменной i32 - 12
    9.  ret
    10. main endp
    11. end
    Здесь в секции данных определена неинициализированная переменная i32. Сохраним в эту переменную число 12. Для этого загружаем адрес этой переменной в регистр RCX
    Код (ASM):
    1. lea rcx, i32
    Сохраняемое число помещаем вначале в регистр RDX
    Код (ASM):
    1. mov rdx, 12
    Далее помещаем данные из регистра RDX в участок памяти, адрес которой хранится в регистре RCX, то есть по сути адрес переменной i32:
    Код (ASM):
    1. mov [rcx], rdx
    Стоит отметить, что ассемблер не позволяет напрямую сохранить по адресу числовую константу, поэтому надо сначала числовую константу поместить в регистр, и затем сохранить значение этого регистра по адресу переменной.
    Установка смещения
    Дополнительно для адреса можно установить смещение в байтах. В качестве смещения выступает 32-разрядное число со знаком
    Код (ASM):
    1. [reg64 ±смещение]
    Например:
    Код (ASM):
    1. .data
    2.  n1 dword 1
    3.  n2 dword 3
    4. .code
    5. main proc
    6.  lea rbx, n1  ; загружаем в регистр RBX адрес переменной n1
    7.  mov eax, [rbx + 4]  ; в регистр EAX загружаем данные с адреса [rbx + 4], то есть переменной n2
    8.  ret
    9. main endp
    10. end
    Здесь в регистр RBX помещается адрес переменной n1. Эта переменная занимает 4 байта (тип dword), соответственно адрес следующей переменной - n2 равен [RBX + 4]. И если мы возьмем данные по этому адресу, то фактически мы получим значение переменной n2.
    Аналогично можно сохранять данные по адресу, используя смещение
    Код (ASM):
    1. .data
    2.  n1 dword 1
    3.  n2 dword 3
    4. .code
    5. main proc
    6.  lea rbx, n1  ; загружаем в регистр RBX адрес переменной n1
    7.  mov rdx, 15  ; в регистр RDX помещаем число 15
    8.  mov [rbx + 4], rdx  ; в память по адресу [RBX] помещаем значение из RDX
    9.  mov eax, n2  ; в регистр EAX помещаем значение переменной n2 - 15
    10.  ret
    11. main endp
    12. end
    При этом смещение также может быть отрицательным. В этом случае адрес сдвигается на указанное число байтов назад.
    Использование смещений очень удобно, когда речь идет об обращении к частям каких-то сложных данных, например, массивов.
    Адресация с масштабированием
    Адресация с масштабированием позволяет комбинировать два регистра плюс смещение и умножать значение индексного регистра на коэффициент масштабирования 1, 2, 4 или 8, чтобы вычислить адрес данных:
    Код (ASM):
    1. [base_reg64 + index_reg64*scale]
    2. [base_reg64 + index_reg64*scale + offset]
    3. [base_reg64 + index_reg64*scale - offset]
    base_reg64 представляет любой 64-битный регистр общего назначения, index_reg64 представляет любой 64-битный регистр общего назначения, кроме RSP, а scale (коэффициент масштабирования) должен быть одной из констант 1, 2, 4 или 8. Дополнительно можно прибавлять или отнимать смещение.
    Например, у нас есть набор некоторых данных, и нам надо в этом наборе получить какой-то определенный элемент:
    Код (ASM):
    1. .data
    2.  numbers dword 11, 12, 13, 14, 15, 16, 17, 18
    3. .code
    4. main proc
    5.  lea rbx, numbers  ; загружаем в регистр RBX адрес переменной numbers
    6.  mov rsi, 5  ; индекс получаемого двойного слова - получаем 6 элемент
    7.  mov eax, [rbx + rsi*4]  ; в регистр EAX загружаем данные с адреса [rbx + rsi*4], то есть число 16
    8.  ret
    9. main endp
    10. end
    Здесь данные представлены набором двойных слов numbers. Каждое число в этом наборе занимает 4 байта. Допустим, мы хотим получить 6-е число. Для этого в регистр RSI помещаем индекс нужного нам элемента - число 5. То есть, чтобы получить 6-е число, нам надо пройти 5 предыдущих чисел.
    Код (ASM):
    1. mov rsi, 5
    Затем загружаем в регистр EAX значение 6-го числа из numbers, то есть число 16.
    Код (ASM):
    1. mov eax, [rbx + rsi*4]
    Адрес складывается из следующих компонентов. Во-первых, в регистре RBX адрес начала набора numbers, то есть адрес первого элемента из этого набора. Чтобы дойти к адресу 6-го элемента от адреса 1-го элемента, нам надо пройти 5 элементов, поэтому в регистре RSI число 5. Каждый элемент занимает 4 байта, соответственно, чтобы пройти от 1-го элемента до 6-го, нам надо пройти rsi*4 = 5 *4 = 20 байтов.
    Преобразование данных
    Как и в общем случае при косвенной адресации можно выполнять преобразования данных. Например, возьмем следующую программу:
    Код (ASM):
    1. .data
    2.  n1 byte 11
    3.  n2 byte 12
    4.  n3 byte 13
    5.  n4 byte 14
    6. .code
    7. main proc
    8.  lea rbx, n1  ; загружаем в регистр RBX адрес переменной n1
    9.  mov eax, [rbx]  ; в регистр EAX загружаем данные с адреса [rbx], то есть переменной n1
    10.  ret
    11. main endp
    12. end
    В регистр RBX помещается адрес переменной n1, затем данные из памяти по адресу из RBX помещаются в регистр EAX. То есть мы ожидаем, что в регистре EAX в итоге будет число 11 - значение переменной n1. Однако если мы скомпилируем программу с помощью MASM, запустим и проверим содержимое регистра EAX, то мы увидим, что там не число 11:
    c:\asm>ml64 hello.asm /link /entry:main
    Microsoft (R) Macro Assembler (x64) Version 14.36.32532.0
    Copyright (C) Microsoft Corporation. All rights reserved.

    Assembling: hello.asm
    Microsoft (R) Incremental Linker Version 14.36.32532.0
    Copyright (C) Microsoft Corporation. All rights reserved.

    /OUT:hello.exe
    hello.obj
    /entry:main

    c:\asm>hello

    c:\asm>echo %ERRORLEVEL%
    235736075

    c:\asm>

    Число 235736075 вряд ли равно значению переменной n1. Поскольку значение помещается в 32-разрядный регистр EAX, то по адресу из RBX выбирается 4 байта, то есть все последующие переменные - n2, n3 и n4. Но нам надо получить только значение переменной n1. Для этого мы можем получить значение в 8-разрядный регистр:
    Код (ASM):
    1. .data
    2.  n1 byte 11
    3.  n2 byte 12
    4.  n3 byte 13
    5.  n4 byte 14
    6. .code
    7. main proc
    8.  lea rbx, n1  ; загружаем в регистр RBX адрес переменной n1
    9.  mov rax, 0  ; обнуляем регистр rax
    10.  mov al, [rbx]  ; AL = 11 (и EAX = 11)
    11.  ret
    12. main endp
    13. end
    Либо мы можем применить преобразование до байта:
    Код (ASM):
    1. .data
    2.  n1 byte 11
    3.  n2 byte 12
    4.  n3 byte 13
    5.  n4 byte 14
    6. .code
    7. main proc
    8.  lea rbx, n1
    9.  movzx eax, byte ptr[rbx]
    10.  ret
    11. main endp
    12. end
    Таким образом, данные по адресу из RBX преобразуются до 1 байта, а инструкция movzx заполняется старшие разряды нулями, благодаря чему нет несоответствия по размеру между операндами.
     
    Последнее редактирование: 29 окт 2026 в 13:54
  4. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    4.364

    Работа с данными и памятью

    Выравнивание

    Для ускорения доступа объектам в памяти в архитектуре x86-64 может применяться выравнивание. Выравнивание данных не всегда необходимо, так как кэш-архитектура современных процессоров x86-64 на самом деле обрабатывает большинство невыровненных данных. Тем не менее иногда доступ к данным, которые не выровнены по соответствующему адресу, может потребовать дополнительного времени. И если скорость доступа к подобным данным критична, то можно использовать выравнивание.
    Выравнивание означает, что адрес объекта является четным и кратен определенному размеру. В качестве такого размера обычно выбирают размер объекта, если он не более 32 байт и является степенью двойки. Для объектов размером более 32 байт обычно достаточно выравнивание объекта по 8-, 16- или 32-байтовой адресной границе. Для установки выравнивания применяется директива align
    Код (ASM):
    1. align выравнивание
    В качестве операнда директиве передается целочисленное значение: 1, 2, 4, 8 или 16. Как правило, если нужен максимально быстрый доступ, то необходимо выбирать значение, равное размеру объекта, по которому надо выровнять. То есть для выравнивания по слову (16 бит) применяется выражение align 2, для выравнивания по двойному слову (4 байта) применяется выражение align 4, по четверным словам - align 8 и т.д. Если размер объекта не является степенью числа 2, то обычно для выравнивания используется первая ближайщая степень числа 2 (максимум до 16 байт), большее размера объекта. При этом объекты real80 (и tbyte) выравниваются только по 8 байтам. Например:
    Код (ASM):
    1. .data
    2.  d1 dword 33
    3.  w1 word 22
    4.  b1 byte 11
    5. align 2
    6.  w2 word 10
    Если MASM встречает директиву align в разделе .data, он выравняет последующую переменную по адресу, который кратен указанному выравниванию. Если MASM определяет, что текущий адрес директивы align не является целым числом, кратным указанному значению, то после предыдущей переменной добавляются дополнительные байты до тех пор, пока текущий адрес в разделе .data не станет кратен указанному значению.
    Первое объявление .data в программе (на Windows) размещает свои переменные по адресу, который кратен 4096 байтам. Какая бы переменная ни появилась первой в этом объявлении .data, она гарантированно будет выровнена. Каждая последующая переменная размещается по адресу, который представляет собой сумму размеров всех предыдущих переменных плюс начальный адрес этой секции .data.
    Например:
    Код (ASM):
    1. .data
    2.  d1 dword 33
    3.  w1 word 22
    4.  b1 byte 11
    5. align 2
    6.  w2 word 10
    7. .code
    8. main proc
    9.  mov rax, 0
    10.  mov ax, [w2 - 4]  ; смещаемся назад на 4 байта - [w2-4] = [w1]
    11.  ret
    12. main endp
    13. end
    Здесь первые три переменных в сумме занимают 7 байт (4 + 2 + 1):
    Код (ASM):
    1. d1 dword 33
    2. w1 word 22
    3. b1 byte 11
    Но за ними идет еще одна переменная с выравниванием по 2 байтам:
    Код (ASM):
    1. align 2
    2. w2 word 10
    Поэтому после переменной b1 добавляется еще один байт, чтобы последующая переменная w2 располагалась по четному адресу, который кратен 2. Соотвественно, чтобы перейти от адреса переменной w2 к адресу переменной w1, надо вычесть 4 байта (размер переменной w1 + размер переменной b1 + 1 байт для установки выравнивания). И в данном случае в регистре AX окажется значение переменной w1.
    Код (ASM):
    1. mov ax, [w2 - 2]  ; [w2-2] = [b1]
    Без выравнивания для перехода от адреса переменной w2 к адресу переменной w1 потребовалось бы смещение назад на 3 байта:
    Код (ASM):
    1. .data
    2.  d1 word 33
    3.  w1 word 22
    4.  b1 byte 11
    5.  w2 word 10  ; переменная w2 не выравнена
    6. .code
    7. main proc
    8.  mov rax, 0
    9.  mov ax, [w2-3]  ; смещаемся назад на 3 байта
    10.  ret
    11. main endp
    12. end
    Выравнивание делает программу немного больше (на несколько байтов), однако в обмен получаем более быстрый доступ к данным. Учитывая, что программа увеличится всего на несколько байтов, то это может быть неплохим решением.
     
    Последнее редактирование: 29 окт 2026 в 14:00
  5. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    4.364

    Работа с данными и памятью

    Стек

    Стек — это динамическая структура данных, которая хранит важную информация о программе, включая локальные переменные, информацию о подпрограммах и временные данные. В архитектуре x86-64 стек реализуется с помощью сегмента стека. Процессор x86-64 управляет стеком через регистр RSP (указатель стека). Когда программа начинает выполняться, операционная система инициализирует регистр RSP адресом последней ячейки памяти в сегменте стека. В процессе работы программы данные записываются в сегмент стека или, наоборот, извлекаются из стека.
    Стек растет от больших адресов к меньшим, то есть при добавлении в стек данных, адрес добавляемых данных будет уменьшаться. При запуске программы обычно стек выровнен по 8-байтовой границе. Например, RSP может хранить адрес 0x0000009BA9EFF9A8 (адрес оканчивается на 8).
    Для добавления данных в стек применяется инструкция push, которая имеет следующий синтаксис:
    Код (ASM):
    1. push reg16
    2. push reg64
    3. push mem16
    4. push mem64
    5. pushw constant16
    6. push constant32  ; расширяется до 64 бит
    Итак, мы можем добавить в стек значения 16- и 64-разрядного регистра, 16- и 64-разрядной переменной и 16- и 32-разрядной константы (32-битная констранта расширяется до 64 бит).
    При выполнении инструкции push от значения регистра RSP вычитается размер операнда. А по адресу, который хранится в стеке, помещается значение операнда.
    Код (ASM):
    1. RSP = RSP - размер операнда
    2. [RSP] = значение операнда
    Инструкция pop позволяет, наоборот, взять из стека значение, адрес которого хранится в текущий момент в регистре RSP. Эта инструкция имеет следующий синтаксис
    Код (ASM):
    1. pop reg16
    2. pop reg64
    3. pop memory16
    4. pop memory64
    Инструкция в качестве операнда получает место, куда надо сохранить данные из стека. Это может быть или 16- и 64-разрядный регистр, или 16- и 64-разрядная переменная. При выполнении этой инструкции в операнд помещается значение, которое хранится в адресе из RSP. А само значение RSP увеличивается на размер операнда:
    Код (ASM):
    1. operand = [RSP]
    2. RSP = RSP + размер операнда
    Например, возьмем следующую программу:
    Код (ASM):
    1. .code
    2. main proc
    3.  mov rdx, 15
    4.  push rdx  ; в конец стека помещаем содержимое регистра RDX
    5.  pop rax  ; значение из конца стека помещаем в регистр RAX
    6.  ret
    7. main endp
    8. end
    Допустим, регистр RSP содержит изначально адрес 00FF_FFF8h.
    [​IMG]
    Пусть в регистре RDX хранится некоторое значение, которое с помощи инструкции push заталкивается в стек:
    Код (ASM):
    1. push rdx
    В результате в последующие 8 байт начиная с адреса, который хранится в RSP, помещается значение из регистра RDX (в данном случае число 15). А в регистр RSP будет помещен адрес RSP-8, то есть условно 00FF_FFF0h и сохранит текущее значение регистра RDX в ячейках памяти начиная с 00FF_FFF0h по 00FF_FFF7h, то есть займет 8 байт.
    [​IMG]
    Затем извлекаем из стека значение по адресу, который хранится в RSP, в регистр RAX:
    Код (ASM):
    1. pop rax
    В результате в RAX помещается число из стека (в данном случае число 15). А в регистр RSP будет помещен адрес RSP+8, то есть условно 00FF_FFF0h.
    [​IMG]
    Принцип LIFO и сохранение регистров в стек
    Наиболее распространенное использование команд push и pop — это сохранение значений регистров во время промежуточных вычислений. Поскольку регистры — лучшее место для хранения временных значений, и регистры также могут потребоваться для других операций, поэтому в процессе программы легко исчерпать регистры. Инструкции push и pop позволяют сохранить начальные значения регистров при старте программы, а при завершении программы восстановить эти значения.
    Следует учитывать, что стек представляет структуру LIFO (Last In, First Out или Последний вошел, первый вышел), что значит, что получение данных из стека происходит в порядке, обратном их добавлению. Рассмотрим следующую программу:
    Код (ASM):
    1. .code
    2. main proc
    3.  mov rax, 11
    4.  mov rdx, 33
    5.  push rax
    6.  push rdx
    7.  pop rax
    8.  pop rdx
    9.  ret
    10. main endp
    11. end
    Допустим, в самом начале программы до добавления данных стек регистр RSP хранит адрес 00FF_FFF8.
    [​IMG]
    Затем добавляем в стек значение регистра RAX:
    Код (ASM):
    1. push rax
    Адрес в RSP смещается на 8 байтов и указывает на адрес значения из регистра RAX
    [​IMG]
    Далее добавляем в стек значение регистра RDX:
    Код (ASM):
    1. push rdx
    Адрес в RSP смещается на 8 байтов и указывает на адрес значения из регистра RDX
    [​IMG]
    После добавления мы последовательно извлекаем данные. Первая инструкция извлекает данные, на которые указывает регистр RSP, в регистр RAX:
    Код (ASM):
    1. pop rax
    [​IMG]
    Однако поскольку RSP перед операцией извлечения указывал на адрес последнего добавленного значения - значения регистра RDX, то регистр RAX получит значение регистра RDX. Соответственно при последующей инструкции pop:
    Код (ASM):
    1. pop rdx
    Регистр RDX получить значение регистра RAX, которое было в регистре RAX до добавления в стек.
    Поэтому если мы хотим восстановить начальные значения регистров, то нам надо извлекать значения в порядке, обратном добавлению
    Код (ASM):
    1. push rax
    2. push rdx
    3. pop rdx ; Последним добавлено значение RDX, поэтому сначала извлекаем в RDX
    4. pop rax
    В любом случае стоит помнить, что количество инструкций push и pop должно быть равно, сколько раз мы добавили данные в стек, столько раз мы должны получить данные из стека.
    Сохранение флагов состояния
    Ассемблер предоставляет дополнительную пару инструкций pushfq и popfq для сохранения и восстановления соответственно регистра RFLAGS (и всех флагов состояния). Например:
    Код (ASM):
    1. .code
    2. main proc
    3.  pushfq  ; сохраняем значения флагов
    4.  mov al, 255
    5.  add al, 2  ; 255 + 2 = 257 - флаг CF будет установлен
    6.  popfq  ; восстанавливаем значения флагов
    7.  jc set  ; если флаг CF установлен, переход к метке set
    8.  mov rax, 0
    9.  ret
    10. set:
    11.  mov rax, 1
    12.  ret
    13. main endp
    14. end
    Здесь инструкцией pushfq сначала сохраняем флаги. По умолчанию флаг переноса CF будет равен 0.
    Затем выполняем сложение 255 + 2, что даст 257 и что очевидно за пределы разрядности регистра AL, соответственно будет установлен флаг переноса CF. Далее с помощью инструкции jc set переходим к метке set, если флаг CF установлен. Однако перед этой инструкцией мы восстанавливаем флаги - popfq. То есть флаг CF получит свое значение 0, и никакого перехода к метке set не произойдет.
    Восстановление стека без извлечения данных
    При завершении программы следует восстановить адрес в RSP. Как выше было показано, для этого мы можем использовать инструкцию pop. Однако может сложиться ситуация, что данные не требуется извлекать из стека. Например, в зависимости от некоторых условий данные могут понадобиться, а могут не понадобиться. Если данные не нужны, извлекать каждые 8 байт отдельно с помощью инструкции pop не имеет смысла, особенно если надо извлечь много данных из стека. И в этом случае мы можем восстановить адрес в RSP, просто прибавив нужное значение - смещение относительно начального адреса. Например:
    Код (ASM):
    1. .code
    2. main proc
    3.  mov rax, 2
    4.  mov rdx, 3
    5.  push rax
    6.  push rdx
    7.  add rsp, 16  ; прибавляем к адресу в RSP 16 байт
    8.  ret
    9. main endp
    10. end
    Здесь в стек помещаем значения двух регистров - RAX и RDX, то есть адрес в RSP уменьшится на 16 байт (совокупный размер двух регистров). И чтобы быстро восстановить стек, прибавляем к адресу в RSP 16 байт:
    Код (ASM):
    1. add rsp, 16
    Подобным образом можно вычитать из адреса в RSP определенное число, тем самым резервируя в стеке некоторое пространство:
    Код (ASM):
    1. .code
    2. main proc
    3.  sub rsp, 16  ; смещаем адрес в RSP на 16 байт
    4.  mov rax, 2
    5.  add rsp, 16  ; восстанавливаем адрес в RSP
    6.  ret
    7. main endp
    8. end
    Вычитание определенного количество байтов из стека может потребоваться при взаимодействии с некоторыми внешними функциями, например, на языках C/С++ для резервирования места для параметров функций или для выравнивания стека. Так, до вызова функций в Windows в соответствии с Microsoft ABI стек должен быть выровнен по 16-байтовой границе. При использовании инструкции push для сохранения значения регистра перед вызовом внешней функции надо убедиться, адрес RSP кратен 16 байтам.
    Косвенная адресация в стеке
    Как и в случае с любым другим регистром, в отношении регистра стека RSP можно использовать косвенную адресацию и обращаться к данным в стеке без смещения указателя RSP. Например:
    Код (ASM):
    1. .code
    2. main proc
    3.  sub rsp, 16  ; резервируем в стеке 16 байт
    4.  mov rdx, 11
    5.  mov [rsp], rdx  ; помещаем в стек значение регистра RDX
    6.  mov rax, [rsp]  ; в RAX помещаем значение по адресу из RSP - число 11
    7.  add rsp, 16  ; восстанавливаем указатель стека
    8.  ret
    9. main endp
    10. end
    В данном случае в стек помещаем число из регистра RDX - число 11.
    Код (ASM):
    1. mov [rsp], rdx
    Подобную форму размещения данных в стеке можно рассматривать как альтернативу инструкции push, если нам не надо изменять значение указателя стека RSP. То есть мы можем сохранить таким образом данные по адресу в RSP, но после этого RSP продолжает хранить тот же адрес.
    Далее в регистр RAX помещаем значение, которое располагается по адресу из RSP. Фактически это тот адрес, где располагается число 11.
    Код (ASM):
    1. mov rax, [rsp]
    Аналогично можно применять смещения и масштабирование. Например:
    Код (ASM):
    1. .code
    2. main proc
    3.  push 12
    4.  push 13
    5.  push 14
    6.  mov rax, [rsp + 8]  ; [rsp + 8] - адрес значения 13
    7.  ; извлекаем сохраненные значения в r8, r9, r10
    8.  pop r8
    9.  pop r9
    10.  pop r10
    11.  ret
    12. main endp
    13. end
    Здесь в стек последовательно помещаются три числа 12, 13, 14. Каждое число будет занимать 8 байт. После добавления адрес в RSP будет указывать на адрес последнего добавленного числа - 14. И чтобы, например, получить предыдущее число - 13, нам надо к адресу в RSP прибавить 8. И в данном случае получаем это число в регистр RAX.
    Код (ASM):
    1. mov rax, [rsp + 8]
    Соотвественно чтобы получить из стека первое число - 12, надо к адресу в RSP прибавить 16:
    Код (ASM):
    1. mov rax, [rsp + 16]
    Другой пример:
    Код (ASM):
    1. .code
    2. main proc
    3.  sub rsp, 16  ; резервируем в стеке 16 байт
    4.  mov rbx, 12
    5.  mov rdx, 13
    6.  mov [rsp], rbx  ; помещаем значение из RBX по адресу RSP
    7.  mov [rsp+8], rdx  ; помещаем значение из RDX по адресу RSP+8
    8.  mov rax, [rsp]  ; в RAX помещаем значение по адресу из RSP
    9.  add rax, [rsp+8]  ; складываем с числом из RSP+8
    10.  add rsp, 16  ; восстанавливаем данные из стека
    11.  ret
    12. main endp
    13. end
    Здесь по адресу RSP располагается значение региста RBX, а по адресу RSP+8 - регистра RDX. В RAX извлекаем значение по адресу RSP (12), и затем складываем его со значением из RSP+8 (13). Таким образом, в RAX будет число 25.
    Ограничения места в стеке
    По умолчанию данные в стеке занимают то место, которое соответствует размеру регистра. Так, инструкция mov [rsp], rbx помещала в стек 8 байт из RBX. То есть данные в стеке занимали 8 байт. Однако в примере выше в RBX хранится число, для которого достаточно и 1 байта, для него не требуется аж 8 байт. Пространство в стеке расходуется не экономично. В этом случае с помощью преобразования данных мы можем явным образом указать, сколько байтов мы хотим использовать в стеке
    Код (ASM):
    1. .code
    2. main proc
    3.  sub rsp, 16  ; резервируем в стеке 16 байт
    4.  mov rbx, 11
    5.  mov rdx, 12
    6.  mov [rsp], bl  ; помещаем байт из BL по адресу RSP
    7.  mov [rsp+1], dl  ; помещаем байт из DL по адресу RSP+1
    8.  movzx rax, byte ptr [rsp]  ; в AL помещаем значение по адресу из RSP
    9.  add al, byte ptr [rsp+1]  ; складываем с числом из RSP+1
    10.  add rsp, 16  ; восстанавливаем данные из стека
    11.  ret
    12. main endp
    13. end
    Здесь в RAX извлекаем только один байт:
    Код (ASM):
    1. movzx rax, byte ptr [rsp]
    Затем добавляем в AL еще один байт из стека, который хранится по адресу RSP+1:
    Код (ASM):
    1. add al, byte ptr [rsp+1]
    Преобразования данных могут быть особенно актуальны, если необходимо поместить в стек непосредственный операнд:
    Код (ASM):
    1. .code
    2. main proc
    3.  sub rsp, 16  ; резервируем в стеке 16 байт
    4.  mov qword ptr [rsp], 11  ; в стек помещаем число 11 - оно занимает 8 байт
    5.  mov word ptr [rsp+8], 15  ; по адресу в RSP+8 помещаем 2-байтное число 15
    6.  mov rax, qword ptr [rsp]  ; RAX = 11
    7.  add ax, word ptr [rsp+8]  ; складываем с числом из RSP+8
    8.  add rsp, 16  ; восстанавливаем данные из стека
    9.  ret
    10. main endp
    11. end
    Здесь сначала помещаем в стек число 11, которое будет занисать 8 байт, так как мы приводим к типу qword:
    Код (ASM):
    1. mov qword ptr [rsp], 11
    Затем по адресу RSP+2 помещаем число 15, которое будет занимать 2 байта:
    Код (ASM):
    1. mov word ptr [rsp+8], 15
    Затем извлекаем эти данные и складываем их:
    Код (ASM):
    1. mov rax, qword ptr [rsp]  ; RAX = 11
    2. add ax, word ptr [rsp+8]  ; складываем с числом из RSP+8
    Стоит обратить внимание, что тип извлекаемых данных соответствует размеру регистра, либо при извлечение 1 байта применется операция расширения movzx/movsx. Некорректное извлечение может привести к некорректным результатам:
    Код (ASM):
    1. .code
    2. main proc
    3.  sub rsp, 16  ; резервируем в стеке 16 байт
    4.  mov byte ptr [rsp], 10  ; по адресу в RSP помещаем число 10
    5.  mov byte ptr [rsp+1], 11  ; по адресу в RSP+1 помещаем число 11
    6.  mov rax, [rsp]  ; RAX = ?
    7.  add rsp, 16  ; восстанавливаем данные из стека
    8.  ret
    9. main endp
    10. end
    Здесь в стек последовательно помещаем два байта - числа 10 и 11. Затем извлекам из стека данные. Но обратите внимание как идет извлечение
    Код (ASM):
    1. mov rax, [rsp]  ; RAX = ?
    Какое число окажется в регистре RAX? Здесь в регистр RAX из стека скопируются 8 байт, из которых первые два байта - это числа 10 и 11, остальные 6 байт неопределены. Соответственно результат в RAX неопределен. И чтобы взять только нужную нам порцию данных, следует использовать преобразование типов:
    Код (ASM):
    1. .code
    2. main proc
    3.  sub rsp, 16  ; резервируем в стеке 16 байт
    4.  mov byte ptr [rsp], 10  ; по адресу в RSP помещаем число 10
    5.  mov byte ptr [rsp+1], 11  ; по адресу в RSP+1 помещаем число 11
    6.  movzx rax, byte ptr [rsp]  ; RAX = 10
    7.  add rsp, 16  ; восстанавливаем данные из стека
    8.  ret
    9. main endp
    10. end
    Расположение данных в стеке
    Может возникнуть вопрос, как числа размером более одного байта сохраняются в стеке? Рассмотрим следующую программу:
    Код (ASM):
    1. .code
    2. main proc
    3.  mov rdx, 0102030405060708h  ; в RDX 8-байтное число
    4.  push rdx  ; сохраняем число из RDX в стек
    5.  movzx rax, byte ptr [rsp] ; в RAX сохраняем 1 байт по адресу в RSP
    6.  pop rdx  ; извлекаем число из стека в RDX
    7.  ret
    8. main endp
    9. end
    Здесь в регистр RDX сохраняем 16-ричное число 0x0102030405060708. Далее это число помещаем в стек.
    Затем с помощью инструкции movzx помещаем в RAX 1 байт, извлеченный из стека по адресу который хранится в RSP. Чему будет равен этот байт - 0, 1 или 8?
    При добавлении в стек старшие биты числа располагаются по старшим адресам, а младшие биты по младшим адресам:
    [​IMG]
    Соответственно в регистре RAX окажется число 0х08
    Аналогично, если мы хотим получить старший байт 8-битного числа, то мы можем сместиться относительно адреса RSP вниз на 7 байт (то есть RSP+7):
    Код (ASM):
    1. movzx rax, byte ptr [rsp+7]  ; RAX = 0x01
     
    Последнее редактирование: 29 окт 2026 в 14:52
  6. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    4.364

    Работа с данными и памятью

    Указатели

    Ассемблер позволяет определять указатели - переменные, которые хранят адрес других объектов. Для получения адреса объекта применяется выражение
    Код (ASM):
    1. offset переменная
    Поскольку адрес в архитектуре x86-64 занимает 8 байт, то для хранения адреса нам надо определить переменную с типом qword. Например, определим переменную и получим ее адрес:
    Код (ASM):
    1. .data
    2.  n dword 22
    3.  pn qword ?
    4. .code
    5. main proc
    6.  ; установка указателя
    7.  mov rax, offset n  ; загружаем адрес переменной n
    8.  mov pn, rax  ; сохраняем адрес в переменную pn
    9.  ; получение данных по указателю
    10.  mov rbx, pn  ; в rbx помещаем адрес из переменной pn
    11.  mov rax, [rbx]  ; обращаемся по адресу из rbx
    12.  ret
    13. main endp
    14. end
    Здесь определена переменная-указатель pn, которая представляет тип qword и которая будет хранить адрес переменной n. Сначала получаем адрес в регистр RAX и затем передаем его в указатель pn:
    Код (ASM):
    1. mov rax, offset n
    2. mov pn, rax
    Если нам надо использовать значение по указателю, можно сначала поместить значение указателя в регистр, а затем использовать косвенную адресацию для получения самого значения по указателю.
    Код (ASM):
    1. mov rbx, pn  ; в rbx помещаем адрес из переменной p
    2. mov rax, [rbx]  ; обращаемся по адресу из rbx
    В итоге в регистре RAX окажется число 22 - значение переменной n.
    При этом мы можем получить адрес уже при определении переменной-указателя:
    Код (ASM):
    1. .data
    2.  n dword 11
    3.  pn qword offset n  ; получаем адрес переменной n
    4. .code
    5. main proc
    6.  ; получение данных по указателю
    7.  mov rbx, pn  ; в rbx помещаем адрес из переменной pn
    8.  mov rax, [rbx]  ; в RAX число 11
    9.  ret
    10. main endp
    11. end
    Указатель необязательно должен представлять переменную - это может быть и константа:
    Код (ASM):
    1. pn equ offset n  ; константа pn указывает на переменную n
    2. .data
    3.  n dword 12
    4. .code
    5. main proc
    6.  ; получение данных по указателю
    7.  mov rbx, pn  ; в rbx помещаем адрес из переменной pn
    8.  mov rax, [rbx]  ; в RAX число 11
    9.  ret
    10. main endp
    11. end
    Операции с адресом
    При получении адреса мы можем использовать ряд операций:
    Код (ASM):
    1. offset переменная [смещение]
    2. offset переменная + смещение
    3. offset переменная - смещение
    В этом случае мы можем получить адрес, который смещен относительно адреса переменной на несколько байтов вперед или назад. Выражение переменная [смещение] фактически эквивалентно выражению переменная + смещение. Например:
    Код (ASM):
    1. .data
    2.  nums dword 11, 12, 13, 14, 15, 16
    3.  p qword ?
    4. .code
    5. main proc
    6.  ; установка указателя
    7.  mov rax, offset nums + 4  ; загружаем адрес переменной nums + 4
    8.  mov p, rax  ; сохраняем адрес в переменную p
    9.  ; получение данных по указателю
    10.  mov rbx, p  ; в rbx помещаем адрес из переменной p
    11.  mov rax, [rbx]  ; обращаемся по адресу из rbx
    12.  ret
    13. main endp
    14. end
    Здесь переменная nums представляет набор 32-битных чисел. Выражение offset nums возвращает адрес перемнной num - адрес первого числа из этого набора. Соответственно чтобы получить адрес второго числа из этого набора, нам надо прибавить размер одного элемента в байтах, то есть 4 байта - offset nums + 4
     
    Последнее редактирование: 29 окт 2026 в 14:59
  7. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    4.364

    Работа с данными и памятью

    Строки

    Строки представляют набор символовю Если нам надо определить строку, то мы можем указать набор символов в одинарных или двойных кавычках:
    Код (ASM):
    1. .data
    2.  text byte "Hello"
    3. .code
    4. main proc
    5.  mov al, text  ; в регистре AL числовой код символа H - 72
    6.  ret
    7. main endp
    8. end
    Здесь переменная text представляет набор из пяти байтов. Если мы поместим переменную, которая представляет набор байт, в регистр, то он будет содержать код первого байта.
    Также можно через запятую перечислять числовые коды символов, которые входят в строку:
    Код (ASM):
    1. .data
    2.  text byte 72, 101, 108, 108, 111
    3. .code
    4. main proc
    5.  xor rax, rax
    6.  mov al, text  ; в регистре AL числовой код символа H
    7.  ret
    8. main endp
    9. end
    Строки с нулевым байтом
    Распространенным типом строк являются строки, которые оканчиваются на нулевой байт. Что может быть очень удобно при операциях со строками, поскольку мы знаем где у нее завершение. И, например, при взаимодействии с кодом C/C++ обычно требуется, чтобы строки завершались нулевым байтом - признаком завершения строки. В этом случае при определении строк в ассемблере можно в конце строки дописать числовой код символа:
    Код (ASM):
    1. .data
    2.  text byte "Hello", 0
    Здесь к строке Hello добавляется нулевой байт.
    Используем нулевой байт для подсчета количества символов в строке:
    Код (ASM):
    1. .data
    2.  text byte "Hello", 0
    3. .code
    4. main proc
    5.  lea rbx, text
    6.  xor rax, rax  ; в регистре RAX по умолчанию 0
    7. startWhile:
    8.  cmp byte ptr [rbx+rax*1], 0  ; сравниваем значение по адресу [rbx+rax*1] c 0
    9.  je endWhile  ; если дошли до нулевого байта, выходим из цикла
    10.  inc rax  ; увеличиваем счетчик на 1
    11.  jmp startWhile  ; переходим обратно к startWhile
    12. endWhile:
    13.  ret
    14. main endp
    15. end
    В данном случае регистр RAX выступает в качестве счетчика - он хранит количество символов и по умолчанию равен 0.
    В регистр RBX загружаем адрес строки text. На метку startWhile проецируем начило цикла, в котором будем подсчитывать количество символов. Для подсчета с помощью инструкции cmp сравниваем значение по адресу [rbx+rax*1] с нулевым байтом.
    Код (ASM):
    1. cmp byte ptr [rbx+rax*1], 0
    В начале программы, поскольку RAX равен 0, то выражение [rbx+rax*1] даст нам адрес первого символа в строке text. Если символ равен нулевому байту, то переходим к метке endWhile, то есть фактически выходим из цикла
    Код (ASM):
    1. je endWhile
    Если же символ по адресу [rbx+rax*1] не нулевой байт, то увеличиваем значение в регистре RAX на 1 и переходим опять к метке startWhile. И таким образом, сравниваем следующий символ строки text с нулевым байтом.
    С одной строны, это удобно, мы всегда знаем, где у строки конец, и можем определить строку из произвольного количества символов. В то же время в каких=то ситуациях это может быть не оптимальный способ представления строки. Например, опять же, чтобы узнать количество символов, нам надо перебрать всю строку.
    Строки с фиксированным количеством символов
    В строках с фиксированным количеством символов обычно первый символ указывает на количество символов:
    Код (ASM):
    1. .data
    2.  text byte 5, "Hello"
    3. .code
    4. main proc
    5.  lea rbx, text
    6.  movzx rax, byte ptr [rbx]  ; в регистре RAX - 5
    7.  ret
    8. main endp
    9. end
    Однако в этом случае мы ограничены строками длиной 255 байт (из которых первый байт - длина строки). Также можно автоматически вычислять длину с помощью констант:
    Код (ASM):
    1. .data
    2.  text byte textLength, "Hello World"
    3.  textLength = $-text-1
    4. .code
    5. main proc
    6.  lea rbx, text
    7.  mov rax, textLength  ; в регистре RAX - 11
    8.  ret
    9. main endp
    10. end
     
    Последнее редактирование: 29 окт 2026 в 15:05
  8. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    4.364

    Работа с данными и памятью

    Сортировка массива

    Рассмотрим сортировку массива:
    Код (ASM):
    1. .data
    2.  ; массив для сортировки:
    3.  numbers dword 2, 1, 14, 11
    4.  dword 5, 9, 4, 10
    5.  dword 6, 8, 16, 12
    6.  dword 7, 3, 15, 13
    7.  elemSize = 4  ; размер одного элемента
    8.  numbersLength = ($ - numbers) / elemSize  ; число элементов
    9.  swap byte ? ; swap - индикатор, был ли на предыдущей итерации обмен значениями
    10. .code
    11. ; Параметры
    12. ; в RCX - адрес массива
    13. ; в RDX - количество элементов
    14. sort proc
    15.  dec rdx ; нам надо пройти на 1 цикл меньше, поэтому уменьшаем на 1
    16. ; внешний цикл:
    17. outer:
    18.  mov swap, 0
    19.  xor rbx, rbx  ; RBX = 0 - счетчик проверенных элементов
    20. inner:
    21.  cmp rbx, rdx  ; пока не сравним все элементы, то есть RBX < RDX
    22.  jnb innerEnd
    23.  mov eax, [rcx + rbx*elemSize]  ; EAX = numbers[RBX]
    24.  cmp eax, [rcx + rbx*elemSize + elemSize] ; сравниваем, большем ли число из EAX, чем numbers[RBX + 1]
    25.  jna withoutSwap ; если EAX не больше, то переходим к метке withoutSwap
    26.  
    27.  ; если же numbers[RBX] > numbers[RBX + 1], то обмениваем значениями
    28.  mov r8d, [rcx + rbx*elemSize + elemSize]
    29.  mov [rcx + rbx*elemSize + elemSize], eax
    30.  mov [rcx + rbx*elemSize], r8d
    31.  mov swap, 1  ; устанавливаем, что был совершен обмен
    32. withoutSwap:
    33.  inc rbx  ; увеличиваем счетчик элементов
    34.  jmp inner  ; переход для сравнения новой пары элементов
    35. ; Если был совершен обмен, то переходим обратно к outer
    36. innerEnd:
    37.  cmp swap, 1
    38.  je outer
    39.  ret
    40. sort endp
    41. main proc
    42.  lea rcx, numbers
    43.  mov rdx, numbersLength ; 16 элементов в массиве
    44.  call sort
    45.  ; для примера получим последний элемент
    46.  lea rbx, numbers
    47.  mov eax, [rbx + (numbersLength-1) * elemSize ]
    48.  ret
    49. main endp
    50. end
    В секции данных определен массив numbers, который будет сортировать, а также длина массива в элементах - numbersLength, размер одного элемента - elemSize и переменная swap - индикатор обмена значениями между элементами. При сравнении если предыдущий элемент больше следующего, мы меняем их местами. Чтобы указать, что обмен был, переменной swap будет присваиваться значение 1. Если обмена не было, то есть предыдущий элемент оказался меньше последующего, то swap будет равно 0.
    Затем идет определение функции sort, которая сортирует массив. Предполагается, что адрес массива будет передаваться через регистр RCX, а количество элементов в массиве - через RDX. Поскольку при сортировке пузырьком нам надо провести numbersLength-1 сравнений, то уменьшаем значение RDX на единицу:
    Код (ASM):
    1. dec rdx
    На метку outer проецируется внешний цикл. В нем у переменной swap устанавливаем значение 0:
    Код (ASM):
    1. mov swap, 0
    И обнуляем регистр RBX, который будет выступать в качестве счетчика сравнений:
    Код (ASM):
    1. xor rbx, rbx
    Дальше идет условный внутренний цикл, который проецируется на метку inner. В нем мы сравниваем значения RBX и RDX - количество проведенных сравнений и максимальное количество сравнений.
    Код (ASM):
    1. cmp rbx, rdx
    2. jnb innerEnd
    Если значение RBX равно значению RDX, значит все сравнения проведены, и переходим к метке innerEnd.
    Если же не все значения сравнены, то в регистр EAX помещаем первый элемент для сравнения (по адресу [rcx + rbx*elemSize] ) и сравниваем его со следующим элементом (по адресу [rcx + rbx*elemSize + elemSize])
    Код (ASM):
    1. mov eax, [rcx + rbx*elemSize]
    2. cmp eax, [rcx + rbx*elemSize + elemSize]
    Если первый элемент оказался меньше, то переходим к метке withoutSwap:
    Код (ASM):
    1. jna withoutSwap
    Если же первый элемент оказался больше второго, то обмениваем их значения:
    Код (ASM):
    1. mov r8d, [rcx + rbx*elemSize + elemSize]
    2. mov [rcx + rbx*elemSize + elemSize], eax
    3. mov [rcx + rbx*elemSize], r8d
    4. mov swap, 1
    После обмена переменная swap получает значение 1.
    Далее на метке withoutSwap увеличиваем счетчик сравнений и переходим обратно к метке inner, пока не сравним все элементы.
    Код (ASM):
    1. withoutSwap:
    2.  inc rbx
    3.  jmp inner
    Если сравнены все элементы, срабатывает код после метки innerEnd:
    Код (ASM):
    1. innerEnd:
    2.  cmp swap, 1
    3.  je outer
    Если сравнены все элементы, но произошел обмен, и соответственно переменная swap равна 1, то значит, что еще может потребоваться обмен значений. Поэтому, если swap равна 1, то переходим обратно к метке outer и повторно сравниваем все элементы друг с другом.
    Если же swap равна 0, то есть при сравнениях не было не одного обмена, а это значит, что массив уже отсортирован, то выходим из функции.
    В процедуре main вызываем процедуру sort, передавая в регистры RCX и RDX адрес массива и количество элементов:
    Код (ASM):
    1. lea rcx, numbers
    2. mov rdx, numbersLength
    3. call sort
     
    Последнее редактирование: 29 окт 2026 в 15:13
  9. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    4.364

    Работа с данными и памятью

    Многомерные массивы

    Для ассемблера не существует многомерных массивов - все данные в наборе располагаются в памяти последовательно. Однако на логическом уровне может быть удобно представлять данные в виде многомерного массива. Например, возьмем двухмерные массивы, которые можно представить в виде таблицы, где строки представляют одно измерение, а столбцы - второе измерение:
    Код (ASM):
    1. numbers dword 1, 2, 3, 4
    2.  dword 5, 6, 7, 8
    3.  dword 9, 10, 11, 12
    Здесь условно у нас есть три строки. В каждой строке по четрые столбца. В памяти такой набор данных будет располагаться последовательно, то есть фактически будет аналогичен следующему определению:
    Код (ASM):
    1. numbers dword 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12
    Но в ряде задач довольно удобно работать с данными как с двухмерными массивами, например, при математических вычислениях с матрицами, для определения карт в играх и т.д. Соотвественно может возникнуть вопрос получения конкретного элемента из подобного двухмерного массива.
    Если речь идет о двухмерном массиве/таблице, то формула получения элемента из определенной строки и столбца выглядит следующим образом:
    Код (ASM):
    1. element_address = base_address + (row_index * col_size + col_index) *element_size
    Где base_address - адрес начала массива (его первого элемента), row_index - номер строки элемента, col_size - количество столбцов/элементов в строке, col_index - номер столбца элемента, а element_size - размер элементов в байтах. Нумерация строк и столбцов начинается с нуля, то есть номер первой строки или первого столбца - 0.
    Например, возьмем выше определенный двухмерных массив numbers. Предположим, нам надо получить элемент в 2-й строке и 3-столбце (число 7). Тогда расчет адреса элемента будет выглядеть следующим образом:
    Код (ASM):
    1. element_address = адрес_numbers + (1 * 4 + 2) *4
    То есть мы получим, что для получения адреса числа 7 нам надо к адресу массива numbers прибавить (1 * 4 + 2) *4 = 6 * 4 = 24 байт. Посмотрим, как это будет выглядеть в программе на ассемблере:
    Код (ASM):
    1. .data
    2.  numbers dword 1, 2, 3, 4
    3.  dword 5, 6, 7, 8
    4.  dword 9, 10, 11, 12
    5.  elemSize = 4  ; размер одного элемента - dword
    6.  rows = 3  ; кол-во строк
    7.  cols =  4  ; кол-во столбцов
    8.  rowIndex dword 1  ; обращаемся к 2-й строке
    9.  colIndex dword 2  ; обращаемся к 3-му столбцу
    10. ; Нам надо получить [rbx + (cols * rowIndex + colIndex) * elemSize]
    11. .code
    12. main proc
    13.  lea rbx, numbers  ; в RBX - адрес массива numbers
    14.  xor rdx, rdx
    15.  mov edx, rowIndex  ; в RDX индекс строки - rowIndex
    16.  imul edx, cols  ; cols * rowIndex
    17.  add edx, colIndex  ; cols * rowIndex + colIndex
    18.  mov eax, [rbx + rdx * elemSize]  ; EAX = 7
    19.  ret
    20. main endp
    21. end
    В регистр RBX помещаем адрес массива numbers. Это будет базовый адрес, относительно которого будут идти все расчеты
    Код (ASM):
    1. lea rbx, numbers
    Для вычисления эффективного адреса сначала в регистр EDX помещаем индекс строки элемента - переменную rowIndex
    Код (ASM):
    1. mov edx, rowIndex
    Затем умножаем индекс строки на количество столбцов
    Код (ASM):
    1. imul edx, cols
    Затем прибавляем к значению в EDX индекс столбца нужного элемента:
    Код (ASM):
    1. add edx, colIndex
    Таким образом мы получили индекс элемента внутри массива numbers. Далее мы можем умножить этот индекс на размер элементов и прибавить к базовому адресу - адресу первого элемента в numbers. И таким образом получим адрес числа 7:
    Код (ASM):
    1. mov eax, [rbx + rdx * elemSize]
    Опеределение массива
    Стоит отметить, что если нам надо определить неинициализированный многомерный массив или многомерный массив, где все элементы имеют какое-то одно начальное значение, то мы можем использовать оператор dup:
    Код (ASM):
    1. numbers dword 3*4 dup (1)
    Здесь определяется массив numbers из 12 элементов, каждый из которых равен 1.
    MASM позволяет использовать вложенные выражения dup. Так, вместо предыдущего определения массива мы могли бы написать так:
    Код (ASM):
    1. numbers dword 3 dup (4 dup(1))
    Можно прочитать эту запись так: определяем набор из 3 элементов, где каждый вложенный элемент представляет набор из 4 чисел dword. То есть по сути 3 строки, каждая из которых содержит 4 столбца.
     
    Последнее редактирование: 29 окт 2026 в 16:13
  10. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    4.364

    Работа с данными и памятью

    xlat и поиск в таблицах

    Таблицы представляют некоторые наборы неизменяемых данных. В ряде ситуаций таблицы позволяют ускорить выполнение программы, хотя за хранение самой таблицы придется пожертвовать небольшим количеством дополнительной памяти. А архитектура x86-64 предоставляет специальную инструкцию xlat для упрощения поиска в таблицах.
    Например, возьмем программу, которая переводит символ из нижнего регистра в верхний:
    Код (ASM):
    1. .data
    2.  character byte "b"
    3. .code
    4. main proc
    5.  xor rax, rax
    6.  mov al, character
    7.  cmp al, 'a'
    8.  jb notLower
    9.  cmp al, 'z'
    10.  ja notLower
    11.  and al, 5fh ; преобразуем код символа в верхний регистр
    12.  mov character, al
    13. notLower:
    14. ; остальная часть программы
    15.  ret
    16. main endp
    17. end
    В регистр AL передается код символа из переменной AL. Далее проверяем код символа - соответствует ли он диапазону для букв в нижнем регистре. Если соответствует, с помощью операции AND применяем маску 5fh, которая убирает 6-бит и тем самым преобразует символ в верхний регистр. Однако если символ изначально представляет букву в верхнем регистре или представляет не букву, а цифру или что-то еще, то переходим к метке notLower
    Таблицы позволяют сократить код программы:
    Код (ASM):
    1. .data
    2. table byte 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32
    3.  byte 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64
    4.  byte 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96
    5.  byte 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90
    6. character byte "b"
    7. .code
    8. main proc
    9.  xor rax, rax
    10.  mov al, character
    11.  lea rbx, table
    12.  xlat
    13.  mov character, al
    14. ; остальная часть программы
    15.  ret
    16. main endp
    17. end
    Здесь таблица представлена переменной table, которая хранит коды символов ASCII. Причем, начиная с 97 байта по 123 размещены коды символов ASCII в верхнем регистре, на которые будут заменяться символы в нижнем регистре. Эти коды символов в верхнем регистре располагаются по индексам соответствующих им символов в нижнем регистре. Например, буква "a" имеет код 97. По индексу 97 в таблице располагается код символа "A" - число 65.
    Для замены символов применяется инструкция xlat, которая фактически аналогична инструкции
    Код (ASM):
    1. mov al, [rbx + al * 1]
    Эта инструкция использует текущее значение регистра AL в качестве индекса в массиве, базовый адрес которого находится в RBX. Она выбирает байт по этому индексу в массиве и копирует этот байт в регистр AL.
     
    Последнее редактирование: 29 окт 2026 в 16:18
  11. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    4.364

    Работа с данными и памятью

    Структуры

    Ассемблер MASM позволяет определять структуры - составные типы данных, которые состоят из элементарных типов. Для определения структуры применяется слово struct:
    название_структуры struct
    поля структуры
    название_структуры ends

    Сначала идет название структуры, затем слово struct. Далее идут переменные структуры (еще назваются полями) - те элементы, из которых состоит структуры. Завершается определение структуры также названием структуры, за которым следует слово end. Например:
    point struct ; структура представляет точку
    x word ? ; координата Х
    y word ? ; координата Y
    point ends

    Здесь определена структура point, которая условно представляет точку. В этой структуре определено два поля. Поля определяются также как и обычные переменные. Первая переменная представляет координату X, а вторая переменная - координату Y. Обе переменных представляют тип word.
    Структура можно определяться в любом месте программы до своего использования. Затем мы можем определить переменную этой структуры, например, в секции .data
    Код (ASM):
    1. .data
    2. center point {}
    Здесь определена переменная center, которая представляет структуру point. Для определения переменной структуры применяются фигурные скобки {}.
    Доступ к полям структуры
    При обращении к полям структуры следует понимать, что в памяти поля структуры располагаются рядом в том порядке, в котором они определены. Для доступа к полю структуры применяется имя переменной структуры (которое по сути представляет адрес структуры) и смещение поля относительно начала структуры. Например:
    Код (ASM):
    1. center[0] или center  ; обращение к полю Х
    2. center[2]  ; обращение к полю y
    Поскольку поле x - переменная типа word и занимает 2 байта, то смещение поля y относительно начала структуры будет 2 байта. Пример полностью:
    Код (ASM):
    1. point struct  ; структура представляет точку
    2.  x word ?  ; координата Х
    3.  y word ?  ; координата Y
    4. point ends
    5. .data
    6.  center point {}
    7. .code
    8. main proc
    9.  mov center[0], 5  ; устанавливаем значения полей
    10.  mov center[2], 4
    11.  xor rax, rax
    12.  mov ax, word ptr center[2]  ; EAX = 4
    13.  ret
    14. main endp
    15. end
    При передаче данных из регистра требуется привести поле структуры к типу регистра:
    Код (ASM):
    1. mov rax, 5
    2. mov word ptr center, ax
    Несмотря на то, что поле x структуры point представляет тип word, но все равно преобразуем данные в тип word, поскольку получаем данные из 16-разрядного регистра ax.
    Поскольку манипулировать смещениями тех или иных полей не очень удобно, особенно когда полей довольно много, то MASM позволяет обращаться к полям по их имени:имя_структуры.имя_поля Например:
    Код (ASM):
    1. point struct  ; структура представляет точку
    2.  x word ?  ; координата Х
    3.  y word ?  ; координата Y
    4. point ends
    5. .data
    6.  center point {}
    7. .code
    8. main proc
    9.  mov rdx, 5
    10.  mov center.x, 6
    11.  mov center.y, dx
    12.  xor rax, rax
    13.  mov ax, word ptr center.y  ; EAX = 5
    14.  ret
    15. main endp
    16. end
    Инициализация полей структуры
    При определении переменной структуры можно сразу инициализировать ее поля, передав им значения в фигурных скобках:
    Код (ASM):
    1. .data
    2. center point {10, 15}
    Здесь x=10, а y=15.
    Если поле структуры - набор, то значения этого набора указываются в фигурных скобках:
    Код (ASM):
    1. list struct
    2.  elements byte 5 dup(?)
    3. list ends
    4. .data
    5.  numbers list {{3, 4, 5, 6, 7}}
    6. .code
    7. main proc
    8.  xor rax, rax
    9.  mov al, byte ptr numbers[2]  ; EAX = 5
    10.  ret
    11. main endp
    12. end
    Массив структур
    Также из структур можно создавать массив:
    Код (ASM):
    1. point struct  ; структура представляет точку
    2.  x word ?  ; координата Х
    3.  y word ?  ; координата Y
    4. point ends
    5. .data
    6.  ;points point 3 dup ({})  ; массив из трех точек
    7.  points point{1, 2}
    8.  point {3, 4}
    9.  point {5,6}
    10. .code
    11. main proc
    12.  mov rax, 9
    13.  lea rbx, points
    14.  mov [rbx + 2*4], al  ; обращаемся к координате x третьей точкb
    15.  movzx rax, word ptr [rbx + 2*4]  ; AX = 9
    16.  ret
    17. main endp
    18. end
    В данном случае определяется массив из трех точек - структур point. Для обращения к точкам в регистр RBX загружаем адрес массива points. Каждый элемент point в массиве занимает 4 байта (по 2 байта на каждую координату). Поэтому чтобы обратиться к адресу координаты x третьей точки, применяется выражение [rbx + 2*4]. Соответственно для обращения к координате y этой точки, надо к этому адресу прибавить еще 2 байта: [rbx + 2*4+2]
    Также можно определять неинициализованный массив структур:
    Код (ASM):
    1. .data
    2. points point 3 dup ({})  ; массив из трех точек
    Выравнивание в структурах
    Для более быстрого обращения к полям структуры можно использовать выравнивание, которое устанавливается с помощью директивы align. Например
    Код (ASM):
    1. point3D struct  ; структура представляет точку в пространстве
    2.  x byte ?  ; координата Х
    3.  y byte ?  ; координата Y
    4.  z byte ?  ; координата Z
    5. point3D ends
    Переменная y здесь будет располагаться по нечетному адресу. Применим выравнивание:
    Код (ASM):
    1. point3D struct  ; структура представляет точку
    2.  x byte ?  ; координата Х
    3.  align 2  ; выравнивание по двум байтам
    4.  y byte ?  ; координата Y
    5.  z byte ?  ; координата Z
    6. point3D ends
    Теперь переменная x будет выравнена по 2 байтам - после нее добавляется еще один байт.
    Также можно сразу задать выравнивание для всех переменных в определении структуры:
    Код (ASM):
    1. point3D struct 2  ; выравнивание всех переменных по двум байтам
    2.  x byte ?
    3.  y byte ?
    4.  z byte ?
    5. point3D ends
     
    Последнее редактирование: 29 окт 2026 в 16:35
    Application нравится это.
  12. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    4.364

    Работа с данными и памятью

    Объединения union

    Объединения (union) также позволяют определить свой тип данных и также хранят набор элементов, но в отличие от структуры все элементы объединения имеют нулевое смещение. А это значит, что разные элементы занимают в памяти один и тот же участок, то есть в памяти они накладываются друг на друга.
    Для определения объединений применяется ключевое слово union и следующий формальный синтаксис:
    имя_объединения union
    поля объединения
    имя_объединения ends

    Например, определим простейшее объединение, которое хранит символ и его числовой код из таблицы ASCII:
    Код (ASM):
    1. char union  ; объединение char представляет символ таблицы ASCII
    2.  letter byte ?  ; символ
    3.  num_code dword ?  ; его числовой код
    4. char ends
    Здесь определено объединение с именем char, которое хранит в одном и том же участки памяти объект dword (числовой код символа) и объект byte (сам символ). В итоге это объединение будет занимать 4 байта, и младший байт будет общим для переменной letter и num_code. Таким образом, объединение позволяет сэкономить на размее памяти.
    Затем мы можем создать переменную, которая представляет данное объединение:
    Код (ASM):
    1. .data
    2. symbol char {}
    Здесь переменная объединения char называется symbol. Для определения объединения применяются фигурные скобки {}.
    Для обращения к полям объединения используется точечная нотация - после имени объединения ставится точка и имя поля:
    Код (ASM):
    1. char union  ; структура представляет символ таблицы ASCII
    2.  letter byte ?  ; символ
    3.  num_code dword ?  ; его числовой код
    4. char ends
    5. .data
    6.  symbol char {}
    7. .code
    8. main proc
    9.  mov rdx, "c"
    10.  mov symbol.letter, dl  ; устанавливаем значение letter
    11.  mov eax, symbol.num_code  ; EAX = 99 - код символа c
    12.  ret
    13. main endp
    14. end
    Здесь в регистр rdx помещается символ "c", который имеет код 99 в таблице ASCII. Далее этот символ передаем в переменную letter. Но поскольку переменная letter представляет младший байт переменной num_code, то num_code также будет содержать значение 99.
     
    Последнее редактирование: 29 окт 2026 в 16:42
  13. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    4.364

    Базовые конструкции

    Условные конструкции

    Условные конструкции позволяют направить действие программы по одному из путей в зависимости от определенного условия. В языке ассемблера нет подобных конструкций в отличие от языков высокого уровня. Однако, используя сравнение значения флагов, метки и переходы, мы можем сами определить подобные конструкции.
    Имитация конструкции if..else
    Во многих распространеных языках высокого уровня есть конструкция if..else, которая в зависимости от условия выполняет те или иные действия. С помощью бейсикоподобного синтаксиса обобщенно эту конструкцию можно представить следующим образом:
    IF условие THEN
    выполняемые действия, если условие верно
    ELSE
    выполняемые действия, если условие НЕ верно
    END IF

    Или конструкция if..else в большинстве сиподобных языков
    if(условие)
    {
    выполняемые действия, если условие верно
    }
    else
    {
    выполняемые действия, если условие НЕ верно
    }

    Например, сначала возьмем простейшую форму конструкции if, которая сравнивает два числа и, если они равны, устанавливает некоторую переменную. Например, на сиподобном языке это выглядело бы так:
    if(a == b)
    {
    eax = 1;
    }

    Чтобы определить подобную конструкцию на языке ассемблера, вначале указываются операторы, которые оценивают выражение, а затем выполняют переход, если условие неверно.
    Код (ASM):
    1. .data
    2.  a dword 22
    3.  b dword 22
    4. .code
    5. main proc
    6.  mov eax, a
    7. begin_if:
    8.  cmp eax, b  ; сравниваем a и b
    9.  jne end_if  ; если a и b не равны - к метке end_if
    10.  mov eax, 1  ; если a и b равны
    11. end_if:
    12.  ret
    13. main endp
    14. end
    Для большей наглядности здесь определена необязательная метка begin_if, которая условно определяет начало конструкции. С помощью инструкции cmp сравниваем два значения и далее, если два числа не равны, переходим к метке end_if, которая знаменует конец конструкции.
    Конструкция if во многих языках также позволяет задать альтернативные условия (обычно для этого применяется оператор else). В этом случае в ассемблере надо использовать дополнительную метку, на которую будут проецироваться альтернативные действия
    Код (ASM):
    1. cmp X, Y  ; проверяем условие
    2. jcc ElseCode  ; если условие не верно
    3.  действия, если условие верно
    4. jmp EndOfIf
    5. ElseCode:
    6.  действия, если условие не верно
    7. EndOfIf:
    Например, если условие верно, в EAX поместим 1, если не верно - 0:
    Код (ASM):
    1. .data
    2.  a dword 24
    3.  b dword 22
    4. .code
    5. main proc
    6.  mov eax, a
    7. begin_if:
    8.  cmp eax, b  ; сравниваем a и b
    9.  jne else_if  ; если условие не верно - к метке else_if
    10.  mov eax, 1  ; если a и b равны
    11.  jmp end_if  ; в конец конструкции id
    12. else_if:
    13.  mov eax, 0  ; если a и b не равны
    14. end_if:
    15.  ret
    16. main endp
    17. end
    Здесь, если условие не верно (a и b не равны), то переходим к метке else_if, после которой определено альтернативное действие.
    Сложные условия
    Когда условие очень простое, то его довольно легко описать на языке ассемблера. Однако если условие комплексное и состоит из ряда простых, то это может привести к значительному усложнению кода ассемблера.
    Операция AND
    Например, возьмем условие, которое состоит из двух подусловий, объединенных операцией AND:
    if((a == b) && (c != d)) {действия}
    то есть, условие истинно, если одновременно a равно b и c не равно d. Фактически мы можем представить это выражение как:
    if(a == b) if(c != d) {действия}
    С точки зрения ассемблера нам надо последовательно проверить все условия, которые связаны операцией AND:
    Код (ASM):
    1. .data
    2.  a dword 22
    3.  b dword 22
    4.  c dword 24
    5.  d dword 25
    6. .code
    7. main proc
    8. begin_if:
    9.  mov eax, a
    10.  cmp eax, b  ; (a==b)
    11.  jne end_if  ; если первое условие не верно
    12.  
    13.  mov eax, c
    14.  cmp eax, d  ; (c!=d)
    15.  je end_if  ; если второе условие не верно
    16.  mov eax, 1  ; если оба условия верны
    17. end_if:
    18.  ret
    19. main endp
    20. end
    То есть проверяем первое условие. Если оно неверно, переходим к концу конструкции if. Если первое условие верно, переходим к проверке второго условия. Если второе условие неверно, переходим к концу конструкции if. Если второе условие верно, выполняем указанные действия.
    Операция OR
    Другой распространенный способ объединения условий представляет операция OR или логическое ИЛИ:
    if((a == b) || (c != d)) {действия}
    то есть, условие истинно, если или a равно b, или c не равно d. Фактически мы можем представить это выражение как:
    if(a == b) {действия}
    else if(c != d) {действия}

    Если верно одно из условий, то выполняются те же самые действия.
    С точки зрения ассемблера нам надо последовательно проверить все условия, которые связаны операцией OR:
    Код (ASM):
    1. .data
    2.  a dword 22
    3.  b dword 22
    4.  c dword 24
    5.  d dword 25
    6. .code
    7. main proc
    8. begin_if:
    9.  mov eax, a
    10.  cmp eax, b  ; (a==b)
    11.  je do_if  ; если первое условие верно переходим к do_if
    12.  
    13.  mov eax, c
    14.  cmp eax, d  ; (c != d)
    15.  je end_if  ; если второе условие не верно (то есть если с == d), выходим
    16. do_if:
    17.  mov eax, 1  ; если одно из условий верно
    18. end_if:
    19.  ret
    20. main endp
    21. end
    Сначала проверяем первое условие. Если оно верно, переходим к выполнению действий. Если первое условие неверно, проверяем второе условие. Если второе условие верно, переходим к выполнению действий. Если второе условие тоже не верно, переходим к концу конструкции.
    AND и OR
    На основе выше указанной информации мы можем переложить на ассемблер более сложные условия, например, которые сочетают операции AND и OR:
    if(((a == b) && (c != d)) || (e == f)) {действия}
    В данном случае общее условие истинно, если либо верно подусловие (a == b) && (c != d), либо (e == f). При составлении программы на языке ассемблера целесообразнее вначале проверить третье условие:
    Код (ASM):
    1. .data
    2.  a dword 22
    3.  b dword 22
    4.  c dword 23
    5.  d dword 24
    6.  e dword 25
    7.  f dword 26
    8. .code
    9. main proc
    10. begin_if:
    11.  mov eax, e
    12.  cmp eax, f  ; (e==f)
    13.  je do_if  ; если третье условие верно переходим к do_if
    14.  
    15.  mov eax, a
    16.  cmp eax, b  ; (a==b)
    17.  jne end_if  ; если первое условие не верно
    18.  
    19.  mov eax, c
    20.  cmp eax, d  ; (c!=d)
    21.  je end_if  ; если второе условие не верно
    22. do_if:
    23.  mov eax, 1  ; если одно из условий верно
    24. end_if:
    25.  ret
    26. main endp
    27. end
    То есть вначале проверяем третье условие. Если оно верно, то проверка первых двух условие не имеет смысла, и поэтому переходим к выполнению действий в метку do_if. Если третье условие не верно, то последовательно проверяем первое и второе условие. Если одно из них неверно, то переходим к концу конструкции в метке end_if.
    Если нам важен порядок выполнения, то мы могли бы написать так:
    Код (ASM):
    1. .data
    2.  a dword 21
    3.  b dword 22
    4.  c dword 23
    5.  d dword 24
    6.  e dword 25
    7.  f dword 26
    8. .code
    9. main proc
    10. begin_if:
    11.  
    12.  mov eax, a
    13.  cmp eax, b  ; (a==b)
    14.  jne or_if  ; если первое условие не верно
    15.  
    16.  mov eax, c
    17.  cmp eax, d  ; (c!=d)
    18.  jne do_if  ; если второе условие верно
    19. or_if:
    20.  mov eax, e
    21.  cmp eax, f  ; (e==f)
    22.  jne end_if  ; если третье условие верно переходим к do_if
    23. do_if:
    24.  mov eax, 1  ; если одно из условий верно
    25. end_if:
    26.  ret
    27. main endp
    28. end
    Проверяем первое условие. Если оно неверно, переходим к метке or_if к проверке третьего условия. Если первое условие верно, переходим к проверке второго условия. Если второе условие верно, то проверять третье условие смысла нет, и переходим к метке do_if и выполнению указанных инструкций.
     
    Последнее редактирование: 30 окт 2026 в 02:32
  14. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    4.364

    Базовые конструкции

    Булевые конструкции

    Многие языки программирования оценивают условия как значения логических типов (обычно называются bool или boolean). В ассемблере мы также можем представить условия в виде своего рода логических значений, а по сути чисел, которые равны или 1 (условие верно) или 0 (условие не верно). Для примера возьмем условие
    if(((a == b) && (c != d)) || (e == f))[/TD][/TR][/TBODY]
    С помощью инструкции setXX в зависимости от результата проверки мы можем установить условные логические значения
    Код (ASM):
    1. .data
    2.  a dword 22
    3.  b dword 22
    4.  c dword 23
    5.  d dword 24
    6.  e dword 25
    7.  f dword 26
    8. .code
    9. main proc
    10. begin_if:
    11.  mov eax, a
    12.  cmp eax, b  ; (a==b)
    13.  sete bl  ; результат сравнения (a==b) в регистре BL
    14.  
    15.  mov eax, c
    16.  cmp eax, d  ; (c!=d)
    17.  setne bh  ; результат сравнения (c!=d) в регистре BH
    18.  and bl, bh  ; Помещаем в регистр BL результат (a==b) && (c!=d)
    19.  mov eax, e
    20.  cmp eax, f  ; (e==f)
    21.  sete bh  ; результат сравнения (e==f) в регистре BH
    22.  
    23.  or bl, bh  ; Помещаем в регистр BL результат (((a == b) && (c != d)) || (e == f))
    24.  je end_if  ; если все условие не верно, переходим к end_if
    25.  mov eax, 1  ; если условие верно
    26. end_if:
    27.  ret
    28. main endp
    29. end
    Здесь вначале проверяем первое условие из подусловия AND - (a==b):
    Код (ASM):
    1. mov eax, a
    2. cmp eax, b  ; (a==b)
    3. sete bl  ; результат сравнения (a==b) в регистре BL
    Если числа a и b равны, то в регистр BL помещается 1.
    Затем проверяем второе условие из подусловия AND - (c!=d):
    Код (ASM):
    1. mov eax, c
    2. cmp eax, d  ; (c!=d)
    3. setne bh  ; результат сравнения (c!=d) в регистре BH
    Если числа c и d не равны, то в регистр BH помещается 1.
    Далее с помощью инструкции and можно оценить весь результат подусловия AND:
    Код (ASM):
    1. and bl, bh  ; Помещаем в регистр BL результат (a==b) && (c!=d)
    Таким образом, если оба условия истинны, то в регистре BL будет 1, иначе будет 0.
    Затем проверяем третье условие - подусловие OR - (e==f):
    Код (ASM):
    1. mov eax, e
    2. cmp eax, f  ; (e==f)
    3. sete bh  ; результат сравнения (e==f) в регистре BH
    И с помощью инструкции or проверяем результат всего условия (((a == b) && (c != d)) || (e == f))
    Код (ASM):
    1. or bl, bh  ; Помещаем в регистр BL результат (((a == b) && (c != d)) || (e == f))
    2. je end_if  ; если все условие не верно, переходим к end_if
    Если оба регистра равны 0 (оба подусловия неверны), то устанавливается флаг нуля. Соответственно мы можем проверить этот флаг и, если он установлен, перейти к метке завершения конструкции if.
    Стоит отметить, что примере выше вычисления производились в полной форме, все три условия вычислялись. Однако, если мы возьмем подусловие (a == b) && (c != d), то тут нет смысла вычислять второе условие, если первое условие не верно. Аналогично если верно улсовие ((a == b) && (c != d)), то подусловие (e == f) нет смысла вычислять. Поэтому в определенных ситуациях может быть предпочтительно вычисление по укороченной схеме или то что называется "short-circuit", как было продемонстрировано в предыдущем примере.
     
    Последнее редактирование: 30 окт 2026 в 02:49
  15. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    4.364

    Базовые конструкции

    Имитация конструкции switch..case

    В ряде языков высокого уровня есть конструкция switch..case, которая последовательно сравнивает некоторое выражение с набором значений:
    switch(выражение){
    case значение_1: действия_1;
    case значение_2: действия_2;
    case значение_3: действия_3;
    default: действия_по_умолчанию;
    }

    Если найдено совпадение, то выполняются инструкции из соотвествующего блока case
    Общий алгоритм данной конструкции может быть следующим:
    1. С помощью инструкции CMP сравниваем значение регистра с некоторым значением.
    2. Если сравнение оказалось НЕ верным, то переходим к следующей метке, которая сравнивает значение регистра с новым значением.
      Если сравнение оказалось верным, то выполняем определенные действия и затем переходим к метке завершения конструкции.
    3. Если ни одно из сравнений в инструкциях CMP НЕ является верным, выполняем действия по умолчанию.
    Пример реализации:
    Код (ASM):
    1. .data
    2.  number dword 2
    3. .code
    4. main proc
    5.  
    6.  mov eax, number ; сравниваемое значение
    7.  cmp eax, 1  ; number == 1
    8.  jne case_2  ; если не равно, переходим к метке case_2
    9.  mov eax, 11  ; действия, если числа равны
    10.  jmp end_switch  ; выходим из конструкции
    11. case_2:
    12.  cmp eax, 2  ; number == 2
    13.  jne case_2  ; если не равно, переходим к метке case_3
    14.  mov eax, 22  ; действия, если числа равны
    15.  jmp end_switch  ; выходим из конструкции
    16. case_3:
    17.  cmp eax, 3  ; number == 3
    18.  jne default  ; если не равно, переходим к мет
    19.  mov eax, 33  ; действия, если числа равны
    20.  jmp end_switch  ; выходим из конструкции
    21. default:
    22.  mov eax, 44  ; действия по умолчанию, если предыдущие равенства не верны
    23. end_switch:  ; конец конструкции
    24.  ret
    25. main endp
    26. end
    Здесь значение переменной number последовательно сравнивается с тремя значениями, и в зависимости от результатов сравнения в регистр eax помещается то или иное значение.
    Поскольку достижение последней метки требует большего времени, то сравнения с наиболее вероятными значениями следует помещать ближе к началу конструкции.
    Оптимизация
    Приведенный выше пример довольно просто, однако имеет один большой недостаток - при большом количестве меток потребуется некоторое время чтобы дойти до последней метки. Например, если число равно 10, то будет выполняться кода на метке default. Но чтобы дойти до нее надо пройти все предыдущие метки и инструкции сравнения. При большом количестве переходов и меток это несколько снижает производительность. И здесь есть различные возможности оптимизации программы.
    Если сравниваемые значения располагаются последовательно, как примере выше - 1, 2, 3, то эффективнее определить набор меток, к которым происходит переход при помощи косвенной адресации:
    Код (ASM):
    1. .data
    2.  number dword 2
    3. .code
    4. main proc
    5.  
    6.  mov eax, number ; сравниваемое значение
    7.  cmp eax, 1
    8.  jb default  ; если меньше 1
    9.  cmp eax, 3
    10.  ja default  ; если больше 3
    11.  
    12.  lea rcx, labels ; загружаем все метки
    13.  jmp qword ptr [rcx][rax * 8 - 8]  ; переходим к одной из меток
    14. labels qword case_1, case_2, case_3  ; адреса меток
    15. case_1:
    16.  mov eax, 11  ; действия, если числа равны
    17.  jmp end_switch  ; выходим из конструкции
    18. case_2:
    19.  mov eax, 22  ; действия, если числа равны
    20.  jmp end_switch  ; выходим из конструкции
    21. case_3:
    22.  mov eax, 33  ; действия, если числа равны
    23.  jmp end_switch  ; выходим из конструкции
    24. default:
    25.  mov eax, 44  ; действия по умолчанию, если предыдущие равенства не верны
    26. end_switch:  ; конец конструкции
    27.  ret
    28. main endp
    29. end
    Здесь опять же сравниваем с теми же числами - 1, 2, 3. Вначале проверяем попадает ли число в этот диапазон - если оно меньше 1 и больше 3, то переходим к метке default. Для остальных переходов определяем специальную переменную - labels, которая хранит адреса трех меток. Важно, что эта переменная определена в таком месте процедуры, в где она точно не будет выполняться как инструкция - в данном случае перед переменной идет переход к одной из меток.
    Адрес этой переменной загружается в регистр RCX. И далее на основе значения в регистре EAX вычисляем адрес и переходим к нему:
    Код (ASM):
    1. jmp qword ptr [rcx][rax * 8 - 8]
    То есть [rcx] представляет адрес переменной labels. Адрес первой метки это также [rcx] или [rcx] + 0. В RAX мы ожидаем число 1, 2 или 3. Соответственно с помощью выражения [rax * 8 - 8] мы можем вычислить смещение метки для определенного числа в EAX. Например, если в EAX число 2, то мы получим [rcx][2*8-8] или [rcx][8], то есть адрес метки case_2.
    Но что, если нам надо сравнить с числами, которые идут не по порядку, например, 1, 2, 4, 8? Здесь между числами небольшой разбор - от 1 до 8, и мы могли бы сделать так:
    Код (ASM):
    1. .data
    2.  number dword 2
    3. .code
    4. main proc
    5.  
    6.  mov eax, number ; сравниваемое значение
    7.  cmp eax, 1
    8.  jb default  ; если меньше 1
    9.  cmp eax, 8
    10.  ja default  ; если больше 8
    11.  
    12.  lea rcx, labels ; загружаем все метки
    13.  jmp qword ptr [rcx][rax * 8 - 8]
    14. labels qword case_1, case_2, default, case_4,
    15.  default, default, default, case_8  ; адреса меток
    16. case_1:
    17.  mov eax, 12
    18.  jmp end_switch
    19. case_2:
    20.  mov eax, 23
    21.  jmp end_switch
    22. case_4:
    23.  mov eax, 45
    24.  jmp end_switch
    25. case_8:
    26.  mov eax, 89
    27.  jmp end_switch
    28. default:
    29.  mov eax, 101  ; действия по умолчанию, если предыдущие равенства не верны
    30. end_switch:  ; конец конструкции
    31.  ret
    32. main endp
    33. end
    Здесь все то же самое, только теперь адресов меток у нас 8, но 4 из них - адреса метки default. То есть если у нас будут для сравнения числа 3, 5, 6, 7, то для них будет выбираться метка default.
    Хотя в примере выше сравниваемые значение не идут по порядку, но все равно находятся довольно близко - в пределах первой восьмерки. Что делать, если разброс значений гораздо больше, например, 1, 2, 10, 11, 100, 101, 103, 1000, 1001? В этом случае мы можем сгруппировать близкие значения в отдельные группы и для каждой групп определить свой набор меток:
    Код (ASM):
    1. .data
    2.  number dword 2
    3. .code
    4. main proc
    5.  
    6.  mov eax, number ; сравниваемое значение
    7.  cmp eax, 100
    8.  jb case0_11
    9.  cmp eax, 103
    10.  ja case1000_1001
    11.  lea rcx, labels100
    12.  jmp qword ptr [rcx][rax * 8 - 100 * 8]
    13.  labels100 qword case_100, case_101, default, case_103  ; метки для 100, 101, 103
    14. case0_11:
    15.  cmp eax, 11
    16.  ja default  ; если больше 10
    17.  lea rcx, labels0_11
    18.  jmp qword ptr [rcx][rax * 8]
    19.  labels0_11 qword default, case_1, case_2, default, default, default, default,
    20.  default, default, default, case_10, case_11
    21. case1000_1001:
    22.  cmp eax, 1000
    23.  jb default
    24.  cmp eax, 1001
    25.  ja default
    26.  lea rcx, labels1000
    27.  jmp qword ptr [rcx][rax * 8 - 1000 * 8]
    28.  labels1000 qword case_1000, case_1001
    29. case_1:
    30.  mov eax, 21
    31.  jmp end_switch
    32. case_2:
    33.  mov eax, 22
    34.  jmp end_switch
    35. case_10:
    36.  mov eax, 23
    37.  jmp end_switch
    38. case_11:
    39.  mov eax, 24
    40.  jmp end_switch
    41. case_100:
    42.  mov eax, 25
    43.  jmp end_switch
    44. case_101:
    45.  mov eax, 26
    46.  jmp end_switch
    47. case_103:
    48.  mov eax, 26
    49.  jmp end_switch
    50. case_1000:
    51.  mov eax, 27
    52.  jmp end_switch
    53. case_1001:
    54.  mov eax, 28
    55.  jmp end_switch
    56. default:
    57.  mov eax, 30  ; действия по умолчанию, если предыдущие равенства не верны
    58. end_switch:  ; конец конструкции
    59.  ret
    60. main endp
    61. end
    Здесь все сравниваемые значения разбиты на три группы, для каждой из которых определен свой набор адресов меток. Данный способ в какой-то степени можно назвать бинарным поиском - среди групп находим группу со срединными значениями и остальные группы разбиваем на две части - одна со значениями меньше середины, а другая - со значениями больше середины.
    С одной стороны, это ведет к усложнению программы и снижению ее читабельности, а с другой, увеличению скорости при большом количестве меток.
     
    Последнее редактирование: 30 окт 2026 в 02:55
  16. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    4.364

    Базовые конструкции

    Машина состояний

    Машина состояний предполагает программу, которая может находиться в определенном состоянии из конечного набора состояний. Программа запоминает свое состояние и определяет действия для перехода из одного состояния в другое.
    На уровне ассемблера простейшая реализация машины состояний состоит в последовательном сравнении текущего состояния с состояниями из набора. Если сравнение успешно, два значения равны, то выполняются действия, определенные для этого состояния. Если сравниваемые значения не равны, то сравниваем со следующим состоянием:
    Код (ASM):
    1. .data
    2.  state byte 0  ; условное состояние
    3. .code
    4. state_machine proc
    5. state_0:
    6.  cmp state, 0  ; сраниваем состояние
    7.  jne state_1
    8.  add eax, ecx  ; состояние 0: складываем ECX и EAX и переключаемся на состояние 1
    9.  inc state  ; изменяем состояние
    10.  jmp exit
    11. state_1:
    12.  cmp state, 1
    13.  jne state_2
    14.  sub eax, ecx  ; состояние 1: вычитаем ECX из EAX и переключаемся на состояние 2
    15.  inc state  ; изменяем состояние
    16.  jmp exit
    17. state_2:
    18.  cmp state, 2
    19.  jne state_3
    20.  imul eax, ecx  ; состояние 2: умножаем ECX на EAX и переключаемся на состояние 3
    21.  inc state  ; изменяем состояние
    22.  jmp exit
    23. state_3:
    24.  ; cmp state, 3  ; предположим, что это состояние будет в любом случае, но также можно сравнивать
    25.  xor edx, edx  ; для деления расширяем нулями регистр EAX на EDX
    26.  div ecx  ; состояние 3: делим EAX на ECX и переключаемся на состояние 0
    27.  mov state, 0  ; изменяем состояние
    28. exit:
    29.  ret
    30. state_machine endp
    31. main proc
    32.  mov eax, 5
    33.  mov ecx, 4
    34.  call state_machine
    35.  ret
    36. main endp
    37. end
    Для данной программы предположим, что все возможные состояния описываются числами 0, 1, 2, 3, которые представляют определенную арифметическую операцию: сложение (0), вычитание (1), умножение (3) и деление (4). Для хранения состояния в программе определена переменная state. Для управления состоянием определена процедура state_machine, в которой мы сравниваем значение state с возможными состояниями. Если сравнение успешно, выполняем арифметическую операцию и изменяем состояние. Либо переходим к следующей метке для проверки нового состояния.
    Через регистры EAX и ECX процедура state_machine получает значения:
    Код (ASM):
    1. mov eax, 5
    2. mov ecx, 4
    3. call state_machine
    Таким образом, после выполнения процедуры будет выполняться операция сложения (так как переменная state на старте программы равна 0). В EAX окажется число 9, а переменная state получит значение 1.
    Подобным образом мы могли бы несколько раз вызывать процедуру state_machine, передавая ей через RCX различные значения:
    Код (ASM):
    1. main proc
    2.  mov eax, 5
    3.  mov ecx, 4
    4.  call state_machine  ; EAX = 9, state = 1
    5.  mov ecx, 3
    6.  call state_machine  ; EAX = 6, state = 2
    7.  mov ecx, 4
    8.  call state_machine  ; EAX = 24, state = 3
    9.  mov ecx, 2
    10.  call state_machine  ; EAX = 12, state = 0
    11.  ret
    12. main endp
    Хотя этот пример представляет довольно простую схему для реализации состояний, но тем не менее последовательный поиск и сравнение значений не очень эффективны. И в этом случае мы можем оптимизировать программу следующим образом:
    Код (ASM):
    1. .data
    2.  state qword state_0  ; условное состояние
    3. .code
    4. option noscoped  ; делаем метки глобальными
    5. state_machine proc
    6.  jmp state
    7. state_0:
    8.  add eax, ecx  ; состояние 0: складываем ECX и EAX и переключаемся на состояние 1
    9.  lea rcx, state_1
    10.  mov state, rcx
    11.  ret
    12. state_1:
    13.  sub eax, ecx  ; состояние 1: вычитаем ECX из EAX и переключаемся на состояние 2
    14.  lea rcx, state_2
    15.  mov state, rcx
    16.  ret
    17. state_2:
    18.  imul eax, ecx  ; состояние 2: умножаем ECX на EAX и переключаемся на состояние 3
    19.  lea rcx, state_3
    20.  mov state, rcx
    21.  ret
    22. state_3:
    23.  xor edx, edx  ; для деления расширяем нулями регистр EAX на EDX
    24.  div ecx  ; состояние 3: делим EAX на ECX и переключаемся на состояние 0
    25.  lea rcx, state_0
    26.  mov state, rcx
    27.  ret
    28. state_machine endp
    29. main proc
    30.  mov eax, 5
    31.  mov ecx, 4
    32.  call state_machine  ; EAX = 9, state = state_1
    33.  mov ecx, 3
    34.  call state_machine  ; EAX = 6, state = state_2
    35.  mov ecx, 4
    36.  call state_machine  ; EAX = 24, state = state_3
    37.  mov ecx, 2
    38.  call state_machine  ; EAX = 12, state = state_0
    39.  ret
    40. main endp
    41. end
    Теперь состояния представляют метки state_N, на которые процецируются действия. Чтобы глобальная переменная state могла хранить адрес определенной метки внутри процедуры, перед процедурой указываем директиву option noscoped. В самой процедуре с помощью инструкции jmp сразу переходим к нужному состоянию:
    Код (ASM):
    1. jmp state
     
    Последнее редактирование: 30 окт 2026 в 03:22
  17. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    4.364

    Базовые конструкции

    Циклы

    Циклические конструкции позволяют выполнить некоторый набор инструкций определенное количество раз. На ассемблере можно реализовать различные типы циклов различным образом. Но, как правило, они вовлекают инструкцию сравнения результатов CMP и переход к определенной метке.
    while
    В ряде языков программирования есть тип циклов - цикл while, который выполняет некоторые действия, пока истинно некоторое условие:
    BASIC-подобный синтаксисC-подобный синтаксис
    WHILE условие
    действия цикла
    END WHILE
    while(условие){
    действия цикла
    }
    Пример цикла на сиподобных языках:
    i = 0;
    while(i < 100)
    {
    i++;
    }

    Здесь в цикле увеличиваем переменную i на 1, пока она меньше 100. Когда переменная i окажется равна 100, выходим из цикла.
    Реализация подобного цикла на ассемблере:
    Код (ASM):
    1. .data
    2.  i dword 0
    3. .code
    4. main proc
    5.  mov i, 0
    6. while_start:
    7.  cmp i, 10
    8.  jnl while_end  ; если i не меньше 10, переход к метке while_end
    9.  ; выполняемые действия
    10.  inc i
    11.  jmp while_start
    12. while_end:
    13.  mov eax, i  ; EAX = 10
    14.  ret
    15. main endp
    16. end
    Здесь метка while_start знаменует начало цикла, в котором сначала сравниваем значение i с числом 10. Если значение i НЕ меньше 10 (jnl), переходим к концу цикла - метке while_end. Иначе инкрементируем значение переменной и переходим к началу цикла - метке while_start.
    цикл do..while
    Еще одним типом циклов является цикл do..while - он сначала выполняет некоторые действия, а потом проверяет условие. Если условие верно, повторяет действия. Если условие НЕ верно, завершает работу:
    C-подобный синтаксис
    do{
    выполняемые действия
    }
    while(условие);
    С точки зрения реализации на ассемблере это самый легкий тип цикла:
    Код (ASM):
    1. .code
    2. main proc
    3.  mov eax, 1  ; помещаем в регистр EAX  число 1
    4. do_while:
    5.  ; выполняемые действия
    6.  add eax, 1  ; действия цикла - EAX = EAX + 1
    7.  cmp eax, 10  ; сравнивание значение регистра EAX с числом 10
    8.  jl do_while  ; если EAX меньше 10, переход к метке do_while
    9.  ret
    10. main endp
    11. end
    Здесь помещаем в регистр EAX число 1. Затем после метки do_while производим действия цикла - увеличиваем значение регистра X0 на единицу. Далее сравниваем значение регистра EAX с числом 10. И если EAX меньше числа 10, то переходим обратно к метке do_while и повторяем действия цикла. Когда EAX станет равным 10, то произодейт выход из цикла.
    цикл for
    В ряде языков высокого уровня, например, есть циклическая конструкция for:
    BASIC-подобный синтаксисC-подобный синтаксис
    FOR i = M TO N
    выполняемые действия
    NEXT i
    for(var i = M; i < N; i++)
    {
    выполняемые действия
    }
    то есть есть некоторый счетчик i, и пока этот счетчик не достигнет значения N, будут выполняться некоторые действия цикла.
    Реализация цикла for в ассемблере:
    Код (ASM):
    1. .code
    2. main proc
    3.  mov eax, 0  ; в EAX помещаем число - условный счетчик
    4.  
    5. for_start:  ; метка, на которую проецируется цикл
    6.  cmp eax, 10  ; сравниваем с некоторым пределом
    7.  jnl for_end  ; условие - если счетчик больше или равен пределу, выход из цикла
    8.  ; выполняемые действия
    9.  add eax, 1  ; действия цикла - увеличение счетчика
    10.  jmp for_start  ; повторяем цикл
    11. for_end:
    12.  ret
    13. main endp
    14. end
    В данном случае в EAX помещаем некоторое значение, которое будет выполнять роль счетчика цикла. Собственно цикл начинается с метки for_start. Сначала мы проверяем условие. Допустим, нам надо, чтобы цикл повторялся, пока в регистре EAX не окажется число 10. Если в EAX чило 10 или больше, то выходим из цикла - переходим к метке for_end. Если в EAX все еще меньше 10, то выполняем действия цикла - увеличиваем EAX на 1 и затем переходим обратно к метке for_start для повторения цикла.
    Оптимизация циклов
    Если мы посмотрим на ассемблерную реализацию циклов while и for, то увидим, что они в принципе похожи:
    Код (ASM):
    1. ;цикл while
    2. mov eax, 0
    3. while_start:
    4.  cmp eax, 10
    5.  jnl while_end
    6.  add eax, 1
    7.  jmp while_start
    8. while_end:
    9. ; цикл for
    10. mov eax, 0
    11. for_start:
    12.  cmp eax, 10
    13.  jnl for_end
    14.  add eax, 1
    15.  jmp for_start
    16. for_end:
    Такая реализация очень проста и понятна. Однако реализация цикла do_while гораздо эффективнее
    Код (ASM):
    1.  mov eax, 1
    2. do_while:
    3.  add eax, 1
    4.  cmp eax, 10
    5.  jl do_while
    В отличие от while/for здесь отсутствует инструкция jmp, и сама конструкция будет теоретически выполняться быстрее. Но в циклах while/for нам важно выполнять определенные действия только при соблюдении некоторого условия. Соответственно это условие вначале надо проверить. Однако мы могли бы вынести действия цикла вперед:
    Код (ASM):
    1. .code
    2. main proc
    3. mov eax, 0
    4. jmp for_condition ; переходим к проверке условия
    5. for_start:
    6.  add eax, 1
    7. for_condition:
    8.  cmp eax, 10
    9.  jl for_start  ; если условие соблюдено переходим к метке for_start для выполнения действий
    10.  ret
    11. main endp
    12. end
    Здесь цикл начинается с перехода к метке for_condition, где проверяется условие - EAX должен быть меньше 10. Если это условие верно, переходим к метке for_start, где выполняем действия цикла. Затем опять проверяем условия и выполняем действия цикла, пока не дойдем до ситуации, когда EAX = 10.
    Таким образом, инстркция jmp выполняется только один раз, а в остальном цикл for/while будет соответствовать циклу do-while.
    Но и цикл do-while мы тоже можем до некоторой степени оптимизировать. Перепишем пример следующим образом:
    Код (ASM):
    1. .code
    2. main proc
    3.  mov eax, 9  ; помещаем в регистр EAX число 9
    4. do_while:
    5.  ; выполняемые действия
    6.  sub eax, 1  ; действия цикла - EAX = EAX - 1
    7.  jnz do_while  ; если флаг нуля установлен, то есть EAX=0, переход к метке do_while
    8.  ret
    9. main endp
    10. end
    Здесь идет обратный отсчет - от 9 до 1. И теперь значение в регистре EAX уменьшается на 1. Но цикл срабатывает столько же раз, сколько и в предыдущей реализации. Поскольку когда результат операции sub окажется равен 0 (то есть EAX = 0), то будет установлен флаг нуля. Соответственно мы можем проверить этот флаг, и если он еще не установлен, продолжить цикл. Таким образом, мы отбрасываем инструкцию cmp, поскольку нам не надо вручную сравнивать значения.
    Если нам надо, чтобы EAX допускал значение 0 (от 9 до 0 - в этом случае флаг нуля мы так проверить не сможем), то мы можем проверять флаг знака, который устанавливается, если результат отрицательный:
    Код (ASM):
    1. .code
    2. main proc
    3.  mov eax, 9  ; помещаем в регистр EAX число 9
    4. do_while:
    5.  ; выполняемые действия
    6.  sub eax, 1  ; действия цикла - EAX = EAX - 1
    7.  jns do_while  ; если флаг знака SF =1, переход к метке do_while
    8.  ret
    9. main endp
    10. end
     
    Последнее редактирование: 30 окт 2026 в 03:47
    Application нравится это.
  18. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    4.364

    Арифметика больших чисел

    Сложение и вычитание больших чисел

    Поскольку регистры общего назначения в архитектуре x86-64 64-разрядные, то загрузить в один регистр число большей разрядности у нас не получится. Тем не менее мы не ограничены только 64-разрядными числами и можем работать с числами большей разрядности, например, 128-, 192-, 256-битными и т.д.
    Например, возьмем 128-разрядные числа. Определенно такое число не помещается в один 64-разрядный регистр. Но мы можем разбить число и поместить его младшие 64 бита в один регистр, а старшие 64 бита в другой. Аналогично 192-разрядные числа разносятся по трем 64-разрядным регистрам.
    При работе с большими числами, которые не укладываются в рамки одного регистра, основная идея состоит в том, чтобы разбить большую операцию на последовательность более мелких. Поскольку семейство процессоров x86 способно одновременно складывать не более 64 бит (используя регистры общего назначения), операция должна выполняться блоками по 64 бита или меньше.
    Сложение больших чисел
    Например, возьмем сложение 128-разрядных чисел. Сначала складываем младшие 64 разряда чисел. Если при сложении новое число выходит за пределы 64 бит, то процессор устанавливает флаг переноса CF. Затем складываем старшие 64 бита чисел и бит переноса.
    Для сложения двух операндов с битом переноса применяется инструкция adc
    adc dest, source ; dest = dest + source + CF
    Она, как и инструкция add, складывает два числа, но также к результату добавляет бит из флага переноса CF. Рассмотрим пример:
    Код (ASM):
    1. .data
    2.  x oword 01ffffffffffffffffh
    3.  y oword 010000000000000002h
    4.  z oword ?
    5. .code
    6. main proc
    7.  mov rax, qword ptr x  ; В RAX загружаем младшие 64 бита числа x
    8.  add rax, qword ptr y  ; складываем их с младшими 64 битами числа y
    9.  mov qword ptr z, rax  ; результат сложения помещаем в младшие 64 бита числа z
    10.  mov rax, qword ptr x[8] ; В RAX загружаем старшие 64 бита числа x
    11.  adc rax, qword ptr y[8] ; складываем их со старшими 64 битами числа y и с битом переноса
    12.  mov qword ptr z[8], rax ; результат сложения помещаем в младшие 64 бита числа z
    13.  ret
    14. main endp
    15. end
    Для хранения данных здесь определяем три переменных типа oword (восьмеричное слово - 128-разрядное число). Сначала помещаем в регистр RAX младшие 64 бита переменной x и складываем их с младшими 64 битами переменной y. Чтобы получить младшие 64 бита, оператор qword ptr приводит переменные x, y и z к 64 битам (по сути берет младшие 64 бита).
    После сложения младших 64 бит складываем старшие 64 бита. Для этого помещаем в RAX старшие 64 бита переменной x, складываем их со старшими 64 битами переменной y и помещаем результат в старшие 64 разряда переменной z. Чтобы получить старшие 64 бита, применяется выражение в форме x[8] вместе с оператором qword ptr (число 8 указывает на 8 байт).
    В итоге мы получаем вычисления
    01ffffffffffffffffh + 010000000000000002h = 030000000000000001h
    В младших 64 бита будет число 1, а в старших 64 битах - число 3.
    Подобным механизм можно распространить на числа и большей разрядности. Например, возьмем сложение 192-разрядных чисел:
    Код (ASM):
    1. .data
    2. BigInt1 qword 0ffffffffffffffffh,  ; младшие 64 бит
    3.  0ffffffffffffffffh,  ; средние 64 бит
    4.  00000000000000001h  ; старшие 64 бит
    5. BigInt2 qword 00000000000000002h,
    6.  00000000000000000h,
    7.  00000000000000001h
    8. BigInt3 qword 3 dup (?)
    9. .code
    10. main proc
    11.  mov rax, BigInt1[0]
    12.  add rax, BigInt2[0]
    13.  mov BigInt3[0], rax
    14.  
    15.  mov rax, BigInt1[8]  ; 64 бита начиная с 8 байт
    16.  adc rax, BigInt2[8]
    17.  mov BigInt3[8], rax
    18.  
    19.  mov rax, BigInt1[16]  ; 64 бита начиная с 16 байт
    20.  adc rax, BigInt2[16]
    21.  mov BigInt3[16], rax
    22.  ret
    23. main endp
    24. end
    Здесь каждое число представлено переменной BigInt1/BigInt2/BigInt3, которое по сути представляет набор из 3 значений qword - младших, средних и старших 64 бит числа. И мы также по отдельности складываем эти части. А для учета при сложении флага переноса применяем инструкцию adc. То есть получаем вычисления
    0000000000000001 ffffffffffffffff ffffffffffffffffh + 0000000000000001 0000000000000000 00000000000000002h = 0000000000000003 0000000000000000 00000000000000001h
    Вычитание больших чисел
    Аналогично можно применять вычитание больших чисел, разрядность которых больше 64 бит. В этом случае для вычитания младших 64 бит применяется стандартная инструкция sub, а для вычитания последующих 64 бит применяется инструкция sbb ("subtract with borrow" или вычитание с заимствованием), которая имеет аналогичный синтаксис:
    sbb dest, source ; dest=dest-source-CF
    Из dest вычитается source и бит флага переноса, который устанавливается при заимствовании. И результат помещается в dest
    Рассмотрим пример:
    Код (ASM):
    1. .data
    2.  x oword 030000000000000001h
    3.  y oword 01ffffffffffffffffh
    4.  z oword ?
    5. .code
    6. main proc
    7.  mov rax, qword ptr x  ; помещаем в RAX младшие 64 бит числа x
    8.  sub rax, qword ptr y  ; вычитаем из RAX младшие 64 бит числа y
    9.  mov qword ptr z, rax  ; результат помещаем в младшие 64 бит числа z
    10.  
    11.  mov rax, qword ptr x[8]  ; помещаем в RAX старшие 64 бит числа x
    12.  sbb rax, qword ptr y[8]  ; вычитаем из RAX старшие 64 бит числа y
    13.  mov qword ptr z[8], rax  ; результат помещаем в старшие 64 бит числа z
    14.  ret
    15. main endp
    16. end
    Здесь из 128-разрядного числа x вычитаем другое 128-разрядное число y. Сначала вычитаем из младшие 64 бита, а затем старшие. Таким образом получим:
    30000000000000001h - 1ffffffffffffffffh = 10000000000000002h
    Если смотреть поэтапно, то сначала вычитаем младшие 64 бита
    0000000000000001h - ffffffffffffffffh = 0000000000000002h
    Значение первого числа меньше значения второго числа, поэтому необходимо заимствование в первом числе из старших 64 бит.
    Подобным механизм можно вычитать числа большей разрядности. Например, возьмем вычитание 192-разрядных чисел:
    Код (ASM):
    1. .data
    2. BigInt1 qword 00000000000000001h,  ; младшие 64 бит
    3.  00000000000000000h,  ; средние 64 бит
    4.  00000000000000003h  ; старшие 64 бит
    5. BigInt2 qword 0ffffffffffffffffh,  ; младшие 64 бит
    6.  0ffffffffffffffffh,  ; средние 64 бит
    7.  00000000000000001h  ; старшие 64 бит
    8. BigInt3 qword 3 dup (?)
    9. .code
    10. main proc
    11.  mov rax, BigInt1[0]
    12.  sub rax, BigInt2[0]
    13.  mov BigInt3[0], rax
    14.  
    15.  mov rax, BigInt1[8]  ; средние 64 бита - начиная с 8 байт
    16.  sbb rax, BigInt2[8]  ; вычитание с заимствованием
    17.  mov BigInt3[8], rax
    18.  
    19.  mov rax, BigInt1[16]  ; старшие 64 бита - начиная с 16 байт
    20.  sbb rax, BigInt2[16]  ; вычитание с заимствованием
    21.  mov BigInt3[16], rax
    22.  ret
    23. main endp
    24. end
    Здесь будет идет следующее вычисление:
    0000000000000003 0000000000000000 0000000000000001h -
    0000000000000001 ffffffffffffffff ffffffffffffffffh = 0000000000000001 0000000000000000 0000000000000002h
     
    Последнее редактирование: 30 окт 2026 в 15:38
  19. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    4.364

    Арифметика больших чисел

    Сравнение больших чисел

    Сравение чисел с разрядностью больше 64 бит - довольно простой процесс. Cначала сравниваем старшие 64 бита - если они не равны, то сравнивать остальные части двух чисел нет смысла. Если же старшие 64 бита равны, сравниваем следующие 64 разряда чисел. Например, сравним 192-разрядные числа:
    Код (ASM):
    1. .data
    2. BigInt1 qword 00000000000000001h,  ; младшие 64 бит
    3.  00000000000000000h,  ; средние 64 бит
    4.  00000000000000003h  ; старшие 64 бит
    5. BigInt2 qword 0ffffffffffffffffh,  ; младшие 64 бит
    6.  0ffffffffffffffffh,  ; средние 64 бит
    7.  00000000000000001h  ; старшие 64 бит
    8. .code
    9. main proc
    10.  mov rax, BigInt1[16]  ; сравниваем старшие 64 бита
    11.  cmp rax, BigInt2[16]
    12.  ja First  ; если первое число больше
    13.  jb Second  ; если второе число больше
    14.  mov rax, BigInt1[8]  ; сравниваем средние 64 бита
    15.  cmp rax, BigInt2[8]
    16.  ja First
    17.  jb Second
    18.  mov rax, BigInt1[0]  ; сравниваем младшие 64 бита
    19.  cmp rax, BigInt2[0]
    20.  ja First
    21.  jb Second
    22.  mov rax, 0  ;  если числа равны, в RAX = 0
    23.  jmp cmp_end
    24. First:
    25.  mov rax, 1  ; если первое число больше, в RAX = 1
    26.  jmp cmp_end
    27. Second:
    28.  mov rax, -1  ; если второе число больше, в RAX = -1
    29. cmp_end:
    30.  ret
    31. main endp
    32. end
    В данном случае в зависимости от результата сравнения помещаем в RAX 0, 1 или -1. Поскольку здесь предполагается, что числа беззнаковые, то для сравнения и перехода к определенной метке применяются инструкции ja и jb
     
    Последнее редактирование: 30 окт 2026 в 15:40
  20. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    4.364

    Арифметика больших чисел

    Умножение больших чисел

    При умножении больших чисел нам надо умножить все части одного числа на все части другого числа и полученные результаты произведений сложить. Например, что такое произведение двух 32 разрядных чисел? Очевидно произведение 32 бит одного числа на 32 бита другого числа. Соответственно произведение двух 64-разрядных чисел - это произведение 64 бит одного числа на 64 бита другого числа. Однако мы можем представлить 64-разрядное число как число, у которого есть старшие 32 бита и младшие 32 бита. Тогда получится, что произведение двух 64-разрядных чисел равно
    младшие 32 бита первого числа × младшие 32 бита второго числа
    + старшие 32 бита первого числа × младшие 32 бита второго числа × 232
    + младшие 32 бита первого числа × старшие 32 бита второго числа × 232
    + старшие 32 бита первого числа × старшие 32 бита второго числа × 264
    Что можно представить графически следующим образом:
    [​IMG]
    То есть вычисляем по отдельности произведения всех отдельных 32-разрядных частей двух чисел и складываем их. Рассмотрим пример:
    Код (ASM):
    1. .data
    2.  op1 qword 0ffffffffffffffffh
    3.  op2 qword 16
    4.  result oword ?
    5. .code
    6. main proc
    7.  ; умножаем младшие 32 бита двух чисел
    8.  mov eax, dword ptr op1
    9.  mul dword ptr op2
    10.  mov dword ptr result, eax  ; сохраняем младшие 32 бита результата
    11.  mov ecx, edx  ; сохраняем старшие 32 бита результата
    12.  ; умножаем младшие 32 бита op1 на старшие 32 бита op2
    13.  mov eax, dword ptr op1
    14.  mul dword ptr op2[4]  ; получаем старшие 32 бита числа op2
    15.  add eax, ecx  ; добавляем промежуточный результат умножения из ECX
    16.  adc edx, 0  ; сложение с битом переноса
    17.  mov ebx, eax  ; сохраняем промежуточный результат
    18.  mov ecx, edx
    19.  ; умножаем старшие 32 бита op1 на младшие 32 бита op2
    20.  mov eax, dword ptr op1[4] ; получаем старшие 32 бита числа op1
    21.  mul dword ptr op2  ; умножаем на младшие 32 бита op2
    22.  add eax, ebx  ; добавляем промежуточный результат умножения из EBX
    23.  mov dword ptr result[4], eax ; сохраняем частичный результат
    24.  adc ecx, edx  ; сложение с битом переноса
    25.  
    26.  ; умножаем старшие 32 бита двух чисел
    27.  mov eax, dword ptr op1[4]
    28.  mul dword ptr op2[4]
    29.  add eax, ecx  ; добавляем промежуточный результат из ECX
    30.  adc edx, 0  ; сложение с битом переноса
    31.  mov dword ptr result[8], eax ; сохраняем младшую часть результата
    32.  mov dword ptr result[12], edx  ; сохраняем старшую часть результата
    33.  mov eax, dword ptr result[8]  ; eax = 15 или 0fh
    34.  ret
    35. main endp
    36. end
    Здесь умножаем два числа типа qword (то есть 64-разрядные числа) - op1 и op2. Результат теоретически может быть 128-разрядным, и для его хранения определяем переменную result, которая представляет тип oword (128-разрядное число). Умножать мы будем по отдельности 32 бита каждого из числа. То есть у нас идет умножение 32 бита на 32 бита, поэтому результат будет располагаться в регистрах EAX (младшие 32 бита результата) и EDX (старшие 32 бита). Для беззнакового умножения применяем инструкцию mul, для которой первый операнд всегда помещается в EAX.
    1. Вначале умножаем младшие 32 бита обоих чисел:
      Код (ASM):
      1. mov eax, dword ptr op1
      2. mul dword ptr op2
      3. mov dword ptr result, eax  ; сохраняем младшие 32 бита результата
      4. mov ecx, edx
      Младшие 32 бита, которые помещаются в EAX, сохраняем в младшие 32 бита переменной result. Старшие 32 бита - в регистр ECX.
    2. Далее умножаем младшие 32 бита op1 на старшие 32 бита op2:
      Код (ASM):
      1. mov eax, dword ptr op1
      2. mul dword ptr op2[4]  ; получаем старшие 32 бита числа op2
      3. add eax, ecx  ; добавляем промежуточный результат умножения из ECX
      4. adc edx, 0  ; сложение с битом переноса
      5. mov ebx, eax  ; сохраняем промежуточный результат
      6. mov ecx, edx
      Запись op2[4] означает, что получаем данные начиная с 5 байта числа op1.
      После умножения младшие 32 бита, которые помещаются в EAX, складываем со старшими 32 битами предыдущего умножения, которые были помещены в регистр ECX. В процессе сложения может возникнуть перенос, и будет установлен флаг переноса. Поэтому бит переноса прибавляем к старшим 32 битам текущего умножения из регистра EDX.
    3. Далее умножаем старшие 32 бита op1 на младшие 32 бита op2:
      Код (ASM):
      1. mov eax, dword ptr op1[4] ; получаем старшие 32 бита числа op1
      2. mul dword ptr op2  ; умножаем на младшие 32 бита op2
      3. add eax, ebx  ; добавляем промежуточный результат умножения из EBX
      4. mov dword ptr result[4], eax ; сохраняем частичный результат
      5. adc ecx, edx  ; сложение с битом переноса
      В данном случае мы опять же получаем данные для вторых 32 битов числа result. После умножения младшие 32 бита, которые помещаются в EAX, складываем с младшими 32 битами предыдущего умножения (так как это данные одного порядка) и помещаем результат во вторые 32 бита переменной result. .В процессе сложения может возникнуть перенос, и будет установлен флаг переноса. Поэтому опять же бит переноса прибавляем к старшим 32 битам текущего умножения из регистра EDX и складываем эти данные со старшими битами из предыдущего результата умножения.
    4. На последнем этапе умножаем старшие 32 бита двух чисел.
      Код (ASM):
      1. mov eax, dword ptr op1[4]
      2. mul dword ptr op2[4]
      3. add eax, ecx  ; добавляем промежуточный результат из ECX
      4. adc edx, 0  ; сложение с битом переноса
      5. mov dword ptr result[8], eax ; сохраняем младшую часть результата
      6. mov dword ptr result[12], edx  ; сохраняем старшую часть результата
      Младшие биты результата текущего умножения складываем с суммой старших 32 битов предыдущих двух результатов. И сохраняем эти данные в третьи 32 бита числа result. Страшие 32 бита складываем с битом переноса и сохраняем в старшие 32 бита числа result.
    И в данном случае мы получим следующие вычисления:
    ffffffffffffffffh * 10h (16 в десятичной - аналогично сдвигу влево на 4 разряда) = ffffffffffffffff0h
    или 00000000 0000000f ffffffff fffffff0

    Используя данный алгоритм, можно умножать числа и большей разрядности.
     
    Последнее редактирование: 30 окт 2026 в 15:54