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

Последнее обновление: 02.07.2023

Для деления чисел в ассемблере мы можем использовать инструкции div и idiv. Причем эти операции позволяют разделить максимум 128-битное значение, которое помещается в RDX:RAX, а делителем в этом случае является 64-битный операнд. После деления частное помещается в RAX, а остаток - в RDX. Однако в этом случае результат деления должен умещаться в 64 бита, что естественно происходит не всегда (например, любое число больше 64 бит разделить на 1).

Один из методов для решения этой задачи состоит в делении старших 32 битов числа (дополненного нулем или знаком) на делитель, затем делится остаток и младшие 32 бита:

.data
    op1 oword 100000000000000006h ; делимое
    op2 qword 2                     ; делитель
    result qword 2 dup (?)
    remainder qword ?       ; для остатка
.code
main proc

    mov rax, qword ptr op1[8]
    xor edx, edx    ; дополнение нулем для беззнакового деления
    div op2
    mov result[8], rax ; сохраняем в старшие 64 байта
    mov rax, qword ptr op1[0]    
    div op2             ; делим остаток и младшие 64 бита
    mov result[0], rax  ; сохраняем в младшие 64 байта
    mov remainder, rdx ; сохраняем остаток
    ret
main endp
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-разрядное:

.data
    op1 oword 2222eeeeccccaaaa8888666644440000h
        oword 2222eeeeccccaaaa8888666644440000h ; делимое - 256 бит
    op2 qword 2                     ; делитель
    result qword 2 dup (?)
    remainder qword ?       ; для остатка
.code
main proc

    mov rax, qword ptr op1[24]    ; старшие 64 бита в RAX
    xor rdx, rdx        ; расширение нулями для беззнакового деления
    div op2             ; делим старшие 64 бита
    mov result[24], rax ; сохраняем старшие 64 бита результата
 
    mov rax, qword ptr op1[16]    ; следующие 64 бита
    div op2 
    mov result[16], rax ; сохраняем вторую порцию 64 бит
 
    mov rax, qword ptr op1[8]
    div op2 
    mov result[8], rax

    mov rax, qword ptr op1[0]     ; младшие 64 бита
    div op2 
    mov result[0], rax  ; сохраняем младшие 64 бита
    mov remainder, rdx  ; сохраняем остаток
    
    ret
main endp
end

Стоит отметить, что данный алгоритм деления работает только для беззнаковых операндов. Чтобы разделить два числа со знаком, надо зафиксировать их знаки, взять их абсолютные значения, выполнить беззнаковое деление, а затем установить знак результата на основе знаков операндов.

Делитель произвольного размера

При мере выше мы могли реализовать делимое произвольного размера, но делитель оставался 64-разрядным. Для реализации делителей размером более 64 бит одним из вариантов решения проблемы является сочетание сдвига и вычитания. В общем случае алгоритм будет выглядеть следующим образом:

Quotient = Dividend;
Remainder = 0;
for i = 1 to NumberBits do
    Remainder:Quotient = Remainder:Quotient SHL 1;
    if Remainder >= Divisor then
        Remainder = Remainder - Divisor;
        Quotient = Quotient + 1;
    endif
endfor

Где Quotient - результат/частное, Dividend - делимое, Remainder - остаток от деления, Divisor - делитель, а NumberBits - количество битов в Remainder/Quotient/Divisor/Dividend. Здесь делимое и результат деления представляют один и тот же объект. Суть алгоритма - сдвигаем Remainder:Quotient на один бит влево. Таким образом в остатке будет первый бит делимого. Если получение значение в остатке больше или равно делителю, то вычитаем из остатка делитель и увеличиваем результат на 1. И так, пока не обработаем все биты в делители.

Реализация на примере деления 128-разрядного числа на другое 128-разрядное:

.data
    op1 oword 00000000000000040000000000000008h     ; делимое и результат - 128 бит
    op2 oword 2                                     ; делитель - 128 бит
    remainder oword ?
.code
main proc
    mov cl, 128 ; в CL количество бит, которые надо разделить

    ; Remainder:Quotient = Remainder:Quotient SHL 1
repeatLp: 
    ; сдвиг влево на 1 разряд всего делимого и остатка
    shl qword ptr op1[0], 1 
    rcl qword ptr op1[8], 1 
    rcl qword ptr remainder[0], 1
    rcl qword ptr remainder[8], 1

    ; Выполняем 128-разрядное сравнение, чтобы узнать, больше ли остаток чем делитель
    mov rax, qword ptr remainder[8]
    cmp rax, qword ptr op2[8]
    ja isGE         ; если больше или равно
    jb notGE        ; если меньше
    mov rax, qword ptr remainder 
    cmp rax, qword ptr op2
    ja isGE
    jb notGE

    ; Remainder = Remainder - Divisor
isGE: 
    mov rax, qword ptr op2
    sub qword ptr remainder, rax
    mov rax, qword ptr op2[8]
    sbb qword ptr remainder[8], rax

    ; Quotient = Quotient + 1
    add qword ptr op1, 1
    adc qword ptr op1[8], 0
notGE: 
    dec cl
    jnz repeatLp    ; продолжаем пока не пройдем по всем 128 битам

    ; mov eax, dword ptr op1        ; 4
    ; mov eax, dword ptr op1[8]     ; 2
    ret
main endp
end

В данном случае переменная op1 выступает в качестве делимого и одновременно результата операции. Переменная op2 является делителем, а remainder - хранит остаток. В регистре CL хранится счетчик битов, которые надо разделить.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850