Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Assembly (x86-64, AArch64, RISC-V)  Module 5 – Arithmetic, logic and flags

Division: traps, defined results, remainders

Divide signed and unsigned integers on x86-64, AArch64 and RISC-V, predict division by zero and INT_MIN / -1, and read the multiply a compiler uses for x / 10.

  • Intermediate
  • 20 minutes
  • Examples run with Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Compute the quotient and remainder of signed and unsigned integers on x86-64, AArch64 and RISC-V
  • Set up the x86-64 dividend with cqo or xor edx, edx and explain what goes wrong without it
  • Predict the result of division by zero and of INT64_MIN / -1 on each ISA
  • Explain how a compiler replaces division by a constant with a multiplication and a shift

Before you start

What you’ll learn · Before you start

What you will learn

  • Compute the quotient and remainder of signed and unsigned integers on x86-64, AArch64 and RISC-V
  • Set up the x86-64 dividend with cqo or xor edx, edx and explain what goes wrong without it
  • Predict the result of division by zero and of INT64_MIN / -1 on each ISA
  • Explain how a compiler replaces division by a constant with a multiplication and a shift

Before you start

On this page

Divide 7 by 0 in x86-64 assembly and the process dies with exit status 136; the same division on AArch64 returns 0 and the program carries on, and RISC-V returns all ones. Even −7 ÷ 2 can come back as −3, as −4 or as 9,223,372,036,854,775,804, depending on the language and on one instruction before the divide.

This lesson runs the divide instructions of x86-64, AArch64 and RISC-V on positive and negative operands, sets up the x86-64 dividend correctly and wrongly, provokes division by zero and INT64_MIN / -1 on each ISA, and takes apart the multiplication a compiler emits for x / 10.

Integer division rounds towards zero, and the remainder follows the dividend

Each version of sdivmod(a, b, &r) below returns a / b and stores the remainder at r, and udivmod does the same for unsigned operands. Compare how each ISA gets the remainder:

sdivmod and udivmod in three ISAs

AArch64 · aarch64/divmod.s

// sdivmod and udivmod for AArch64. AArch64 has no remainder instruction: r = a - (a / b) * b.
.text
.p2align 2
.globl _sdivmod
_sdivmod:                   // x0 = a, x1 = b, x2 = r
    sdiv x3, x0, x1         // x3 = a / b, rounded towards zero
    msub x4, x3, x1, x0     // x4 = x0 - x3 * x1, the remainder
    str x4, [x2]
    mov x0, x3
    ret
.globl _udivmod
_udivmod:
    udiv x3, x0, x1
    msub x4, x3, x1, x0
    str x4, [x2]
    mov x0, x3
    ret

x86-64 · x86_64/divmod.s

# sdivmod and udivmod for x86-64, GNU as Intel syntax (Mach-O names carry a leading underscore).
# long sdivmod(long a, long b, long *r): returns a / b and stores a % b at *r.
# div and idiv divide the 128-bit number rdx:rax, so rdx must hold the high half of the dividend first.
.intel_syntax noprefix
.text
.globl _sdivmod
_sdivmod:
    mov rcx, rdx            # r arrives in rdx, which idiv overwrites: keep the pointer in rcx
    mov rax, rdi
    cqo                     # rdx = 64 copies of the sign bit of rax
    idiv rsi                # rax = quotient, rdx = remainder
    mov [rcx], rdx
    ret
.globl _udivmod
_udivmod:
    mov rcx, rdx
    mov rax, rdi
    xor edx, edx            # unsigned dividend: the high half is 0 (a 32-bit write clears all of rdx)
    div rsi
    mov [rcx], rdx
    ret

RISC-V · riscv/divmod.s

# sdivmod and udivmod for RV64 with the M extension (Linux ELF names have no underscore).
# The M chapter recommends div then rem on the same sources, quotient register not a source.
.text
.globl sdivmod
sdivmod:                    # a0 = a, a1 = b, a2 = r
    div a3, a0, a1          # a3 = a / b, rounded towards zero
    rem a4, a0, a1          # a4 = a % b, same sign as a
    sd a4, 0(a2)
    mv a0, a3
    ret
.globl udivmod
udivmod:
    divu a3, a0, a1
    remu a4, a0, a1
    sd a4, 0(a2)
    mv a0, a3
    ret

As in the multiplication lesson, the AArch64 build runs natively on Apple silicon under macOS, the x86-64 build runs through Rosetta 2, and the RISC-V file is assembled and read back. The driver divides ±7 by ±2, then the bits of −7 as an unsigned number:

divmod.c C · divmod.c
// Calls the assembly sdivmod and udivmod on the same operands and prints quotient and remainder.
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>

int64_t sdivmod(int64_t a, int64_t b, int64_t *r);
uint64_t udivmod(uint64_t a, uint64_t b, uint64_t *r);

static void s(int64_t a, int64_t b) {
    int64_t r, q = sdivmod(a, b, &r);
    printf("signed    %3" PRId64 " / %2" PRId64 " = %2" PRId64 "  remainder %2" PRId64 "\n", a, b, q, r);
}

static void u(uint64_t a, uint64_t b) {
    uint64_t r, q = udivmod(a, b, &r);
    printf("unsigned  0x%016" PRIx64 " / %" PRIu64 " = %" PRIu64 "  remainder %" PRIu64 "\n", a, b, q, r);
}

int main(void) {
    s(7, 2);
    s(-7, 2);
    s(7, -2);
    s(-7, -2);
    u(7, 2);
    u((uint64_t)-7, 2);     // the bits of -7, divided as an unsigned number
    return 0;
}
Six divisions on AArch64 and x86-64, RISC-V assembled Python · divmod_session.py
import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session

# One C driver, two assembly files: AArch64 runs natively, the x86-64 build runs through Rosetta 2.
session.run("""
clang -O1 -o divmod_a64 divmod.c aarch64/divmod.s
./divmod_a64 | tee a64.txt
clang -O1 -arch x86_64 -o divmod_x64 divmod.c x86_64/divmod.s
./divmod_x64 | diff a64.txt - && echo "x86-64 printed the same 6 lines"
""")

