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

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

  1. Mikl___

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

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

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

    Деление больших чисел

    Для деления чисел в ассемблере мы можем использовать инструкции div и idiv. Причем эти операции позволяют разделить максимум 128-битное значение, которое помещается в RDX:RAX, а делителем в этом случае является 64-битный операнд. После деления частное помещается в RAX, а остаток - в RDX. Однако в этом случае результат деления должен умещаться в 64 бита, что естественно происходит не всегда (например, любое число больше 64 бит разделить на 1).
    Один из методов для решения этой задачи состоит в делении старших 32 битов числа (дополненного нулем или знаком) на делитель, затем делится остаток и младшие 32 бита:
    Код (ASM):
    1. .data
    2.  op1 oword 100000000000000006h ; делимое
    3.  op2 qword 2  ; делитель
    4.  result qword 2 dup (?)
    5.  remainder qword ?  ; для остатка
    6. .code
    7. main proc
    8.  mov rax, qword ptr op1[8]
    9.  xor edx, edx  ; дополнение нулем для беззнакового деления
    10.  div op2
    11.  mov result[8], rax ; сохраняем в старшие 64 байта
    12.  mov rax, qword ptr op1[0]
    13.  div op2  ; делим остаток и младшие 64 бита
    14.  mov result[0], rax  ; сохраняем в младшие 64 байта
    15.  mov remainder, rdx ; сохраняем остаток
    16.  ret
    17. main endp
    18. end
    Переменная частного имеет размер 128 бит, потому что для результата может потребоваться столько же битов, сколько и для делимого (например, при делении на 1). Независимо от размера операндов делимого и делителя, остаток никогда не превышает 64 бита (в данном случае).
    Для деления 128-разрядного числа на 64-разрядное, сначала делим старшие 64 бит делимого на делитель. Частное из этого первого деления становится старшими 32 битами конечного результата. Остаток от этого деления остается в RDX для второй операции деления.
    Вторая операция деления делит rdx:op1[0] на делитель для получения младших 32 бит результата и остатка от деления.
    Данная операция деления (128 на 64) является частным случаем общего алгоритма деления значений произвольного размера на 64-битный делитель. Общий алгоритм следующий:
    1. 64 старших бита делимого помещаются в RAX с расширением нулями в RDX.
    2. Деление этих 64 бит на делитель.
    3. Результат деления из регистра RAX сохраняется в соответствующей позиции переменной результата
    4. В RAX загружаются следующие 64 бита без изменения RDX (там должен быть остаток).
    5. Повторение шагов 2-4 пока не будут обработаны все 64-битные части делимого
    Пример деления 256-разрядного числа на 64-разрядное:
    Код (ASM):
    1. .data
    2.  op1 oword 2222eeeeccccaaaa8888666644440000h
    3.  oword 2222eeeeccccaaaa8888666644440000h ; делимое - 256 бит
    4.  op2 qword 2  ; делитель
    5.  result qword 2 dup (?)
    6.  remainder qword ?  ; для остатка
    7. .code
    8. main proc
    9.  mov rax, qword ptr op1[24]  ; старшие 64 бита в RAX
    10.  xor rdx, rdx  ; расширение нулями для беззнакового деления
    11.  div op2  ; делим старшие 64 бита
    12.  mov result[24], rax ; сохраняем старшие 64 бита результата
    13.  
    14.  mov rax, qword ptr op1[16]  ; следующие 64 бита
    15.  div op2
    16.  mov result[16], rax ; сохраняем вторую порцию 64 бит
    17.  
    18.  mov rax, qword ptr op1[8]
    19.  div op2
    20.  mov result[8], rax
    21.  mov rax, qword ptr op1[0]  ; младшие 64 бита
    22.  div op2
    23.  mov result[0], rax  ; сохраняем младшие 64 бита
    24.  mov remainder, rdx  ; сохраняем остаток
    25.  
    26.  ret
    27. main endp
    28. end
    Стоит отметить, что данный алгоритм деления работает только для беззнаковых операндов. Чтобы разделить два числа со знаком, надо зафиксировать их знаки, взять их абсолютные значения, выполнить беззнаковое деление, а затем установить знак результата на основе знаков операндов.
    Делитель произвольного размера
    При мере выше мы могли реализовать делимое произвольного размера, но делитель оставался 64-разрядным. Для реализации делителей размером более 64 бит одним из вариантов решения проблемы является сочетание сдвига и вычитания. В общем случае алгоритм будет выглядеть следующим образом:
    Код (ASM):
    1. Quotient = Dividend;
    2. Remainder = 0;
    3. for i = 1 to NumberBits do
    4.  Remainder:Quotient = Remainder:Quotient SHL 1;
    5.  if Remainder >;= Divisor then
    6.  Remainder = Remainder - Divisor;
    7.  Quotient = Quotient + 1;
    8.  endif
    9. endfor
    Где Quotient - результат/частное, Dividend - делимое, Remainder - остаток от деления, Divisor - делитель, а NumberBits - количество битов в Remainder/Quotient/Divisor/Dividend. Здесь делимое и результат деления представляют один и тот же объект. Суть алгоритма - сдвигаем Remainder:Quotient на один бит влево. Таким образом в остатке будет первый бит делимого. Если получение значение в остатке больше или равно делителю, то вычитаем из остатка делитель и увеличиваем результат на 1. И так, пока не обработаем все биты в делители.
    Реализация на примере деления 128-разрядного числа на другое 128-разрядное:
    Код (ASM):
    1. .data
    2.  op1 oword 00000000000000040000000000000008h  ; делимое и результат - 128 бит
    3.  op2 oword 2  ; делитель - 128 бит
    4.  remainder oword ?
    5. .code
    6. main proc
    7.  mov cl, 128 ; в CL количество бит, которые надо разделить
    8.  ; Remainder:Quotient = Remainder:Quotient SHL 1
    9. repeatLp:
    10.  ; сдвиг влево на 1 разряд всего делимого и остатка
    11.  shl qword ptr op1[0], 1
    12.  rcl qword ptr op1[8], 1
    13.  rcl qword ptr remainder[0], 1
    14.  rcl qword ptr remainder[8], 1
    15.  ; Выполняем 128-разрядное сравнение, чтобы узнать, больше ли остаток чем делитель
    16.  mov rax, qword ptr remainder[8]
    17.  cmp rax, qword ptr op2[8]
    18.  ja isGE  ; если больше или равно
    19.  jb notGE  ; если меньше
    20.  mov rax, qword ptr remainder
    21.  cmp rax, qword ptr op2
    22.  ja isGE
    23.  jb notGE
    24.  ; Remainder = Remainder - Divisor
    25. isGE:
    26.  mov rax, qword ptr op2
    27.  sub qword ptr remainder, rax
    28.  mov rax, qword ptr op2[8]
    29.  sbb qword ptr remainder[8], rax
    30.  ; Quotient = Quotient + 1
    31.  add qword ptr op1, 1
    32.  adc qword ptr op1[8], 0
    33. notGE:
    34.  dec cl
    35.  jnz repeatLp  ; продолжаем пока не пройдем по всем 128 битам
    36.  ; mov eax, dword ptr op1  ; 4
    37.  ; mov eax, dword ptr op1[8]  ; 2
    38.  ret
    39. main endp
    40. end
    В данном случае переменная op1 выступает в качестве делимого и одновременно результата операции. Переменная op2 является делителем, а remainder - хранит остаток. В регистре CL хранится счетчик битов, которые надо разделить.
     
    Последнее редактирование: 30 окт 2026 в 15:59
  2. Mikl___

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

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

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

    Поразрядные операции с большими числами

    Арифметическое отрицание
    Арифметическое отрицание эквивалентно вычитанию значения из 0, поэтому можно легко имитировать отрицание для очень больших чисел, используя инструкции sub и sbb:
    Код (ASM):
    1. .data
    2.  ; 192-разрядное число
    3.  number sqword 6,  ; младшие 64 бита
    4.  0,  ; средние 64 бита
    5.  0  ; старшие 64 бита
    6. .code
    7. main proc
    8.  xor rax, rax  ; RAX = 0
    9.  sub rax, number ; вычитание первых 64 бит
    10.  mov number, rax
    11.  
    12.  mov rax, 0
    13.  sbb rax, number[8] ; вычитания вторых 64 бит с учетом флага CF
    14.  mov number[8], rax
    15.  
    16.  mov rax, 0
    17.  sbb rax, number[16] ; вычитания старших 64 бит с учетом флага CF
    18.  mov number[16], rax
    19.  ret
    20. main endp
    21. end
    В данном случае повледовательно вычитаем каждый из трех четверных слов числа number из 0 и сохраняем результат обратно в переменную number.
    Если надо сделать отрицательным 128-разрядное число, то можно применять чуть более эффективный способ - комбинировать инструкции neg и sbb:
    Код (ASM):
    1. .data
    2.  ; 192-разрядное число
    3.  number qword 5,  ; младшие 64 бита
    4.  0  ; старшие 64 бита
    5. .code
    6. main proc
    7.  neg number[8],
    8.  neg number
    9.  sbb number[8], 0  ; если после neg установлен знак переноса
    10.  ret
    11. main endp
    12. end
    neg фактически также вычитает операнд из 0, и если при вычитании младших 64 бит имело место заимствование, и соответственно был установлен флаг переноса CF, то также применяем инструкцию sbb для вычитания бита из старших 64 бит.
    Логические операции AND, OR, XOR
    Для выполнения операции логического умножения AND для больших чисел просто выполняется операция AND между соответствующими байтами двух операндов с сохранением результата. Например, логическое умножение 192-разрядных чисел:
    Код (ASM):
    1. .data
    2.  ; 192-разрядные числа
    3.  n1 qword 5, 3, 2
    4.  n2 qword 4, 2, 3
    5.  result qword 3 dup(?)  ; для хранения результата
    6. .code
    7. main proc
    8.  mov rax, qword ptr n1  ; младший байт
    9.  and rax, qword ptr n2
    10.  mov qword ptr result, rax
    11.  mov rax, qword ptr n1[8]  ; средний байт
    12.  and rax, qword ptr n2[8]
    13.  mov qword ptr result[8], rax
    14.  mov rax, qword ptr n1[16]  ; старший байт
    15.  and rax, qword ptr n2[16]
    16.  mov qword ptr result[16], rax
    17.  mov rax, qword ptr result[0]
    18.  ret
    19. main endp
    20. end
    Аналогичным образом мы можем выполнять операции OR и XOR. Например, операция OR:
    Код (ASM):
    1. .data
    2.  ; 192-разрядные числа
    3.  n1 qword 5, 3, 2
    4.  n2 qword 4, 2, 3
    5.  result qword 3 dup(?)  ; для хранения результата
    6. .code
    7. main proc
    8.  mov rax, qword ptr n1  ; младший байт
    9.  or rax, qword ptr n2
    10.  mov qword ptr result, rax
    11.  mov rax, qword ptr n1[8]  ; средний байт
    12.  or rax, qword ptr n2[8]
    13.  mov qword ptr result[8], rax
    14.  mov rax, qword ptr n1[16]  ; старший байт
    15.  or rax, qword ptr n2[16]
    16.  mov qword ptr result[16], rax
    17.  mov rax, qword ptr result[8]
    18.  ret
    19. main endp
    20. end
    Для поразрядного отрицания применяем ко всем частям числа инструкцию neg:
    Код (ASM):
    1. .data
    2.  ; 192-разрядное число
    3.  num qword  0ffffffffffffffffh,
    4.  0fffffffffffffffeh,
    5.  0fffffffffffffffdh
    6. .code
    7. main proc
    8.  neg qword ptr num  ; младшие 64 бита
    9.  neg qword ptr num[8]  ; средние 64 бита
    10.  neg qword ptr num[16]  ; старшие 64 бита
    11.  ret
    12. main endp
    13. end
     
    Последнее редактирование: 30 окт 2026 в 16:04
  3. Mikl___

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

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

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

    Операции сдвига больших чисел

    Реализация операций сдвига в отношении больших чисел подразумевает сочетание операций сдвига и вращения. Для реализации сдвига влево можно применить инструкции shl и rcl
    Например, чтобы выполнить сдвиг влево 128-разрядного числа, сначала необходимо сдвинуть младшие 64 разрядов влево (с помощью инструкции shl). При этом надо также получить значение самого старшего бита из этих младших 64 разрядов, который отбрасывается при сдвиге влево. Для этого применяется обычно флаг переноса CF. Затем надо сдвинуть этот бит в самый младший бит старщих 64 разрядов числа, одновременно сдвигая все остальные биты влево. Для этого применяется инструкция rcl.
    Рассмотрим на примере сдвига влево 192-разрядного числа:
    Код (ASM):
    1. .data
    2.  ; 192-разрядное число
    3.  num qword  0ffffffffffffffffh,
    4.  0e000000000000000h,
    5.  0
    6. .code
    7. main proc
    8.  shl qword ptr num, 1  ; сдвигаем младшие 64 бита влево
    9.  rcl qword ptr num[8], 1  ; средние 64 бита
    10.  rcl qword ptr num[16], 1  ; старшие 64 бита
    11.  ret
    12. main endp
    13. end
    Чтобы учесть сдвигаемый старший бит из предыдущих 64 разрядов для сдвига вторых и последующих 64 разрядов числа применяется инструкция вращения влево с переносом rcl. Однако поскольку нам надо учитывать сдвигаемый старший бит из предыдущих 64 разрядов, то одномоментно мы можем сдвинуть число только на 1 бит. Что может быть не очень удобно, особенно если нам надо сдвинуть на большее количество разрядов. В этом случае можно использовать циклические конструкции:
    Код (ASM):
    1. .data
    2.  ; 192-разрядное число
    3.  num qword  0ffffffffffffffffh,
    4.  0e000000000000000h,
    5.  0
    6. .code
    7. main proc
    8.  mov cl, 4  ; сдвигаем на 4 разряда
    9. shift:
    10.  shl qword ptr num, 1  ; сдвигаем младшие 64 бита влево
    11.  rcl qword ptr num[8], 1  ; средние 64 бита
    12.  rcl qword ptr num[16], 1  ; старшие 64 бита
    13.  dec cl
    14.  jnz shift  ; пока не сдвинем все разряды, повторяем цикл
    15.  mov rax, qword ptr num[16] ; RAX = 0eh или 14
    16.  ret
    17. main endp
    18. end
    Аналогичным образом можно использовать сдвиг вправо (инструкция shr) и арифметический сдвиг вправо (инструкция sar), только сдвиг начинается со старших 64 разрядов числа:
    Код (ASM):
    1. ; обычный сдвиг вправо
    2.  shr qword ptr num[16], 1
    3.  rcr qword ptr num[8], 1
    4.  rcr qword ptr num[0], 1
    5. ; арифметический сдвиг вправо
    6.  sar qword ptr num[16], 1
    7.  rcr qword ptr num[8], 1
    8.  rcr qword ptr num[0], 1
    Многобитный сдвиг
    Для сдвига на несколько бит мы можем использовать циклическую конструкцию, как было показано выше. Однако ассемблер также предоставляет и другой способ - инструкции shld (сдвиг влево) и shrd (сдвиг вправо). Они позволяют сдвинуть число на несколько бит. Они имеют следующий синтаксис:
    Код (ASM):
    1. shld Operand1, Operand2, constant
    2. shld Operand1, Operand2, cl
    3. shrd Operand1, Operand2, constant
    4. shrd Operand1, Operand2, cl
    В качестве первого операнда регистр или переменная, которые могут быть 16-, 32- или 64-разрядными. В первый операнд помещается результат сдвига. Второй операнд - собственно сдвигаемое число, в качестве которого должен выступать регистр. Оба операнда должен иметь одинаковый размер. Третий операнд - константа или регистр CL указывают, на сколько битов надо выполнить сдвиг.
    При сдвигах с помощью данных иснтрукций также могут меняться знаки. При сдвиге влево инструкцией shld флаг переноса CF содержит последний сдвинутый старший бит из первого операнда. Если сдвигаем только на 1 бит, флаг переполнения OF будет содержать 1, если при сдвиге изменится знак первого операнда. Если сдвигаем на большее число битов, флаг переполнения не определен. Флаг будет ZF равен 1, если результатом сдвига является 0. Флаг знака SF будет содержать старший бит результата.
    [​IMG]
    При сдвиге вправо инструкцией shrd флаг переноса CF содержит последний сдвинутый младший бит из первого операнда. Если сдвигаем только на 1 бит, флаг переполнения OF будет содержать 1, если при сдвиге изменится старший бит первого операнда. Если сдвигаем на большее число битов, флаг переполнения не определен. Флаг будет ZF равен 1, если результатом сдвига является 0. Флаг знака SF будет содержать старший бит результата.
    [​IMG]
    Например, перепишем предыдущий пример со сдвигом 192-разрядного числа на 4 бита с помощью инструкции shld:
    Код (ASM):
    1. .data
    2.  ; 192-разрядное число
    3.  num qword  0ffffffffffffffffh,  ; младшие 64 бита
    4.  0e000000000000000h,  ; средние 64 бита
    5.  0  ; старшие 64 бита
    6. .code
    7. main proc
    8.  mov rax, num[8]
    9.  shld num[16], rax, 4
    10.  mov rax, num[0]
    11.  shld num[8], rax, 4
    12.  shl num[0], 4
    13.  ; mov rax, qword ptr num[16]  ; RAX = 14
    14.  ; mov rax, qword ptr num[8]  ; RAX = 15
    15.  ret
    16. main endp
    17. end
    Первая инструкция shld сдвигает биты из num[8] в num[16], не затрагивая значения в num[8]. Вторая инструкция shld сдвигает биты из num в num[8]. Наконец, инструкция shl сдвигает младшие 64 бита из num на 4 разряда влево.
    Аналогичным образом сдвиг вправо с помощью shrd.
     
    Последнее редактирование: 30 окт 2026 в 16:13
  4. Mikl___

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

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

    Процедуры

    Определение и вызов процедур

    Процедуры или функции в ассемблере позволяют разбить программу на подпрограммы, где каждая подпрограмма выполняет какой-то определенный набор действий.
    Для определения процедуры применяется выражение
    proc_name proc
    где proc_name - имя процедуры.
    Завершается определение процедуры выражением
    proc_name endp
    где proc_name - также имя процедуры.
    Между proc_name proc и proc_name endp идет произвольный набор инструкций. Перед завершением процедуры помещается инструкция ret, которая передает управление в вызывающий код. Например:
    Код (ASM):
    1. .code
    2. setReg proc  ; начало процедуры setReg
    3.  mov rax, 10
    4.  ret
    5. setReg endp  ; конец процедуры setReg
    6. main proc
    7.  ret
    8. main endp
    9. end
    Здесь определены две процедуры - главная функция main и процедура setReg. В процедуре setReg устанавливается значение регистра rax. При компиляции подобной программы (допустим, она находится в файле hello.asm) с помощью команды
    ml64 hello.asm /link /entry:main
    благодаря флагу /entry:main будет создавать файл, при запуске которого выполняется процедура с именем main. Но в нашем случае эта процедура пока ничего не делает, а процедура setReg автоматически не выполняется. Чтобы выполнить одну процедуру/функцию в другой, необходимо использовать инструкцию call, после которой указывается имя выполняемой процедуры:
    call proc_name
    Инструкция call помещает в стек 64-битный адрес инструкции, которая идет сразу после вызова. Значение, которое вызов помещает в стек, называется адресом возврата. Когда процедура завершает выполнение, для возвращения к вызывающему коду она выполняет инструкцию ret. Команда ret извлекает 64-битный адрес возврата из стека и косвенно передает управление на этот адрес.
    Например, выполним процедуру setReg в функции main:
    Код (ASM):
    1. .code
    2. setReg proc  ; начало процедуры setReg
    3.  mov rax, 10
    4.  ret
    5. setReg endp  ; конец процедуры setReg
    6. main proc
    7.  call setReg ; вызов процедуры setReg
    8.  ret
    9. main endp
    10. end
    Вызываемые процедуры могут, в свою оцередь вызывать другие процедуры. Например:
    Код (ASM):
    1. .code
    2. inner proc
    3.  add rax, 1
    4.  ret
    5. inner endp
    6. outer proc
    7.  call inner
    8.  add rax, 1
    9.  ret
    10. outer endp
    11. main proc
    12.  mov rax, 1
    13.  call outer
    14.  ret
    15. main endp
    16. end
    Здесь функция main вызывает процедуру outer, а та вызывает процедуру inner. В итоге к завершению программы в регистре RAX будет число 3.
    Стек и процедуры
    При работе со стеком в процедурах следует учитывать, что вызов процедуры с помощью инструкции call помещает в стек адрес возврата. При завершении процедуры инструкция ret извлечет этот адрес возврата из стека и перейдет по этому адресу. Таким образом, выполнение вернется в код, где была вызвана процедура. Поэтому при вызове инструкции ret (при завершении процедуры) адрес возврата должен быть в верхушке стека.
    Но при невнимательности это требование может быть нарушено. Например:
    Код (ASM):
    1. .code
    2. sum proc
    3.  push rbx  ; сохраняем регистр RBX в стек
    4.  add rax, rbx
    5.  ret  ; регистр RBX НЕ восстанавливаем
    6. sum endp
    7. main proc
    8.  mov rax, 1
    9.  mov rbx, 2
    10.  call sum
    11.  ret
    12. main endp
    13. end
    Здесь вызывается процедура sum, в которой в стек сохраняется регистр RBX. Однако в конце процедуры регистр RBX не восстанавливается. Поэтому в качестве адреса вохврата будет рассматриваться значение регистра RBX, которое при вызовае инструкции ret будет находится в верхушке стека. В итоге поведение программы неопределено, и скорее всего она завершится ошибкой.
    Другой пример - извлечение адреса возврата до завершения процедуры:
    Код (ASM):
    1. .code
    2. sum proc
    3.  pop rbx  ; извлекаем данные из стека в регистр RBX
    4.  add rax, rbx
    5.  ret  ; адрес возврата неопределен
    6. sum endp
    7. main proc
    8.  mov rax, 1
    9.  mov rbx, 2
    10.  call sum
    11.  ret
    12. main endp
    13. end
    Здесь в регистр RBX извлекаются данные из стека - по сути в него извлекается адрес возврата. В результате опять же поведение программы неопределено.
    Поэтому процедура должна извлекать из стека все ранее сохраненные в ней данные и извлекать ровно столько, сколько было сохранено, чтобы адрес возврата сохранялся в стеке и к концу программы оказался в верхушке стека.
     
    Последнее редактирование: 30 окт 2026 в 16:24
  5. Mikl___

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

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

    Процедуры

    Сохранение регистров и переменных при вызове процедур


    Процедура может активно задействовать регистры для различных задач. Например:
    Код (ASM):
    1. .code
    2. sum proc
    3.  mov rbx, 5
    4.  mov rax, 10
    5.  add rax, rbx
    6.  ret
    7. sum endp
    8. main proc
    9.  mov rbx, 125
    10.  call sum
    11.  add rax, rbx  ; RAX = 20
    12.  ret
    13. main endp
    14. end
    Для примера здесь определена процедура sum, которая устанавливает регистры rbx и rax и складывает их значения, помещая результат в регистр rax.
    В функции main помещаем в регистр RBX чило 125, затем вызываем процедуру sum. Далее результат процедуры sum - содержимое регистра RAX складываем с числом из регистра RAX. Однако, поскольку вызов процедуры sum изменил значение регистра RBX, то в после ее вызова в этом регистре будет не число 125, а число 5. Соответственно итоговый результат может отличаться от ожидаемого. В данном случае в регистре RAX будет число 20, а не 140.
    Кончено, мы могли бы выбрать разные регистры, но данный пример - упрощение, в реальной процедуре одновременно может быть задействовано для сложных вычислений множество регистров. Кроме того, разрабытываемые процедуры могут использоваться во внешней программе, и при разработке процедуры мы можем не знать, какие именно регистры и как именно будет использовать внешняя программа. Поэтому хорошей, а иногда и необходимой практикой при вызове процедуры является сохранение значений используемых регистров в стек, а при завершении процедуры - их восстановление. Например:
    Код (ASM):
    1. .code
    2. sum proc
    3.  push rbx  ; сохраняем в стек регистр rbx
    4.  mov rbx, 5
    5.  mov rax, 10
    6.  add rax, rbx
    7.  pop rbx  ; восстанавливаем из стека регистр rbx
    8.  ret
    9. sum endp
    10. main proc
    11.  mov rbx, 125
    12.  call sum
    13.  add rax, rbx  ; RAX = 140
    14.  ret
    15. main endp
    16. end
    В данном случае в начале процедуры sum сохраняем значение rbx, а при завершении восстанавливаем его. Подобным образом стоит сохранять и восстанавливать все используемые регистры в стек. Регистр RAX в данном случае не сохраняется, так как мы ожидаем, что через него функция main получить результат процедуры sum.
    При этом в принципе необяхательно сохранять регистры в вызываемой процедуре, мы могли бы это сделать и в вызывающей процедуре:
    Код (ASM):
    1. .code
    2. sum proc
    3.  mov rbx, 5
    4.  mov rax, 10
    5.  add rax, rbx
    6.  ret
    7. sum endp
    8. main proc
    9.  mov rbx, 125
    10.  push rbx  ; сохраняем в стек регистр rbx
    11.  call sum
    12.  pop rbx  ; восстанавливаем из стека регистр rbx
    13.  add rax, rbx
    14.  ret
    15. main endp
    16. end
    Однако сохранение регистров в вызываемой процедуре может быть более предпочтительным, так как функция main может вызывать кучу других процедур и постоянно сохранять/восстанавливать регистры при каждом вызове значительно утяжелит код функции main.
    Стоит отметить, что при взаимодействии с внешними API может потребоваться в обязательном порядке сохранять данные некоторых регистров. Так, если мы взаимодействуем с Windows API, то есть общие соглашения, что при вызове функций WinAPI необходимо схоранять в стек, а после вызова восстанавливать регистры RBX, RBP, RDI, RSI, RSP, R12, R13, R14 и R15, а также XMM6 - XMM15.
    Сохранение и восстановление переменных
    Все, что касается регистров, также можно отнести и к глобальным переменным, которые используются в процедурах:
    Код (ASM):
    1. .data
    2.  num word 45
    3. .code
    4. sum proc
    5.  mov num, 15
    6.  mov rax, 10
    7.  add ax, num  ; AX = AX + num = 10 + 15 = 25
    8.  ret
    9. sum endp
    10. main proc
    11.  call sum
    12.  add ax, num  ; AX = AX + num = 25 + 15 = 40
    13.  ret
    14. main endp
    15. end
    В данном случае в функции main мы ожидаем, что переменная num будет равна 45, но вызов процедуры sum меняет ее значение на 45. И подобным образом в вызываемой процедуре переменную можно сохранить в стек:
    Код (ASM):
    1. .data
    2.  num word 45
    3. .code
    4. sum proc
    5.  push num  ; сохраняем значение num в стек
    6.  mov num, 15
    7.  mov rax, 10
    8.  add ax, num  ; AX = AX + num = 10 + 15 = 25
    9.  pop num  ; восстанавливаем num из стека
    10.  ret
    11. sum endp
    12. main proc
    13.  call sum
    14.  add ax, num  ; AX = AX + num = 25 + 45 = 70
    15.  ret
    16. main endp
    17. end
     
    Последнее редактирование: 30 окт 2026 в 16:29
  6. Mikl___

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

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

    Процедуры

    Параметры

    Через параметры процедуры могут принимать из вне некоторые значения. Обычно для передачи параметров применяются регистры. Но поскольку количество регистров ограничено, и они могут использоваться для других целей, также можно передавать значения параметров через стек или через глобальные переменные. Можно комбинировать различные подходы.
    Если параметров немного, распространённым способом является передача значений в процедуру через регистры. Например, определим простейшую процедуру, которая получает получает извне два числа и складывает их:
    Код (ASM):
    1. .code
    2. ; Процедура sum принимает два параметра
    3. ; RCX - первое число
    4. ; RDX - второе число
    5. sum proc
    6.  mov rax, rcx  ; в RAX копируем число из RCX
    7.  add rax, rdx  ; складываем с числом из RDX
    8.  ret
    9. sum endp
    10. main proc
    11.  mov rcx, 3  ; первый параметр для процедуры sum
    12.  mov rdx, 4  ; второй параметр для процедуры sum
    13.  call sum
    14.  ret
    15. main endp
    16. end
    Здесь мы предполагаем, что два числа в процедуру sum будут передаваться через регистры RCX и RDX. В процедуре sum помещаем в RAX первое число из RCX и складываем его с числом из RDX. В итоге после выполнения этой программы в регистре RAX будет число 7.
    Если мы сами пишем все процедуры программы на ассемблере, то мы можем установить себе свои собственные правила, каким образом, через какие именно регистры будут передаваться параметры в процедуру. Однако если мы задействуем какие-то внешний функционал, например, взаимодействуем с функциями С/С++, то нам придется также применять те условности, которые накладывают эти функции и конкретные ОС. В частности, в при вызове функций C/C++ на Windows надо следовать следующим соглашениям:
    • Вызывающий код передает первые четыре параметра в регистры, а в стеке надо резервировать память для этих параметров.
    • Для передачи в функцию первых четырех параметров (целочисленных) используются регистры RCX, RDX, R8 и R9 соответственно. Если параметры представляют числа с плавающей точкой, то они передаются через регистры XMM0, XMM1, XMM2 и XMM3.
    • Параметры всегда представляют собой 8-байтовые значения.
    • Вызывающий код должен зарезервировать для параметров в стеке как минимум 32 байта, даже если параметров меньше пяти (плюс 8 байтов для каждого дополнительного параметра, если параметров пять или более).
    Передача параметров через стек
    Еще один способ передачи параметров представляет передача через стек:
    Код (ASM):
    1. .code
    2. sum proc
    3.  mov rax, [rsp+24]  ; RAX = 1
    4.  add rax, [rsp+16]  ; RAX = RAX + 2 = 3
    5.  add rax, [rsp+8]  ; RAX = RAX + 3 = 6
    6.  ret
    7. sum endp
    8. main proc
    9.  push 1
    10.  push 2
    11.  push 3
    12.  call sum
    13.  add rsp, 24
    14.  ret
    15. main endp
    16. end
    В функции main процедуре sum через стек передаются три параметра с помощью инструкции push
    Код (ASM):
    1. push 1
    2. push 2
    3. push 3
    Поскольку инструкция push помещает в стек 8-байтные значения, то в данном случае три добавленных числа займут в стеке пространство в 24 байта.
    После вызова процедуры sum это пространство очищается
    Код (ASM):
    1. add rsp, 24
    В процедуре sum для получения параметров используем косвеную адресацию и смещение относительно указателя RSP. При вызовае процедуры sum стек условно будет выглядеть следующим образом:
    [​IMG]
    Регистр RSP будет указывать на ячейку с адресом возврата. Соответственно, чтобы получить тот или иной параметр, нам надо использовать смещение 8, 16 или 24:
    Код (ASM):
    1. mov rax, [rsp+24]  ; RAX = 1
    В итоге после выполнения программы в регистре RAX будет число 6.
    Однако добавление в стек с помощью инструкции push может быть не оптимальным способом. В частности, выделенная память для стека может быть избыточно. В нашем случае для наших трех чисел необязательно выделять 24 байта, можно обойтись гораздо меньшим объемом. В этом случае вызывающий код может вручную помещать значения в нужные области стека, используя регистр RSP и смещения:
    Код (ASM):
    1. .code
    2. sum proc
    3.  xor rax, rax  ; обнуляем регистр
    4.  mov ax, [rsp+14]  ; RAX = 2
    5.  add ax, [rsp+12]  ; RAX = RAX + 4 = 6
    6.  add ax, [rsp+10]  ; RAX = RAX + 6 = 12
    7.  ret
    8. sum endp
    9. main proc
    10.  sub rsp, 8  ; резервируем для параметров 8 байт
    11.  mov ax, 2
    12.  mov [rsp+6], ax
    13.  mov ax, 4
    14.  mov [rsp+4], ax
    15.  mov ax, 6
    16.  mov [rsp+2], ax
    17.  call sum
    18.  add rsp, 8  ; восстанавливаем указатель стека
    19.  ret
    20. main endp
    21. end
    Также в стек помещаются три числа, но теперь они представляют 2-байтные числа. Соответственно нам нужно всего лишь 6 байтов. Однако поскольку стек должен быть выровнен по 8 байтам, и соответственно выделенное пространство должно быть не меньше общего размера помещаемых элементов и также должно быть кратным 8, поэтому выделяем 8 байт, где 2 байта будут незаполненными. Далее помещаем данные через регистр AX в с соответствующую область в стеке:
    Код (ASM):
    1. mov ax, 2
    2. mov [rsp+6], ax
    При получении данных в процедуре sum при установке смещения по прежнему учитываем 8 байтов адреса возврата:
    Код (ASM):
    1. mov ax, [rsp+14]  ; RAX = 2
    Стоит отметить, что при взаимодействии с функциями со сторонними API, например, с функциями C/C++ и API операционных систем есть свои ограничения на работу со стеком. Например, на Windows при работе с C/C++ требуется, чтобы каждый элемент в стеке все равно было равен 8 байтам, даже несмотря на то, что в реальности ему достаточно места. Например:
    Код (ASM):
    1. .code
    2. sum proc
    3.  xor rax, rax  ; обнуляем регистр
    4.  mov ax, [rsp+24]  ; AX = 1
    5.  add ax, [rsp+16]  ; AX = AX + 2 = 3
    6.  add ax, [rsp+8]  ; AX = AX + 3 = 6
    7.  ret
    8. sum endp
    9. main proc
    10.  sub rsp, 24  ; резервируем для параметров 24 байта
    11.  mov ax, 1
    12.  mov [rsp+16], ax
    13.  mov ax, 2
    14.  mov [rsp+8], ax
    15.  mov ax, 3
    16.  mov [rsp], ax
    17.  call sum
    18.  add rsp, 24
    19.  ret
    20. main endp
    21. end
    Здесь также по сути помещаем в стек 2-байтные числа:
    Код (ASM):
    1. mov ax, 1
    2. mov [rsp+16], ax
    В итоге получится, что из 8 байт в стеке только младшие 2 байта будут содержать значимую информацию - число 1. Остальные 6 байт по сути будут содержать случайные значения, условно говоря, "мусор". Но поскольку при получении соответствующего значения из стека мы его получаем также в 2-байтный регистр
    Код (ASM):
    1. mov ax, [rsp+24]  ; AX = 1
    то проблем с интерпретацией значения или получением мусорных значений у нас не будет - мы опять же берем только 2 младших байта в регистр.
    Определение параметров с директивой proc
    В MASM директива proc позволяет сразу при определении процедуры определить и ее параметры в виде:
    имя_процедупы proc параметр1:тип, параметр2:тип, ... параметрN:тип
    После слова proc перечисляются через запятую параметры, где в каждом определении параметра указывается имя параметра и через двоеточие его тип. Параметры могут представлять различных стандартные типа - byte, word, dword и т.д. (массивы не допускаются), однако в стеке для каждого параметра в любом случае необходимо резервировать 8 байт:
    Код (ASM):
    1. .code
    2. sum proc a:byte, b:word, c:dword
    3.  mov eax, c  ; EAX = 8
    4.  add eax, dword ptr b  ; EAX = EAX + 4 = 12
    5.  add eax, dword ptr a  ; EAX = EAX + 2 = 14
    6.  ret
    7. sum endp
    8. main proc
    9.  sub rsp, 24  ; резервируем для параметров 24 байта
    10.  mov eax, 8
    11.  mov [rsp+16], eax  ; параметр c
    12.  mov ax, 4
    13.  mov [rsp+8], ax  ; параметр b
    14.  mov al, 2
    15.  mov [rsp], al  ; параметр a
    16.  call sum
    17.  add rsp, 24
    18.  ret
    19. main endp
    20. end
    Процедура sum принимает три параметра - a, b и c, которые представляют типы byte, word и dword. При этом MASM автоматически связывает определенные значения из стека с этими параметрами. Это довольно удобно, поскольку нам можно не использовать регистр RSP и смещения для получения значений параметров.
    Для передачи значений через стек нам надо учитывать, что для каждого параметра в стеке должно выделяться 8 байт, и что в стеке они должны располагаться в порядке, обратном определению. Так, последний параметр - с, который имеет тип dword. Поэтому вначале в стек помещается значение для этого параметра
    Код (ASM):
    1. mov eax, 8
    2. mov [rsp+16], eax  ; параметр c
    Далее помещаем в стек значение для параметра b, и в конце - значение параметра a.
     
    Последнее редактирование: 30 окт 2026 в 21:04
  7. Mikl___

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

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

    Процедуры

    Результат процедуры

    Нередко процедуры выполняют некоторые вычисления и генерируют некоторый результат этих вычислений. Вполне вероятно, что мы захотим передать этот результат во внешний код. Во многих языках программирования высокого уровня есть механизм возвращения результата функции - обычно для этого применяется оператор return. В ассемблере такого механизма нет. Наиболее удобным местом для возврата результатов процедуры в архитектуре x86-64 являются регистры.
    Как правило, результат процедуры помещается в регистр RAX (или его подрегистры EAX, AX, AL), хотя в реальности, если мы сами пишем всю программу на ассемблере, мы можем использовать для этой цели любой регистр общего назначения. Однако на регистр RAX завязано много соглашений. Так, именно в RAX большинство языков высокого уровня помещают результат функции. А согласно интерфейсу Microsoft ABI при взаимодействии с WinAPI целочисленный результат помещается в регистр RAX, а если результат представляет число с плавающей точкой - то в регистр XMM0.
    Если нужно вернуть результат, размер которого превышает 64 бита (например, 128 бит или 256 бит) и соответственно не помещается в RAX или в другой 64-разрядный регистр общего назначения, то можно разделить результат на части и вернуть эти части в двух или более регистрах. Часто можно увидеть, как функции возвращают 128-битные значения в паре регистров RDX:RAX. Конечно, регистры XMM/YMM — еще одно хорошее место для возврата больших значений.
    Если надо вернуть большой объект, например, массив из 1000 элементов, то, очевидно, регистры не подходят. И в этом случае можно вместо значения возвратить его адрес (который занимает 8 байт).
    Простейший пример возвращения результата:
    Код (ASM):
    1. .code
    2. ; Процедура sum выполняет сложение двух чисел
    3. ; RCX - первое число
    4. ; RDX - второе число
    5. ; RAX - результат сложения
    6. sum proc
    7.  mov rax, rcx
    8.  add rax, rdx
    9.  ret
    10. sum endp
    11. main proc
    12.  ; устанавливаем параметры для процедуры sum
    13.  mov rcx, 5
    14.  mov rdx, 6
    15.  call sum  ; после вызова в RAX - результат сложения
    16.  add rax, 4  ; далее мы можем использовать этот результат
    17.  ret
    18. main endp
    19. end
    Здесь процедура sum принимает через регистры RCX и RDX два параметра - два числа, складывает их и помещает результат в регистр RAX. После вызова процедуры sum мы можем использовать полученный результат в регистре RAX каким-нибудь образом, например, прибавить к нему еще число.
    Другой пример найдем длину массива и поместим результат в RAX:
    Код (ASM):
    1. .data
    2.  nums1 word 10, 20, 30, 15, 15
    3.  nums1Len = $-nums1
    4.  numSize = 2  ; размер типа word
    5. .code
    6. ; Процедура sum выполняет сложение чисел массива
    7. ; RCX - адрес массива
    8. ; RDX - длина массива - количество элементов
    9. ; RAX - результат сложения
    10. sum proc
    11.  mov rdi, 0
    12.  xor rax, rax  ; обнуляем RAX
    13. while_begin:
    14.  cmp rdi, rdx  ; сравниваем количество обработанных элементов
    15.  je while_end  ; если все элементы обработали, выходим из цикла
    16.  add ax, word ptr [rcx + rdi*numSize]  ; складываем значение по адресу [rcx + rdi*numSize]
    17.  inc rdi  ; инкрементируем счетчик обработанных элементов
    18.  jmp while_begin ; переходим к обработке нового элемента массива
    19. while_end:
    20.  ret
    21. sum endp
    22. main proc
    23.  ; устанавливаем параметры для процедуры sum
    24.  lea rcx, nums1  ; в RCX адрес массива
    25.  mov rdx, nums1Len / numSize  ; в RDX - количество элементов массива
    26.  call sum  ; после вызова в RAX - результат сложения
    27.  ret
    28. main endp
    29. end
    Здесь функция sum принимает адрес массива и количество элементов в массиве. В цикле проходит пор всем элементам массива, используя косвенную адресацию и счетчик элементов - регистр RDI и помещает в регистр RAX сумму элементов массива.
     
    Последнее редактирование: 30 окт 2026 в 21:08
  8. Mikl___

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

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

    Процедуры

    Фрейм стека и локальные переменные

    При вызове процедура имеет доступ ко всему пространству стека, выделенному в программе. Тем не менее каждая процедура имеет свою какую-то специфичную информацию, включая адрес возврата, параметры и локальные переменные. И чтобы разграничить данные каждой отдельной процедуры в рамках стека для процедуры выделяется область стека, которая еще называется фреймом стека (stack frame).
    В качестве указателя на базовый адрес фрейма стека в архитектуре x86-64 предназначен регистр RBP (BP - base pointer или базовый указатель).
    Создание фрейма стека начинается в коде, который вызывает процедуру. Вызывающий код может передавать через стек значения для параметров. Вне зависимости, передаются ли параметры, инструкция call помещает адрес возврата в стек.
    Далее создание фрейма стека продолжается внутри самой процедуры. Что мы можем добавить в этот фрейм в самой процедуре? Во многих языках программирования высокого уровня есть локальные переменные, которые доступны только в рамках какой-то определенной функции или блока кода. В ассемблере нет локальных переменных, все переменные, которые определяются в секции .data и других секциях, являются глобальными, доступны из любой части программы на ассемблере. Но использование стека позволяет определить данные, которые будут применяться только в данной процедуре. То есть такие данные будут локальны по отношению к процедуре. При завершении процедуры в ней очищается данная область стека, и соответственно данные удаляются и недоступны для других процедур. Конечно, для каких-то локальных данных мы могли бы использовать и регистры, но однако данных может быть очень много, для всего регистров может не хватить. Кроме того, немногочисленные регистры лучше использовать для вычислений.
    Рассмотрим простейший пример:
    Код (ASM):
    1. .code
    2. sum proc
    3.  ; добавляем в стек число 5 - условная безымянная локальная переменная
    4.  push 5  ; RSP указывает на адрес числа 5
    5.  mov rax, rcx  ; в RAX значение параметра из RCX
    6.  add rax, [rsp]  ; RAX = RAX + 5
    7.  add rsp, 8  ; особождаем стек
    8.  ret
    9. sum endp
    10. main proc
    11.  mov rcx, 11 ; устанавливаем параметр для процедуры sum
    12.  call sum  ; после вызова в RAX - результат сложения
    13.  ret
    14. main endp
    15. end
    Здесь процедура sum выполняет сложение двух чисел. Первое число передается через параметр - регистр RCX
    Второе число мы сохраняем в стек в самой процедуре:
    push 5
    Пусть это будет число 5. Поскольку добавляем с помощью инструкции push, то для числа в стеке будет выделено 8 байт. После этого указатель стека RSP будет указвать на адрес, где хранится это число. Соответственно адрес возврата в стеке будет находиться по адресу [RSP+8].
    В самой процедуре sum через адрес [rsp] складываем со значением регистра-параметра RCX, а результат помещается в регистр RAX. При этом я отмечу, что в данном случае идет речь не просто о константном числе 5, а именно о локальной переменной - некоторые данные, которые существуют во время работы процедуры и которые могут изменяться. Например, мы можем увеличить эту переменную:
    Код (ASM):
    1. .code
    2. sum proc
    3.  push 5  ; По адресу [rsp] локальная переменная, которая равна 5
    4.  add byte ptr [rsp], 2  ; увеличиваем переменную на 2 - она равна 7
    5.  mov rax, rcx  ; в RAX значение параметра из RCX
    6.  add rax, [rsp]  ; RAX = RAX + 7
    7.  add rsp, 8  ; особождаем стек
    8.  ret
    9. sum endp
    10. main proc
    11.  mov rcx, 11
    12.  call sum  ; RAX = 18
    13.  ret
    14. main endp
    15. end
    Здесь увеличиваем условную локальную переменную на 2:
    add byte ptr [rsp], 2
    В данном случае рассматриваем ее как значение типа byte, так как число 5 вполне укладывается в диапазон типа byte.
    Нередко значения параметров, которые передаются через регистры, также помещаются в локальные переменные. Благодаря этому мы сможем высвободить регистры для вычислений:
    Код (ASM):
    1. .code
    2. sum proc
    3.  sub rsp, 8  ; выделяем для двух переменных в стеке 8 байт
    4.  mov dword ptr [rsp], 5  ; По адресу [rsp] первая локальная переменная, которая равна 5
    5.  mov dword ptr [rsp+4], ecx  ; По адресу [rsp+4] вторая локальная переменная, которая равна ECX
    6.  mov eax, [rsp]  ; в EAX значение первой переменной
    7.  add eax, [rsp+4]  ; EAX = EAX + вторая переменная
    8.  add rsp, 8  ; особождаем стек
    9.  ret
    10. sum endp
    11. main proc
    12.  mov rcx, 11
    13.  call sum  ; RAX = 16
    14.  ret
    15. main endp
    16. end
    Для наглядности вместо инструкции push данные добавляются посредством смещений, что позволит выделить меньшую область стека для данных. В частности, предполагаем, что обе переменных будут представлять 4-байтные числа (dword), соответственно нам хватит 8 байт. И сначала выделяем эту область
    sub rsp, 8
    Потом помещаем в нее данные с помощью смещений. Учитываем, что поскольку мы не применяем инструкцию push, по умолчанию RSP указывает на адрес возврата. Чтобы обратиться к определенной области, прибавляем к адресу в RSP 0 и 4 (так как наши числа занимают 4 байта):
    mov dword ptr [rsp], 5
    mov dword ptr [rsp+4], ecx

    Затем при операциях с переменными можно использовать смещения относительно RSP:
    Код (ASM):
    1. mov eax, [rsp]
    2. add eax, [rsp+4]
    Установка имен переменных
    Выше обе наших локальных переменных были безымянными. Для нас фактически они существуют лишь как смещения относительно указателя стека RSP. Однако манипулировать смещения не очень удобно, в процессе написания программы мы можем перепутать спещения. Но с помощью констант мы можем им назначить переменным определенные имена:
    Код (ASM):
    1. _a equ [rsp]
    2. _b equ [rsp+4]
    3. .code
    4. sum proc
    5.  sub rsp, 8
    6.  mov dword ptr _a, 6  ; переменная по адресу [rsp]
    7.  mov dword ptr _b, ecx  ; переменная по адресу [rsp+4]
    8.  mov eax, _a
    9.  add eax, _b
    10.  add rsp, 8
    11.  ret
    12. sum endp
    13. main proc
    14.  mov rcx, 11
    15.  call sum  ; RAX = 17
    16.  ret
    17. main endp
    18. end
    Здесь определены константы для смещений относительно стека под названием _a и _b:
    Код (ASM):
    1. _a equ [rsp]
    2. _b equ [rsp+4]
    Названия переменных произвольные (главное, чтобы они не были зарезервированными словами в ассемблере). В итоге везде, где в программе встретятся эти константы, фактически будут применяться адреса [rsp] и [rsp+4]:
    Код (ASM):
    1. mov dword ptr _a, 6
    2. ....................
    3. mov eax, _a
    Регистр RBP
    Для управления доступом к различным частям фрейма стека Intel предоставляет специальный регистр - RBP (Base Pointer). А для доступа к объектам во фрейме стека можно использовать смещение до нужного объекта относительно адреса из регистра RBP.
    Вызывающий процедуру отвечает за выделение памяти для параметров в стеке и перемещение данных параметра в соответствующее место. Инструкция call помещает адрес возврата в стек. Процедура несет ответственность за создание остальной части фрейма, в частности, за добавление локальных переменных. Для этого при вызове процедуры значение RBP помещается в стек (поскольку при вызове процедуры в RBP значение вызывающего кода, и это значение надо сохранить), а значение указателя стека RSP копируется в RBP. Затем в стеке освобождается место для локальных переменных.
    push rbp ; срхраняем старое значение регистра RBP
    mov rbp, rsp ; помещаем указатель стека RSP в регистр RBP
    sub rsp, пространство_для_локальных переменны

    Локальные переменные не должны быть кратны 8 байтам, однако весь блок локальных переменных должен иметь размер, кратный 8 байтам, чтобы RSP оставался выровненным по 8-байтовой границе (при взаимодействии с функциями C/C++ в Windows согласно Microsoft ABI блок локальных переменных должен быть кратен 16 байтам). Локальные переменные занимают пространство, равное их собственному размеру (символы - 1 байт, слова word - 2 байта и т.д.).
    Прежде чем процедура возвратит управление в вызывающий код, ей необходимо очистить фрейм стека. Если процедура не имеет параметров, последовательность выхода проста. Для этого требуется всего три инструкции:
    mov rsp, rbp ; удаляем локальные переменные и очищаем стек
    pop rbp ; восстанавливаем в RBP значение для вызывающего кода
    ret ; возвращаемся в вызывающий код

    Место для параметров обычно очищается вызывающим кодом. Однако в принципе возможна также очистка памяти параметров и в вызванной процедуре. Для этого оператору ret передается количество байтов стека, которые надо освободить:
    mov rsp, rbp ; удаляем локальные переменные и очищаем стек
    pop rbp ; восстанавливаем в RBP значение для вызывающего кода
    ret N_байтов ; возвращаемся в вызывающий код и очищаем N байтов в стеке, где лежат параметры

    Также Intel представляет специальную инструкцию - leave, которая копирует значение RBP в RSP и извлекает ранее сохраненное значение регистра RBP.
    leave
    ret N_байтов ; возвращаемся в вызывающий код и очищаем N байтов в стеке, где лежат параметры

    Для доступа к объектам во фрейме стека необходимо использовать смещение до нужного объекта относительно адреса из регистра RBP. Для обращения к параметрам применяется положительное смещение относительно значения регистра RBP; для доступа к локальным переменным - отрицательное смещение. Следует с осторожностью использовать регистр RBP для общих расчетов, потому что если вы произвольно измените значение в регистре RBP, вы можете потерять доступ к параметрам текущей процедуры и локальным переменным.
    Рассмотрим простейший пример:
    Код (ASM):
    1. sum proc
    2.  push rbp
    3.  mov rbp, rsp
    4.  sub rsp, 8  ; выделяем место для двух переменных
    5.  mov dword ptr[rbp-4], 7  ; По адресу [rsp-4] первая локальная переменная, равная 7
    6.  mov [rbp-8], ecx  ; По адресу [rsp-8] вторая локальная переменная, равная ECX
    7.  mov eax, [rbp-4]  ; в EAX значение переменной из [rbp-4]
    8.  add eax, [rbp-8]  ; EAX = EAX + [rbp-8]
    9.  mov rsp, rbp  ; освобождаем стек
    10.  pop rbp  ; восстанавливем RBP
    11.  ret
    12. sum endp
    13. main proc
    14.  mov rcx, 12
    15.  call sum  ; RAX = 19
    16.  ret
    17. main endp
    18. end
    Опять же у нас функция sum использует две локальных переменных. Пусть обе переменных будут представлять тип dword, соответственно для них нужно в совокупности 8 байт:
    Код (ASM):
    1. push rbp
    2. mov rbp, rsp
    3. sub rsp, 8
    Затем определяем значения локальных переменных в стеке, используя смещение относительно RBP:
    Код (ASM):
    1. mov dword ptr [rbp-4], 7
    2. mov [rbp-8], ecx
    То есть первая локальная переменная, которая равна 7, будет располагаться в стеке по адресу [rbp-4], а вторая переменная, которая получает значение из ECX - по адресу [rbp-8].
    Далее для обращения к этим переменным применяется эти же адреса:
    Код (ASM):
    1. mov eax, [rbp-4]
    2. add eax, [rbp-8]
    В данном случае мы опять же могли бы использовать константы для именования переменных:
    Код (ASM):
    1. _a equ [rbp-4]
    2. _b equ [rbp-8]
    3. .code
    4. sum proc
    5.  push rbp
    6.  mov rbp, rsp
    7.  sub rsp, 8  ; выделяем место для двух переменных
    8.  mov dword ptr _a, 7  ; локальная переменная _a равна 7
    9.  mov _b, ecx  ; локальная переменная _b равна ECX
    10.  mov eax, _a  ; в EAX значение переменной _a
    11.  add eax, _b  ; EAX = EAX + _b
    12.  mov rsp, rbp  ; освобождаем стек
    13.  pop rbp  ; восстанавливем RBP
    14.  ret
    15. sum endp
    16. main proc
    17.  mov rcx, 12
    18.  call sum  ; RAX = 19
    19.  ret
    20. main endp
    21. end
    Директива local
    Для упрощения создания локальных переменных MASM предоставляет специальную директиву local, которая имеет следующий синтаксис:
    local переменная1: тип, переменная2: тип, ... переменнаяN: тип
    После директивы local указываются определения переменных, где для каждой переменной указывается имя и через двоеточие ее тип.
    Директива local должна идти в процедуре сразу же после объявления процедуры до остальных инструкций. Процедура может иметь более одной директивы local. ; если имеется более одной локальной директивы, все они должны появиться вместе после объявления proc. Пример использования директивы локальных переменных:
    Код (ASM):
    1. .code
    2. sum proc
    3.  local _a:word, _b:dword  ; определяем две переменных
    4.  push rbp
    5.  mov rbp, rsp
    6.  sub rsp, 8  ; выделяем место для двух переменных
    7.  mov _a, 5
    8.  mov _b, ecx
    9.  movzx eax, _a
    10.  add eax, _b
    11.  mov rsp, rbp  ; освобождаем стек
    12.  pop rbp  ; восстанавливем RBP
    13.  ret
    14. sum endp
    15. main proc
    16.  mov rcx, 22
    17.  call sum  ; RAX = 27
    18.  ret
    19. main endp
    20. end
    Здесь определяются две переменных - _a и _b, которые представляют типы word и dword. MASM автоматически связывает соответствующие смещения с каждой переменной из директивы local. MASM назначает смещения переменным, вычитая размер переменной из текущего смещения (начиная с нуля), а затем округляя до смещения, кратного размеру объекта. При этом все равно надо выделять в стеке место для хранения локальных переменных.
    Поскольку установка значения для регистра RBP, выделение места в стеке, а затем освобождение места и восстановление RBP представляют рутинную работу, которая может повторяться из процедуры в процедуру, то для упрощения управления стеком MASM позволяет сократить код, используя директиву option:
    option prologue:PrologueDef
    option epilogue:EpilogueDef

    Данные выражения ставится перед процедурой. В итоге MASM будет автоматически выполнять все действия по выделению и освобождению места в стеке и сохранению/восстановлению регистра RBP:
    Код (ASM):
    1. .code
    2. option prologue:PrologueDef
    3. option epilogue:EpilogueDef  ; вручную выделять память больше не надо
    4. sum proc
    5.  local _a:word, _b:dword
    6.  mov _a, 6
    7.  mov _b, ecx
    8.  movzx eax, _a
    9.  add eax, _b
    10.  ret
    11. sum endp
    12. main proc
    13.  mov rcx, 22
    14.  call sum  ; RAX = 28
    15.  ret
    16. main endp
    17. end
     
    Последнее редактирование: 30 окт 2026 в 21:30
  9. Mikl___

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

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

    Процедуры

    Указатель на процедуру

    Архитектура x86-64 допускает три основные формы вызова процедуры: через имя процедуры, путем косвенной адресации через 64-битный регистр общего назначения и через переменную-указатель
    Код (ASM):
    1. call имя_процедуры  ; прямой вызов
    2. call регистр  ; косвенный вызов через регистр
    3. call указатель  ; косвенный вызов через указатель-адрес процедуры
    При вызове процедуры через регистр процессор x86-64 сначала помещает адрес возврата в стек, а затем начинает выборку следующего байта кода операции (инструкции) с адреса, указанного значением в регистре. При этом регистр должен быть 64-разрядным. Третья форма инструкции call извлекает адрес первой инструкции процедуры из переменной-указателя. Вычислить адрес процедуры можно либо с помощью оператора offset, либо с помощью инструкции lea.
    Пример вызова через регистр:
    Код (ASM):
    1. .code
    2. sum proc
    3.  mov rax, rcx
    4.  add rax, rdx
    5.  ret
    6. sum endp
    7. main proc
    8.  mov rcx, 22
    9.  mov rdx, 24
    10.  mov rax, offset sum  ; в RAX помещаем адрес процедуры sum
    11.  call rax  ; вызываем процедуру sum
    12.  ret
    13. main endp
    14. end
    Здесь с помощью оператора offset получаем адрес процедуры sum и загружаем его в регистр RAX. Затем для вызова процедуры передаем регистр RAX инструкции call. Также можно было бы использовать инструкцию lea
    Код (ASM):
    1. lea rax, sum  ; загружаем адрес процедуры sum
    При сохранении адреса в переменную она должна представлять тип qword:
    Код (ASM):
    1. .data
    2. ptrToSum qword offset sum  ; указатель содержит адрес процедуры sum
    3. .code
    4. sum proc
    5.  mov rax, rcx
    6.  add rax, rdx
    7.  ret
    8. sum endp
    9. main proc
    10.  mov rcx, 22
    11.  mov rdx, 25
    12.  call ptrToSum  ; вызываем процедуру через указатель
    13.  ret
    14. main endp
    15. end
    Здесь получаем адрес сразу при определении переменной-указателя. Но можно также динамически получать адрес процедуры в программе:
    Код (ASM):
    1. lea rax, sum
    2. mov ptrToSum, rax
     
    Последнее редактирование: 30 окт 2026 в 21:39
  10. Mikl___

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

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

    Организация программы

    Вставка кода и директива include

    Директива include позволяет вставить код из определенного файла в другой файл. В качестве операнда она принимает имя файла, код которого будет вставляться:
    Строковые инструкции используют определенные регистры в качестве операндов:
    include имя_файла
    В качестве файла может выступать любой файл с кодом на ассемблере. Например, пусть у нас есть файл sum.asm со следующим кодом:
    Код (ASM):
    1. ; через RCX и RDX передаются параметры
    2. ; через RAX возвращается результат
    3. sum proc
    4.  mov rax, rcx
    5.  add rax, rdx
    6.  ret
    7. sum endp
    Здесь определена процедура sum, которая через регистры RCX и RDX принимает два числа, складывает их и результат помещает в регистр RAX.
    Подключим файл sum.asm в главный файл программы, который располагается в той же папке и который пусть называется main.asm:
    Код (ASM):
    1. .code
    2. include sum.asm ; подключаем код из файла sum.asm
    3. main proc
    4.  mov rcx, 6
    5.  mov rdx, 2
    6.  call sum
    7.  ret
    8. main endp
    9. end
    В данном случае на место:
    include sum.asm
    при компиляции будет вставляться код из файла sum.asm. Компиляция проходит также как и в общем случае. Например, в 64-рязном MASM с помощью команды:
    ml64 main.asm /link /entry:main
    Таким образом, благодаря директиве include мы можем вынести некоторые части программы, например, процедуры, во внешние файлы и при необходимости подключать их. Причем подключать в разные файлы с кодом на ассемблере.
    Управление дублированием кода
    Хотя директива include довольно удобна, но по мере роста и развития программы из-за этой директивы может возникнуть путаница, особенно когда подключается множество файлов, которые, в свою очередь, подключают другие файлы. Если один код подключается больше одно раза, это приведет к ошибке компиляции, например:
    Код (ASM):
    1. .code
    2. include sum.asm ; подключаем код из файла sum.asm
    3. ; некоторый код
    4. include sum.asm ; повторно подключаем код из файла sum.asm
    5. main proc
    6.  mov rcx, 6
    7.  mov rdx, 2
    8.  call sum
    9.  ret
    10. main endp
    11. end
    Решением в данном случае может быть использование оператора ifndef:
    Код (ASM):
    1. .code
    2. ifndef sum_asm
    3.  sum_asm = 0
    4.  include sum.asm ; подключаем код из файла sum.asm
    5. endif
    6. ; некоторый код
    7. ifndef sum_asm
    8.  sum_asm = 0
    9.  include sum.asm ; подключаем код из файла sum.asm
    10. endif
    11. main proc
    12.  mov rcx, 6
    13.  mov rdx, 3
    14.  call sum
    15.  ret
    16. main endp
    17. end
    Оператор условной компиляции ifndef определяет условную конструкцию, внутри которой подключается файл. В качестве операнда оператору ifndef передается некоторая переменная. Если она не определена, то устанавливаем ее значение (например, присваиваем число 0) и подключаем нужный файл. Если же данная переменная уже установлена (то есть файл ранее уже подключен), то повторного подключения файла не будет.
     
    Последнее редактирование: 30 окт 2026 в 21:47
  11. Mikl___

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

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

    Организация программы

    Разбиение программы на модули и подключение внешних модулей

    При сборке программы ассемблер создает из файла программы и других файлов, которые он включает, один модуль - объектный файл с расширением .obj. Затем компоновщик берет несколько объектных файлов (созданных MASM или другими компиляторами) и объединяет их в один исполняемый файл (файл .exe). Это позволяет выносить в отдельные файлы различные компоненты программы и компилировать их независимо дург от друга и при необходимости подключать в другие программы. Например, мы можем определить процедуру в отдельном файле, скомпилировать ее в объектный файл и потом подключать при необходимости в другие программы. Причем подобным образом мы можем подключать объектные файлы, скомпилированный из кода на C/C++ и других языков программирования.
    Для подключения компонентов из внешних модулей MASM имеет две директивы: extern и externdef:
    extern идентификатор:тип
    externdef идентификатор:тип

    После директивы идет идентификатор - название процедуры/функции или какого-то объекта. И через двоеточие указывается тип идентифкатора. Можно использовать следующие типы:
    • proc (указывает, что идентификатор - название процедуры/функции или метки)
    • любые встроенные типы MASM (например, byte, word, dword, qword, oword и т.д.)
    • любые пользовательские типы (например, имя структуры)
    • имя константы
    Рассмотрим на примере. Пусть у нас есть файл sum.asm, в котором определена простейшая процедура sum:
    Код (ASM):
    1. .code
    2. ; через RCX и RDX передаются параметры
    3. ; через RAX возвращается результат
    4. sum proc
    5.  mov rax, rcx
    6.  add rax, rdx
    7.  ret
    8. sum endp
    9. end
    Данная процедура определена в секции .code. Она принимает два числа через регистры RCX и RDX и результат их сложения помещает в регистр RAX.
    Пусть в той же папке располагается главный файл программы main.asm со следующим кодом:
    Код (ASM):
    1. extern sum:proc  ; подключаем внешнюю процедуру sum
    2. .code
    3. main proc
    4.  mov rcx, 6
    5.  mov rdx, 1
    6.  call sum
    7.  ret
    8. main endp
    9. end
    Здесь первой строкой кода подключаем внешнюю процедуру sum, для чего директиве extern передается имя процедуры (sum) и тип proc. Благодаря этому при в процедуре main можно использовать процедуру sum для вычисления суммы чисел.
    Чтобы скомпилировать файл программы, нам надо оба файла - и sum.asm, и main.asm скомпилировать в объектные файлы и затем их объединить в исполняемый файл. В MASM для этого можно применить команду:
    ml64 main.asm sum.asm /link /entry:main

    Данная команда сначала сгенерирует два объектных файла - sum.obj и main.obj и затем скомпонует их в один файл main.exe.
    Также можно выполнить компиляцию по отдельности. Например, сначала скомпилируем файл sum.asm с помощью команды:
    ml64 /c sum.asm

    В результате будет создан объектный файл sum.obj.
    Затем для компиляции главного файла main.asm и сборки всей программы выполним команду:
    ml64 main.asm sum.obj /link /entry:main

    Подобным образом вместо sum.obj можно подключать любые другие объектные файлы, в том числе сгенерированные по коду на C/C++.
    Но стоит отметить, что объектные файлы влияют на итоговый размер программы: при компоновке исполняемого файла компоновщик MASM берет весь код из объектного файла и добавляет его в выходной исполняемый файл. То есть есть объектный файл содержит 100 процедур, но вы используете в программе только одну из них, то все равно код всех 100 процедур будет добавляться в итоговый исполняемый файл.
    public
    Чтобы компонент был виден вне своего модуля, при его определении применяется директива public, которой передается имя компонента:
    Код (ASM):
    1. .code
    2. public sum  ; функция sum общедоступна
    3. sum proc
    4.  mov rax, rcx
    5.  add rax, rdx
    6.  ret
    7. sum endp
    8. end
    Подключение переменных
    Как и процедуры, можно подключать и другие компоненты из внешних модулей, например, переменные. Пусть у нас есть следующий файл numbers.asm:
    Код (ASM):
    1. .data
    2. public num1
    3. num1 dword 32
    4. public num2
    5. num2 word 16
    6. public num3
    7. num3 byte 8
    8. end
    Для примера здесь определены три переменных, причем для каждой из них применяется директива public, чтобы переменная была видна вне файла.
    В файле main.asm подключим и используем эти переменные:
    Код (ASM):
    1. extern num1:dword, num2:word, num3:byte
    2. .code
    3. main proc
    4.  mov eax, num1
    5.  ret
    6. main endp
    7. end
    При подключении внешних компонентов мы можем перечислить их через запятую после директивы extern. Хотя также мы могли бы их определить по отдельности:
    Код (ASM):
    1. extern num1:dword
    2. extern num2:word
    3. extern num3:byte
    externdef
    Директива externdef ведет себя как директива extern, когда подключаемый компонент отсутствует в исходном файле, и ведет себя как директива public, когда компонент определен в исходном файле. При этом в одном исходном файле может быть несколько объявлений externdef для одного и того же символа. Например:
    Код (ASM):
    1. externdef sum:proc
    2. .code
    3. sum proc
    4.  mov rax, rcx
    5.  add rax, rdx
    6.  ret
    7. sum endp
    8. main proc
    9.  mov rdx, 1
    10.  mov rcx, 2
    11.  call sum
    12.  ret
    13. main endp
    14. end
    В данном случае функция sum определена непосредственно в текущем файле, поэтому применение директивы externdef фактически будет означать, что к процедуре sum применяется директива public, и процедура sum поэтому видна вне своего модуля.
    Какой в этом смысл? Дело в том, что если проект содержит множество файлов на ассемблере, и мы захотим во все эти файлы подключить все используемые функции, то мы можем выделить все внешние подключения в один заголовочный файл и подключать его во все файлы с помощью директивы include. И чтобы избежать ошибки, когда какой-то компонент будет подключаться в тот же файл, где он и определен, как раз и применяется директива externdef
     
    Последнее редактирование: 30 окт 2026 в 22:45
  12. Mikl___

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

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

    Организация программы

    Библиотеки

    При сборке программы, если она использует множество объектных файлов, то код всех этих файлов добавляется его в выходной исполняемый файл. То есть есть объектный файл содержит 100 процедур, но в программе используется только одна из них, то все равно код всех 100 процедур будет добавляться в итоговый исполняемый файл. В качестве варианта можно создать определить каждую процедуру в отдельном файле и по отдельности скомпилировать. В итоге для 100 процедур получится 100 объектных файлов. И эти файлы при необходимости подключать при сборке программы. Однако управлять таким количеством файлов не очень удобно. И MASM предоставляет другое более удобное решение в виде библиотек.
    Библиотека по сути представляет хранилище объектых файлов. Обычно библиотека представляет файл с расширением .lib, который можно использовать при компоновке программы также, как и обычные объектные файлы. Когда компоновщик обрабатывает файл библиотеки, он выберет единственный объектный файл с процедурой, которую использует программа, и включит только этот объектный файл в окончательный исполняемый файл. Как правило, это намного эффективнее, чем компоновка одного объектного файла со 100 процедурами.
    Компиляция библиотеки
    Для компиляции библиотеки в MASM применяется программа Microsoft Library Manager - lib.exe, которому передается команда
    lib /out:имя_файла.lib список_объектных_файлов

    Допустим, мы хотим создать библиотеку для работы с 128-разрядными числами. Пусть у нас будет файл sum128.asm, в котором определена процедура сложения двух 128-разрядных чисел:
    Код (ASM):
    1. .code
    2. ; в RAX:RDX -первое число
    3. ; в RBX:RCX - второе число
    4. ; через RAX:RDX возвращается результат
    5. sum128 proc
    6.  add rax, rbx
    7.  adc rdx, rcx
    8.  ret
    9. sum128 endp
    10. end
    Здесь через регистры RAX и RBX передаются младшие 64 бита двух чисел. После их сложения складываются старшие 64 бита двух чисел, которые передаются через регистры RDX и RCX, плюс знак переноса. Результат сложения - младшие 64 бит в регистре RAX, а старшие 64 бит в регистре RDX.
    Также определим файл sub128.asm, который будет содержать процедуру для вычитания 128-разрядных чисел:
    Код (ASM):
    1. .code
    2. ; в RAX:RDX -первое число
    3. ; в RBX:RCX - второе число
    4. ; через RAX:RDX возвращается результат
    5. sub128 proc
    6.  sub rax, rbx
    7.  sbb rdx, rcx
    8.  ret
    9. sub128 endp
    10. end
    Для создания библиотеки перейдем в консоли к папке с файлами и выполним команду:
    ml64 /c sum128.asm sub128.asm

    Эта команда скомпилирует объектные файлы sum128.obj и sub128.obj.
    Далее сформируем из этих файлов библиотеку и для этого выполним команду:
    lib /out:math128.lib sum128.obj sub128.obj

    После выполнения этой команды в текущей папке появится файл math128.lib - собственной файл библиотеки.
    Подключение библиотеки
    Теперь подключим выше созданную библиотеку. Допустим, главный файл программы называется main.asm, и он использует процедуру sum128 для сложения двух 128-разрядных чисел:
    Код (ASM):
    1. externdef sum128:proc  ; подключаем внешнюю процедуру sum128
    2. .data
    3.  num1 qword 0ffffffffffffffffh,  ; младшие 64 байт
    4.  00000000000000002h  ; старшие 64 байт
    5.  num2 qword 00000000000000007h,  ; младшие 64 байт
    6.  0h  ; старшие 64 байт
    7. .code
    8. main proc
    9.  mov rax, num1
    10.  mov rdx, num1[64]
    11.  mov rbx, num2
    12.  mov rcx, num2[64]
    13.  call sum128
    14.  ret
    15. main endp
    16. end
    Код в данном файле использует внешнюю процедуру sum128 для сложения двух 128-разрядных чисел num1 и num2. Для компиляции и сборки этой программы с использованием библиотеки выполним следующую команду:
    ml64 main.asm math128.lib /link /entry:main

    После этого будет сгенерирован файл main.exe, который будет использовать функционал из объектного файла sum128.obj.
    Директива includelib
    Директива includelib позволяет подключить функционал библиотеки непосредственно в файле кода на ассемблере. Например, изменим файл main.asm следующим образом:
    Код (ASM):
    1. includelib math128.lib  ; подключаем библиотеку math128.lib
    2. externdef sum128:proc  ; подключаем внешнюю процедуру sum128
    3. .data
    4.  num1 qword 0ffffffffffffffffh,  ; младшие 64 байт
    5.  00000000000000002h  ; старшие 64 байт
    6.  num2 qword 00000000000000006h,  ; младшие 64 байт
    7.  0h  ; старшие 64 байт
    8. .code
    9. main proc
    10.  mov rax, num1
    11.  mov rdx, num1[64]
    12.  mov rbx, num2
    13.  mov rcx, num2[64]
    14.  call sum128
    15.  ret
    16. main endp
    17. end
    Первой строкой подключается библиотека math128.lib. В этом случае при компиляции и компоновке программы можно не указывать библиотеку:
    ml64 main.asm /link /entry:main
     
    Последнее редактирование: 30 окт 2026 в 22:55
  13. Mikl___

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

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

    FPU, SSE и числа с плавающей точкой

    Регистры FPU

     
    Последнее редактирование: 30 окт 2026 в 22:58
  14. Mikl___

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

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

    FPU, SSE и числа с плавающей точкой

    Числа с плавающей точкой

     
  15. Mikl___

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

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

    FPU, SSE и числа с плавающей точкой

    Загрузка данных и преобразование чисел в FPU

     
  16. Mikl___

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

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

    FPU, SSE и числа с плавающей точкой

    Сложение чисел с плавающей точкой в FPU

     
  17. Mikl___

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

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

    FPU, SSE и числа с плавающей точкой

    Вычитание чисел с плавающей точкой в FPU

     
  18. Mikl___

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

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

    FPU, SSE и числа с плавающей точкой

    Умножение чисел с плавающей точкой в FPU

     
  19. Mikl___

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

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

    FPU, SSE и числа с плавающей точкой

    Деление чисел с плавающей точкой в FPU

     
  20. Mikl___

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

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

    FPU, SSE и числа с плавающей точкой

    Дополнительные математические инструкции FPU