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

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

  1. Mikl___

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

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

    SIMD

    Загрузка в регистры константных значений

    Расширения SSE/AVX не поддерживают загрузку в регистры XMM/YMM константных значений, например, числовых литералов. В качестве решения мы, конечно, можно определить переменную с нужным значением и затем использовать одну из инструкций, например, movdqa для загрузки переменной в регистр XMM/YMM.
    Код (ASM):
    1. .data
    2.  num word 11
    3. .code
    4. main proc
    5.  movdqu xmm0, oword ptr num  ; загружаем данные
    6.  ret
    7. main endp
    8. end
    Но это не самый быстрый путь. В качестве альтернативы можно применять различные комбинации логических, арифметических операций и операций сравнения. Посмотрим возможные варианты.
    Вначала загружаем в один регистр число 0. Для этого используем операцию XOR с помощью инструкции pxor:
    1pxor xmm0, xmm0 ; XMM0 = 0
    Чтобы установить все биты в регистре XMM или YMM в 1, можно использовать инструкцию pcmpeqb
    1pcmpeqb xmm1, xmm1 ; XMM1 = -1
    Так как любой регистр XMM или YMM равен самому себе, эта инструкция сохраняет 0FFh во всех байтах XMM1.
    Например, чтобы получить 1, мы можем вычесть из 0 число -1:
    1
    2
    3
    pxor xmm0, xmm0
    pcmpeqb xmm1, xmm1
    psubb xmm0, xmm1 ; 0 - (-1) = 1
    Здесь применяется вычитание байтов с помощью инструкции psubb, но аналогично можно применять инструкции psubw или psubd для получения 16- или 32-битного результата
    Если необходимо получить другое число, то можно использовать, например, инструкцию pslld. Например, если вы хотите загрузить в регистр XMM0 значение 8080808080808080h, вы можете использовать следующую последовательность инструкций:
    1
    2
    3
    4
    pxor xmm0, xmm0
    pcmpeqb xmm1, xmm1
    psubb xmm0, xmm1
    pslld xmm0, 7 ; сдвигаем 01h на 4 разряда влево, получаем 80h в каждом байте
    Сдвигая число в регистре на разное количество бит, можно получать различные значения.
    Для загрузки 2n – 1 (когда все биты вплоть до n-го бита в числе равны 1) можно применять следующий способ:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ; для 16-битных дорожек
    pcmpeqd xmm0, xmm0
    psrlw xmm0, 16 - n ; Очищаем правые 16 - n бит в xmm0
    ; для 32-битных дорожек
    pcmpeqd xmm0, xmm0
    psrld xmm0, 32 - n ; Очищаем правые 32 - n бит в xmm0
    ; для 64-битных дорожек
    pcmpeqd xmm0, xmm0
    psrlq xmm0, 64 - n ; Очищаем правые 64 - n бит в xmm0
    Подобным образом можно сдвигать влево:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ; для 16-битных дорожек
    pcmpeqd xmm0, xmm0
    psllw xmm0, n ; сбрасываем левые n бит в xmm0
    ; для 32-битных дорожек
    pcmpeqd xmm0, xmm0
    pslld xmm0, n ; сбрасываем левые n бит в xmm0
    ; для 64-битных дорожек
    pcmpeqd xmm0, xmm0
    psllq xmm0, n ; сбрасываем левые n бит в xmm0
    Например, загрузим в XMM1 число 7:
    Код (ASM):
    1. pcmpeqd xmm1, xmm1
    2. psrld xmm1, 32 - 3  ; XMM1 = 7
    Здесь сдвигаем двойное слово (32-битное число) на 29 разрядов влево, то есть остается 3 разряда, которые равны 111 или 7 в десятичной.
     
    Последнее редактирование: 1 ноя 2026 в 02:27
  2. Mikl___

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

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

    SIMD

    Сохранение состояния регистров SSE/AVX

    При работе с инструкциями расширений SSE/AVX нам может потребоваться сохранить значения регистров XMM/YMM. Рассмотрим на примере сохранения первых 6 регистров XMM/YMM:
    Код (ASM):
    1. .data
    2.  AVXSupport = 10000000h  ; проверяем бит 28
    3. .code
    4. main proc
    5.  ; Резервируем в стеке место  для регистров AVX/SSE
    6.  ; Для 6 регистров SSE необходимо только 96 байт
    7.  ; Для 6 регистров AVX 196 байт
    8.  ; Выделяем одно пространство для двух типов расширений - 196 байт
    9.  ; Тогда SSE просто игнорируют дополнительные 64 байта
    10.  sub rsp, 192
    11.  ; Определяем, доступны ли расширения AVX и соответственно регистры YMM
    12.  mov eax, 1
    13.  cpuid
    14.  and ecx, AVXSupport  ; проверяем на поддержку AVX бит 28
    15.  jnz saveAVX  ; если AVX доступно, переходим к метке preserveAVX
    16.  ; Если поддержка расширений AVX отсутствует, сохраняет состояние регистров XMM0-XMM5
    17.  movdqu xmmword ptr [rsp + 00], xmm0
    18.  movdqu xmmword ptr [rsp + 16], xmm1
    19.  movdqu xmmword ptr [rsp + 32], xmm2
    20.  movdqu xmmword ptr [rsp + 48], xmm3
    21.  movdqu xmmword ptr [rsp + 64], xmm4
    22.  movdqu xmmword ptr [rsp + 80], xmm5
    23.  jmp afterSave
    24.  ; сохраняем состояние регистров YMM0-YMM5
    25. saveAVX:
    26.  vmovdqu ymmword ptr [rsp + 000], ymm0
    27.  vmovdqu ymmword ptr [rsp + 032], ymm1
    28.  vmovdqu ymmword ptr [rsp + 064], ymm2
    29.  vmovdqu ymmword ptr [rsp + 096], ymm3
    30.  vmovdqu ymmword ptr [rsp + 128], ymm4
    31.  vmovdqu ymmword ptr [rsp + 160], ymm5
    32. afterSave:  ; здесь какие-нибудь действия в программе
    33.  mov rax, 15 ; например, поместим в регистр RAX какое-нибудь число
    34. ; восстанавливаем значения регистров - в регистре ECX по прежнему значение для AVXSupport
    35. ; можно его использовать, чтобы заново не вызывать инструкцию cpuid
    36.  and ecx, AVXSupport  ; проверяем на поддержку AVX бит 28
    37.  jnz restoreAVX  ; если AVX доступно, переходим к метке restoreAVX
    38. ; если не доступно AVX, восстанавливаем регистры XMM
    39.  movdqu xmm0, xmmword ptr [rsp + 00]
    40.  movdqu xmm1, xmmword ptr [rsp + 16]
    41.  movdqu xmm2, xmmword ptr [rsp + 32]
    42.  movdqu xmm3, xmmword ptr [rsp + 48]
    43.  movdqu xmm4, xmmword ptr [rsp + 64]
    44.  movdqu xmm5, xmmword ptr [rsp + 80]
    45.  jmp exit
    46. ; восстанавливаем регистры YMM
    47. restoreAVX:
    48.  vmovdqu ymm0, ymmword ptr [rsp + 000]
    49.  vmovdqu ymm1, ymmword ptr [rsp + 032]
    50.  vmovdqu ymm2, ymmword ptr [rsp + 064]
    51.  vmovdqu ymm3, ymmword ptr [rsp + 096]
    52.  vmovdqu ymm4, ymmword ptr [rsp + 128]
    53.  vmovdqu ymm5, ymmword ptr [rsp + 160]
    54. exit:
    55.  add rsp, 192
    56.  ret
    57. main endp
    58. end
    Как и в общем случае, проблема может заключаться в том, что мы можем не знать точно, какие именно расширения поддерживаются на текущей машине. Поэтому сначала с помощью инструкции cpuid проверяем наличие поддержки регистров YMM. Для этого к результату инструкции в регистре ECX применяем маску AVXSupport, которая выявляет установку 28 бита (который собственно и говорит о поддержке AVX):
    Код (ASM):
    1. mov eax, 1
    2. cpuid
    3. and ecx, AVXSupport  ; проверяем на поддержку AVX бит 28
    4. jnz saveAVX
    Если бит установлен, соответственно после выполнения инструкции AND результат ненулевой, то переходим к метке saveAVX для сохранения состояния регистров YMM. Если бит не установлен, то сохраняем регистры XMM.
    Для сохранения 6 регистров XMM нужно 16 байт * 6 = 96 байт. На сохранение 6 регистров YMM - 196 байт. В данном случае для простоты для обоих типов регистров в стеке выделяется 196 байт. Затем последовательно сохраняем регистры:
    Код (ASM):
    1. movdqu xmmword ptr [rsp + 00], xmm0
    2. movdqu xmmword ptr [rsp + 16], xmm1
    3. movdqu xmmword ptr [rsp + 32], xmm2
    4. movdqu xmmword ptr [rsp + 48], xmm3
    5. movdqu xmmword ptr [rsp + 64], xmm4
    6. movdqu xmmword ptr [rsp + 80], xmm5
    7. jmp afterSave
    8. ; сохраняем состояние регистров YMM0-YMM5
    9. saveAVX:
    10. vmovdqu ymmword ptr [rsp + 000], ymm0
    11. vmovdqu ymmword ptr [rsp + 032], ymm1
    12. vmovdqu ymmword ptr [rsp + 064], ymm2
    13. vmovdqu ymmword ptr [rsp + 096], ymm3
    14. vmovdqu ymmword ptr [rsp + 128], ymm4
    15. vmovdqu ymmword ptr [rsp + 160], ymm5
    При завершении программы аналогично восстанавливаем регистры.
     
    Последнее редактирование: 1 ноя 2026 в 02:32
  3. Mikl___

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

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

    Строки

    Операции со строками

    Строки представляют набор значений, который хранится в непрерывной области памяти. Процессор x86-64 поддерживает работу с 4 типами строк: строки значений byte, строки из чисел word, строки из двойных слов и строки из четверных слов. Для работы с этим строками процессор x86-64 предоставляет ряд инструкций для копирования, сравнения, поиска в строке и ряд других инструкций, которые далее будут рассмотрены.
    Строковые инструкции используют определенные регистры в качестве операндов:
    • RSI: хранит индекс значений исходной строки
    • RDI: хранит индекс значений целевой строки
    • RCX: счетчик значений
    • AL, AX, EAX и RAX
    • Флаги в регистре FLAGS
    Копирование строк
    Для копирования строк применяются следующие инструкции:
    • movsb: извлекает байт по адресу RSI, сохраняет его по адресу RDI, а затем увеличивает или уменьшает регистры RSI и RDI на 1.
    • movsw: выбирает слово по адресу RSI, сохраняет его по адресу RDI, а затем увеличивает или уменьшает RSI и RDI на 2.
    • movsd: извлекает двойное слово по адресу RSI, сохраняет его по адресу RDI, а затем увеличивает или уменьшает регистры RSI и RDI на 4.
    • movsq: извлекает четверное слово по адресу RSI, сохраняет его по адресу RDI, а затем увеличивает или уменьшает регистры RSI и RDI на 8.
    Например, скопируем одну строку в другую:
    Код (ASM):
    1. .data
    2. nums1 word 10, 11, 12, 13, 14, 15, 16, 17
    3. nums2 word 8 dup (?)
    4. .code
    5. main proc
    6.  lea rsi, nums1
    7.  lea rdi, nums2
    8.  mov rcx, 8  ; 8 элементов в nums1
    9.  movsw
    10.  ret
    11. main endp
    12. end
    Здесь копируем данные из nums1 в nums2. Для этого адрес исходной строки nums1 загружаем в RSI, а адрес целевой строки nums2 - в регистр RDI. В RCX помещаем количество элементов , которые надо скопировать. Важно, что речь идет о количестве элементов, а не байт. Так, в данном случае мы хотим скопировать все 8 элементов из nums1 в nums2, которые в целом занимают 16 байт. И для копирования слов выполняем инструкцию movsw.
    Однако по умолчанию все инструкции копирования помещают только первый элемент исходной строки в целевую строку. Но нам то нужно в данном случае скопировать 8 чисел. Для повторения копирования применяется префикс rep
    Префикс rep
    Префикс rep (repeat повторять) указывается перед инструкцией, которая работает со строками. Так, если префикс rep присутствует, при выполнении инструкции movsb процессор проверяет RCX, чтобы увидеть, содержит ли этот регистр число 0. Если не содержит, то процессор перемещает байт из RSI в RDI и уменьшает значение в регистре RCX. Этот процесс повторяется до тех пор, пока RCX не станет равным 0. Если RCX содержит 0 при первоначальном выполнении, инструкция movsb не будет копировать байты данных.
    Например, изменим предыдущую программу, применив rep:
    Код (ASM):
    1. .data
    2. nums1 word 10, 11, 12, 13, 14, 15, 16, 17
    3. nums2 word 8 dup (?)
    4. .code
    5. main proc
    6.  lea rsi, nums1
    7.  lea rdi, nums2
    8.  mov rcx, 8
    9.  rep movsw
    10.  movzx rax, nums2[4] ; проверяем 3-й элемент из nums2, он должен быть равен 12
    11.  ret
    12. main endp
    13. end
    Префикс rep указывает процессору повторить эту операцию столько раз, сколько указано в регистре RCX.
    Стоит отметить, что конкретно в случае выше процесс копирования не является оптимальным: нам надо скопировать 8 слов или 16 байт, и это количество кратно 8. На первый взгляд для копирования слов надо использовать инструкцию movsw, которая собственно и предназначена для копирования слов. Но для ассемблера в данном случае нет разницы - скопирует он 8 раз по 2 байта последовательно расположенные данные или скопирует теже данный 2 раза но по 8 байт. Поэтому в случае выше мы могли бы применить копирование по восьмеричным словам:
    Код (ASM):
    1. .data
    2. nums1 word 10, 11, 12, 13, 14, 15, 16, 17
    3. nums2 word 8 dup (?)
    4. .code
    5. main proc
    6.  lea rsi, nums1
    7.  lea rdi, nums2
    8.  mov rcx, 2  ; 8 слов - это два четверных слова
    9.  rep movsq  ; копируем по четверному слову
    10.  movzx rax, nums2[4] ; проверяем 3-й элемент из nums2, он должен быть равен 12
    11.  ret
    12. main endp
    13. end
    Если количество байто не кратно 8 или вообще нечетное, то для большей эффективности можно комбинровать копирование различных данных:
    Код (ASM):
    1. .data
    2. nums1 word 10, 11, 12, 13, 14, 15, 16, 17, 18
    3. nums2 word 9 dup (?)
    4. .code
    5. main proc
    6.  lea rsi, nums1
    7.  lea rdi, nums2
    8.  mov rcx, 2  ; 9 слов - это 2 четверных слова + 1 слово
    9.  rep movsq  ; копируем по четверному слову
    10.  movsw  ; копируем одно слово
    11.  movzx rax, nums2[8*2] ; RAX = 18
    12.  ret
    13. main endp
    14. end
    В данном случае для копирования данных потребуется две инструкции movsq и одна инструкция movsw. Даная схема наиболее эффективна, если оба массива выровнены по границам четырех слов. Если подобного выравнивания нет, то можно переместить инструкции, которые копируют меньшее количество данных - в данном случае movsw до инструкции movsq, чтобы movsq работала с данными, выровненными по четырём словам:
    Код (ASM):
    1. .data
    2. rnd1 dword 1
    3. rnd2 word 2
    4. nums1 word 10, 11, 12, 13, 14, 15, 16, 17, 18 ; не выровнено по четверным словам
    5. nums2 word 9 dup (?)
    6. .code
    7. main proc
    8.  lea rsi, nums1
    9.  lea rdi, nums2
    10.  mov rcx, 2  ; 9 слов - это два четверных слова + 2 двойных слова
    11.  movsw  ; копируем 1 слово
    12.  rep movsq  ; копируем по четверному слову
    13.  movzx rax, nums2[6*2] ; RAX = 18
    14.  ret
    15. main endp
    16. end
    Направление копирования
    Флаг направления в регистре FLAGS управляет навправлением копирования элементво строк. Если флаг направления сброшен, процессор увеличивает RSI в RDI после обработки каждого элемента строки. Например, инструкции movs скопируют байт, слово, двойное слово или четверное слово из RSI в RDI, а затем увеличат RSI и RDI на 1, 2, 4 или 8 (соответственно). Если флаг направления установлен, x86-64 уменьшает RSI в RDI после обработки каждого элемента строки.
    Для управления флагами направления применяются инструкции cld (сброс флага направления) и std (установка флага направления). Например, скопируем данные из одной строки в другую в противоположном направлении:
    Код (ASM):
    1. .data
    2. nums1 word 10, 11, 12, 13, 14, 15, 16, 17
    3. len = $ - nums1  ; размер массива
    4. count = len / 2  ; количество элементов
    5. elemSize = 2  ; размер одного элемента
    6. nums2 word 8 dup (?)
    7. .code
    8. main proc
    9.  lea rsi, nums1
    10.  add rsi, len - elemSize ; указывает на последний элемент
    11.  lea rdi, nums2
    12.  add rdi, len - elemSize ; указывает на последний элемент
    13.  
    14.  mov rcx, count; RCX = 8
    15.  std  ; устанавливаем флаг направления
    16.  rep movsw
    17.  movzx rax, nums2[7 * elemSize] ; проверяем последний элемент, он должен быть равен 17
    18.  ret
    19. main endp
    20. end
    Здесь регистры RSI и RDI перед копированием строки указывают на последний элемент строки. И при вызове инструкции movsw сначала копируются последниие элементы строки, потом предпоследние и так далее вплость до первого элемента.
    Стоит отметить, что при работе с внешними API могут применяться определенные требования к флагам направления. Так, Microsoft ABI требует, чтобы флаг направления был сброшен при вызове функции, которая совместима с Microsoft ABI (например, функции C/C++ на Windows). Поэтому, если внутри процедуры на ассемблере требуется установить флаг направления, то после завершения использования флага его надо сбросить (в частности перед вызовом любого другого кода или возвратом из процедуры).
    Копирование в обратном направлении может быть применяться, когда нам надо сдвинуть элементы строки. Например:
    Код (ASM):
    1. .data
    2. nums1 word 0
    3. nums2 word 10, 11, 12, 13, 14, 15, 16, 17
    4. len = $ - nums2  ; размер массива
    5. count = len / 2  ; количество элементов
    6. elemSize = 2  ; размер одного элемента
    7. .code
    8. main proc
    9.  lea rsi, nums1
    10.  add rsi, len - elemSize ; указывает на предпоследний элемент nums2
    11.  lea rdi, nums2
    12.  add rdi, len - elemSize  ; указывает на последний элемент
    13.  
    14.  mov rcx, count; RCX = 8
    15.  std  ; устанавливаем флаг направления
    16.  rep movsw
    17.  movzx rax, nums2[7*elemSize] ; проверяем 7-й (последний) элемент, он должен быть равен 16
    18.  ret
    19. main endp
    20. end
    Здесь следует отметить одну особенность - поскольку данные в памяти будут располагаться последовательно, и за числом nums1 сразу будет идти nums2, то на уровне программы мы можем рассматривать nums1 не просто как одиночное число, но и как массив, который перекрывает nums2. Соответственно выражение nums1[7*elemSize] - аналогично обращению к 6-му элементу массива nums2 (nums1[6*elemSize]). И при установки источника копирования регистр RSI содержит адрес предпоследнего элемента в nums2:
    Код (ASM):
    1. lea rsi, nums1
    2. add rsi, len - elemSize
    Хотя в принципе мы могли бы использовать в качестве источника и массив nums2:
    Код (ASM):
    1. lea rsi, nums2
    2. add rsi, len - 2 * elemSize ; указывает на предпоследний элемент
    В итоге при копировании последний элемент nums1 (предпоследний элемент nums2) копируется в последний элемент nums2 и так далее:
    Код (ASM):
    1. 0, 10, 11, 12, 13, 14, 15, 16, 17
    2. 0,  0, 10, 11, 12, 13, 14, 15, 16
     
    Последнее редактирование: 1 ноя 2026 в 02:43
  4. Mikl___

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

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

    Строки

    Сравнение строк

    Для сравнения строк из различных типов чисел применяются инструкции
    • cmpsb: сравнивает строку байтов
    • cmpsw: сравнивает строку из слов
    • cmpsd: сравнивает строку из двойных слов
    • cmpsq: сравнивает строку из четверных слов
    Процессор сравнивает значение, на которое ссылается регистр RDI, со значением, на которое указывает регистр RSI. RCX содержит количество элементов в исходной строке (не путать с количеством обрабатываемых байтов).
    Вместе с этой инструкцией можно использовать ряд префиксов:
    • Префикс repe указывает повторять операцию до тех пор, пока сравнение равно, или до количества раз, указанного RCX (в основном применяется для сравнений символьных строк).
    • Префикс repne повторяет операцию, пока сравнение не равно, или до количества раз, указанного RCX
    Без применения этих префиксов инструкции cmps вычитают значение в RDI из значения в RSI и обновляет флаги в соответствии с результатом. После сравнения двух чисел cmps увеличивает или уменьшает регистры RSI и RDI на 1, 2, 4 или 8 (для cmpsb, cmpsw, cmpsd и cmpsq соответственно). cmps увеличивает регистры RSI и RDI, если флаг направления сброшен, и уменьшает их, если флаг установлен. Например, сравненим равные по длине массивы:
    Код (ASM):
    1. .data
    2. nums1 word 10, 11, 12, 13, 14, 15, 16, 17
    3. nums2 word 10, 11, 12, 13, 14, 15, 16, 17
    4. .code
    5. main proc
    6.  lea rsi, nums1
    7.  lea rdi, nums2
    8.  mov rcx, 8  ; 8 слов
    9.  repe cmpsw  ; сравниваем слова
    10.  jz equal
    11.  mov eax, 0  ; если строки не равны
    12.  jmp exit
    13. equal:
    14.  mov eax, 1  ; если строки равны
    15. exit:
    16.  ret
    17. main endp
    18. end
    Здесь сравниваем два массива чисел. Если они равны, помещаем в EAX число 1, если не равны - число 0.
    Если строки имеют или могут иметь разную длину, то задача немного усложняется. Две строки равны, когда их длины равны и элементы в соответствующих позициях равны. Если одна из строк меньше другой, но все ее элементы равны соответствующим символам второй строки (то есть первая строка является подстрокой второй строки), то мы можем считать что первая строка меньше второй.
    В этом случае в регистр RCX помещается длина наименьшей строки. Далее обе строки сравниваются как и вобщем случае. И если они равны, сравниваются их длины.
    Пример сравнения двух строк:
    Код (ASM):
    1. .data
    2. str1 byte "Hello World", 0
    3. str1_len = $ - str1  ; длина первой строки
    4. str2 byte "Hello World", 0
    5. str2_len = $ - str2  ; длина второй строки
    6. .code
    7. main proc
    8.  lea rsi, str1
    9.  lea rdi, str2
    10.  mov rcx, str1_len
    11.  cmp rcx, str2_len  ; сравниваем длины строк
    12.  jbe compare  ; если длина num1 равна или меньше
    13.  mov rcx, str2_len  ; если длина num2 меньше
    14. compare:
    15.  repe cmpsb  ; сравниваем байты
    16.  jnz notequal  ; если не равны
    17.  mov rcx, str1_len
    18.  cmp rcx, str2_len  ; если равны, сравниваем длины строк
    19.  jnz notequal
    20.  mov eax, 1  ; если строки равны
    21.  jmp exit
    22. notequal:
    23.  mov eax, 0  ; если строки не равны
    24. exit:
    25.  ret
    26. main endp
    27. end
    Строки символов обычно сравниваются в алфавитном порядке. Поскольку символы фактически представлены их числовами кодами, то сравнение символов выливается в сравнение их числовых кодов (в данном случае для простоты считаем, что символы в разных регистрах - это разные символы).
    Сравнение больших чисел
    Инструкции cmps удобны для сравнения очень больших чисел, которые выходят за пределы 64 бит. Например:
    Код (ASM):
    1. .data
    2. num1 qword 0000000000000005h, ; младшие 64 бита
    3.  0000000000000002h  ; старшие 64 бита
    4. num2 qword 0000000000000005h, ; младшие 64 бита
    5.  0000000000000002h  ; старшие 64 бита
    6. .code
    7. main proc
    8.  lea rsi, num1[8]  ; загружаем адрес старших 64 битов
    9.  lea rdi, num2[8]  ; загружаем адрес старших 64 битов
    10.  mov rcx, 2  ; сравниваем 2 qword - младшие и старшие 64 бита
    11.  std  ; устанавливаем обратный порядок сравнения
    12.  repe cmpsq  ; сравниваем qword
    13.  cld  ; после сравнения сбрасываем флаг направления
    14.  jnz notequal
    15.  mov eax, 1  ; если строки равны
    16.  jmp exit
    17. notequal:
    18.  mov eax, 0  ; если строки не равны
    19. exit:
    20.  ret
    21. main endp
    22. end
    Здесь сравниваются два 128-разрядных числа. Каждое число фактически представляет массив из двух чисел qword - младших и старших 64 битов. Общий алгоритм сравнения подобных чисел заключается в последовательном сравнении их соответствующих 64 разрядов. Если старшие 64 разряда не равны, то числа уже не равны. Если старшие 64 разряда равны, то сравниваются следующие 64 разряда и так далее.
    Поскольку вначале массива, который представляет 128-разрядное число располагается младшие 64 бит, то нам надо сравнивать с конца. Для этого загражаем в регистры RSI и RDI старшие 64 разряды и устанавливаем флаг направления с помощью инструкции std. Для сравнения каждой 64-разрядной части применяем инструкцию cmpsq
     
    Последнее редактирование: 1 ноя 2026 в 02:46
  5. Mikl___

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

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

    Строки

    Поиск в строке

    Для поиска определенного элемента в строке применяется следующий набор инструкций:
    • scasb: поиск в строке байтов
    • scasw: поиск в строке слов
    • scasd: поиск в строке двойных слов
    • scasq: поиск в строке четверных слов
    Данные инструкции принимают только один операнд - строку, в которую помещается результат поиска и адрес которой загружается в регистр RDI. Строка для поиска загружается в регистре AL (scasb), AX (scasw), EAX (scasd) или RAX (scasq). Инструкции scas сравнивают значение в аккумуляторе (AL, AX, EAX или RAX) со значением, на которое указывает RDI, а затем увеличивает (или уменьшает) значение RDI на 1, 2, 4 или 8. В соответствии с результатом сравнения процессор устанавливает флаги. Так, если символ найден, то устанавливается флаг нуля, а регистр RDI указывает на символ, следующий за найденным.
    При применении префикса repe инструкции scas просматривают строку в поисках элемента, не соответствующего значению в аккумуляторе. При использовании префикса repne scas сканирует строку в поисках первого элемента, равного значению в аккумуляторе. Регистр RCX в этом случае хранит количество элементов, а не байтов, которые необходимо обработать при сканировании.
    Например, найдем индекс символа в строке:
    Код (ASM):
    1. .data
    2. text byte "Hello", 0
    3. len = $ - text  ; длина первой строки
    4. char byte 'l'  ; символ для поиска
    5. .code
    6. main proc
    7.  lea rdi, text  ; загружаем адрес переменной text
    8.  mov rsi, rdi  ; копируем адрес в RSI для последующего поиска индекса
    9.  mov al, char  ; в AL символ для поиска
    10.  mov rcx, len  ; в RCX количество символов строки
    11.  repne scasb  ; ищем байт
    12.  jz found  ; если символ найден, устанавливается флаг нуля ZF
    13.  mov rax, -1  ; если символ не найден, в RAX число -1
    14.  jmp exit
    15. found:
    16.  sub rdi, rsi  ; если символ найден, получаем индекс следующего символа после найденного
    17.  dec rdi  ; отнимает 1 байт - получем индекс найденного символа
    18.  mov rax, rdi  ; в RAX индекс
    19. exit:
    20.  ret
    21. main endp
    22. end
    Здесь в строке "Hello" ищем символ "l". Для этого строку для поиска загружаем в RDI, а в AL - символ (байт), который будем искать.
    Если при выполнении инструкции scas элемент был найден, то устанавливается флаг нуля. И мы можем это проверить и выполнить определенные действия. В данном случае, если символ найден, от значения в RDI (адрес следующего символа за найденным) отнимаем начальный адрес строки text (адрес первого символа строки text), который для удобства сохранили в регистр RSI и таким образом получаем расстояние в байтах между первым символом и следующим за найденным символом. Поскольку все элементы строки - байты, то это расстояние будет представлять индекс следующего символа за найденным. Соответственно, чтобы получить индекс найденного символа (а не следующего за ним), надо от этого расстояния отнять 1 (размер одного элемента в байтах). Например, при поиске символа "l" в строке "Hello" мы получим 2 - это индекс третьего символа.
    Если символ не найден, то в регистр RAX помещаем -1.
     
    Последнее редактирование: 1 ноя 2026 в 02:48
  6. Mikl___

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

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

    Строки

    Сохранение и получение элементов строк

    stos
    Инструкция stosX сохраняет значение аккумулятора в месте, на которое указывает регистр RDI. После сохранения значения процессор увеличивает или уменьшает значение в RDI в зависимости от состояния флага направления. Данная инструкция имеет следующие формы:
    • stosb: сохраняет байт из регистра AL в место по адресу из регистра RDI
    • stosw: сохраняет слово из регистра AX в место по адресу из регистра RDI
    • stosd: сохраняет двойное слово из регистра EAX в место по адресу из регистра RDI
    • stosq: сохраняет четверное слово из регистра RAX в место по адресу из регистра RDI
    Например, изменим изменим определенный элемент массива чисел word, сохранив в него число:
    Код (ASM):
    1. .data
    2. numbers word 20, 21, 22, 23, 24, 25
    3. len = $ - numbers  ; длина строки
    4. elemSize = 2  ; размер одного элемента
    5. num word 33  ; число для сохранения
    6. .code
    7. main proc
    8.  lea rdi, numbers[2*elemsize]  ; загружаем адрес третьего элемента массива numbers
    9.  xor rax, rax
    10.  mov ax, num  ; загружаем слово для замены
    11.  stosw  ; сохраняем по адресу в RDI слово num
    12.  mov ax, word ptr numbers[2 * elemSize]  ; проверяем третий элемент массива numberss
    13.  ret
    14. main endp
    15. end
    В данном случае сохраняем число word по адресу в RDI, который указывает на третий элемент в массиве numbers/
    С префиксом rep этот процесс повторяется столько раз, сколько указано в регистре RCX.
    Частое применение данной инструкции - инициализация массивов и строк некоторым значением. Например, если имеется массив, который надо заполнить числом 2, то можно использовать следующий код
    Код (ASM):
    1. .data
    2. numbers word 20, 21, 22, 23, 24, 25
    3. len = $ - numbers  ; длина строки
    4. elemSize = 2  ; размер одного элемента
    5. count = len / elemSize  ; количество элементов
    6. num word 2  ; число для инициализации массива
    7. .code
    8. main proc
    9.  lea rdi, numbers
    10.  mov rcx, count  ; количество обрабатываемых слов
    11.  xor rax, rax
    12.  mov ax, num
    13.  rep stosw
    14.  mov ax, word ptr numbers[2 * elemSize]  ; проверяем третий элемент массива numberss
    15.  ret
    16. main endp
    17. end
    lods
    Инструкция lods копирует байт, слово, двойное слово или четверное слово, указанное RSI, в регистр AL, AX, EAX или RAX, после чего увеличивает или уменьшает значение регистра RSI на 1, 2, 4 или 8. Данная инструкция имеет следующие формы:
    • lodsb: сохраняет в регистр AL байт по адресу из регистра RSI
    • lodsw: сохраняет в регистр AX слово по адресу из регистра RSI
    • lodsd: сохраняет в регистр EAX двойное слово по адресу из регистра RSI
    • lodsq: сохраняет в регистр RAX четверное слово по адресу из регистра RSI
    Например, загрузим в регистр AX третьее число из массива слов:
    Код (ASM):
    1. .data
    2. numbers word 20, 21, 22, 23, 24, 25
    3. elemSize = 2
    4. .code
    5. main proc
    6.  lea rsi, numbers[2*elemSize]  ; адрес третьего числа
    7.  xor rax, rax
    8.  lodsw  ; AX = 22
    9.  ret
    10. main endp
    11. end
     
    Последнее редактирование: 1 ноя 2026 в 02:51
  7. Mikl___

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

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

    Макросы

    Определение макросов

    В ассемблере макрос представляет идентификатор, который ассемблер преобразует в дополнительный код. Макросы позволяют заменить длинные повторяющиеся последовательности код гораздо более короткими последовательностями. В каком-то смысле макросы MASM можно назвать процедурами/функциями времени компиляции.
    Определение макроса в общем случае имеет следующую форму:
    имя_макроса macro аргументы_макроса
    код макроса
    endm

    Сначала идет объявление макроса - имя макроса, затем оператор macro и после указываются аргументы макроса. Аргументы необязательны, макрос может вообще не иметь аргументов. Завершается макрос оператором endm. Между объявлением макроса и endm располагается собственно код макроса.
    Рассмотрим пример макроса, который складывает 128-разрядное число с 64-разрядным:
    Код (ASM):
    1. .data
    2.  num qword 0ffffffffffffffffh,  ; младшие 64 бит
    3.  4  ; старшие 64 бит
    4. .code
    5. add128 macro
    6.  add rax, rbx
    7.  adc rdx, 0
    8. endm
    9. main proc
    10.  mov rax, num  ; младшие 64 бит - в RAX
    11.  mov rdx, num[8]  ; старшие 64 бит - в RDX
    12.  mov rbx, 3
    13.  add128  ; вставляем макрос
    14.  ret
    15. main endp
    16. end
    В данном случае макрос называется add128:
    Код (ASM):
    1. add128 macro
    2.  add rax, rbx
    3.  adc rdx, 0
    4. endm
    Здесь подразумеваем, что 128-разрядное число располагается в регистрах RDX:RAX: младшие 64 битов в RAX, а старшие 64 битов - в RDX. Прибавляемое 64-битное число передается через регистр RBX. Работа макроса проста - складываем 64-битное число RBX с младшими 64 битами RAX и, если при этом устанавливается флаг переноса, прибавляем бит к старшим 64-битам в RDX.
    Здесь 128-битное число представляет массив num из двух подчисел qword, которое загружается в RAX и RDX. Для вставки макроса в код применяется имя макроса. То есть макрос не вызывается в процессе выполнения, его код вставляется в место вызова макроса на этапе компиляции. То есть фактически мы получим программу:
    Код (ASM):
    1. .data
    2.  num qword 0ffffffffffffffffh,  ; младшие 64 бит
    3.  4  ; старшие 64 бит
    4. .code
    5. main proc
    6.  mov rax, num  ; младшие 64 бит - в RAX
    7.  mov rdx, num[8]  ; старшие 64 бит - в RDX
    8.  mov rbx, 3
    9.  add rax, rbx
    10.  adc rdx, 0
    11.  ret
    12. main endp
    13. end
    Макросы или функции?
    В приведенном выше примере мы могли бы определить макрос сложения чисел в виде отдельной процедуры. И тут может возникнуть вопрос: что выбрать - макросы или функции? Минусом макросов является то, что их код вставляется в каждое место, где они используются. Что ведет к общему увеличению объема программы. С другой стороны, макросы позволяют избежать переходов и сохранений/восстановлений адреса следующей инструкции, что положительно влияет на производительность. Макросы немного быстрее, чем вызовы процедур, потому что вы не выполняете вызов и соответствующие инструкции ret. Кроме того, с макросами сам код становится чуть более читабельным. В общем случае рекомендуется применять макросы для коротких, критичных по времени частей программы. Тогда как процедуры применяются для более длинных блоков кода и когда время выполнения не так критично.
     
    Последнее редактирование: 1 ноя 2026 в 02:58
  8. Mikl___

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

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

    Макросы

    Параметры макроса

    С помощью параметров макросы могут получать извне некоторые данные. Параметры указываются в объявлении макроса через запятую после слова macro:
    имя_макроса macro параметр1, параметр2, ... параметрN
    код макроса
    endm

    При вызове макроса после его имени указываются через запятую значения для параметров:
    имя_макроса значение_параметра1, значение_параметра2, ... значение_параметраN
    Например, определим макроса, который складывает 128-разрядное число с 64-разрядным и оба числа получает через параметры:
    Код (ASM):
    1. .data
    2.  num qword 0ffffffffffffffffh,  ; младшие 64 бит
    3.  4  ; старшие 64 бит
    4. .code
    5. add128 macro l64, h64, number64
    6.  add l64, number64
    7.  adc h64, 0
    8. endm
    9. main proc
    10.  mov rax, num  ; младшие 64 бит - в RAX
    11.  mov rdx, num[8]  ; старшие 64 бит - в RDX
    12.  mov rbx, 3
    13.  add128 rax, rdx, rbx  ; вставляем макрос
    14.  ret
    15. main endp
    16. end
    В данном случае макрос называется add128 и принимает три параметра: l64 (младшие 64 бита 129-разрядного числа), h64 (старшие 64 бита 129-разрядного числа), number64 (прибавляемое число)
    Код (ASM):
    1. add128 macro l64, h64, number64
    2.  add l64, number64
    3.  adc h64, 0
    4. endm
    Внутри макроса проводим все арифметические операции с числами. При этом на момент написания макроса мы можем не знать, как конкретно будут передаваться эти числа - через какие регистры и т.д.
    При вызове макроса в процедуре main передаем его параметрам значения по позиции:
    Код (ASM):
    1. add128 rax, rdx, rbx
    То есть параметру l64 передается значение регистра RAX, параметру h64 - значение регистра RDX, а параметру number64 - регистр RBX.
    В качестве значений для параметров мы можем передавать в макросы регистры (как в случае выше), переменные, константы, главное, чтобы они удовлетворяли требованиям инструкций, с которыми они используются.
    Проверка параметров
    Вполне может быть ситуация, когда по какой-то причине макросу будет передано меньшее количество значений, чем у него есть параметров. К примеру, если мы используем макрос, который написан не нами и к исходному коду которого у нас нет доступа. Например:
    Код (ASM):
    1. add128 rax, rdx
    В этом случае при компиляции ассемблер может нам отобразить какую-то общую ошибку, что что-то не так с вызовом макроса. Мы можем проверить подобную ситуацию и настроить сообщение об ошибке, которое даст понимание, что именно не так. Для проверки параметров MASM предоставляет ряд специальных конструкций, которые аналогичны условной конструкции if:
    • ifnb arg: условие истинно, если значение для параметра не указано
    • ifdif arg1, arg2: условие истинно, если оба параметра различаются (учитывается регистр символов)
    • ifdifi arg1, arg2: условие истинно, если оба параметра различаются (регистр символов не учитывается)
    • ifidn arg1, arg2: условие истинно, если оба параметра идентичны (учитывается регистр символов)
    • ifidni arg2, arg2: условие истинно, если оба параметра идентичны (регистр символов не учитывается)
    • ifb arg: условие истинно, если значение для параметра не указано. Фактически является сокращением выражения ifidn <arg>, <>
    В данном случае arg, arg1, arg2 - значения, которые передаются операторам if
    Проверим наличие значения для параметра:
    Код (ASM):
    1. .data
    2.  num qword 0ffffffffffffffffh,  ; младшие 64 бит
    3.  4  ; старшие 64 бит
    4. .code
    5. add128 macro l64, h64, number64
    6.  ; проверяем наличие параметра number64
    7.  ifb <;number64>
    8.  .err <;add128 requires 3 operands>
    9.  endif
    10.  add l64, number64
    11.  adc h64, 0
    12. endm
    13. main proc
    14.  mov rax, num  ; младшие 64 бит - в RAX
    15.  mov rdx, num[8]  ; старшие 64 бит - в RDX
    16.  mov rbx, 3
    17.  add128 rax, rdx  ; передаем только два значения
    18.  ret
    19. main endp
    20. end
    В коде макроса с помощью выражения
    ifb <;number64>
    проверяем отсутствие значения для параметра number64. И если это значение отсутствует, выполняем выражение .err
    .err <;add128 requires 3 operands>
    Оператору .err в угловых скобках передается сообщение об ошибке, которое будет отображаться при компиляции при отстуствии значения для number64.
    В качестве альтернативы проверки значения параметра мы можем использовать суффикс :req, который указывает, что параметр обязательный (required):
    Код (ASM):
    1. add128 macro l64:req, h64:req, number64:req
    2.  add l64, number64
    3.  adc h64, 0
    4. endm
    В этом случае, если параметру не будет передано значение, в процессе компиляции мы получим ошибку типа следующей:
    hello.asm(14) : error A2125:missing macro argument
    Другое решение проблемы отсутствующих параметров - установка для параметров значений по умолчанию:
    Код (ASM):
    1. .data
    2.  num qword 0ffffffffffffffffh,  ; младшие 64 бит
    3.  4  ; старшие 64 бит
    4. .code
    5. add128 macro l64:=<;rax>, h64:=<rdx>, number64:=<0>
    6.  add l64, number64
    7.  adc h64, 0
    8. endm
    9. main proc
    10.  mov rax, num  ; младшие 64 бит - в RAX
    11.  mov rdx, num[8]  ; старшие 64 бит - в RDX
    12.  mov rbx, 3
    13.  add128  ; без аргументов
    14.  ret
    15. main endp
    16. end
    Для установки значения по умолчанию с помощью оператора := параметру присваивается в угловых скобках значение по умолчанию. Например:
    l64:=<;rax>
    В данном случае, если параметру l64 не передано значения, то он берет значение из регистра RAX. Аналогичным образом можно установить конкретные значения. Например, для параметра number64 значение по умолчанию - число 0:
    number64:=<;0>
    Оператор opattr
    Чтобы проверить, что именно представляет аргумент макроса, применяется оператор opattr:
    opattr выражение
    В качестве операнда он принимает некоторое выражение - это может быть и аргумент макроса, и какое-то более сложное по структуре выражение.
    opattr возвращает целочисленное значение, которые представляет битовую маску и которое позволяет определить тип выражения:
    БитЗначение
    0Выражение представляет метку
    1Выражение представляет перемещаемое значение
    2Выражение представляет константу
    3Выражение представляет прямую адресацию
    4Выражение представляет регистр
    5устравшее (выражение содержит неопределенный идентификатор)
    6Выражение представляет обращение к памяти стека
    7Выражение ссылается на внешний идентификатор
    Применим оператор opattr:
    Код (ASM):
    1. .data
    2.  num qword 0ffffffffffffffffh,  ; младшие 64 бит
    3.  4  ; старшие 64 бит
    4. .code
    5. add128 macro l64, h64, number64
    6.  ; проверяем, что l64 и h64 - регистры
    7.  if (((opattr l64) and 10h) AND ((opattr h64) and 10h))
    8.  add l64, number64
    9.  adc h64, 0
    10.  else
    11.  .err <;the first two operands of add128 should be registers>
    12.  endif
    13. endm
    14. main proc
    15.  mov rax, num  ; младшие 64 бит - в RAX
    16.  mov rdx, num[8]  ; старшие 64 бит - в RDX
    17.  mov rbx, 3
    18.  add128 rax, 4, 3  ; второй параметр должен быть регистром, а не константой
    19.  ret
    20. main endp
    21. end
    Здесь мы проверяем, что первые два параметра макроса - l64 и h64 являются регистрами. Если параметр - регистр, то выражение opattr l64 (как и opattr h64) возвратит байт, у которого установлен 4 бит. Чтобы проверить его установку, мы можем применить логическое умножение на 10h и ли число 16 в лесятичной системе
    (opattr l64) and 10h
    И если 4-й бит установлен, то выражение возвратит 1. Подобным образом проверяем и второй параметр макроса.
    Если первые два параметра представляют регистры, то выполняем сложение, если нет - то генерируем ошибку с определенным сообщением.
    Для проверки параметра можно использовать следующие маски:
    • 0: неопределенный символ или некорректное выражение
    • 34 / 22h: доступ к памяти в виде [reg + const]
    • 36 / 24h: константа
    • 37 / 25h: имя процедуры или метка с символом :
    • 38 / 26h: выражение в виде offset var, где var - переменная из секции .data
    • 42 / 2Ah: глобальный идентификатор (например, из секции .data)
    • 43 / 2Bh: выражение доступа к памяти в виде [reg + label], где label представляет имя процедуры или метка
    • 48 / 30h: регистр общего назначения, XMM, YMM, ZMM, ST, либо другой регистр специального назначения
    • 98 / 62h: обращение к стеку в виде [rsp + xxx] и [rbp + xxx]
    Подобным образом мы можем проверить и конктерный регистр:
    Код (ASM):
    1. add128 macro l64, h64, number64
    2.  ; проверяем, представляет ли l64 регистр RAX
    3.  ifidn <;l64>, <rax>
    4.  ; действия, если регистр - RAX
    5.  else
    6.  ; действия, если регистр - не RAX
    7.  endif
    8. endm
     
    Последнее редактирование: 1 ноя 2026 в 03:18
  9. Mikl___

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

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

    Макросы

    Метки в макросах

    Если макросы используют метки, то при вызове макроса мы можем столкнуться с проблемами:
    Код (ASM):
    1. .code
    2. equal macro op1, op2
    3.  cmp op1, op2
    4.  jz isEqual
    5.  mov rax, 0
    6.  jmp exit
    7. isEqual:
    8.  mov rax, 1
    9. exit:
    10. endm
    11. main proc
    12.  mov rcx, 2
    13.  mov rdx, 3
    14.  equal rcx, 2
    15.  equal rdx, 2
    16.  ret
    17. main endp
    18. end
    Здесь макрос equal принимает два параметра и проверяет их на равенство. Если они равны осуществляется переход к метке isEqual, на которой в регистр RAX помещается число 1. Если числа не равны, в RAX помещается число 0, и происходит переход к метке exit.
    Поскольку код макросов вставляется в место их вызова при компиляции, то мы получим ситуацию, что метки isEqual и exit будут определены два раза, что недопустимо, так как метка должна быть определена только один раз. Чтобы решить эту проблему, мы можем использовать локальные метки, которые задаются внутри макроса с помощью оператора local
    local метка1, метка2, ... меткаN
    Оператору local передаются через запятую идентификаторы, которые используются внутри макроса. Так, изменим предыдущий пример, используя локальные метки:
    Код (ASM):
    1. .code
    2. equal macro op1, op2
    3.  local isEqual, exit  ; объявляем локальные метки
    4.  cmp op1, op2
    5.  jz isEqual
    6.  mov rax, 0
    7.  jmp exit
    8. isEqual:
    9.  mov rax, 1
    10. exit:
    11. endm
    12. main proc
    13.  mov rcx, 2
    14.  mov rdx, 3
    15.  equal rcx, 2
    16.  equal rdx, 2
    17.  ret
    18. main endp
    19. end
     
    Последнее редактирование: 1 ноя 2026 в 03:21
  10. Mikl___

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

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

    Макросы

    Условная компиляция и циклы

    Ассемблер MASM поддерживает условную компиляцию, которая позволяет при определенных условиях компилировать определенный код. Для этого применяется конструкция if:
    Код (ASM):
    1. if условие
    2.  текст
    3. endif
    После слова if указывается условие. В качестве условия может принимае Это должно быть константное выражение (константа или операция над константами) или вызов функции на этапе компиляции, результатом которого является целочисленное значение. Во время компиляции ассемблер оценивает это выражение. Если выражение оценивается как истинное (не равно нулю), MASM обрабатывает код, который идет до endif. Если выражение оценивается как ложное (равно нулю), MASM обрабатывает весь текст между if и соответствующим предложением endif, как если бы это был комментарий. Причем поскольку условие оценивается в момент сборки, то оно не должно вовлекать переменные, значение которых известно только во время выполнение, а не во время компиляции.
    Конструкция if поддерживает необязательные выражения elseif и else, которые позволяют добавить альтернативные действия, если условие после if неверно:
    Код (ASM):
    1. if условие1
    2.  текст
    3. elseif условие2
    4.  текст
    5. else
    6.  текст
    7. endif
    Если условие после if оценивается как истинное, то MASM обрабатывает текст до выражения elseif. Затем он пропускает весь текст (то есть обрабатывает его как комментарий) до тех пор, пока не встретит предложение endif.
    Если условие после if оценивается неверно, MASM пропускает весь текст, пока не встретит выражение elseif, else или endif.
    Если встречается выражение elseif, MASM оценивает условие после elseif. Если оно верно, MASM обрабатывает текст между предложениями elseif и else (или до выражения endif, если выражение else отсутствует). Конструкция if может содержать множество выражений elseif. Если условие после elseif неверно, MASM пропускает связанный с ними текст и начинает обрабатывать текст в выражении else при его наличии.
    Обычно условная компиляция применяется при необходимости разграничить компиляцию в зависимости от внешних условий. Например, в зависимости от процессора могут отличать возможности, которые нам предоставляются для создания программы. Так, если процессор не поддерживает 64-разрядные регистры, то соответственно мы не сможем их использовать. И тут мы могли бы применить условную компиляцию:
    Код (ASM):
    1. cpu64 = 1
    2. .code
    3. main proc
    4.  if cpu64
    5.  mov rax, 64
    6.  else
    7.  mov eax, 32
    8.  endif
    9.  ret
    10. main endp
    11. end
    Здесь условие представлено константой cpu64. Будет считать, что если процессор - 64-разрядный, то для ее устанавливаем значение 1. Для простоты, если это условие равно 1, то используем 64-разрядный регистр RAX. Если условие равно 0 (то есть процессор 32-разрядный), то используем 32-разрядный регистр EAX.
    Другое частое применение условной компиляции представляет тестирование или отладка, когда при тестировании и отладке выводится информация о состоянии регистров или переменных на консоль.
    Циклические конструкции
    Операторы MASM while..endm, for..endm и forc..endm предоставляют циклические конструкции, которые выполняются во время компиляции.
    Оператор while указывает MASM многократно обрабатывать одну и ту же последовательность операторов во время сборки. Это удобно для построения таблиц данных, а также для обеспечения традиционной циклической структуры для программ времени компиляции. Этот оператор имеет следующий синтаксис:
    while условие
    код
    endm

    Если условие после while является неверным, MASM пропускает текст между выражениями while и endm (аналогично оператору if). Если выражение является истинным, MASM обработает операторы между выражениями while и endm, а затем повторяет этот процесс, пока истинно условие. Рассмотрим простейший пример:
    Код (ASM):
    1. .data
    2.  nums dword 1, 2, 3, 4
    3. .code
    4. i = 0
    5. main proc
    6.  xor rax, rax
    7.  while i LT lengthof nums
    8.  add eax, nums[i * 4]  ; константа i в качестве индекса
    9.  i = i + 1
    10.  endm
    11.  ret
    12. main endp
    13. end
    Здесь оператор while проверяет условие i LT lengthof nums. Для определения условия применяется оператор LT (меньше чем), то есть, чтобы условие было истинным, левый операнд - значение константы i должно быть меньше правого операнда - lengthof nums. Константа i представляет своего рода индекс для обращения к элементам к числам в массиве nums. Правый операнд - lengthof nums с помощью оператора lengthof вычисляет количество элементов в массиве nums. Поскольку в массиве nums 4 элемента, то действия внутри конструкции while будут повторяться, покак значение константы i не окажется равным 4. Каждое повторение цикла называется итерацией, то есть в данном случае выполняться 4 итерации.
    Для примера внутри конструкции while складываем каждое число из массива nums с содержимым регистра EAX и увеличиваем значение i на единицу. В итоге данная конструкция сработает 4 раза.
    Стоит обратить внимание, что данный код срабатывает именно на этапе сборки, а не выполнения. И в итоге после сборки будет сгенерирован код:
    Код (ASM):
    1. .data
    2.  nums dword 1, 2, 3, 4
    3. .code
    4. i = 0
    5. main proc
    6.  xor rax, rax
    7.  add eax, nums[0]  ; константа i в качестве индекса
    8.  add eax, nums[1]  ; константа i в качестве индекса
    9.  add eax, nums[2]  ; константа i в качестве индекса
    10.  add eax, nums[3]  ; константа i в качестве индекса
    11.  ret
    12. main endp
    13. end
    Цикл for
    Конструкция for имеют следующий синтаксис:
    Код (ASM):
    1. for идентификатор, <;arg1, arg2, ..., argN>
    2.  код
    3. endm
    Циклическая конструкция for повторяет код один раз для каждого из аргументов - от arg1 до argN, которые указываются внутри угловых скобок <...>. При каждом повторении цикла текущий аргумент присваивается идентификатору: на первой итерации цикла идентификатору присваивается значение arg1, на второй итерации — arg2 и так далее до последней итерации, когда установлено значение argN. Например, следующий цикл for сгенерирует код, который помещает в стек регистры RAX, RBX, RCX и RDX:
    Код (ASM):
    1. for reg, <;rax, rbx, rcx, rdx>
    2. push reg
    3. endm
    Этот код фактически эквивалентен следующему коду:
    Код (ASM):
    1. push rax
    2. push rbx
    3. push rcx
    4. push rdx[/COD]
     
    Последнее редактирование: 1 ноя 2026 в 03:29
  11. Mikl___

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

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

    Макросы

    Генерация таблиц для поиска

    Одной и распространенных ситуаций применения макроконструкций представляет генерация таблиц для поиска. Например, нам нужен набор чисел типа byte от 1 до 100. В общем случае мы могли бы написать так:
    Код (ASM):
    1. .data
    2.  nums byte 1, 2, 3, 4, 5, 6, ....., 99, 100
    Писать вручную - не очень удобно, тем более всегда можно допустить ошибку на таком большом диапазоне чисел. И вместо ручного создания массива чисел можно использовать циклы:
    Код (ASM):
    1. .data
    2. index = 1
    3. last = 101
    4. nums equ this byte
    5. while index lt last
    6.  byte index
    7.  index = index + 1
    8. endm
    9. .code
    10. main proc
    11.  xor rax, rax
    12.  mov al, nums  ; al = 1
    13.  ret
    14. main endp
    15. end
    Здесь чуть больше кода. Для хранения добавляемого в массив значения определена переменная index, а для хранения максимального значения - константа last. Для задания массива nums применяется следующая запись:
    nums equ this byte
    Цикл while на каждой итерации определяет в массиве nums один байт, который равен index, пока index не станет равным last. То есть в итоге мы опять же получим массив nums со 100 числами.
    Код (ASM):
    1. while index lt last
    2.  byte index
    3.  index = index + 1
    4. endm
    И мы можем пойти чуть дальше и определить макрос, который будет генерировать определенный диапазон значений. Например, создадим таблицу для преобразования символов из нижнего регистра в верхний. Для этого определим следующий код:
    Код (ASM):
    1. ; макрос генерирует последовательность чисел от start до last
    2. createRange macro start, last
    3.  index = start
    4.  while index lt last
    5.  byte index
    6.  index = index + 1
    7.  endm
    8. endm
    9. .data
    10. character byte "y"  ; символ для преобразования в верхний регистр
    11. ; Таблица для преобразования символов из нижнего регистра в верхний
    12. table equ this byte
    13.  createRange 0, "a"  ; на позиции 0-96 добавляем символьные коды от 0 до "a" не включая
    14.  createRange "A", "Z"+1  ; на позиции 97-122 добавляем символьные коды от "A" до "Z" включая
    15.  createRange "z"+1, 128  ; на позиции 123-127 добавляем оставшиеся символьные коды таблицы ASCII
    16. .code
    17. main proc
    18.  xor rax, rax
    19.  mov al, character  ; AL = 121 - код символа y
    20.  lea rbx, table
    21.  xlat
    22.  mov character, al  ; AL = 89 (код символа Y)
    23.  ret
    24. main endp
    25. end
    Макрос createRange принимает два параметра - числовой код начального символа диапазона и числовой код конечного символа. Для генерации диапазона применяется цикл while. В секции .data с помощью этого макроса генерируем три поддиапазона:
    Код (ASM):
    1. createRange 0, "a"  ; на позиции 0-96 добавляем символьные коды от 0 до "a" не включая
    2. createRange "A", "Z"+1  ; на позиции 97-122 добавляем символьные коды от "A" до "Z" включая
    3. createRange "z"+1, 128  ; на позиции 123-127 добавляем оставшиеся символьные коды таблицы ASCII
    В общем случае по каждому индексу в таблице table будет располагать равный индексу числовой код символа (например, по индексу 10 - числовой код 10) за исключением диапазаона индексов 97-122, поскольку они представляют числовые коды символов в нижнем регистре. И на эти позиции мы повторно помещаем числовые коды символов в верхнем регистре - от "A" до "Z". Таким образом, мы получаем таблицу с числовыми кодами символов.
    Для замены числового кода символа применяется инструкция xlat, которая фактически аналогична инструкции
    Код (ASM):
    1. mov al, [rbx + al * 1]
    Эта инструкция использует текущее значение регистра AL в качестве индекса в массиве, базовый адрес которого находится в RBX. Она выбирает байт по этому индексу в массиве и копирует этот байт в регистр AL.
     
    Последнее редактирование: 1 ноя 2026 в 03:34
  12. Mikl___

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

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

    Взаимодействие с кодом на C/C++

    Первая программа на MASM/C

    Ассемблер MASM позволяет использовать гибридный подход, при котором часть программы пишется на ассемблере, а часть - на С/С++. Это позволяет сократить и упростить программу. Например, определим файл app.c с кодом на языке C:
    А в файле app.c определим следующий код:
    Код (C++):
    1. #include <stdio.h>
    2. // подключаем внешнюю функцию hello из файла hello.asm
    3. extern void hello(void);
    4. int main()
    5. {
    6.  printf("Hello starts\n");
    7.  hello();
    8.  printf("Hello ends\n");
    9. }
    Это обычная программа на языке С, в которой с помощью оператора extern подключаем некоторую внешнюю функцию hello, которая ничего не возвращает и не принимает никаких параметров. В функции main вызываем эту внешнюю функцию hello.
    Теперь в той же папке определим файл hello.asm, который будет содержать следующий код на ассемблере:
    Код (ASM):
    1. .code
    2. option casemap:none
    3.  
    4. ; определяем процедуру hello
    5. public hello
    6. hello proc
    7.  ret ; возвращаемся в вызывающий код
    8.  
    9. hello endp
    10. end
    Секция .code здесь начинается с директивы option
    Код (ASM):
    1. option casemap:none
    Директива option указывает MASM сделать все символы чувствительными к регистру. Это необходимо, потому что MASM по умолчанию нечувствителен к регистру и отображает все идентификаторы в верхнем регистре (поэтому hello() станет HELLO()). Но язык C чувствителен к регистру и рассматривает hello() и HELLO() как два разных идентификатора.
    Далее идет определение функции hello. С помощью оператора public указываем, что функция hello будет видна вне исходного/объектного файла MASM:
    Код (ASM):
    1. public hello
    Без этого оператора функция hello() была бы доступна только внутри текущего файла, и при компиляции компилятор не смог бы ее найти.
    Затем между операторами hello proc и hello endp определяется функция hello, которая ничего не делает.
    Скомпилируем оба файла - app.c и hello.asm в одну программу. Рассмотрим напримере ассемблера MASM64 и компилятора для Visual C++ в Visual Studio. Для компиляции откроем программу x64 Native Tools Command Prompt for VS 2022, которая устанавливается вместе с Visual Studio, и перейдем в ней к папке, где располагаются файлы. Затем выполним следующую команду
    ml64 /c hello.asm

    Опция /c говорит, что файл hello.asm надо только скомпилировать в объектный файл. И после этой команды в папке программы будет скомпилирован файл hello.obj:
    **********************************************************************
    ** Visual Studio 2022 Developer Command Prompt v17.5.5
    ** Copyright (c) 2022 Microsoft Corporation
    **********************************************************************
    [vcvarsall.bat] Environment initialized for: 'x64'

    C:\Program Files\Microsoft Visual Studio\2022\Community>cd c:\asm

    c:\asm>ml64 /c hello.asm
    Microsoft (R) Macro Assembler (x64) Version 14.35.32217.1
    Copyright (C) Microsoft Corporation. All rights reserved.

    Assembling: hello.asm

    c:\asm>

    Далее используем скомпилируем всю программу с помощью команды:
    cl app.c hello.obj

    В итоге будет скомпилирован файл app.exe, который мы можем запустить и который при запуске выполнит ассемблерную функцию hello:
    c:\asm>cl app.c hello.obj
    Microsoft (R) C/C++ Optimizing Compiler Version 19.35.32217.1 for x64
    Copyright (C) Microsoft Corporation. All rights reserved.

    app.c
    Microsoft (R) Incremental Linker Version 14.35.32217.1
    Copyright (C) Microsoft Corporation. All rights reserved.

    /out:app.exe
    app.obj
    hello.obj

    c:\asm>app
    Hello starts
    Hello ends

    c:\asm>

    Вывод строки на консоль
    Программа на C может вызывать код из файла на ассемблере, так и код на ассемблере может вызывать функции языка С. Например, используем стандартную функцию printf() для вывода строки на консоль в программе на ассемблере. Для этого определим в файле hello.asm следующий код:
    Код (ASM):
    1. option casemap:none
    2. .data
    3.  text byte "Hello, world!", 10, 0  ; определяем выводимые данные
    4. .code
    5. ; подключаем определение функции printf() из C/C++
    6.  externdef printf:proc
    7.  
    8. ; определяем процедуру hello
    9. public hello
    10. hello proc
    11.  sub rsp, 40  ; резервируем в стеке 40 байт
    12.  lea rcx, text  ; в регистр rcx загружаем адрес строки text
    13.  call printf  ; вызываем функцию printf
    14.  add rsp, 40  ; восстанавливаем значение в стеке
    15.  
    16.  ret ; возвращаемся в вызывающий код
    17.  
    18. hello endp
    19. end
    Здесь для хранения данных определяем секцию .data
    Код (ASM):
    1. .data
    2.  text byte "Hello, world!", 10, 0  ; определяем выводимые данные
    Здесь определена строка text, которая представляет набор байт. Причем предпоследний байт - представляет число 10 - это числовой код перевода строки. Последний байт - 0, так как строки в Си должны завершаться нулевым байтом.
    Для использования функции printf подключаем ее с помощью директивы.
    Код (ASM):
    1. externdef printf:proc
    Директива externdef имеет следующую форму
    Код (ASM):
    1. externdef symbol:type
    Где symbol — это внешный идентификатор, который мы хотим определить - в данном случае функция printf. А type — тип этого идентификатора (для функций это тип proc).
    Стоит отметить, что директива externdef должна применяться в секции .code
    Функция printf() принимает как минимум один параметр - выводимую строку. Эта строка передается в функцию через регистр RCX. Для загрузки адреса строки в регистр rcx применяем инструкцию lea и затем вызываем функцию printf.
    Код (ASM):
    1. lea rcx, text
    2. call printf
    Снова сначала скомпилируем код ассемблера из файла hello.asm с помощью команды:
    ml64 /c hello.asm

    И скомпилируем файл app.c в файл приложения с помощью команды:
    cl app.c hello.obj

    После компиляции запустим файл app.exe:
    c:\asm>ml64 /c hello.asm
    Microsoft (R) Macro Assembler (x64) Version 14.35.32217.1
    Copyright (C) Microsoft Corporation. All rights reserved.

    Assembling: hello.asm

    c:\asm>cl app.c hello.obj
    Microsoft (R) C/C++ Optimizing Compiler Version 19.35.32217.1 for x64
    Copyright (C) Microsoft Corporation. All rights reserved.

    app.c
    Microsoft (R) Incremental Linker Version 14.35.32217.1
    Copyright (C) Microsoft Corporation. All rights reserved.

    /out:app.exe
    app.obj
    hello.obj

    c:\asm>app
    Hello starts
    Hello, world!
    Hello ends

    c:\asm>
     
    Последнее редактирование: 1 ноя 2026 в 05:27
  13. Mikl___

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

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

    Взаимодействие с кодом на C/C++

    Передача параметров из ассемблера в функцию C/C++

    При обращении к функциями C/C++ в коде ассемблера необходимо следовать некоторым соглашениям, в соответствии с которыми работает данные функции. Используя MASM, нам следует следовать соглашениям Microsoft Windows ABI (Application Binary Interface). Рассмотрим основные принципы этих соглашений
    Между базовыми типами в C/C++ и ассемблера применяются следующие соответствия:
    С/С++Размер
    (в байтах)
    Ассемблер
    char/signed char1sbyte
    unsigned char1byte
    short int2sword
    short unsigned2word
    int4sdword
    unsigned (unsigned int)4dword
    long / long int4sdword
    unsigned long4dword
    long long8sqword
    unsigned long long8qword
    float4real4
    double4real8
    указатель8qword
    Также в плане использования регистров существуют следующие соглашения:
    • Вызывающий код передает первые четыре параметра в регистры. Для передачи в функцию первых четырех параметров (целочисленных) используются регистры RCX, RDX, R8 и R9 соответственно. Если параметры представляют числа с плавающей точкой, то они передаются через регистры XMM0, XMM1, XMM2 и XMM3.
      Параметры начиная с 5-го передаются через стек. Так, 5-й параметр должен занимать в стеке место по адресу RSP+32, 6-й параметр - по адресу RSP+40 и так далее.
      Если процедура имеет смешанные параметры - одновременно и целочисленные, и с плавающей запятой, то каждый параметр помещается в соответствующий его номеру регистр RCX/RDX/R8/R9 или XMM0/XMM1/XMM2/XMM3. Например, если у нас есть следующая функция C/C++
      1void someFunc(int a, double b, char *c, double d)
      то параметры будут размещаться следуюшим образом
      RCXa
      XMM1b
      R8c
      XMM3d
    • Параметры всегда представляют собой 8-байтовые значения.
    • Вызывающий код должен зарезервировать для параметров в стеке как минимум 32 байта, даже если параметров меньше четырех - это так называемое shadow storage или теневое хранилище. Условно можно сказать, что первый параметр занимает в стеке память по адресу RSP, второй - по адресу RSP+8, третий - RSP+16, четвертный - RSP+24. Но в реальности это хранидище можно использовать для своих целей, например, как место хранения локальных переменных
    • Указатель стека RSP должен быть выровнен по 16 байтам непосредственно перед тем, как инструкция call поместит адрес возврата в стек.
    • Регистры в Microsot Windows ABI делятся на две группы:
      • Volatile-регистры (изменяемые регистры) - они могут изменять свои значения, и функции/процедуре не нужно сохранять значения этих регистров при вызове другой функции/процедуры. Это такие регистры как RAX, RCX, RDX, R8, R9, R10 и R11, а также XMM0/YMM0 – XMM5/YMM5
      • Nonvolatile-регистры (неизменяемые регистры) - они должны сохранять информацию при вызове функций/процедур. Поэтому процедура/функция должна сохранять значения этих регистров во время вызова. Если процедура изменяет один из этих регистров, она должна сохранить значение регистра перед первой такой модификацией и восстановить значение регистра из сохраненного местоположения перед возвратом. Это такие регистры как RBX, RBP, RDI, RSI, RSP, R12, R13, R14 и R15, а также XMM6/YMM6 - XMM15/YMM16 (старшую половину регистра YMM6-YMM16 можно изменить)
    • Если функция возвращает целое число, то оно помещается в регистр RAX, если число с плавающей точкой - то в регистр XMM0.
    Передача параметров из ассемблера в функцию C/C++
    Например, возьмем функцию printf(). Через регистр RCX в функцию передается выводимая строка, а через остальные регистры передаются значения, которые передаются на место спецификаторов в строке, например, %s, %d и т.д. Рассмотрим передачу параметров в эту функцию. Пусть у нас определен файл hello.asm со следующим кодом:
    Код (ASM):
    1. option casemap:none
    2. .data
    3.  user byte "Tom", 0  ; для спецификатора %s
    4.  age dword 38  ; для спецификатора %d
    5.  text byte "Name: %s  Age: %d", 10, 0
    6. .code
    7. ; подключаем определение функции printf() из C/C++
    8.  externdef printf:proc
    9.  
    10. ; определяем процедуру hello
    11. public hello
    12. hello proc
    13.  sub rsp, 40  ; резервируем в стеке 40 байт
    14.  lea rcx, text  ; в регистр rcx загружаем адрес строки text
    15.  lea rdx, user  ; в регистр rdx загружаем адрес строки name
    16.  mov r8d, age  ; в регистр r8 загружаем адрес переменной age
    17.  call printf  ; вызываем функцию printf
    18.  add rsp, 40  ; восстанавливаем значение в стеке
    19.  
    20.  ret ; возвращаемся в вызывающий код
    21.  
    22. hello endp
    23. end
    В секции данных определены три переменных. Прежде всего это строка text, которая содержит спецификаторы %s и %d. Поскольку все строки в С должны завершаться нулевым байтом, то последний байт этой строки - 0. Также для спецификатора %s определена строка user, а для спецификатора %d - переменная age.
    В соответствие с требованиями Windows ABI, перед вызовом функции printf() резервируем в стеке 40 байт:
    Код (ASM):
    1. sub rsp, 40  ; резервируем в стеке 40 байт
    Из них 32 байта - это так называемое shadow storage или теневое хранилище, необходимое для вызова любой функции С/С++, даже если она не имеет пароаметров. Но при вызове функции hello в стек уже помещается адрес возврата и таким образом 8 байт в нем занято. Соответственно получим 32 байта (shadows storage) + 8 байт (адрес возврата) = 40 байт - это значение НЕ выровнено по 16 байтам (то есть не кратно 16 байтам). Нам же в соответствии с Windows ABI обязательно надо иметь стек выровненным по 16 байтам - чтобы 4 младших бита в RSP имели значение 0. Поэтому к 32 байтам для выравнивания добавляем 8 байт.
    Для передачи этих значений в качестве параметров в функцию printf сначала загружаем адрес строки text в регистр rcx:
    Код (ASM):
    1. lea rcx, text
    В rdx загружаем адрес строки user для первого спецификатора - %s.
    Код (ASM):
    1. lea rdx, user
    Значение для второго спецификатора - %d помещаем в регистр r8:
    Код (ASM):
    1. mov r8d, age
    Стоит отметить, что поскольку age - 32-битное число, и для спецификатора %d требуется целое 32-битное число, то данные помещаем в младшие 32 бита регистра r8 - r8d. Кроме того, для этого используем инструкцию mov.
    Для вызова ассемблерного кода определим файл app.c со следующим кодом на языке C:
    Код (C):
    1. #include <stdio.h>
    2. extern void hello(void);
    3. int main()
    4. {
    5.  hello();
    6. }
    Сначала скомпилируем код ассемблера из файла hello.asm с помощью команды:
    ml64 /c hello.asm

    И скомпилируем файл app.c в файл приложения с помощью команды:
    cl app.c hello.obj

    После компиляции запустим файл app.exe:
    c:\asm>ml64 /c hello.asm
    Microsoft (R) Macro Assembler (x64) Version 14.36.32532.0
    Copyright (C) Microsoft Corporation. All rights reserved.

    Assembling: hello.asm

    c:\asm>cl app.c hello.obj
    Microsoft (R) C/C++ Optimizing Compiler Version 19.36.32532 for x64
    Copyright (C) Microsoft Corporation. All rights reserved.

    app.c
    Microsoft (R) Incremental Linker Version 14.36.32532.0
    Copyright (C) Microsoft Corporation. All rights reserved.

    /out:app.exe
    app.obj
    hello.obj

    c:\asm>app
    Name: Tom Age: 38

    c:\asm>

    Передача параметров по ссылке
    Если функция C/C++ принимает указатель, то, поскольку указатели в архитектуре x86-64 имеют размер 8 байт, соответствующий параметр тоже должен быть равен 8 байтам. В качестве указателя из кода на ассемблере можно передать адрес объекта, который вычисляется либо с помощью оператора offset, либо с помощью инструции lea. Например, определим в файле app.c следующую программу на С:
    Код (C):
    1. // подключаем внешнюю функцию calc из файла hello.asm
    2. extern void calc(void);
    3. void square(unsigned int* n)
    4. {
    5.  *n = *n * *n;
    6. }
    7. int main(void)
    8. {
    9.  calc();
    10. }
    Здесь определена функция square, которая принимает указатель на число типа unsigned int и возводит его в квадрат.
    Основные действия разворачиваются в функции calc, которая подключается как внешняя функция на ассемблере. Определим ее в файле hello.asm:
    Код (ASM):
    1. option casemap:none
    2. .data
    3. number dword 5
    4. text byte "number = %u", 10, 0
    5. .code
    6. externdef square:proc
    7. externdef printf:proc
    8. calc proc
    9.  sub rsp, 40
    10.  lea rcx, number
    11.  call square
    12.  lea rcx, text  ; в регистр rcx загружаем адрес строки text
    13.  mov edx, number  ; в регистр rdx загружаем адрес числа number
    14.  call printf  ; вызываем функцию printf
    15.  add rsp, 40
    16.  ret
    17. calc endp
    18. end
    Здесь данные, которые будут передаваться в функцию square, определены в виде переменной number. Перед вызовом функции square в регистр RCX помещаем адрес переменной number.
    Код (ASM):
    1. lea rcx,number
    В качестве альтернативы можно было бы использовать оператор offset:
    Код (ASM):
    1. mov rcx, offset number
    Но инструкция lea является более предпочтительной, потому что занимает меньше байт (7 против 10 у offset).
    После выполнения функции square проверяем значение переменной number с помощью функции printf. Результат работы программы:
    c:\asm>app
    number = 25
     
    Последнее редактирование: 1 ноя 2026 в 05:42
  14. Mikl___

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

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

    Взаимодействие с кодом на C/C++

    Возвращение результата из процедуры ассемблера в код на C/C++

    Мы можем вызывать как в коде ассемблера функции C/C++, так и в коде С/С++ процедуры ассемблера. А также передавать параметры и получать результаты.
    Возвращение результата из ассемблера
    Согласно Windows ABI, функции (процедуры) возвращают целые числа и значения указателей (которые умещаются в 64 бита) в регистре RAX. Поэтому, если какой-то код С/C++ ожидает, что процедура ассемблера вернет целочисленный результат, то код на ассемблере должен поместить целочисленный результат в регистр RAX перед возвратом из процедуры.
    Например, определим следующий файл hello.asm
    Код (ASM):
    1. option casemap:none
    2. .data
    3.  text byte "Hello Asm", 0
    4. .code
    5. ; определяем процедуру hello
    6. public hello
    7. hello proc
    8.  lea rax, text  ; в регистр rax загружаем адрес строки text
    9.  ret
    10. hello endp
    11. end
    Здесь функция hello формально возвращает строку text. Фактически же это выражается в загрузке в регистр rax адрес строки text.
    Также определим файл app.c, где получим строку и выведем ее на консоль:
    Код (C):
    1. #include <stdio.h>
    2. extern char* hello(void);
    3. int main()
    4. {
    5.  char* message = hello();
    6.  printf(message);
    7. }
    Поскольку в регистр rax загружается адрес строки, то для C/C++ возвращаемым типом функции будет тип char*. Соответственно мы можем получить эту строку:
    Код (C):
    1. char* message = hello();
    Скомпилируем и запустим программу
    c:\asm>ml64 /c hello.asm
    Microsoft (R) Macro Assembler (x64) Version 14.36.32532.0
    Copyright (C) Microsoft Corporation. All rights reserved.

    Assembling: hello.asm

    c:\asm>cl app.c hello.obj
    Microsoft (R) C/C++ Optimizing Compiler Version 19.36.32532 for x64
    Copyright (C) Microsoft Corporation. All rights reserved.

    app.c
    Microsoft (R) Incremental Linker Version 14.36.32532.0
    Copyright (C) Microsoft Corporation. All rights reserved.

    /out:app.exe
    app.obj
    hello.obj

    c:\asm>app
    Hello Asm
    c:\asm>

    Передача параметров в процедуры ассемблера
    Рассмотрим передачу параметров в процедуру на ассемблере и для этого возьмем следующий пример. Пусть в файле hello.asm определен следующий ассемблерный код:
    Код (ASM):
    1. option casemap:none
    2. .code
    3. ; определяем функцию sum
    4. ; мы ожидаем два параметра - два числа
    5. ; по соглашениям первые два параметра в регистрах RCX и RDX
    6. ; возвращаемое значение - в регистре RAX
    7. public sum
    8. sum proc
    9.  mov rax, rcx  ; помещаем в rax первый параметр - из регистра rcx
    10.  add rax, rdx  ; складываем со вторым параметром - из регистра rdx
    11.  ret
    12. sum endp
    13. end
    Здесь определена процедура sum, которая должна складывать два числа и возращать их сумму. Согласно соглашениям первые два параметра передаются через регистры RCX и RDX, а результат помещается в регистр RAX. Поэтому функция sum получает из регистров RCX и RDX переданные значения и их сумму помещает в RAX.
    Определим файл app.c, в котором вызовем данную процедуру и получим ее результат:
    Код (C):
    1. #include <stdio.h>
    2. #define int64  unsigned long long
    3. extern int64 sum(int64, int64);
    4. int main()
    5. {
    6.  int64 a = 5;
    7.  int64 b = 6;
    8.  int64 result = sum(a, b);
    9.  printf("result = %lld \n", result); // result = 11
    10. }
    Все параметры и результат у нас представляют беззнаковый 64-разрядный целочисленный тип, то есть unsigned long long, и для краткости для него определен псевдоним int64
    В вызов функции sum передаются значения переменных a и b. В итоге значение переменной a будет помещено в регистр RCX, а значение переменной b - в регистр RDX. Значение из регистра RAX мы можем получить в качестве результата функции
    Код (C):
    1. int64 result = sum(a, b);
    Результат работы программы:
    c:\asm>ml64 /c hello.asm
    Microsoft (R) Macro Assembler (x64) Version 14.36.32532.0
    Copyright (C) Microsoft Corporation. All rights reserved.

    Assembling: hello.asm

    c:\asm>cl app.c hello.obj
    Microsoft (R) C/C++ Optimizing Compiler Version 19.36.32532 for x64
    Copyright (C) Microsoft Corporation. All rights reserved.

    app.c
    Microsoft (R) Incremental Linker Version 14.36.32532.0
    Copyright (C) Microsoft Corporation. All rights reserved.

    /out:app.exe
    app.obj
    hello.obj

    c:\asm>app
    result = 11

    c:\asm>
     
    Последнее редактирование: 1 ноя 2026 в 05:51
  15. Mikl___

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

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

    Взаимодействие с кодом на C/C++

    Практически пример. Обработка консольного ввода

    Благодаря взаимодействию с C/C++ мы можем получать в коде ассемблера ввод пользователя и обрабатывать его, используя встроенные функции C/C++. Рассмотрим, как это сделать. Для этого определим файл app.c со следующим кодом на языке C:
    Код (C):
    1. #include <stdio.h>
    2. extern void hello(void);
    3. int readLine(char *dest, int maxLen)
    4. {
    5.  // fgets считывает одну строку и возвращает NULL, если произошла ошибка
    6.  char *result = fgets(dest, maxLen, stdin);
    7.  if(result != NULL)
    8.  {
    9.  // удаляем символ перевода строки в конце строки
    10.  int len = strlen(result);
    11.  if(len > 0)
    12.  {
    13.  dest[len - 1] = 0;
    14.  }
    15.  return len;
    16.  }
    17.  return -1; // если произошла ошибка
    18. }
    19. int main(void)
    20. {
    21.  hello();
    22. }
    Здесь мы подключаем внешнюю функцию hello, которая не принимает никаких параметров и ничего не возвращает и которая будет определена в коде ассемблера.
    Для считывания данных с консоли определяем функцию readLine, которая принимает буфер, в который считываются данные, и количество считываемых данных. Для непосредственного считывания строки с консоли эта функция использует встроенную функцию fgets. Если при считывании произошла ошибка, то fgets возвращает NULL. Если же считывание прошло удачно, то последний считанный байт будет представлять символ перевода строки, который мы заменяем нулевым байтом.
    При успешном считывании функция readLine возвращает количество считанных символов, при неудачном считывании - число -1. Данный результат позволит внешнему коду определить, удачно ли завершилось считывание или нет.
    Также определим файл hello.asm со следующим кодом на ассемблере
    Код (ASM):
    1. option casemap:none
    2. maxLen equ 128  ; максимальное количество символов
    3. .data
    4. prompt byte "Enter your name: ", 0  ; приглашение к вводу строки
    5. message byte "Hello %s!", 10, 0  ; выводимое сообщение
    6. username byte maxLen dup (0)  ; буфер для ввода данных
    7. .code
    8. externdef printf:proc  ; подключаем функцию printf
    9. externdef readLine:proc  ; подключаем функцию readLine
    10. public hello
    11. hello proc
    12.  sub rsp, 40  ; устанавливаем стек
    13.  lea rcx, prompt ; загружаем преглашение к вводу
    14.  call printf  ; вызываем функцию printf для отображения приглашения к вводу
    15.  ; считаем строку с консоли
    16.  lea rcx, username  ; в rcx - строка, в которую считываем данные
    17.  mov rdx, maxLen  ; в rdx - максимальное количество считываемых символов
    18.  call readLine  ; вызываем readLine для считывания строки
    19.  ; выводим считанное значение с помощью функции printf
    20.  lea rcx, message  ; в rcx - выводимая строка
    21.  lea rdx, username  ; в rdx - значение для спецификатора %s
    22.  call printf  ; выводим строку
    23.  add rsp, 40  ; восстанавливаем стек
    24.  ret  ; возвращаемся в программу на С
    25. hello endp
    26. end
    Смысл программы будет заключаться в том, что пользователь вводит с консоли свое имя, и консоль в ответ выводит приветствие. С помощью константы maxLen задается максимальное количество вводимых символов. В данном случае это 128 символов.
    В секции данных определяем три переменных. Переменная prompt представляет текст-приглашение к вводу имени. Переменная message представляет финальное сообщение, в которое вместо спецификатора %s будет подставлятсья введенное имя. И переменная username представляет строку для ввода имени. Она имеет 128 символов и по умолчанию инициализирована нулями.
    В секции кода прежде всего пдключаем функции printf и readLine из кода С:
    Код (C):
    1. externdef printf:proc
    2. externdef readLine:proc
    Далее определяется процедура hello. В ней вначале выводим приглашение вводу с помощью функции printf
    Код (ASM):
    1. lea rcx, prompt ; загружаем преглашение к вводу
    2. call printf  ; вызываем функцию printf для отображения приглашения к вводу
    Далее вызываем функцию readLine и тем самым считываем строку с консоли - имя пользователя:
    Код (ASM):
    1. lea rcx, username  ; в rcx - строка, в которую считываем данные
    2. mov rdx, maxLen  ; в rdx - максимальное количество считываемых символов
    3. call readLine  ; вызываем readLine для считывания строки
    После ввода имени выводим его на консоль с помощью функции printf:
    Код (ASM):
    1. lea rcx, message  ; в rcx - выводимая строка
    2. lea rdx, username  ; в rdx - значение для спецификатора %s
    3. call printf  ; выводим строку
    Сначала скомпилируем код ассемблера из файла hello.asm с помощью команды:
    ml64 /c hello.asm

    И скомпилируем файл app.c в файл приложения с помощью команды:
    cl app.c hello.obj

    После компиляции запустим файл app.exe:
    c:\asm>ml64 /c hello.asm
    Microsoft (R) Macro Assembler (x64) Version 14.36.32532.0
    Copyright (C) Microsoft Corporation. All rights reserved.

    Assembling: hello.asm

    c:\asm>cl app.c hello.obj
    Microsoft (R) C/C++ Optimizing Compiler Version 19.36.32532 for x64
    Copyright (C) Microsoft Corporation. All rights reserved.

    app.c
    Microsoft (R) Incremental Linker Version 14.36.32532.0
    Copyright (C) Microsoft Corporation. All rights reserved.

    /out:app.exe
    app.obj
    hello.obj

    c:\asm>app
    Enter your name: Mikl___
    Hello Mikl___!

    c:\asm>
     
    Последнее редактирование: 1 ноя 2026 в 06:00
  16. Mikl___

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

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

    Взаимодействие с WinAPI

    Запись в файл и вывод на консоль

    WinAPI предоставляет множество различных функций для работы с файлами. Рассмотрим некоторые из них:
    • CreateFileA: открывает существующие файлы или создает новые
    • WriteFile: записывает данные в файл
    • ReadFile: считывает данные из файла
    • CloseHandle: закрывает файл
    • GetStdHandle: возвращает дескриптор одного из стандартных устройств ввода или вывода
    • GetLastError: возвращает код ошибки Windows, если ошибка возникает при выполнении любой из выше перечисленных функций.
    За работу с файлами отвечает библиотека kernel32.lib
    Запись в файл
    Для записи в файл применяется функция WriteFile, которая имеет следующее определение на языке C++:
    Код (C):
    1. BOOL WriteFile(
    2.  [in]  HANDLE  hFile,
    3.  [in]  LPCVOID  lpBuffer,
    4.  [in]  DWORD  nNumberOfBytesToWrite,
    5.  [out, optional]  LPDWORD  lpNumberOfBytesWritten,
    6.  [in, out, optional] LPOVERLAPPED lpOverlapped
    7. );
    WriteFile имеет следующие аргументы:
    • hFile: дескриптор файла, передается через регистр RCX
    • lpBuffer: адрес буфера для записи, передается через регистр RDX
    • nNumberOfBytesToWrite: размер буфера, передается через регистр R8 (поскольку представляет тип dword и занимает 32 бита, то передайется в R8D)
    • lpNumberOfBytesWritten: адрес переменной типа dword для получения количества байтов, записанных в файл. Равно размеру буфера, если операция записи прошла успешно. Передается через регистр R9
    • lpOverlapped: обычно имеет значение NULL (0), передается через стек по адресу [rsp + 32]
    После выполнения функция WriteFile помещает в регистр RAX ненулевое значение, если запись прошла успешно, и ноль, если произошла ошибка. Если произошла ошибка, можно вызвать другую функцию WinAPI - GetLastError, чтобы получить код ошибки.
    Используем функцию WriteFile для записи (вывода) данных на консоль, а точнее в стандартный поток вывода:
    Код (ASM):
    1. includelib kernel32.lib  ; подключаем библиотеку kernel32.lib
    2. ; подключаем функции WriteFile и GetStdHandle
    3. extrn WriteFile: PROC
    4. extrn GetStdHandle: PROC
    5. .data
    6. text byte "Hello METANIT.COM!"  ; выводимая строка
    7. len = $-text
    8. bytesWritten dword ?  ; количество записанных байтов
    9. .code
    10. main proc
    11.  sub  rsp, 40  ; Для параметров функций WriteFile и GetStdHandle
    12.  mov  rcx, -11  ; Аргумент для GetStdHandle - STD_OUTPUT
    13.  call GetStdHandle  ; вызываем функцию GetStdHandle
    14.  mov  rcx, rax  ; Первый параметр WriteFile - в регистр RCX помещаем дескриптор файла - консоли
    15.  lea  rdx, text  ; Второй параметр WriteFile - загружаем указатель на строку в регистр RDX
    16.  mov  r8d, len  ; Третий параметр WriteFile - длина строки для записи в регистре R8D
    17.  lea  r9, bytesWritten  ; Четвертый параметр WriteFile - адрес для получения записанных байтов
    18.  mov  qword ptr [rsp + 32], 0  ; Пятый параметр WriteFile
    19.  call WriteFile ; вызываем функцию WriteFile
    20.  test rax, rax ; проверяем на наличие ошибки
    21.  mov eax, bytesWritten ; если все нормально, помещаем в RAX количество записанных байтов
    22.  jnz exit
    23.  mov rax, -1 ; Возвращаем через RAX код ошибки
    24. exit:
    25.  add  rsp, 40
    26.  ret
    27. main endp
    28. end
    Здесь функции GetStdHandle в качестве параметра через регистр RCX передается значение -11, которое указывает, что надо получить дескриптор стандартного вывода.
    Полученный через регистр RAX дескриптор передаем функции WriteFile через регистр RCX. Через регистр RDX передается адрес выводимой строки - переменной text, а через регистр R8D - длина строки. В регистр R9 загружаем адрес переменной bytesWritten, через которую получим реальное число записанных байтов.
    После выполнения функции WriteFile и соответственно вывода строки на консоль проверяем результат функции в регистре RAX - если он не равен 0 (то есть все прошло без ошибок), то в RAX помещаем количество записанных байтов. Если же произошла ошибка, то в RAX помещаем число -1, которое будет условно обозначать, что произошла ошибка.
    Для абстрагирования от конкретной строки и упрощения вывода на консоль мы можем выделить все соответствующие действия в отдельную процедуру:
    Код (ASM):
    1. includelib kernel32.lib  ; подключаем библиотеку kernel32.lib
    2. ; подключаем функции WriteFile и GetStdHandle
    3. extrn WriteFile: PROC
    4. extrn GetStdHandle: PROC
    5. .data
    6. text byte "Hello WASM.IN!"  ; выводимая строка
    7. len = $-text
    8. stdout qword 0  ; дескриптор консольного вывода
    9. stdoutSet byte 0  ; установлен ли дескриптор консольного вывода
    10. .code
    11. ; Параметры:
    12. ; RSI - адрес строки
    13. ; RCX - длина строки
    14. ; RAX - дескриптор файла
    15. ; Результат - через RAX возвращаем количество записанных байтов
    16. write proc
    17.  sub  rsp, 56  ; 40 байт (5 параметров WriteLine) + 8 байт (bytesWritten) + 8 байт выравнивание
    18.  mov rdx, rsi  ; Второй параметр - строка
    19.  mov r8, rcx  ; Третий параметр - длина строки
    20.  mov  rcx, rax  ; Первый параметр WriteFile - в регистр RCX помещаем дескриптор файла - консольного вывода
    21.  lea  r9, bytesWritten  ; Четвертый параметр WriteFile - адрес для получения записанных байтов
    22.  mov qword ptr [rsp + 32], 0  ; Пятый параметр WriteFile
    23.  call WriteFile
    24.  test rax, rax ; проверяем на наличие ошибки
    25.  mov eax, bytesWritten ; если все нормально, помещаем в RAX количество записанных байтов
    26.  jnz exit
    27.  mov rax, -1 ; Возвращаем через RAX код ошибки
    28. exit:
    29.  add  rsp, 56
    30.  ret
    31. bytesWritten equ [rsp+40]
    32. write endp
    33. ; Процедура вывода произвольной строки на консоль
    34. ; Параметры
    35. ; RSI - адрес строки
    36. ; RCX - количество символов
    37. ; Результат - в RAX количество записанных байтов или -1, если произошла ошибка
    38. writeToConsole proc
    39.  cmp stdoutSet, 1
    40.  jz writeData  ; если дескриптор консольного вывода установлен
    41.  sub rsp, 32
    42.  push rcx  ; сохраняем количество символов
    43.  mov rcx, -11  ; Аргумент для GetStdHandle - STD_OUTPUT
    44.  call GetStdHandle  ; вызываем функцию GetStdHandle
    45.  pop rcx  ; восстанавливаем RCX - количество символов
    46.  add rsp, 32
    47.  mov stdout, rax  ; помещаем в stdout дескриптор консольного вывода
    48.  mov stdoutSet, 1  ; дескриптор консольного вывода установлен
    49. writeData:
    50.  mov rax, stdout
    51.  call write
    52.  ret
    53. writeToConsole endp
    54. main proc
    55.  lea rsi, text  ; загружаем адрес строки
    56.  mov rcx, len  ; загружаем размер строки
    57.  call writeToConsole  ; выводим строку на консоль
    58.  ret
    59. main endp
    60. end
    Здесь запись разбита на две процедуры. Первая процедура - write собственно выполняет запись в файл. Через регистр RAX она получает дескриптор файла. Причем это может быть дескриптор консольного вывода, а может быть и дескриптор какого-то другого файла. Таким образом, мы абстрагируемся от консоли и опредлеяем универсальную функцию записи. Через регистр RSI она получает адрес строки, а через RCX - размер строки. Внутри функции устанавливаются все необходимые регистры и вызывается внешняя функция WriteFile. Результат функции - или количество записанных байтов, которое хранится в стеке по адресу rsp+40, либо -1, если произошла ошибка. В данном случае мы никак не обыгрываем данную ситуацию, но в принципе мы можем проверить результат данной процедуры для последующих действий, например, для вывода пользователю сообщения об ошибке и т.д.
    Непосредственно для вывода данных на консоль определена функция writeToConsole. В ней получаем дескриптор консольного вывода с помощью вызова функции GetStdHandle и сохраняем в переменную stdout. Чтобы указать, что дескриптор стандартного вывода установлен, присваиваем переменной stdoutSet. Сравнивая ее значение, мы можем определить, был ли ранее получен дескриптор. Это позволит при повторном вызове процедуры writeToConsole избежать ненужных обращений к функции GetStdHandle. После получения дескриптора передаем его процедуре write и с ее помощью собственно выполняем запись.
    Благодаря этому мы можем удобным образом многократно выполнять вывод на консоль строк. Например:
    Код (ASM):
    1. main proc
    2.  lea rsi, text1
    3.  mov rcx, len1
    4.  call writeToConsole  ; выводим первую строку на консоль
    5.  lea rsi, text2
    6.  mov rcx, len2
    7.  call writeToConsole  ; выводим вторую строку на консоль
    8.  ret
    9.  text1 byte "Hello WASM.IN!",10
    10.  len1 = $-text1
    11.  text2 byte "Hello Assembler!",10
    12.  len2 = $-text2
    13. main endp
    Вывод символов с переводом на новую строку
    В примере выше, чтобы при выводе строк на консоль они не сливались, в конце строки определялся байт 10, который в таблице ASCII соответствует символу \n или символу перевода строки. Чтобы уйти от необходимости определять в конце каждой строки такой символ, можно его добавлять при выводе строки на консоль. Например:
    Код (ASM):
    1. includelib kernel32.lib  ; подключаем библиотеку kernel32.lib
    2. ; подключаем функции WriteFile и GetStdHandle
    3. extrn WriteFile: PROC
    4. extrn GetStdHandle: PROC
    5. .data
    6. stdout qword 0  ; дескриптор консольного вывода
    7. stdoutSet byte 0  ; установлен ли дескриптор консольного вывода
    8. .code
    9. ; Параметры:
    10. ; RSI - адрес строки
    11. ; RCX - длина строки
    12. ; RAX - дескриптор файла
    13. ; Результат - через RAX возвращаем количество записанных байтов
    14. write proc
    15.  sub  rsp, 56
    16.  mov rdx, rsi  ; Второй параметр - строка
    17.  mov r8, rcx  ; Третий параметр - длина строки
    18.  mov  rcx, rax  ; Первый параметр WriteFile - в регистр RCX помещаем дескриптор файла - консольного вывода
    19.  lea  r9, bytesWritten  ; Четвертый параметр WriteFile - адрес для получения записанных байтов
    20.  mov qword ptr [rsp + 32], 0  ; Пятый параметр WriteFile
    21.  call WriteFile
    22.  test rax, rax ; проверяем на наличие ошибки
    23.  mov eax, bytesWritten ; если все нормально, помещаем в RAX количество записанных байтов
    24.  jnz exit
    25.  mov rax, -1 ; Возвращаем через RAX код ошибки
    26. exit:
    27.  add  rsp, 56
    28.  ret
    29. bytesWritten equ [rsp+40]
    30. write endp
    31. ; Процедура вывода строки на консоль
    32. ; Параметры
    33. ; RSI - адрес строки
    34. ; RCX - количество символов
    35. ; Результат - в RAX количество записанных байтов или -1, если произошла ошибка
    36. writeToConsole proc
    37.  cmp stdoutSet, 1
    38.  jz writeData  ; если дескриптор консольного вывода установлен
    39.  sub rsp, 32
    40.  push rcx  ; сохраняем количество символов
    41.  mov rcx, -11  ; Аргумент для GetStdHandle - STD_OUTPUT
    42.  call GetStdHandle  ; вызываем функцию GetStdHandle
    43.  pop rcx  ; восстанавливаем RCX - количество символов
    44.  add rsp, 32
    45.  mov stdout, rax  ; помещаем в stdout дескриптор консольного вывода
    46.  mov stdoutSet, 1  ; дескриптор консольного вывода установлен
    47. writeData:
    48.  mov rax, stdout
    49.  call write
    50.  ret
    51. writeToConsole endp
    52. ; Процедура вывода символов c переводом на новую строку
    53. ; Параметры
    54. ; RSI - адрес строки
    55. ; RCX - количество символов
    56. writeLine proc
    57.  call writeToConsole  ; записываем текст из RSI длиной RCX байт
    58.  lea rsi, crlf
    59.  mov rcx, 2
    60.  call writeToConsole  ; после записи текста записываем перевод строки = \r\n
    61.  ret
    62.  crlf byte 10, 13  ; для перевода на новую строку
    63. writeLine endp
    64. main proc
    65.  lea rsi, text1
    66.  mov rcx, len1
    67.  call writeLine  ; выводим первую строку на консоль
    68.  lea rsi, text2
    69.  mov rcx, len2
    70.  call writeLine  ; выводим вторую строку на консоль
    71.  ret
    72.  text1 byte "Hello WASM.IN!"
    73.  len1 = $-text1
    74.  text2 byte "Hello Assembler!"
    75.  len2 = $-text2
    76. main endp
    77. end
    Здесь добавлена процедура writeLine, которая принимает строку, выводит ее на консоль с помощью writeToConsole, а затем дополнительно выводит символы \r\n, которые хранятся в строке crlf.
    Вывод строк с концевым символом
    Для идентификации конца строки может использоваться, как в примере выше, размер строки. Но, например, язык C и ряд других языков использует другой подход - завершение строки специальным концевым символом - нулевым байтом. В качестве концевого символа не обязательно должен выступать нулевой байт. Но если коду на ассемблере предстоит работать с такими строками, то мы можем просто расчитать количество символов до подобного концевого символа и также обрабатывать строку
    Код (ASM):
    1. includelib kernel32.lib  ; подключаем библиотеку kernel32.lib
    2. ; подключаем функции WriteFile и GetStdHandle
    3. extrn WriteFile: PROC
    4. extrn GetStdHandle: PROC
    5. .data
    6. stdout qword 0  ; дескриптор консольного вывода
    7. stdoutSet byte 0  ; установлен ли дескриптор консольного вывода
    8. .code
    9. ; Параметры:
    10. ; RSI - адрес строки
    11. ; RCX - длина строки
    12. ; RAX - дескриптор файла
    13. ; Результат - через RAX возвращаем количество записанных байтов
    14. write proc
    15.  sub  rsp, 56
    16.  mov rdx, rsi  ; Второй параметр - строка
    17.  mov r8, rcx  ; Третий параметр - длина строки
    18.  mov  rcx, rax  ; Первый параметр WriteFile - в регистр RCX помещаем дескриптор файла - консольного вывода
    19.  lea  r9, bytesWritten  ; Четвертый параметр WriteFile - адрес для получения записанных байтов
    20.  mov qword ptr [rsp + 32], 0  ; Пятый параметр WriteFile
    21.  call WriteFile
    22.  test rax, rax ; проверяем на наличие ошибки
    23.  mov eax, bytesWritten ; если все нормально, помещаем в RAX количество записанных байтов
    24.  jnz exit
    25.  mov rax, -1 ; Возвращаем через RAX код ошибки
    26. exit:
    27.  add  rsp, 56
    28.  ret
    29. bytesWritten equ [rsp+40]
    30. write endp
    31. ; Процедура вывода строки на консоль
    32. ; Параметры
    33. ; RSI - адрес строки
    34. ; RCX - количество символов
    35. ; Результат - в RAX количество записанных байтов или -1, если произошла ошибка
    36. writeToConsole proc
    37.  cmp stdoutSet, 1
    38.  jz writeData  ; если дескриптор консольного вывода установлен
    39.  sub rsp, 32
    40.  push rcx  ; сохраняем количество символов
    41.  mov rcx, -11  ; Аргумент для GetStdHandle - STD_OUTPUT
    42.  call GetStdHandle  ; вызываем функцию GetStdHandle
    43.  pop rcx  ; восстанавливаем RCX - количество символов
    44.  add rsp, 32
    45.  mov stdout, rax  ; помещаем в stdout дескриптор консольного вывода
    46.  mov stdoutSet, 1  ; дескриптор консольного вывода установлен
    47. writeData:
    48.  mov rax, stdout
    49.  call write
    50.  ret
    51. writeToConsole endp
    52. ; Процедура вывода символов c переводом на новую строку
    53. ; Параметры
    54. ; RSI - адрес строки
    55. ; RCX - количество символов
    56. writeLine proc
    57.  call writeToConsole  ; записываем текст из RSI длиной RCX байт
    58.  lea rsi, crlf
    59.  mov rcx, 2
    60.  call writeToConsole  ; после записи текста записываем перевод строки = \r\n
    61.  ret
    62.  crlf byte 10, 13  ; для перевода на новую строку
    63. writeLine endp
    64. ; Процедура вывода произвольной строки на консоль
    65. ; Параметры
    66. ; RSI - адрес строки
    67. ; Результат - в RAX количество записанных байтов или -1, если произошла ошибка
    68. writeText proc
    69.  ; вычисляем длину строки
    70.  mov rcx, -1  ; счетчик символов строки
    71. calculateLen:
    72.  inc rcx
    73.  cmp byte ptr [rsi][rcx * 1], 0  ; пока не дойдем до нулевого байта
    74.  jne calculateLen
    75.  call writeLine
    76.  ret
    77. writeText endp
    78. main proc
    79.  lea rsi, text1
    80.  call writeText  ; выводим первую строку на консоль
    81.  lea rsi, text2
    82.  call writeText  ; выводим вторую строку на консоль
    83.  ret
    84.  text1 byte "Hello C/C++",0
    85.  text2 byte "Hello Assembler!",0
    86. main endp
    87. end
    Здесь добавлена процедура writeText, которая получает извне через RSI строку и вычисляет ее длину, пока не встретит концевой символ - нулевой байт и затем вызывает процедуру writeLine:
    Код (ASM):
    1. ; вычисляем длину строки
    2.  mov rcx, -1  ; счетчик символов строки
    3. calculateLen:
    4.  inc rcx
    5.  cmp byte ptr [rsi][rcx * 1], 0  ; пока не дойдем до нулевого байта
    6.  jne calculateLen
    7.  call writeLine
    В процедуре main при вызове writeText передаются строки с концевым нулевым байтом:
    Код (ASM):
    1. main proc
    2.  lea rsi, text1
    3.  call writeText  ; выводим первую строку на консоль
    4.  lea rsi, text2
    5.  call writeText  ; выводим вторую строку на консоль
    6.  ret
    7.  text1 byte "Hello C/C++",0
    8.  text2 byte "Hello Assembler!",0
    9. main endp
    Однако если переданные строки не будут содержать нулевой байт, то выполнение программы завершится печальным образом.
     
    Последнее редактирование: 1 ноя 2026 в 08:11
  17. Mikl___

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

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

    Взаимодействие с WinAPI

    Считывание файла и ввод с консоли

    Для считывания данных в WinAPI применяется функция ReadFile, которая имеет следующее определение на языке C++:
    Код (C):
    1. [TD]BOOL ReadFile(
    2.  [in]  HANDLE  hFile,
    3.  [out]  LPVOID  lpBuffer,
    4.  [in]  DWORD  nNumberOfBytesToRead,
    5.  [out, optional]  LPDWORD  lpNumberOfBytesRead,
    6.  [in, out, optional] LPOVERLAPPED lpOverlapped
    7. );
    Она принимает следующие параметры:
    • hFile: дескриптор файла для считывания, передается через регистр RCX
    • lpBuffer: буфер, который получает считываемые данные, передается через регистр RDX
    • nNumberOfBytesToRead: максимальное число байтов для чтения., передается через регистр R8
    • lpNumberOfBytesRead: адрес переменной, которая получает количество считанных байтов, передается через регистр R9
    • lpOverlapped: обычно имеет значение NULL (0), передается через стек по адресу [rsp + 32]
    После выполнения функция ReadFile помещает в регистр RAX ненулевое значение, если запись прошла успешно, и ноль, если произошла ошибка.
    Считаем данные с консоли и для этого определим следующую программу:
    Код (ASM):
    1. includelib kernel32.lib  ; подключаем библиотеку kernel32.lib
    2. ; подключаем функции ReadFile и GetStdHandle
    3. extrn GetStdHandle: PROC
    4. extrn ReadFile: PROC
    5. .data
    6. buffer byte 200 dup (?)  ; буфер для считывания данных
    7. len = $ - buffer
    8. bytesRead qword ?  ; количество считанных данных
    9. .code
    10. main proc
    11.  sub rsp, 40  ; 32 байта (shadow storage) + 8 байт (5-й параметр функции ReadFile)
    12.  mov  rcx, -10  ; Аргумент для GetStdHandle - STD_INPUT
    13.  call GetStdHandle  ; вызываем функцию GetStdHandle
    14.  mov rcx, rax ; Первый параметр ReadFile - дескриптор файла
    15.  lea rdx, buffer ; Второй параметр ReadFile - адрес буфера
    16.  mov r8, len  ;Третий параметр ReadFile - размер буфера
    17.  lea r9, bytesRead  ; Четвертый параметр ReadFile - количество считанных байтов
    18.  mov qword ptr [rsp + 32], 0  ; Пятый параметр ReadFile - 0
    19.  call ReadFile  ; вызов функции RaedFile
    20.  test rax, rax  ; проверяем на ошибку
    21.  mov rax, bytesRead  ; если ошибки нет, в RAX количество считанных байтов
    22.  jnz exit  ; если в RAX ненулевое значение
    23.  mov rax, -1  ; помещаем условный код ошибки
    24. exit:
    25.  add rsp, 40
    26.  ret
    27. main endp
    28. end
    При считывании с консоли ввода пользователя нам надо получить дескриптор стандартного ввода. Для этого функции GetStdHandle число -10. После вызова этой функции в RAX будет дескриптор консольного ввода.
    Полученный дескриптор передаем через регистр RCX в функцию ReadFile. Для считывания данных в RDX загружаем адрес переменной buffer, в которую будет считываться ввод пользователя. В регистр R8 помещается размер этого буфера. А в R9 загружается адрес переменной bytesRead для хранения реально считанных байтов, так как пользователь может ввести всего 1-2 символа, что будут меньше размера буфера.
    После вызова функции ReadFile инструкцией test проверяем результат функции из регистра RAX. Если он не равен нулю (то есть функция ReadFile выполнена успешно), то в RAX помещаем количество реально считанных байтов. Если результат функции - 0, то в RAX помещаем условный код ошибки - число -1.
    Пример компиляции и работы программы (в моем случае программа располагается в файле hello.asm):
    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
    hello
    c:\asm>echo %ERRORLEVEL%
    7
    c:\asm>

    В консольном выводе выше видно, что я ввожу текст "hello", однако реально считанное количество равно 7, поскольку при завершении ввода в строку добавляются символы "/r/n" (перевод каретки и перевод строки).
    Мы можем пойти дальше и вынести считывание данных в отдельную процедуру, отделив от взаимодействия с консолью:
    Код (ASM):
    1. includelib kernel32.lib  ; подключаем библиотеку kernel32.lib
    2. ; подключаем функции ReadFile и GetStdHandle
    3. extrn GetStdHandle: PROC
    4. extrn ReadFile: PROC
    5. .data
    6. buffer byte 200 dup (?)  ; буфер дляссчитывания данных
    7. len = $ - buffer
    8. .code
    9. ; Процедура для считывания данных
    10. ; Параметры:
    11. ; RAX - дескриптор файла,
    12. ; RDI - буфер для считывания
    13. ; RCX - длина буфера
    14. ; Результат - в RAX количество записанных байтов или -1, если произошла ошибка
    15. read proc
    16.  sub rsp, 56  ; 32 байта (shadow storage) + 8 байт (5-й параметр ReadFile) + 8 байт (bytesRead) + 8 байт (выравнивание)
    17.  mov r8, rcx  ;Третий параметр ReadFile - размер буфера
    18.  mov rcx, rax ; Первый параметр ReadFile - дескриптор файла
    19.  mov rdx, rdi ; Второй параметр ReadFile - адрес буфера
    20.  lea r9, bytesRead  ; Четвертый параметр ReadFile - количество считанных байтов
    21.  mov qword ptr [rsp + 32], 0  ; Пятый параметр ReadFile - 0
    22.  call ReadFile  ; вызов функции RaedFile
    23.  test rax, rax  ; проверяем на ошибку
    24.  mov eax, bytesRead  ; если ошибки нет, в RAX количество считанных байтов
    25.  jnz exit  ; если в RAX ненулевое значение
    26.  mov rax, -1  ; помещаем условный код ошибки
    27. exit:
    28.  add rsp, 56
    29.  ret
    30. bytesRead equ [rsp+40] ; область в стеке для хранения количества считанных байтов
    31. read endp
    32. ; Процедура для считывания данных с консоли
    33. ; Параметры:
    34. ; RDI - буфер для считывания
    35. ; RCX - длина буфера
    36. ; Результат - в RAX количество записанных байтов или -1, если произошла ошибка
    37. readFromConsole proc
    38.  sub rsp, 32
    39.  push rcx  ; сохраняем количество символов
    40.  mov  rcx, -10  ; Аргумент для GetStdHandle - STD_INPUT
    41.  call GetStdHandle  ; вызываем функцию GetStdHandle
    42.  pop rcx
    43.  call read
    44.  add rsp, 32
    45.  ret
    46. readFromConsole endp
    47. main proc
    48.  lea rdi, buffer  ; буфер для считывания данных
    49.  mov rcx, len  ; размер буфера
    50.  call readFromConsole  ; считываем данные с консоли
    51.  ret
    52. main endp
    53. end
    В данном случае процедура считывания данных - read получает через регистр RAX дескриптор файла (в данном случае дескриптор стандартного ввода), через регистр RDI буфер для считывания данных и через RCX - максимальное количество считываемых символов. И также возвращает либо количество считанных символов, либо число -1 - условный код ошибки.
    Процедура readFromConsole получает дескриптор консольного ввода и вызывает процедуру read для считывания с консоли.
    Правда стоит отметить, что это не самый оптимальный способ ввода с клавиатуры - если нам предстоит несколько раз вводить данные с консоли, то каждый раз придется вызывать функцию GetStdHandle для получения дескриптора консольного ввода. Вместо этого мы можем сохранить дескриптор в переменной и затем многократно его использовать:
    Код (ASM):
    1. includelib kernel32.lib  ; подключаем библиотеку kernel32.lib
    2. ; подключаем функции ReadFile и GetStdHandle
    3. extrn GetStdHandle: proc
    4. extrn ReadFile: proc
    5. extrn WriteFile: proc
    6. .data
    7. buffer byte 200 dup (?)  ; буфер для считывания данных
    8. len = $ - buffer
    9. stdin qword 0  ; для дескриптора консольного ввода
    10. stdinSet byte 0  ; был ли установлен дескриптор консольного ввода
    11. .code
    12. ; Процедура для считывания данных
    13. ; Параметры:
    14. ; RAX - дескриптор файла,
    15. ; RDI - буфер для считывания
    16. ; RCX - длина буфера
    17. ; Результат - в RAX количество записанных байтов или -1, если произошла ошибка
    18. read proc
    19.  sub rsp, 56
    20.  mov r8, rcx  ;Третий параметр ReadFile - размер буфера
    21.  mov rcx, rax ; Первый параметр ReadFile - дескриптор файла
    22.  mov rdx, rdi ; Второй параметр ReadFile - адрес буфера
    23.  lea r9, bytesRead  ; Четвертый параметр ReadFile - количество считанных байтов
    24.  mov qword ptr [rsp + 32], 0  ; Пятый параметр ReadFile - 0
    25.  call ReadFile  ; вызов функции RaedFile
    26.  test rax, rax  ; проверяем на ошибку
    27.  mov eax, bytesRead  ; если ошибки нет, в RAX количество считанных байтов
    28.  jnz exit  ; если в RAX ненулевое значение
    29.  mov rax, -1  ; помещаем условный код ошибки
    30. exit:
    31.  add rsp, 56
    32.  ret
    33. bytesRead equ [rsp+40] ; область в стеке для хранения количества считанных байтов
    34. read endp
    35. ; Процедура для считывания данных с консоли
    36. ; Параметры:
    37. ; RDI - буфер для считывания
    38. ; RCX - длина буфера
    39. ; Результат - в RAX количество записанных байтов или -1, если произошла ошибка
    40. readFromConsole proc
    41.  cmp stdinSet, 1  ; если дескриптор стандартного ввода установлен
    42.  jz readData  ; то переходим к считыванию данных
    43.  sub rsp, 32
    44.  push rcx  ; сохраняем количество символов
    45.  mov  rcx, -10  ; Аргумент для GetStdHandle - STD_INPUT
    46.  call GetStdHandle  ; вызываем функцию GetStdHandle
    47.  mov stdin, rax  ; сохраняем дескриптор стандартного ввода
    48.  mov stdinSet, 1
    49.  pop rcx
    50.  add rsp, 32
    51. readData:
    52.  mov rax, stdin
    53.  call read
    54.  ret
    55. readFromConsole endp
    56. main proc
    57.  lea rdi, buffer  ; буфер для считывания данных
    58.  mov rcx, len  ; размер буфера
    59.  call readFromConsole  ; считываем данные с консоли
    60.  ret
    61. main endp
    62. end
    Здесь функция GetStdHandle вызывается один раз, после чего дескриптор консольного ввода сохраняется в переменной stdin и устанавливается переменная stdinSet. Далее используя этот дескриптор, можно многократно выводить данные на консоль.
    Считывание одного символа
    Нередко возникает задача считать один символ. Рассмотрим, как это сделать:
    Код (ASM):
    1. includelib kernel32.lib
    2. extrn GetStdHandle: proc
    3. extrn ReadFile: proc
    4. .data
    5. buffer byte 200 dup (?)  ; буфер для считывания данных
    6. len = $ - buffer
    7. stdin qword 0  ; для хранения дескриптора консольного ввода
    8. stdinSet byte 0  ; индикатор, что дескриптор консольного ввода установлен
    9. .code
    10. ; Процедура для считывания данных
    11. ; Параметры:
    12. ; RAX - дескриптор файла,
    13. ; RDI - буфер для считывания
    14. ; RCX - длина буфера
    15. ; Результат - в RAX количество записанных байтов или -1, если произошла ошибка
    16. read proc
    17.  sub rsp, 56
    18.  mov r8, rcx  ;Третий параметр ReadFile - размер буфера
    19.  mov rcx, rax ; Первый параметр ReadFile - дескриптор файла
    20.  mov rdx, rdi ; Второй параметр ReadFile - адрес буфера
    21.  lea r9, bytesRead  ; Четвертый параметр ReadFile - количество считанных байтов
    22.  mov qword ptr [rsp + 32], 0  ; Пятый параметр ReadFile - 0
    23.  call ReadFile  ; вызов функции RaedFile
    24.  test rax, rax  ; проверяем на ошибку
    25.  mov eax, bytesRead  ; если ошибки нет, в RAX количество считанных байтов
    26.  jnz exit  ; если в RAX ненулевое значение
    27.  mov rax, -1  ; помещаем условный код ошибки
    28. exit:
    29.  add rsp, 56
    30.  ret
    31. bytesRead equ [rsp+40] ; область в стеке для хранения количества считанных байтов
    32. read endp
    33. ; Процедура для считывания данных с консоли
    34. ; Параметры:
    35. ; RDI - буфер для считывания
    36. ; RCX - длина буфера
    37. ; Результат - в RAX количество записанных байтов или -1, если произошла ошибка
    38. readFromConsole proc
    39.  cmp stdinSet, 1  ; если дескриптор стандартного ввода установлен
    40.  jz readData  ; то переходим к считыванию данных
    41.  sub rsp, 32
    42.  push rcx  ; сохраняем количество символов
    43.  mov  rcx, -10  ; Аргумент для GetStdHandle - STD_INPUT
    44.  call GetStdHandle  ; вызываем функцию GetStdHandle
    45.  mov stdin, rax  ; сохраняем дескриптор стандартного ввода
    46.  mov stdinSet, 1
    47.  pop rcx
    48.  add rsp, 32
    49. readData:
    50.  mov rax, stdin
    51.  call read
    52.  ret
    53. readFromConsole endp
    54. ; Процедура считывания одного символа с консоли
    55. ; Результат: в AL код считанного символа
    56. readChar proc
    57.  sub rsp, 8  ; выделяем 8 байт для считывания символа
    58.  mov rdi, rsp  ; в качестве буфера будет использоваться область в стеке
    59.  mov rcx, 1  ; считываем 1 байт
    60.  call readFromConsole
    61.  movzx rax, byte ptr [rsp] ; В RAX (точнее в AL) помещаем считанный символ
    62.  add rsp, 8
    63.  ret
    64. readChar endp
    65. main proc
    66.  call readChar  ; считываем символ
    67.  ret
    68. main endp
    69. end
    Здесь символ считывается процедурой readChar. Эта процедура выделяет в стеке 8 байт в качестве буфера для считывания данных (хотя в реальности нам достаточно 1 байта для хранения символа). Затем для считывания символа с консоли вызываем ранее рассмотренную процедуру readFromConsole. После выполнения процедуры помещаем символ из буфера (в стеке) в регистр AL. Пример работы программы:
    c:\asm>hello.exe
    A
    c:\asm>echo %ERRORLEVEL%
    65
    c:\asm>

    В данном случае был введен символ "A", соответственно в регистре AL был помещен код данного символа - число 65.
    Удаление из ввода перевода строки
    Как выше было указано, при вводе в буфер автоматически добавляются символы возврата каретки и перевода строки /r/n, которые имеют в такблице ASCII коды 13 и 10 соответственно. То есть мы вводим 2 символа, а в буфер записываются 4 символа. Это может быть не очень удобно. Кроме того, возможно, мы захотим, чтобы введенная строка заканчивалась каким-нибудь концевым признаком, например, нулевым байтом, как строки в языке C. Для этого мы можем использовать выше описанную процедуру для считывания символа - считывать по одному символу и проверять его код. Если код представляет обычный символ (не возврат каретки или перевод строки), то добавлять буфер. Возможная реализация:
    Код (ASM):
    1. includelib kernel32.lib  ; подключаем библиотеку kernel32.lib
    2. ; подключаем функции ReadFile и GetStdHandle
    3. extrn GetStdHandle: proc
    4. extrn ReadFile: proc
    5. .data
    6. buffer byte 200 dup (?)  ; буфер для считывания данных
    7. len = $ - buffer
    8. stdin qword 0  ; для хранения дескриптора консольного ввода
    9. stdinSet byte 0  ; индикатор, что дескриптор консольного ввода установлен
    10. .code
    11. ; Процедура для считывания данных
    12. ; Параметры:
    13. ; RAX - дескриптор файла,
    14. ; RDI - буфер для считывания
    15. ; RCX - длина буфера
    16. ; Результат - в RAX количество записанных байтов или -1, если произошла ошибка
    17. read proc
    18.  sub rsp, 56
    19.  mov r8, rcx  ;Третий параметр ReadFile - размер буфера
    20.  mov rcx, rax ; Первый параметр ReadFile - дескриптор файла
    21.  mov rdx, rdi ; Второй параметр ReadFile - адрес буфера
    22.  lea r9, bytesRead  ; Четвертый параметр ReadFile - количество считанных байтов
    23.  mov qword ptr [rsp + 32], 0  ; Пятый параметр ReadFile - 0
    24.  call ReadFile  ; вызов функции RaedFile
    25.  test rax, rax  ; проверяем на ошибку
    26.  mov eax, bytesRead  ; если ошибки нет, в RAX количество считанных байтов
    27.  jnz exit  ; если в RAX ненулевое значение
    28.  mov rax, -1  ; помещаем условный код ошибки
    29. exit:
    30.  add rsp, 56
    31.  ret
    32. bytesRead equ [rsp+40] ; область в стеке для хранения количества считанных байтов
    33. read endp
    34. ; Процедура для считывания данных с консоли
    35. ; Параметры:
    36. ; RDI - буфер для считывания
    37. ; RCX - длина буфера
    38. ; Результат - в RAX количество записанных байтов или -1, если произошла ошибка
    39. readFromConsole proc
    40.  cmp stdinSet, 1  ; если дескриптор стандартного ввода установлен
    41.  jz readData  ; то переходим к считыванию данных
    42.  sub rsp, 32
    43.  push rcx  ; сохраняем количество символов
    44.  mov  rcx, -10  ; Аргумент для GetStdHandle - STD_INPUT
    45.  call GetStdHandle  ; вызываем функцию GetStdHandle
    46.  mov stdin, rax  ; сохраняем дескриптор стандартного ввода
    47.  mov stdinSet, 1
    48.  pop rcx
    49.  add rsp, 32
    50. readData:
    51.  mov rax, stdin
    52.  call read
    53.  ret
    54. readFromConsole endp
    55. ; Процедура считывания одного символа с консоли
    56. ; Результат: в AL код считанного символа
    57. readChar proc
    58.  push rcx
    59.  push rdi
    60.  sub rsp, 8  ; выделяем 8 байт для считывания символа
    61.  mov rdi, rsp  ; в качестве буфера будет использоваться область в стеке
    62.  mov rcx, 1  ; считываем 1 байт
    63.  call readFromConsole
    64.  movzx rax, byte ptr [rsp] ; В RAX (точнее в AL) помещаем считанный символ
    65.  add rsp, 8
    66.  pop rdi
    67.  pop rcx
    68.  ret
    69. readChar endp
    70. ; Процедура для считывания строки текста
    71. ; Параметры
    72. ; RDI - буфер для считывания данных
    73. ; RCX - размер буфера
    74. ; Результат: в RAX - количество считанных символов
    75. readLine proc
    76.  xor rbx, rbx ; счетчик символов
    77.  test rcx, rcx ; проверяем, что размер буфера больше 0
    78.  je exit ; если размер буфера - 0, выходим из процедуры
    79.  dec rcx ; уменьшаем на 1, чтобы потом добавить нулевой байт
    80. while_start:
    81.  cmp rbx, rcx  ; проверяем, что буфер не заполнен
    82.  jz while_end  ; если RBX = RCX, то выходим из цикла
    83.  call readChar ; если буфер не запонен, считываем 1 символ
    84.  test eax, eax ; проверяем, что был считан 1 символ
    85.  jz while_end  ; если считано 0 симвлов, выходим из цикла
    86.  cmp al, 13  ; если нажата клавиша Enter (символ /r)
    87.  je while_end  ; выходим из цикла
    88.  cmp al, 10  ; проверяем на перевод строки - символ /n
    89.  je while_end  ; выходим из цикла
    90.  cmp al, 8  ; проверяем нажатие клавиша backspace
    91.  jne addChar  ; если она не нажата, добавляем новый символ в буфер
    92.  
    93.  ; если нажата клавиша backspace, удаляем предыдущий символ
    94.  test rbx, rbx  ; проверяем, что введено не 0 символов
    95.  jz while_start  ; если в буфере нет символов, переходим к вводу нового символа
    96.  dec rbx  ; если в буфере есть символы, уменьшаем счетчик - как бы сдвигаем ввод назад
    97.  jmp while_start ; и переходим к вводу нового символа
    98. ; Если обычный символ, добавляем символ в буфер
    99. addChar:
    100.  mov [rdi][rbx], al ; сохраняем символ в буфер
    101.  inc rbx  ; инкрементируем счетчик
    102.  jmp while_start  ; переходим к вводу нового символа
    103. ; если пользователь нажал на ENTER, добавляем к строке нулевой байт
    104. while_end:
    105.  mov byte ptr [rdi][rbx], 0
    106.  
    107. exit:
    108.  mov rax, rbx ; в RAX количество считанных символов
    109.  ret
    110. readLine endp
    111. main proc
    112.  lea rdi, buffer
    113.  mov rcx, len
    114.  call readLine
    115.  ret
    116. main endp
    117. end
    Для считывания строки с консоли определена процедура readLine. Через RDI она получает буфер для считывания данных, а через RCX - размер буфера.
    Вначале в этой процедуре устанавливается счетчик считанных символов - в регистре RBX, а также уменьшается на 1 значение в RCX, чтобы выделить место для нулевого байта. Кроме того, проверяем размер буфера, что он не равен 0:
    Код (ASM):
    1. xor rbx, rbx ; счетчик символов
    2. test rcx, rcx ; проверяем, что размер буфера больше 0
    3. je exit ; если размер буфера - 0, выходим из процедуры
    4. dec rcx ; уменьшаем на 1, чтобы потом добавить нулевой байт
    Затем в цикле проверяем количество считанных символов - если оно равно размеру буфера, то прекращаем считывание символов и выходим из цикла:
    Код (ASM):
    1. while_start:
    2. cmp rbx, rcx  ; проверяем, что буфер не заполнен
    3. jz while_end  ; если RBX = RCX, то выходим из цикла
    4. call readChar ; если буфер не запонен, считываем 1 символ
    5. test eax, eax ; проверяем, что был считан 1 символ
    6. jz while_end  ; если считано 0 симвлов, выходим из цикла
    7. cmp al, 13  ; если нажата клавиша Enter (символ /r)
    8. je while_end  ; выходим из цикла
    9. cmp al, 10  ; проверяем на перевод строки - символ /n
    10. je while_end  ; выходим из цикла
    11. cmp al, 8  ; проверяем нажатие клавиша backspace
    12. jne addChar  ; если она не нажата, добавляем новый символ в буфер
    Также проверяем нажатие на Enter, и в этом случае также выходим из цикла ввода символов.
    Если нажата клавиша Backspace (то есть идет удаление введенного символа) и если ранее были уже введены символы, то уменьшаем счетчик символов в RBX на 1, то есть формально сдвигаемся в буфере для ввода нового символа на 1 байт назад:
    Код (ASM):
    1. test rbx, rbx  ; проверяем, что введено не 0 символов
    2. jz while_start  ; если в буфере нет символов, переходим к вводу нового символа
    3. dec rbx  ; если в буфере есть символы, уменьшаем счетчик - как бы сдвигаем ввод назад
    4. jmp while_start ; и переходим к вводу нового символа
    Если введен обычный символ, то добавляем его в буфер, увеличиваем счетчик символов RBX и переходим к вводу нового символа
    Код (ASM):
    1. addChar:
    2.  mov [rdi][rbx], al ; сохраняем символ в буфер
    3.  inc rbx  ; инкрементируем счетчик
    4.  jmp while_start  ; переходим к вводу нового символа
    Если мы достигли конца буфера или нажата клавиша Enter, то добавляем после последнего введенного символа нулевой байт, а количество введенных символов помещаем в регистр RAX - своего рода результат процедуры:
    Код (ASM):
    1. while_end:
    2.  mov byte ptr [rdi][rbx], 0
    3. exit:
    4.  mov rax, rbx ; в RAX количество считанных символов
    Пример работы программы:
    c:\asm>hello
    Hello
    c:\asm>echo %ERRORLEVEL%
    5
    c:\asm>
     
    Последнее редактирование: 1 ноя 2026 в 08:39
  18. Mikl___

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

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

    Взаимодействие с WinAPI

    Практический пример. Обработка ввода

    После ввода введенные данные мы можем обработать. Например, переведем введенную строку в верхний регистр:
    Код (ASM):
    1.  includelib kernel32.lib  ; подключаем библиотеку kernel32.lib
    2. extrn GetStdHandle: proc
    3. extrn ReadFile: proc
    4. extrn WriteFile: proc
    5. .data
    6. buffer byte 200 dup (?)  ; буфер для считывания данных
    7. len = $ - buffer
    8. .code
    9. ; Процедура для считывания данных
    10. ; Параметры:
    11. ; RAX - дескриптор файла,
    12. ; RDI - буфер для считывания
    13. ; RCX - длина буфера
    14. ; Результат - в RAX количество записанных байтов или -1, если произошла ошибка
    15. read proc
    16.  sub rsp, 56  ; 32 (shadow storage) + 8 (5-й параметр ReadFile) + 8 (byttesRead) + 8 (выравнивание)
    17.  mov r8, rcx  ;Третий параметр ReadFile - размер буфера
    18.  mov rcx, rax ; Первый параметр ReadFile - дескриптор файла
    19.  mov rdx, rdi ; Второй параметр ReadFile - адрес буфера
    20.  lea r9, bytesRead  ; Четвертый параметр ReadFile - количество считанных байтов
    21.  mov qword ptr [rsp + 32], 0  ; Пятый параметр ReadFile - 0
    22.  call ReadFile  ; вызов функции RaedFile
    23.  test rax, rax  ; проверяем на ошибку
    24.  mov eax, bytesRead  ; если ошибки нет, в RAX количество считанных байтов
    25.  jnz exit  ; если в RAX ненулевое значение
    26.  mov rax, -1  ; помещаем условный код ошибки
    27. exit:
    28.  add rsp, 56
    29.  ret
    30. bytesRead equ [rsp+40] ; область в стеке для хранения количества считанных байтов
    31. read endp
    32. ; Процедура для считывания данных с консоли
    33. ; Параметры:
    34. ; RDI - буфер для считывания
    35. ; RCX - длина буфера
    36. ; Результат - в RAX количество записанных байтов или -1, если произошла ошибка
    37. readFromConsole proc
    38.  sub rsp, 32
    39.  push rcx  ; сохраняем количество символов
    40.  mov  rcx, -10  ; Аргумент для GetStdHandle - STD_INPUT
    41.  call GetStdHandle  ; вызываем функцию GetStdHandle
    42.  pop rcx
    43.  call read
    44.  add rsp, 32
    45.  ret
    46. readFromConsole endp
    47. ; Процедура перевода символов в верхний регистр
    48. ; Параметры:
    49. ; RSI - строка символов для перевода в верхний регистр
    50. ; RCX - количество символов
    51. toUpper proc
    52.  push rcx  ; изменяем rcx, поэтому сохраним его в стеке
    53. while_start:
    54.  test rcx, rcx
    55.  jz while_end
    56.  dec rcx
    57.  cmp byte ptr [rsi + rcx], 122
    58.  ja while_start
    59.  cmp byte ptr [rsi + rcx], 97
    60.  jb while_start
    61.  and byte ptr [rsi + rcx], 011011111b  ; преобразование к верхнему регистру
    62.  jmp while_start
    63. while_end:
    64.  pop rcx
    65.  ret
    66. toUpper endp
    67. ; Параметры:
    68. ; RSI - адрес строки
    69. ; RCX - длина строки
    70. ; RAX - дескриптор файла
    71. ; Результат - через RAX возвращаем количество записанных байтов
    72. write proc
    73.  sub  rsp, 56
    74.  mov rdx, rsi  ; Второй параметр - строка
    75.  mov r8, rcx  ; Третий параметр - длина строки
    76.  mov  rcx, rax  ; Первый параметр WriteFile - в регистр RCX помещаем дескриптор файла - консольного вывода
    77.  lea  r9, bytesWritten  ; Четвертый параметр WriteFile - адрес для получения записанных байтов
    78.  mov qword ptr [rsp + 32], 0  ; Пятый параметр WriteFile
    79.  call WriteFile
    80.  
    81.  test rax, rax ; проверяем на наличие ошибки
    82.  mov eax, bytesWritten ; если все нормально, помещаем в RAX количество записанных байтов
    83.  jnz exit
    84.  mov rax, -1 ; Возвращаем через RAX код ошибки
    85. exit:
    86.  add  rsp, 56
    87.  ret
    88. bytesWritten equ [rsp+40]
    89. write endp
    90. ; Процедура вывода произвольной строки на консоль
    91. ; Параметры
    92. ; RSI - адрес строки
    93. ; RCX - количество символов
    94. ; Результат - в RAX количество записанных байтов или -1, если произошла ошибка
    95. writeToConsole proc
    96.  sub rsp, 32
    97.  push rcx  ; сохраняем количество символов
    98.  mov rcx, -11  ; Аргумент для GetStdHandle - STD_OUTPUT
    99.  call GetStdHandle  ; вызываем функцию GetStdHandle
    100.  pop rcx  ; восстанавливаем RCX - количество символов
    101.  call write
    102.  add rsp, 32
    103.  ret
    104. writeToConsole endp
    105. main proc
    106.  lea rdi, buffer  ; буфер для считывания данных
    107.  mov rcx, len  ; размер буфера
    108.  call readFromConsole  ; считываем данные с консоли
    109.  
    110.  lea rsi, buffer  ; обрабатываемая строка
    111.  mov rcx, rax  ; длина строки - из readFromConsole
    112.  call toUpper  ; переводим строку в верхний регистр
    113.  call writeToConsole ; выводим преобразованную строку на консоль
    114.  ret
    115. main endp
    116. end
    В данном случае все действия разбиты на процедуры. Процедура readFromConsole считывает данные в буфер, при этом в регистр RAX заносится количество считанных символов. Затем процедура toUpper переводит все символы в нижнем регистре в верхний регистр. И далее процедура writeToConsole выводит преобразованную строку на консоль.
     
    Последнее редактирование: 1 ноя 2026 в 08:32
  19. Mikl___

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

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

    Взаимодействие с WinAPI

    Создание, открытие и закрытие файла

    Для создания файла в WinAPI можно использовать функцию CreateFileA. Она имеет следующий прототип на C++:
    Код (C):
    1. HANDLE CreateFileA(
    2.  [in]  LPCSTR  lpFileName,
    3.  [in]  DWORD  dwDesiredAccess,
    4.  [in]  DWORD  dwShareMode,
    5.  [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
    6.  [in]  DWORD  dwCreationDisposition,
    7.  [in]  DWORD  dwFlagsAndAttributes,
    8.  [in, optional] HANDLE  hTemplateFile
    9. );
    Она принимает следующие параметры:
    • lpFileName: строка - имя файла для открытия, передается через регистр RCX
    • dwDesiredAccess: представляет 32-битное число и задает режим доступа (открытие на чтение, запись или на то и другое), передается через регистр RDX
      Для создания и открытия файла на запись, устанавливаливается 30-й бит числа, а для чтения - 31-й бит. Примеры:
      • Открытие для записи: 040000000h
      • Открытие для чтения: 080000000h
      • Открытие для записи и чтения: 0C0000000h
      • Открытие для записи и дозаписи (если файл существует): 080000004h
    • dwShareMode: режим общего доступа, передается через регистр R8
    • lpSecurityAttributes: указатель на атрибуты безопасности, передается через регистр R9
    • dwCreationDisposition: флаг, который указывает, надо ли создавать новый файл или открывать уже существующий. Передается через стек по адресу [rsp + 32]
      Возможные значения:
      • 1: создавать новый, если только файл не существует, иначе открыть существующий
      • 2: всегда создавать заново, даже если файл с таким именем существует
      • 3: открыть существующий файл
      • 4: всегда открывать файл. Если он не существует, то он создается
    • dwFlagsAndAttributes: атрибуты файла, передается через стек по адресу [rsp + 40]. В частности, для создания обычного файла без атрибутов пережается число 128.
    • hTemplateFile: шаблон дескриптора файла, передается через стек по адресу [rsp + 48]
    После выполнения функция ReadFile помещает в регистр RAX ненулевое значение, если запись прошла успешно, и ноль, если произошла ошибка.
    Полное описание функции можно прочитать в документации.
    К примеру создадим в текущей папке файл "test.txt":
    Код (ASM):
    1. includelib kernel32.lib  ; подключаем библиотеку kernel32.lib
    2. ; подключаем функцию CreateFileA
    3. extrn CreateFileA: PROC
    4. .data
    5. filename byte "test.txt",0  ; имя файла
    6. .code
    7. main proc
    8.  sub rsp, 56
    9.  lea rcx, filename ; имя файла
    10.  mov rdx, 0C0000000h ; доступ на запись и чтение 1100 0000 0000 0000 0000 0000 0000 0000
    11.  xor r8, r8 ; Эксклюзивный доступ
    12.  xor r9, r9 ; атрибуты безопасности отсутствуют
    13.  mov r10, 2 ; Всегда создавать новый файл
    14.  mov [rsp + 32], r10
    15.  mov r10, 128  ; обычный файл без атрибутов
    16.  mov [rsp + 40], r10
    17.  mov [rsp + 48], r9  ; NULL - шаблон не используется
    18.  call CreateFileA
    19.  add rsp, 56
    20.  ret
    21. main endp
    22. end
    В данном случае создаваемый файл будет называться "test.txt". Он будет доступен для чтения и для записи. Для чтения надо установить 31-й бит 32-разрядного числа, а для записи - 30-й бит, поэтому в регистр RDX помещаем число 1100 0000 0000 0000 0000 0000 0000 0000 или 0C0000000h в шестнадцатеричной системе.
    Общий доступ к файлу нам не нужен, поэтому в регистр R8 помещаем число 0. Атрибуты безопасности в данном случае тоже не нужны, поэтому в регистр R9 также помещается число 0.
    Чтобы файл создавался всегда, в стек по адресу [rsp + 32] помещаем число 2.
    Файл будет создаваться обычный без каких-то атрибутов, поэтому по адресу [rsp + 40] в стек помещаем число 128.
    Шаблон дескриптора файла тоже не важен, поэтому по адресу [rsp + 48] в стек помещается число 0.
    После вызова функции CreateFileA будет создан файл "test.txt", а в регистр RAX при успешном открытии файла будет помещен дескриптор этого файла. В дальнейшем, используя данный дескриптор, мы можем записывать или считывать файл. Если же в процессе открытия произошла ошибка, то в RAX помещается число -1.
    Если у нас уже есть файл, который мы хотим открыть для операций чтения-записи, то в стек по адресу [rsp + 32] передается число 3.
    Закрытие файла
    Для закрытия файла применяется функция CloseHandle со следующим прототипом на языке C++:
    Код (C):
    1. BOOL CloseHandle(
    2.  [in] HANDLE hObject
    3. );
    В качестве параметра ей передается через регистр RCX дескриптор закрываемого файла. Результатом функции является ненулевое значение, если закрытие завершилось удачно, и 0, если произошла ошибка. Например:
    Код (ASM):
    1. includelib kernel32.lib  ; подключаем библиотеку kernel32.lib
    2. extrn CreateFileA: proc
    3. extrn CloseHandle: proc
    4. .data
    5. filename byte "test.txt",0  ; имя файла
    6. .code
    7. ; Процедура открытия существующего файла
    8. ; Параметр - в RSI - имя файла
    9. ; Результат - в RAX - дескриптор открытого файла
    10. openFile proc
    11.  sub rsp, 56
    12.  mov rcx, rsi ; имя файла
    13.  mov rdx, 0C0000000h ; доступ на запись и чтение
    14.  xor r8, r8 ; Эксклюзивный доступ
    15.  xor r9, r9 ; атрибуты безопасности отсутствуют
    16.  mov r10, 3 ; открыть существующий файл
    17.  mov [rsp + 32], r10
    18.  mov r10, 128  ; обычный файл без атрибутов
    19.  mov [rsp + 40], r10
    20.  mov [rsp + 48], r9  ; NULL - шаблон не используется
    21.  call CreateFileA
    22.  add rsp, 56
    23.  ret
    24. openFile endp
    25. ; Процедура закрытия существующего файла
    26. ; Параметр - в RAX - дескриптор открытого файла
    27. ; Результат - в RAX - состояние операции (ошибка или нет)
    28. closeFile proc
    29.  sub rsp, 40
    30.  mov rcx, rax  ; В RCX помещаем дескриптор файла
    31.  call CloseHandle
    32.  add rsp, 40
    33.  ret
    34. closeFile endp
    35. main proc
    36.  lea rsi, filename
    37.  call openFile  ; открываем файл
    38.  call closeFile  ; закрываем файл
    39.  ret
    40. main endp
    41. end
    Здесь открытие файла вынесено в процедуру openFile, а закрытие - в closeFile. Перед вызовом closeFile предполагается, что в регистре RAX содержится дескриптор файла, полученный после вызова openFile.
    Получение ошибки
    В процессе работы с файлами могут возникать ошибки. Чтобы идентифицировать их WinAPI предоставляет функцию GetLastError:
    Код (C):
    1. DWORD GetLastError();
    Данная функция не принимает параметров, лишь возвращает код ошибки. Применим эту функцию:
    Код (ASM):
    1. includelib kernel32.lib  ; подключаем библиотеку kernel32.lib
    2. extrn CreateFileA: proc
    3. extrn CloseHandle: proc
    4. extrn GetLastError: proc
    5. .data
    6. filename byte "test.txt",0  ; имя файла
    7. .code
    8. ; Процедура открытия существующего файла
    9. ; Параметр - в RSI - имя файла
    10. ; Результат - в RAX - дескриптор открытого файла
    11. openFile proc
    12.  sub rsp, 56
    13.  mov rcx, rsi ; имя файла
    14.  mov rdx, 0C0000000h ; доступ на запись и чтение
    15.  xor r8, r8 ; Эксклюзивный доступ
    16.  xor r9, r9 ; атрибуты безопасности отсутствуют
    17.  mov r10, 3 ; открыть существующий файл
    18.  mov [rsp + 32], r10
    19.  mov r10, 128  ; обычный файл без атрибутов
    20.  mov [rsp + 40], r10
    21.  mov [rsp + 48], r9  ; NULL - шаблон не используется
    22.  call CreateFileA
    23.  add rsp, 56
    24.  ret
    25. openFile endp
    26. ; Процедура закрытия существующего файла
    27. ; Параметр - в RAX - дескриптор открытого файла
    28. ; Результат - в RAX - состояние операции (ошибка или нет)
    29. closeFile proc
    30.  sub rsp, 40
    31.  mov rcx, rax
    32.  call CloseHandle
    33.  add rsp, 40
    34.  ret
    35. closeFile endp
    36. ; Процедура получения последней ошибки Windows
    37. ; Результат - в RAX код ошибки
    38. getError proc
    39.  sub rsp, 40
    40.  call GetLastError
    41.  add rsp, 40
    42.  ret
    43. getError endp
    44. main proc
    45.  lea rsi, filename
    46.  call openFile  ; открываем файл
    47.  mov rax, 0  ; помещаем в RAX некорректный дескриптор файла
    48.  call closeFile  ; закрываем файл
    49.  call getError ; получаем последнюю ошибку
    50.  ret
    51. main endp
    52. end
    Для имитации ошибки в процедуру closeFile передается некорректный дескриптор файла. Для получения ошибки после закрытия файла вызывается процедура getError, в которой получаем код ошибки. После выполнения программы с помощью команды echo %ERRORLEVEL% можно получить код ошибки, который хранится в RAX:
    c:\asm>hello.exe
    c:\asm>echo %ERRORLEVEL%
    6
    c:\asm>

    Здесь видно, что код ошибки - 6. Что это значит? Мы можем обратиться к документации, в частности, свериться по списку ошибок - https://learn.microsoft.com/ru-ru/windows/win32/debug/system-error-codes--0-499-. И мы увидим, что 6 - это код ошибки "ERROR_INVALID_HANDLE", то есть передан некорректный дескриптор файла.
     
    Последнее редактирование: 1 ноя 2026 в 08:51
  20. Mikl___

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

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

    Взаимодействие с WinAPI

    Запись в текстовый файл

    Для записи в файл мы можем использовать ранее рассмотренную функцию WriteFile, передавая ей дескриптор открытого файла. Например:
    Код (ASM):
    1. includelib kernel32.lib   ; подключаем библиотеку kernel32.lib
    2. extrn WriteFile: proc
    3. extrn CreateFileA: proc
    4. extrn CloseHandle: proc
    5. .data
    6. text byte "Hello METANIT.COM"      ; тест для записи
    7. textLen = $ - text            ; длина текста
    8. filename byte "test.txt",0    ; имя файла
    9. .code
    10. ; Параметры:
    11. ; RSI - адрес строки
    12. ; RCX - длина строки
    13. ; RAX - дескриптор файла
    14. ; Результат - через RAX возвращаем количество записанных байтов
    15. write proc
    16.   sub  rsp, 56
    17.   mov rdx, rsi          ; Второй параметр - строка
    18.   mov r8, rcx           ; Третий параметр - длина строки
    19.   mov  rcx, rax         ; Первый параметр WriteFile - в регистр RCX помещаем дескриптор файла - консольного вывода
    20.   lea  r9, bytesWritten       ; Четвертый параметр WriteFile - адрес для получения записанных байтов
    21.   mov qword ptr [rsp + 32], 0  ; Пятый параметр WriteFile
    22.   call WriteFile
    23.    
    24.   test rax, rax ; проверяем на наличие ошибки
    25.   mov eax, bytesWritten ; если все нормально, помещаем в RAX количество записанных байтов
    26.   jnz exit
    27.   mov rax, -1 ; Возвращаем через RAX код ошибки
    28. exit:
    29.   add  rsp, 56
    30.   ret
    31. bytesWritten equ [rsp+40]
    32. write endp
    33.  
    34. ; Процедура открытия существующего файла
    35. ; Параметр - в RSI - имя файла
    36. ; Результат - в RAX - дескриптор открытого файла
    37. openFile proc
    38.   sub rsp, 56
    39.   mov rcx, rsi ; имя файла
    40.   mov rdx, 080000004h ; доступ на запись и дозапись
    41.   xor r8, r8 ; Эксклюзивный доступ
    42.   xor r9, r9 ; атрибуты безопасности отсутствуют
    43.   mov r10, 1 ; открыть существующий файл, если он не существует - создать
    44.   mov [rsp + 32], r10
    45.   mov r10, 128  ; обычный файл без атрибутов
    46.   mov [rsp + 40], r10
    47.   mov [rsp + 48], r9    ; NULL - шаблон не используется
    48.   call CreateFileA
    49.  
    50.   add rsp, 56
    51.   ret
    52. openFile endp
    53.  
    54. ; Процедура закрытия существующего файла
    55. ; Параметр - в RAX - дескриптор открытого файла
    56. ; Результат - в RAX - состояние операции (ошибка или нет)
    57. closeFile proc
    58.   sub rsp, 40
    59.   mov rcx, rax      ; В RCX помещаем дескриптор файла
    60.   call CloseHandle
    61.   add rsp, 40
    62.   ret
    63. closeFile endp
    64.  
    65. main proc
    66.   lea rsi, filename
    67.   call openFile   ; открываем файл
    68.  
    69.   push rax        ; сохраняем дескриптор файла
    70.   lea rsi, text     ; записываемая строка
    71.   mov rcx, textLen  ; длина записываемой строки
    72.   call write        ; записываем строку
    73.   pop rax           ; восстанавливаем дескриптор файла для закрытия файла
    74.  
    75.   call closeFile  ; закрываем файл
    76.   ret
    77. main endp
    78. end
    Здесь запись в файл производится в процедуре write, которая через регистр RSI получает адрес записываемого текста, через RCX - количество записываемых символов, а через RAX - дескриптор файла. Если файл уже существует, то выполняется дозапись.
    Обработка ошибок
    При записи могут возникнуть ошибки. Так, в программе выше у нас несколько потенциальных ситуации возникновения ошибки - файл не будет открыт, запись не будет произведена, закрытие файла пройдет неудачно. В качестве примера рассмотрим обработку ошибки при открытии файла:
    Код (ASM):
    1. includelib kernel32.lib
    2. extrn GetStdHandle: proc
    3. extrn WriteFile: proc
    4. extrn CreateFileA: proc
    5. extrn CloseHandle: proc
    6. .data
    7. text byte "Hello METANIT.COM", 10  ; тест для записи
    8. textLen = $ - text  ; длина текста
    9. filename byte "test.txt",0  ; имя файла
    10. badOpenMsg byte "Unable to open file", 10
    11. badOpenMsgLen = $ - badOpenMsg
    12. goodWriteMsg byte "File has been written", 10
    13. goodWriteMsgLen = $ - goodWriteMsg
    14. stdout qword 0  ; дескриптор консольного вывода
    15. stdoutSet byte 0  ; установлен ли дескриптор консольного вывода
    16. .code
    17. ; Параметры:
    18. ; RSI - адрес строки
    19. ; RCX - длина строки
    20. ; RAX - дескриптор файла
    21. ; Результат - через RAX возвращаем количество записанных байтов
    22. write proc
    23.  sub  rsp, 56
    24.  mov rdx, rsi  ; Второй параметр - строка
    25.  mov r8, rcx  ; Третий параметр - длина строки
    26.  mov  rcx, rax  ; Первый параметр WriteFile - в регистр RCX помещаем дескриптор файла - консольного вывода
    27.  lea  r9, bytesWritten  ; Четвертый параметр WriteFile - адрес для получения записанных байтов
    28.  mov qword ptr [rsp + 32], 0  ; Пятый параметр WriteFile
    29.  call WriteFile
    30.  
    31.  test rax, rax ; проверяем на наличие ошибки
    32.  mov eax, bytesWritten ; если все нормально, помещаем в RAX количество записанных байтов
    33.  jnz exit
    34.  mov rax, -1 ; Возвращаем через RAX код ошибки
    35. exit:
    36.  add  rsp, 56
    37.  ret
    38. bytesWritten equ [rsp+40]
    39. write endp
    40. ; Процедура вывода произвольной строки на консоль
    41. ; Параметры
    42. ; RSI - адрес строки
    43. ; RCX - количество символов
    44. ; Результат - в RAX количество записанных байтов или -1, если произошла ошибка
    45. writeToConsole proc
    46.  cmp stdoutSet, 1
    47.  jz writeData  ; если дескриптор консольного вывода установлен
    48.  sub rsp, 32
    49.  push rcx  ; сохраняем количество символов
    50.  mov rcx, -11  ; Аргумент для GetStdHandle - STD_OUTPUT
    51.  call GetStdHandle  ; вызываем функцию GetStdHandle
    52.  pop rcx  ; восстанавливаем RCX - количество символов
    53.  add rsp, 32
    54.  mov stdout, rax  ; помещаем в stdout дескриптор консольного вывода
    55.  mov stdoutSet, 1  ; дескриптор консольного вывода установлен
    56. writeData:
    57.  mov rax, stdout
    58.  call write
    59.  ret
    60. writeToConsole endp
    61. ; Процедура открытия существующего файла
    62. ; Параметр - в RSI - имя файла
    63. ; Результат - в RAX - дескриптор открытого файла
    64. openFile proc
    65.  sub rsp, 56
    66.  mov rcx, rsi ; имя файла
    67.  mov rdx, 0B0000004h ; доступ на запись и чтение
    68.  xor r8, r8 ; Эксклюзивный доступ
    69.  xor r9, r9 ; атрибуты безопасности отсутствуют
    70.  mov r10, 4 ; открыть существующий файл, если он не существует - создать
    71.  mov [rsp + 32], r10
    72.  mov r10, 128  ; обычный файл без атрибутов
    73.  mov [rsp + 40], r10
    74.  mov [rsp + 48], r9  ; NULL - шаблон не используется
    75.  call CreateFileA
    76.  add rsp, 56
    77.  ret
    78. openFile endp
    79. ; Процедура закрытия существующего файла
    80. ; Параметр - в RAX - дескриптор открытого файла
    81. ; Результат - в RAX - состояние операции (ошибка или нет)
    82. closeFile proc
    83.  sub rsp, 40
    84.  mov rcx, rax  ; В RCX помещаем дескриптор файла
    85.  call CloseHandle
    86.  add rsp, 40
    87.  ret
    88. closeFile endp
    89. main proc
    90.  lea rsi, filename
    91.  call openFile  ; открываем файл
    92.  cmp eax, -1  ; если произошла ошибка открытия
    93.  je badOpen
    94.  push rax  ; сохраняем дескриптор файла
    95.  lea rsi, text  ; записываемая строка
    96.  mov rcx, textLen  ; длина записываемой строки
    97.  call write  ; записываем строку
    98.  pop rax  ; восстанавливаем дескриптор файла для закрытия файла
    99.  call closeFile  ; закрываем файл
    100.  lea rsi, goodWriteMsg
    101.  mov rcx, goodWriteMsgLen
    102.  call writeToConsole
    103.  jmp exit
    104. badOpen:  ; если не удалось открыть файл
    105.  lea rsi, badOpenMsg
    106.  mov rcx, badOpenMsgLen
    107.  call writeToConsole
    108. exit:
    109.  ret
    110. main endp
    111. end
    Здесь добавлена процедура writeToConsole для вывода диагностического сообщения на консоль. Если при открытии файла возникает проблема, соответственно в регистре EAX отказывается число -1, то переходим к метке badOpen для обработки этой ситуации. В коде выше в этом случае просто выводим сообщение. Если же запись в файл прошла успешно, выводим другое сообщение на консоль.
     
    Последнее редактирование: 1 ноя 2026 в 08:53