# macOS cannot run RISC-V: assemble the M-extension file and read it back.
session.run("""
llvm-mc --triple=riscv64 -mattr=+m -filetype=obj -o divmod_rv.o riscv/divmod.s
llvm-objdump -d --no-show-raw-insn divmod_rv.o
""")

Output

$ clang -O1 -o divmod_a64 divmod.c aarch64/divmod.s
$ ./divmod_a64 | tee a64.txt
signed      7 /  2 =  3  remainder  1
signed     -7 /  2 = -3  remainder -1
signed      7 / -2 = -3  remainder  1
signed     -7 / -2 =  3  remainder -1
unsigned  0x0000000000000007 / 2 = 3  remainder 1
unsigned  0xfffffffffffffff9 / 2 = 9223372036854775804  remainder 1
$ clang -O1 -arch x86_64 -o divmod_x64 divmod.c x86_64/divmod.s
$ ./divmod_x64 | diff a64.txt - && echo "x86-64 printed the same 6 lines"
x86-64 printed the same 6 lines
$ llvm-mc --triple=riscv64 -mattr=+m -filetype=obj -o divmod_rv.o riscv/divmod.s
$ llvm-objdump -d --no-show-raw-insn divmod_rv.o

divmod_rv.o:	file format elf64-littleriscv

Disassembly of section .text:

0000000000000000 <sdivmod>:
       0:      	div	a3, a0, a1
       4:      	rem	a4, a0, a1
       8:      	sd	a4, 0x0(a2)
       c:      	mv	a0, a3
      10:      	ret

0000000000000014 <udivmod>:
      14:      	divu	a3, a0, a1
      18:      	remu	a4, a0, a1
      1c:      	sd	a4, 0x0(a2)
      20:      	mv	a0, a3
      24:      	ret

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 divmod_session.py

−7 / 2 gave −3, not −4: every signed divide here is a truncating division: Integer division that rounds the quotient towards zero, as C and the signed divide instructions of x86-64, AArch64 and RISC-V do: -7 / 2 is -3, and the remainder takes the sign of the dividend., which drops the fraction of −3.5 and so rounds towards zero. The remainder then has to make (a / b) × b + a % b = a hold, which gives it the sign of the dividend: −7 % 2 is −1, and 7 % −2 is 1. C requires exactly this, “the algebraic quotient with any fractional part discarded”, in 6.5.6 of its standard, so compiled C and these instructions agree.

The last line shows that division, unlike addition, cares about signedness. The bits 0xfffffffffffffff9 are −7 as a signed number and 18,446,744,073,709,551,609 as an unsigned one, and udivmod divided the second: 9,223,372,036,854,775,804 remainder 1. So each ISA has a signed and an unsigned divide:

ISA Signed divide Unsigned divide Remainder
x86-64 idiv rsi, after cqo div rsi, after xor edx, edx in rdx, from the same instruction
AArch64 sdiv x3, x0, x1 udiv x3, x0, x1 none: msub x4, x3, x1, x0
RISC-V (RV64) div a3, a0, a1 divu a3, a0, a1 rem a4, a0, a1 or remu

AArch64 has no remainder instruction. msub x4, x3, x1, x0 computes x0 − x3 × x1, the dividend minus the quotient times the divisor, which is the remainder by definition. RISC-V has rem and remu, and its M chapter recommends the div then rem pair on the same sources, with a quotient register that is neither source, so that a core can fuse the two into one division. RV64 adds divw, divuw, remw and remuw, which divide the low 32 bits and sign-extend the 32-bit result.

Porting -7 / 2 from Python

Python’s // rounds down, towards minus infinity: -7 // 2 is −4 and -7 % 2 is 1. C, x86-64, AArch64 and RISC-V round towards zero: −3 and −1. Code that moves a negative division between the two worlds changes its answer without an error; translate // with an explicit adjustment when the signs differ.

Checkpoint

Check: Truncating division and the remainder's sign

  1. Question 1 of 2 What did sdivmod return for 7 / −2, and what remainder did it store?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Quotient −3, remainder 1

    Truncating division drops the fraction of −3.5 and gives −3. The remainder then satisfies (a / b) × b + a % b = a: 7 − 6 = 1. The recorded line was "signed 7 / -2 = -3 remainder 1".

  2. Question 2 of 2 add64 needs no separate unsigned version, but every ISA has a separate unsigned divide. Which recorded line shows why?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The bits 0xfffffffffffffff9 divided by 2: −3 as signed, 9223372036854775804 as unsigned

    Addition gives the same bits for both readings; division does not. 0xfffffffffffffff9 is −7 signed, and −7 / 2 is −3; it is 18446744073709551609 unsigned, and that divided by 2 is 9223372036854775804. Hence idiv and div, sdiv and udiv, div and divu.

x86-64 divides rdx:rax, so the high half must be set first

idiv rsi does not divide rax by rsi. It divides the 128-bit number in the register pair rdx:rax, writes the quotient to rax and the remainder to rdx; the 32-bit idiv esi divides edx:eax the same way. So before every divide, rdx must hold the upper half of the dividend: copies of the sign bit for a signed division, which cqo writes (cdq for edx:eax), or 0 for an unsigned one, which xor edx, edx writes because a 32-bit write clears the whole register. That is also why sdivmod above copies its pointer argument out of rdx first: the divide would overwrite it. Step through −7 / 2, then the same division with the wrong set-up:

