MADD / MSUB 계열 — 곱셈-덧셈 결합 명령어군
Summary
정의: 곱셈과 덧셈(또는 뺄셈)을 한 명령어로 결합하여 수행하는 FMA(Fused Multiply-Add) 계열 명령어. 특히 MSUB는 나머지(remainder) 연산 구현의 핵심 부품
MADD
Multiply-Add
정의: Xd = Xa + (Xn × Xm) — 곱셈 결과를 더함.
문법:
MADD Xd, Xn, Xm, Xa
예시:
mov x0, #3
mov x1, #4
mov x2, #10
madd x3, x0, x1, x2 // x3 = x2 + (x0 × x1) = 10 + 12 = 22
MSUB
Multiply-Subtract
정의: Xd = Xa - (Xn × Xm) — 곱셈 결과를 뺌. 나머지 연산의 핵심 부품임.
문법:
MSUB Xd, Xn, Xm, Xa
예시:
mov x0, #3
mov x1, #4
mov x2, #20
msub x3, x0, x1, x2 // x3 = x2 - (x0 × x1) = 20 - 12 = 8
나머지(Remainder/Modulo) 연산 구현 — 핵심 질문 답변
중요한 사실: ARM64에는 나머지 연산 전용 명령어가 존재하지 않음. x % y를 직접 계산하는 MOD류 명령어는 없으며, 대신 나눗셈 후 MSUB로 역산하는 2단계 조합으로 구현함.
수학적 원리:
remainder = dividend - (quotient × divisor)
구현 절차:
SDIV(signed) 또는UDIV(unsigned)로 몫(quotient) 계산MSUB로dividend - (quotient × divisor)계산 → 나머지
예시 — signed 나머지 연산 (a % b):
.global _start
.align 2
.text
_start:
mov x0, #17 // dividend (a)
mov x1, #5 // divisor (b)
sdiv x2, x0, x1 // x2 = quotient = 17 / 5 = 3
msub x3, x2, x1, x0 // x3 = x0 - (x2 × x1) = 17 - (3×5) = 2
// x3 = 2 (17 % 5 = 2)
mov x0, #0
mov x16, #1
svc #0x80
unsigned 버전 (UDIV 사용):
udiv x2, x0, x1 // unsigned 몫
msub x3, x2, x1, x0 // 나머지 계산은 signed/unsigned 동일한 MSUB 사용
핵심 정리: C언어의 % 연산자를 컴파일하면, 어셈블리 레벨에서는 항상 SDIV(또는 UDIV) + MSUB 2줄 조합으로 변환됨. 이 패턴을 알아두면 디스어셈블된 코드에서 “SDIV 다음에 바로 MSUB가 나오면 이건 나머지 연산이구나”라고 즉시 판단 가능함.
MNEG
Multiply-Negate — 별칭
정의: Xd = -(Xn × Xm) — MSUB에서 Xa를 0으로 고정한 형태의 별칭(alias).
MNEG Xd, Xn, Xm
실체: MSUB Xd, Xn, Xm, XZR와 동일함.
mov x0, #3
mov x1, #4
mneg x2, x0, x1 // x2 = -(3×4) = -12
MUL
순수 곱셈 (Accumulate 없는 버전)
정의: 덧셈/뺄셈 없이 순수 곱셈만 수행하는 명령어. 사실은 MADD의 별칭임.
MUL Xd, Xn, Xm
실체: MADD Xd, Xn, Xm, XZR와 동일함 (Xa 자리에 제로 레지스터).
mov x0, #6
mov x1, #7
mul x2, x0, x1 // x2 = 6 × 7 = 42 (= madd x2, x0, x1, xzr)
관련 명령어 — Long/High Multiply 계열
배경: 64비트 × 64비트 곱셈의 진짜 수학적 결과는 최대 128비트가 될 수 있음. 하지만 레지스터는 64비트뿐이므로, 결과의 “하위 64비트”와 “상위 64비트”를 따로 계산하는 명령어가 필요함.
SMULL
UMULL
32×32 → 64비트 확장 곱셈
정의: 32비트 두 값을 곱해 64비트 결과를 정확하게(overflow 없이) 얻는 명령어.
SMULL Xd, Wn, Wm // signed: Xd = (int64)Wn × (int64)Wm
UMULL Xd, Wn, Wm // unsigned: Xd = (uint64)Wn × (uint64)Wm
용도: 32비트 곱셈에서 오버플로우가 우려될 때, 결과를 64비트로 안전하게 받는 목적.
SMADDL / SMSUBL / UMADDL / UMSUBL
Long + Accumulate 결합
정의: SMULL/UMULL에 누적(Add/Sub)까지 결합한 버전.
| 니모닉 | 연산 |
|---|---|
SMADDL |
Xd = Xa + (signed)(Wn × Wm) |
SMSUBL |
Xd = Xa - (signed)(Wn × Wm) |
UMADDL |
Xd = Xa + (unsigned)(Wn × Wm) |
UMSUBL |
Xd = Xa - (unsigned)(Wn × Wm) |
smaddl x0, w1, w2, x3 // x0 = x3 + (signed 64비트 확장된 w1×w2)
실전 활용: 32비트 정수 연산이 많은 알고리즘(체크섬, 해시 계산)에서 중간 결과 오버플로우를 방지하면서 누적 계산할 때 사용됨.
SMULH
UMULH
128비트 곱셈의 상위 64비트
정의: 64비트 × 64비트 곱셈의 진짜 128비트 결과 중 상위(high) 64비트만 얻는 명령어.
SMULH Xd, Xn, Xm // signed 128비트 곱셈의 상위 64비트
UMULH Xd, Xn, Xm // unsigned 128비트 곱셈의 상위 64비트
하위 64비트는 어떻게?: 하위 64비트는 그냥 일반 MUL(오버플로우 무시)로 얻으면 됨. 즉, 128비트 곱셈 전체 결과가 필요하면 MUL(하위)과 SMULH/UMULH(상위) 두 개를 함께 사용해야 함.
mul x0, x1, x2 // 128비트 결과의 하위 64비트
umulh x3, x1, x2 // 128비트 결과의 상위 64비트
// {x3:x0}가 완전한 128비트 곱셈 결과
용도: 128비트 정수 연산, 암호화 알고리즘(RSA, 큰 수 곱셈), 해시 함수 등 정밀한 대수(big number) 연산에서 필수적으로 사용됨.
종합 정리표
| 니모닉 | 연산 | 비고 |
|---|---|---|
MUL |
Xn × Xm |
MADD의 별칭(Xa=XZR) |
MADD |
Xa + Xn×Xm |
기본 FMA |
MSUB |
Xa - Xn×Xm |
나머지 연산의 핵심 |
MNEG |
-(Xn×Xm) |
MSUB의 별칭(Xa=XZR) |
SMULL / UMULL |
32×32→64비트 | 확장 곱셈 |
SMADDL / UMADDL |
32×32→64비트 + 누적 | 확장 곱셈+덧셈 |
SMSUBL / UMSUBL |
32×32→64비트 - 누적 | 확장 곱셈+뺄셈 |
SMULH / UMULH |
64×64 결과의 상위64비트 | 128비트 곱셈용 |
나머지 연산 최종 요약 (핵심 재확인)
a % b 를 계산하려면:
SDIV(or UDIV) quotient, a, b // 몫 계산
MSUB remainder, quotient, b, a // remainder = a - (quotient × b)
결론: MSUB는 단순히 “곱셈+뺄셈”이라는 범용 명령어이지만, ARM64가 별도의 나머지 연산자를 두지 않은 설계 철학상, 사실상 나머지 연산 구현의 표준 관용구(idiom) 로 자리잡은 명령어임. 디스어셈블된 코드에서 SDIV 바로 다음 줄에 동일 레지스터를 인자로 하는 MSUB가 나오면, 이는 거의 100% C언어의 % 연산자가 컴파일된 결과라고 판단해도 무방함.