Interactive · step by step How x86-64 divides -7 by 2, and how the set-up goes wrong Step through cqo and idiv rsi on the recorded case -7 / 2, then repeat it with xor edx, edx in place of cqo.
How x86-64 divides -7 by 2, and how the set-up goes wrong, step 5 of 5. Showing rdx: high half (0 (xor edx, edx)), rax: low half, rsi: divisor, rax after idiv (≈ 9.2e18), rdx after idiv (1). In focus: rax after idiv.rdx: high half0 (xor edx, edx)rax: low half0x…F9 = -7rsi: divisor2rax after idiv≈ 9.2e18rdx after idiv1

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. A 128-bit dividend. idiv rsi divides the 128-bit number held in rdx:rax by rsi. The function put a = -7 in rax, but rdx still holds whatever the caller left in it.
  2. cqo fills rdx with the sign. cqo copies bit 63 of rax into all 64 bits of rdx. -7 is negative, so rdx becomes all ones, and rdx:rax is -7 as a 128-bit number.
  3. The quotient and the remainder. idiv rounds towards zero: -7 / 2 is -3, written to rax, and the remainder -1 goes to rdx, so that -3 x 2 + (-1) = -7.
  4. The wrong set-up. Now replace cqo with xor edx, edx, the set-up for an unsigned divide. rdx becomes 0, and rdx:rax is no longer -7 but 2^64 - 7, a large positive number.
  5. A positive, wrong quotient. (2^64 - 7) / 2 is 9223372036854775804 with remainder 1. It fits in 64 bits, so nothing traps: the program silently gets a huge positive number for -7 / 2.

Both mistakes pass a quick test. The two functions below get the set-up wrong on purpose: div_junk never clears rdx, so the divide uses whatever its third argument left there, and idiv_zeroed uses the unsigned set-up before a signed divide:

setup.s: two divisions with the wrong high half S · x86_64/setup.s
# Two x86-64 divisions with the dividend's high half set up wrongly, GNU as Intel syntax.
.intel_syntax noprefix
.text
.globl _div_junk
_div_junk:                  # unsigned long div_junk(unsigned long a, unsigned long b, unsigned long junk)
    mov rax, rdi            # the low half of the dividend
    div rsi                 # BUG: no xor edx, edx, so rdx still holds junk, the third argument
    ret
.globl _idiv_zeroed
_idiv_zeroed:               # long idiv_zeroed(long a, long b)
    mov rax, rdi
    xor edx, edx            # BUG: the unsigned set-up before a signed divide; cqo was meant
    idiv rsi
    ret
The same bugs on inputs that hide them and inputs that show them Python · setup_session.py
import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session

# The same two bugs, on inputs that hide them and on inputs that show them (x86-64, through Rosetta 2).
session.run("""
clang -O1 -arch x86_64 -o setup_x64 setup.c x86_64/setup.s
./setup_x64 junk 7 2 0; echo "exit=$?"
./setup_x64 junk 7 2 1; echo "exit=$?"
{ ./setup_x64 junk 7 1 1; } 2>/dev/null; s=$?; echo "exit=$s signal=$(kill -l $s)"
./setup_x64 zeroed 7 2; echo "exit=$?"
./setup_x64 zeroed -7 2; echo "exit=$?"
""")

Output

$ clang -O1 -arch x86_64 -o setup_x64 setup.c x86_64/setup.s
$ ./setup_x64 junk 7 2 0; echo "exit=$?"
rdx = 0: 7 / 2 gave 3
exit=0
$ ./setup_x64 junk 7 2 1; echo "exit=$?"
rdx = 1: 7 / 2 gave 9223372036854775811
exit=0
$ { ./setup_x64 junk 7 1 1; } 2>/dev/null; s=$?; echo "exit=$s signal=$(kill -l $s)"
exit=136 signal=FPE
$ ./setup_x64 zeroed 7 2; echo "exit=$?"
7 / 2 gave 3
exit=0
$ ./setup_x64 zeroed -7 2; echo "exit=$?"
-7 / 2 gave 9223372036854775804
exit=0

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 setup_session.py

With rdx = 0, div_junk printed the right 3. With rdx = 1 the dividend was 2⁶⁴ + 7, and the divide returned 9,223,372,036,854,775,811 without complaint, because that quotient still fits in 64 bits. With the divisor 1, the quotient 2⁶⁴ + 7 does not fit, and the process died with status 136. idiv_zeroed is right for 7 / 2 and wrong for −7 / 2, which it divided as 2⁶⁴ − 7. The Intel manual lists both outcomes under DIV and IDIV: the result goes to rax and rdx, and a quotient too large for rax raises the divide error, #DE, exactly as a zero divisor does.

xor edx, edx before idiv

xor edx, edx then idiv rsi is right only while the dividend is non-negative, so tests with positive numbers pass. Pair the set-up with the divide: cqo before idiv, xor edx, edx before div.

Checkpoint

Check: Setting up rdx:rax before div and idiv

  1. Question 1 of 2 rax holds −7. What does cqo write to rdx?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 0xffffffffffffffff

    CQO sign-extends rax into rdx:rax: every bit of rdx becomes bit 63 of rax. For −7 that is all ones, and the 128-bit dividend rdx:rax is −7.

  2. Question 2 of 2 div_junk(7, 2, 1) runs div rsi with rdx = 1 and rax = 7. What happens?

    Choose one answer.

    A hint

    Write the dividend rdx:rax as one number, then ask whether the quotient fits in 64 bits.

    Show the answer: Question 2 of 2

    Answer: It returns 9223372036854775811, a wrong quotient, and nothing traps

    With rdx = 1 the dividend is 2^64 + 7. Divided by 2 that is 2^63 + 3 = 9223372036854775811, which fits in rax, so the recorded run printed that number with no trap. Only a quotient that does not fit raises #DE.

Division by zero traps on x86-64 and returns a value elsewhere

The two operations that have no correct 64-bit answer are a division by zero and INT64_MIN / -1, whose true quotient, +2⁶³, is one more than the largest signed 64-bit number. This session runs both on each ISA that can run here, each case in its own process so that a crash cannot hide the next one:

edge.c: one division per process C · edge.c
// edge a b: divides a by b with the assembly sdivmod and prints the quotient and the remainder.
// Each case runs as its own process, so a case that traps cannot hide the others.
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

int64_t sdivmod(int64_t a, int64_t b, int64_t *r);

int main(int argc, char **argv) {
    if (argc != 3) return 2;
    int64_t a = strtoll(argv[1], NULL, 0), b = strtoll(argv[2], NULL, 0), r;
    int64_t q = sdivmod(a, b, &r);
    printf("%" PRId64 " / %" PRId64 " = %" PRId64 "  remainder %" PRId64 "\n", a, b, q, r);
    return 0;
}
7 / 0 and INT64_MIN / -1 on AArch64 and x86-64 Python · edge_session.py
import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session

# Division by zero and INT64_MIN / -1, each in its own process. A process killed by a signal ends with status
# 128 + the signal's number; kill -l turns that status back into the signal's name. The braces send the shell's
# own crash message (which names the process number) to /dev/null.
session.run("""
clang -O1 -o edge_a64 edge.c aarch64/divmod.s
./edge_a64 7 0; echo "exit=$?"
./edge_a64 -9223372036854775808 -1; echo "exit=$?"
clang -O1 -arch x86_64 -o edge_x64 edge.c x86_64/divmod.s
./edge_x64 7 2; echo "exit=$?"
{ ./edge_x64 7 0; } 2>/dev/null; s=$?; echo "exit=$s signal=$(kill -l $s)"
{ ./edge_x64 -9223372036854775808 -1; } 2>/dev/null; s=$?; echo "exit=$s signal=$(kill -l $s)"
""")

Output

$ clang -O1 -o edge_a64 edge.c aarch64/divmod.s
$ ./edge_a64 7 0; echo "exit=$?"
7 / 0 = 0  remainder 7
exit=0
$ ./edge_a64 -9223372036854775808 -1; echo "exit=$?"
-9223372036854775808 / -1 = -9223372036854775808  remainder 0
exit=0
$ clang -O1 -arch x86_64 -o edge_x64 edge.c x86_64/divmod.s
$ ./edge_x64 7 2; echo "exit=$?"
7 / 2 = 3  remainder 1
exit=0
$ { ./edge_x64 7 0; } 2>/dev/null; s=$?; echo "exit=$s signal=$(kill -l $s)"
exit=136 signal=FPE
$ { ./edge_x64 -9223372036854775808 -1; } 2>/dev/null; s=$?; echo "exit=$s signal=$(kill -l $s)"
exit=136 signal=FPE

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 edge_session.py

AArch64 never traps on a divide. sdiv by zero returned 0, so msub computed the remainder as 7 − 0 × 0 = 7, and INT64_MIN / -1 returned INT64_MIN with remainder 0: the true quotient wrapped. On x86-64 both cases raised #DE, and macOS ended the process with a signal: A notification the operating system delivers to a process, such as SIGFPE after an integer divide fault; unhandled, it ends the process, and the shell reports status 128 plus the signal number.. Status 136 is 128 + 8, the shell’s way of reporting a process killed by signal 8, and kill -l names it: FPE, the signal that signal(7) lists as 8 on every architecture and describes as an erroneous arithmetic operation. On Linux too, an instruction that raises a math fault gets its thread a SIGFPE, and sigaction(2) lists FPE_INTDIV, integer divide by zero, among the reasons the signal can carry.

RISC-V code does not run on Apple silicon, so its column comes from the table in its M chapter, not from a run:

ISA 7 / 0 7 % 0 INT64_MIN / −1 INT64_MIN % −1
x86-64 idiv #DE: SIGFPE, status 136 #DE #DE: SIGFPE, status 136 #DE
AArch64 sdiv + msub 0 7 INT64_MIN 0
RISC-V div, rem −1 (all ones) 7 INT64_MIN 0

RISC-V’s unsigned divu by zero also returns all ones, the largest unsigned number. Its manual explains why it chose values over a trap: a trap “would be the only arithmetic trap in the standard ISA”, and a language that wants one can add a single, almost never taken branch after the divide. x86-64’s idiv traps on INT64_MIN % -1 as well, although its true remainder, 0, fits: one instruction computes both, and the quotient overflowed.

C does not choose between the ISAs. Its standard makes division by zero undefined behaviour: What the C standard calls an operation for which it sets no requirements at all, such as dividing by zero; a compiler may assume a correct program never performs one., and when a / b is not representable, both a / b and a % b are undefined too, which covers INT64_MIN % -1. A compiler may assume neither case happens, so the ISA’s behaviour only shows through in assembly, and robust code tests b == 0 and a == INT64_MIN && b == -1 before it divides. In tests, clang’s -fsanitize=integer-divide-by-zero and -fsanitize=signed-integer-overflow, which covers INT_MIN / -1, report both cases where they happen.

Key idea

The same two divisions crash an x86-64 program with SIGFPE and quietly return 0, all ones or INT64_MIN on AArch64 and RISC-V, and C leaves them undefined, so check the divisor before you divide rather than relying on any ISA.

Checkpoint

Check: Division by zero and INT64_MIN / -1

  1. Question 1 of 2 On AArch64, sdivmod(7, 0) ran sdiv and then msub. What did it return?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Quotient 0, remainder 7

    AArch64 sdiv and udiv write 0 when the divisor is 0 and never trap. msub then computes the dividend minus 0 × 0, so the "remainder" is 7. The recorded line was "7 / 0 = 0 remainder 7".

  2. Question 2 of 2 Per the M chapter's table, what do RISC-V div and rem return for 7 / 0?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: div returns −1 (all ones) and rem returns 7

    Table 1 of the M chapter: division by zero gives a quotient of all ones (−1 signed, the largest number unsigned) and a remainder equal to the dividend. Overflow, INT64_MIN / −1, gives the dividend and 0.

Compilers divide by a constant with a multiply and a shift

A compiler that knows the divisor in advance does not have to divide at all. This file divides an int by 10 and takes its remainder, and the session compiles it with -O2, the usual optimisation level, on each ISA, then with -Oz, the level that clang’s documentation says reduces code size further than -Os. Apple’s clang has no RISC-V backend, so the RISC-V lines use the clang of Homebrew’s LLVM:

div10.c C · div10.c
// Division and remainder by the constant 10, for 32-bit int.
int div10(int x) { return x / 10; }
int mod10(int x) { return x % 10; }
x / 10 and x % 10 at -O2, then x / 10 at -Oz Python · div10_session.py
import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session

# x / 10 and x % 10 compiled for speed (-O2) on each ISA, then div10 alone compiled for size (-Oz).
session.run("""
clang -O2 -fomit-frame-pointer -arch x86_64 -c -o speed_x64.o div10.c
objdump -d --no-show-raw-insn -M intel speed_x64.o
clang -O2 -c -o speed_a64.o div10.c
otool -tV speed_a64.o | cat
/opt/homebrew/opt/llvm/bin/clang --target=riscv64-linux-gnu -march=rv64gc -O2 -c -o speed_rv.o div10.c
llvm-objdump -d --no-show-raw-insn speed_rv.o
""")

session.run("""
clang -Oz -fomit-frame-pointer -arch x86_64 -c -o size_x64.o div10.c
objdump -d -M intel --disassemble-symbols=_div10 size_x64.o
clang -Oz -c -o size_a64.o div10.c
otool -tV size_a64.o | cat
/opt/homebrew/opt/llvm/bin/clang --target=riscv64-linux-gnu -march=rv64gc -Oz -c -o size_rv.o div10.c
llvm-objdump -d --disassemble-symbols=div10 size_rv.o
""")

Output

$ clang -O2 -fomit-frame-pointer -arch x86_64 -c -o speed_x64.o div10.c
$ objdump -d --no-show-raw-insn -M intel speed_x64.o

speed_x64.o:	file format mach-o 64-bit x86-64

Disassembly of section __TEXT,__text:

0000000000000000 <_div10>:
       0:      	movsxd	rax, edi
       3:      	imul	rax, rax, 0x66666667
       a:      	mov	rcx, rax
       d:      	shr	rcx, 0x3f
      11:      	sar	rax, 0x22
      15:      	add	eax, ecx
      17:      	ret
      18:      	nop	dword ptr [rax + rax]

0000000000000020 <_mod10>:
      20:      	movsxd	rax, edi
      23:      	imul	rcx, rax, 0x66666667
      2a:      	mov	rdx, rcx
      2d:      	shr	rdx, 0x3f
      31:      	sar	rcx, 0x22
      35:      	add	ecx, edx
      37:      	add	ecx, ecx
      39:      	lea	ecx, [rcx + 4*rcx]
      3c:      	sub	eax, ecx
      3e:      	ret
$ clang -O2 -c -o speed_a64.o div10.c
$ otool -tV speed_a64.o | cat
speed_a64.o:
(__TEXT,__text) section
_div10:
0000000000000000	mov	w8, #0x6667
0000000000000004	movk	w8, #0x6666, lsl #16
0000000000000008	smull	x8, w0, w8
000000000000000c	asr	x8, x8, #34
0000000000000010	add	w0, w8, w8, lsr #31
0000000000000014	ret
_mod10:
0000000000000018	mov	w8, #0x6667
000000000000001c	movk	w8, #0x6666, lsl #16
0000000000000020	smull	x8, w0, w8
0000000000000024	asr	x8, x8, #34
0000000000000028	add	w8, w8, w8, lsr #31
000000000000002c	mov	w9, #0xa
0000000000000030	msub	w0, w8, w9, w0
0000000000000034	ret
$ /opt/homebrew/opt/llvm/bin/clang --target=riscv64-linux-gnu -march=rv64gc -O2 -c -o speed_rv.o div10.c
$ llvm-objdump -d --no-show-raw-insn speed_rv.o

speed_rv.o:	file format elf64-littleriscv

Disassembly of section .text:

0000000000000000 <div10>:
       0:      	lui	a1, 0x66666
       4:      	addi	a1, a1, 0x667
       8:      	mul	a0, a0, a1
       c:      	srli	a1, a0, 0x3f
      10:      	srai	a0, a0, 0x22
      12:      	add	a0, a0, a1
      14:      	ret

0000000000000016 <mod10>:
      16:      	lui	a1, 0x66666
      1a:      	addi	a1, a1, 0x667
      1e:      	mul	a1, a0, a1
      22:      	srli	a2, a1, 0x3f
      26:      	srai	a1, a1, 0x22
      28:      	add	a1, a1, a2
      2a:      	slli	a2, a1, 0x1
      2e:      	slli	a1, a1, 0x3
      30:      	add	a1, a1, a2
      32:      	subw	a0, a0, a1
      34:      	ret
$ clang -Oz -fomit-frame-pointer -arch x86_64 -c -o size_x64.o div10.c
$ objdump -d -M intel --disassemble-symbols=_div10 size_x64.o

size_x64.o:	file format mach-o 64-bit x86-64

Disassembly of section __TEXT,__text:

0000000000000000 <_div10>:
       0: 89 f8                        	mov	eax, edi
       2: 6a 0a                        	push	0xa
       4: 59                           	pop	rcx
       5: 99                           	cdq
       6: f7 f9                        	idiv	ecx
       8: c3                           	ret
$ clang -Oz -c -o size_a64.o div10.c
$ otool -tV size_a64.o | cat
size_a64.o:
(__TEXT,__text) section
_div10:
0000000000000000	mov	w8, #0xa
0000000000000004	sdiv	w0, w0, w8
0000000000000008	ret
_mod10:
000000000000000c	mov	w8, #0xa
0000000000000010	sdiv	w9, w0, w8
0000000000000014	msub	w0, w9, w8, w0
0000000000000018	ret
$ /opt/homebrew/opt/llvm/bin/clang --target=riscv64-linux-gnu -march=rv64gc -Oz -c -o size_rv.o div10.c
$ llvm-objdump -d --disassemble-symbols=div10 size_rv.o

size_rv.o:	file format elf64-littleriscv

Disassembly of section .text:

0000000000000000 <div10>:
       0: 45a9         	li	a1, 0xa
       2: 02b5453b     	divw	a0, a0, a1
       6: 8082         	ret

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 div10_session.py

At -O2 no ISA divided. x86-64 multiplied the sign-extended x by 0x66666667 with imul, shifted the 64-bit product right by 34 (0x22), and added the product’s sign bit. AArch64 built the same constant with mov and movk, used smull, a 32 × 32 → 64-bit signed multiply, then asr #34 and an add of the sign bit; RISC-V built it with lui and addi and did the same with mul, srai and srli. mod10 computed the quotient that way, multiplied it back by 10 (x86-64 with add and lea, RISC-V with two shifts and an add, AArch64 with msub) and subtracted. At -Oz all three kept the divide: cdq and idiv ecx in a 9-byte function on x86-64, sdiv on AArch64, divw on RISC-V. The choice shows how the compiler ranks the two: five or six instructions are worth spending to avoid one divide, except when the smallest code is the goal.

The constant is 2³⁴ ÷ 10 rounded up. Multiplying by it scales x / 10 up by 2³⁴, the shift scales it back with rounding down, and the add of the sign bit turns rounding down into rounding towards zero for negative results. Step through both signs:

Interactive · stage by stage x / 10 without a divide, for 47 and -47 Step through the four instructions clang emits for a 32-bit x / 10 and watch both inputs reach the right quotient.
x / 10 without a divide, for 47 and -47, step 5 of 5. Showing Multiply by M (-47 x M), Shift right by 34 (-4.7000000016 -> -5), Add the sign bit (adds 1), The quotient (-4). In focus: Add the sign bit, The quotient.Multiply by M-47 x MShift right by 34-4.7000000016 -> -5Add the sign bitadds 1The quotient-4

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Multiply by M. M = 1717986919 is 2^34 / 10 rounded up. 47 x M = 80745385193, which is 4.7000000016 x 2^34: the product is x / 10, scaled up by 2^34, plus a tiny error.
  2. Shift right by 34. An arithmetic shift right by 34 divides by 2^34 and rounds down. 4.7000000016 rounds down to 4, the quotient of 47 / 10.
  3. No fix for a positive x. The sign bit of the result is 0, so adding it changes nothing. The quotient is 4, the same as 47 / 10 in C.
  4. Now x = -47. -47 x M = -80745385193, which is -4.7000000016 x 2^34. Rounding down gives -5, one too far from zero for C, where -47 / 10 is -4.
  5. The sign bit fixes it. -5 is negative, so its sign bit is 1, and adding it gives -4. One add turns rounding down into rounding towards zero.

Why 34 and not 32? This program builds the constant for each shift from 32 up, tests it against Python’s own division on the hardest inputs (both ends of the int range and around 0) and on 200,000 seeded random ones, and stops at the first shift that passes:

Find the multiplier and shift for x / 10 Python · magic.py
"""Where 0x66666667 comes from: the constant clang uses for a 32-bit int divided by 10.

The compiled code computes q = (x * M) >> S (an arithmetic shift, which rounds down) and then adds 1 when q is
negative, which turns rounding down into rounding towards zero, as C's / requires. M is 2**S / D rounded up.
For each shift S this program builds M, tests it against Python's own division on the hardest inputs and on
200,000 seeded random ones, and stops at the first S that passes.
"""

import random

D = 10                       # the divisor; change it and run again
LOW, HIGH = -2**31, 2**31 - 1


def truncating_div(x, d):
    """C's x / d for int: the quotient rounded towards zero."""
    q = abs(x) // abs(d)
    return q if (x < 0) == (d < 0) else -q


def compiled(x, m, s):
    """What the instructions compute: multiply, shift right with rounding down, add 1 if negative."""
    q = (x * m) >> s
    return q + 1 if q < 0 else q


rng = random.Random(10)
tests = [x for x in range(LOW, LOW + 2**16)] + [x for x in range(-2**16, 2**16)]
tests += [x for x in range(HIGH - 2**16, HIGH + 1)] + [rng.randint(LOW, HIGH) for _ in range(200_000)]

for s in range(32, 40):
    m = -(-(2**s) // D)      # 2**s / D rounded up
    bad = next((x for x in tests if compiled(x, m, s) != truncating_div(x, D)), None)
    if bad is None:
        print(f"S = {s}: M = {m} = {m:#x}, every test agrees ({len(tests)} inputs)")
        excess = m * D - 2**s
        print(f"M * D - 2**S = {excess}, so the largest error is {excess} * 2**31 / ({D} * 2**{s})"
              f" = {excess * 2**31 / (D * 2**s):.4g}, below 1/{D} = {1 / D:.4g}")
        break
    print(f"S = {s}: M = {m:#x} fails first at x = {bad}: {compiled(bad, m, s)} instead of {truncating_div(bad, D)}")

Output

S = 32: M = 0x1999999a fails first at x = -2147483639: -214748364 instead of -214748363
S = 33: M = 0x33333334 fails first at x = -2147483639: -214748364 instead of -214748363
S = 34: M = 1717986919 = 0x66666667, every test agrees (462145 inputs)
M * D - 2**S = 6, so the largest error is 6 * 2**31 / (10 * 2**34) = 0.075, below 1/10 = 0.1

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 magic.py

Rounding the constant up adds a small error to every estimate, and the error grows with x. With shifts of 32 and 33 it reaches 0.2 for an x near 2³¹, too much: −2,147,483,639 ÷ 10 is −214,748,363.9, and 0.2 more pushes the estimate past −214,748,364. At 34, M × 10 − 2³⁴ = 6, so the error is at most 6 × 2³¹ ÷ (10 × 2³⁴) = 0.075 for any 32-bit x. The fractional part of x / 10 is at most 0.9, and 0.9 + 0.075 never reaches the next whole number, so the rounding always lands on the true quotient. This multiplication by a reciprocal: Division by a constant d done as a multiplication by 2^S / d rounded up, a right shift by S and a fix for negative results; with the right S it is exact for every input of its width. is exact, not an approximation.

Key idea

For a constant divisor, compilers optimising for speed multiply by 2^S ÷ d rounded up, shift right by S and fix the rounding of negative results; when optimising for size they keep the shorter divide instruction.

Checkpoint

Check: Division by a constant as a multiplication

  1. One question clang compiles a 32-bit x / 10 as a multiply by 0x66666667 and an arithmetic shift right. By how many bits does it shift the 64-bit product?

    Type a number.

    A hint

    The x86-64 listing writes the shift in hex as 0x22.

    Show the answer: One question

    Answer: 34 bits

    0x66666667 = 1717986919 is 2^34 / 10 rounded up, so the product is x / 10 scaled by 2^34, and the shift right by 34 (0x22) scales it back. magic.py shows that shifts of 32 and 33 fail and 34 is the first that works.

Practise

Use an Apple silicon Mac for these, or the Linux setup from setting up the toolchain; on Linux, write each function name without its leading underscore:

  1. Write safe_divmod(a, b, &q, &r) for AArch64 and for x86-64 that returns 1 and leaves q and r alone for b == 0 and for INT64_MIN / -1, and otherwise stores both and returns 0. Change edge.c to call it, and run the four cases of the edge session on both builds: they must print the same lines and never end with status 136.
  2. Set D = 1000 in magic.py and run it. Then compile int div1000(int x) { return x / 1000; } with -O2 for AArch64, and find the multiplier and the shift that magic.py printed in the listing.
  3. Set D = 7 and run magic.py again. Its multiplier is larger than 2³¹, so it does not fit a signed 32-bit operand: compile x / 7 for AArch64 and find the extra instruction clang adds to make up for it.

Practice that adapts to you

Practise this lesson

12 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 12 What are −7 / 2 and −7 % 2 in C on x86-64, AArch64 and RISC-V?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: −3 and −1

    C defines the quotient as the algebraic quotient with any fractional part discarded, so −3.5 becomes −3, and requires (a / b) × b + a % b == a, so −7 % 2 is −1. idiv, sdiv and div all compute exactly that.

  2. Question 2 of 12 AArch64 has no remainder instruction. Which instruction follows sdiv x3, x0, x1 to put x0 % x1 in x4?

    Choose one answer.

    Show the answer: Question 2 of 12

    Answer: msub x4, x3, x1, x0

    MSUB Xd, Xn, Xm, Xa computes Xa − Xn × Xm. With the quotient in x3, the divisor in x1 and the dividend in x0, msub x4, x3, x1, x0 is x0 − x3 × x1, the remainder of the truncating division.

  3. Question 3 of 12 A checksum written in Python uses (x // 16, x % 16) on values that can be negative. It is ported to C as (x / 16, x % 16) for int x. When do the two versions disagree?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: For negative x that are not multiples of 16

    Python's // is floor division, rounded towards minus infinity; C's / truncates towards zero. They agree whenever the exact quotient is a whole number or positive, and differ by one (with a remainder that differs by 16) for negative x with a fraction, such as −1: Python gives (−1, 15), C gives (0, −1).

  4. Question 4 of 12 Which instruction prepares edx:eax for a signed 32-bit idiv esi?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: cdq

    The 32-bit idiv divides edx:eax, so edx must hold the sign of eax: cdq writes it. clang's -Oz listing of div10 shows exactly this pair, cdq then idiv ecx.

  5. Question 5 of 12 idiv_zeroed(−7, 2) runs xor edx, edx and then idiv rsi. What does it return?

    Choose one answer.

    A hint

    With rdx = 0, what number is the 128-bit dividend rdx:rax?

    Show the answer: Question 5 of 12

    Answer: 9223372036854775804, half of 2^64 − 7

    With rdx = 0 the dividend is 2^64 − 7 = 18446744073709551609, a positive number. Divided by 2 that is 9223372036854775804 remainder 1, which fits in a signed 64-bit rax, so idiv returns it without a trap, as the session recorded.

  6. Question 6 of 12 Which of these x86-64 divisions raise #DE? Choose every one.

    Choose every answer that is right.

    A hint

    #DE comes from a zero divisor or from a quotient that does not fit in rax.

    Show the answer: Question 6 of 12

    Answer:

    • div rsi with rdx = 1, rax = 7, rsi = 1
    • idiv rsi after cqo with rax = INT64_MIN, rsi = −1

    DIV and IDIV raise #DE for a zero divisor and for a quotient too large for the destination. Only the cases whose quotient does not fit trap; the others return wrong but representable quotients.

  7. Question 7 of 12 A process killed by SIGFPE ends with shell status 136. What is the number of the signal?

    Type a number.

    Show the answer: Question 7 of 12

    Answer: 8

    Bash reports a process killed by signal N with status 128 + N, so 136 − 128 = 8. signal(7) lists SIGFPE as 8, and kill -l 136 printed FPE in the run.

  8. Question 8 of 12 The true value of INT64_MIN % −1 is 0, which fits in 64 bits. What does x86-64 do when sdivmod computes it?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: idiv raises #DE and the process ends with SIGFPE, because the same instruction also computes the quotient, which overflows

    The recorded run of INT64_MIN / −1 on x86-64 ended with status 136 before printing anything, and the remainder comes from the same idiv. That is why C makes a % b undefined whenever a / b is not representable.

  9. Question 9 of 12 A C function must divide int64_t values safely on every ISA. Which test, placed before the division, covers every undefined case?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: if (b == 0 || (a == INT64_MIN && b == -1)) return ERROR;

    C's 6.5.6 makes division by zero undefined, and makes both a / b and a % b undefined when the quotient is not representable, which for int64_t happens only for INT64_MIN / −1. Testing both before dividing gives the same behaviour on x86-64, AArch64 and RISC-V.

  10. Question 10 of 12 At -Oz, what did clang emit for x / 10 on AArch64?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: mov w8, #0xa and sdiv w0, w0, w8

    The -Oz listing of div10 is mov w8, #0xa; sdiv w0, w0, w8; ret. At -O2 the same function was six instructions with smull and no divide: speed and size lead to different choices.

  11. Question 11 of 12 For x = −47, the multiply and the shift right by 34 give −5. Why does the compiled code then add 1?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: The arithmetic shift rounds down, and C rounds towards zero, so a negative estimate needs one added

    An arithmetic shift right divides by 2^34 rounding towards minus infinity. For a negative x that always lands one below C's truncated quotient, so the code adds the sign bit of the estimate: 1 when it is negative, 0 otherwise.

  12. Question 12 of 12 magic.py with D = 1000 reports M = 0x10624dd3 and S = 38. Which AArch64 sequence would you expect clang -O2 to emit for x / 1000?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: Build 0x10624dd3 with mov and movk, smull, asr #38, then add the sign bit

    For a divisor d, the compiler picks the smallest shift S whose rounded-up M = 2^S / d keeps the error below 1/d. For 1000 that is S = 38 and M = 0x10624dd3, and the code keeps the shape of x / 10: build M, multiply, shift, fix negative results. The second practice task checks this against clang's listing.

Interview questions

A warm-up for freshers: what are −7 / 2 and −7 % 2 in C on x86-64, AArch64 and RISC-V, and why do all three agree?

−7 / 2 is −3 and −7 % 2 is −1 on all three. C defines integer division as the algebraic quotient with the fraction discarded, so −3.5 becomes −3, rounded towards zero, and requires (a / b) * b + a % b == a, which makes the remainder −1, with the sign of the dividend. The signed divide instructions of the three ISAs, idiv, sdiv and div, all round towards zero as well, so a compiler can use them directly. The answer changes in languages that round down instead: Python’s -7 // 2 is −4 and -7 % 2 is 1, which is the usual surprise when code moves between them.

For interview practice beyond the warm-up, the bank has a question on designing a portable checked division and another on a crash that appears on one ISA only, both with a first step and follow-ups:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    Design int safe_divmod(int64_t a, int64_t b, int64_t *q, int64_t *r) for a library that ships on x86-64, AArch64 and RISC-V. Which inputs must it reject, what should it return for them, and what would each ISA do if the checks were missing?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Senior to staff

    A C service crashes with SIGFPE on its x86-64 servers, never on its Arm servers, after a configuration change that allows a batch size of 0. Explain the difference between the two fleets, say which one is in worse shape, and how you would find every division at risk.

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What are -7 / 2 and -7 % 2 in C on x86-64, AArch64 and RISC-V?

    -3 and -1 on all three. C rounds the quotient towards zero and the remainder takes the dividend's sign, and the signed divide instructions of all three ISAs round the same way. Python's -7 // 2 is -4, with remainder 1.

  2. Which registers do x86-64 div and idiv use, and what must come before each?

    They divide the 128-bit pair rdx:rax, leave the quotient in rax and the remainder in rdx. Before idiv, cqo copies the sign of rax into rdx; before div, xor edx, edx clears it. A wrong rdx gives a huge quotient or a trap.

  3. How does each ISA get the remainder of an integer division?

    x86-64 div and idiv leave it in rdx. AArch64 has no remainder instruction and computes it with msub, as dividend minus quotient times divisor. RISC-V has rem and remu, best placed straight after the matching divide.

  4. What happens on each ISA when a program divides by zero?

    x86-64 raises a divide error (#DE), and the process ends with SIGFPE, status 136 in the shell. AArch64 and RISC-V return a defined value instead and the program carries on. C leaves the case undefined, so check the divisor first.

  5. Why is INT64_MIN / -1 a special case?

    The true quotient, 2 to the power 63, does not fit in a signed 64-bit register. x86-64 traps on it as on a zero divisor, AArch64 and RISC-V return a defined result, and in C it is undefined behaviour.

  6. How does an optimising compiler divide a 32-bit int by 10 without a divide instruction?

    It multiplies by 0x66666667, shifts the product right by 34 and adds a fix for negative values, which is exact for every 32-bit input. When optimising for size (-Oz) it keeps the shorter divide.

Key takeaways

  • Signed division on x86-64, AArch64 and RISC-V rounds towards zero, as C requires, and the remainder takes the sign of the dividend; unlike addition, division needs separate signed and unsigned instructions.
  • x86-64 div and idiv divide rdx:rax and write the quotient to rax and the remainder to rdx, so cqo must come before idiv and xor edx, edx before div; the wrong set-up returns a huge quotient or traps.
  • AArch64 has no remainder instruction and computes it with msub, while RISC-V has rem and remu, best placed right after the matching divide.
  • Division by zero and INT64_MIN / -1 raise #DE on x86-64, which ends the process with SIGFPE and status 136, while AArch64 and RISC-V return defined values, and C leaves both cases undefined.
  • A compiler optimising for speed replaces x / 10 with a multiplication by 0x66666667, a shift by 34 and a sign fix, which is exact for every 32-bit x; at -Oz it keeps the divide.

Terms in this lesson

multiplication by a reciprocal
Division by a constant d done as a multiplication by 2^S / d rounded up, a right shift by S and a fix for negative results; with the right S it is exact for every input of its width.
signal
A notification the operating system delivers to a process, such as SIGFPE after an integer divide fault; unhandled, it ends the process, and the shell reports status 128 plus the signal number.
truncating division
Integer division that rounds the quotient towards zero, as C and the signed divide instructions of x86-64, AArch64 and RISC-V do: -7 / 2 is -3, and the remainder takes the sign of the dividend.
undefined behaviour
What the C standard calls an operation for which it sets no requirements at all, such as dividing by zero; a compiler may assume a correct program never performs one.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.