MOV / MOVN / MOVZ / MOVK 명령어 (Move 계열)
Summary
정의: 레지스터에 즉치값(immediate) 또는 다른 레지스터 값을 이동(설정)하는 명령어군. MOV는 여러 하위 명령어의 별칭(alias) 역할을 하며, 실제로는 MOVZ/MOVN/MOVK가 하드웨어 레벨의 진짜 명령어임.
핵심 배경 — 왜 4개나 필요한가
근본 문제: ARM64 명령어는 고정 32비트 길이이므로, 하나의 명령어 안에 64비트 임의 값을 통째로 담을 공간이 없음. 이 때문에 큰 즉치값을 레지스터에 로드하려면 값을 16비트씩 나누어 여러 명령어로 조합해야 함.
분할 단위: 64비트 레지스터를 16비트씩 4개 구간(chunk)으로 나눔:
비트: 63-48 | 47-32 | 31-16 | 15-0
구간: #3 | #2 | #1 | #0
MOVZ
Move Wide with Zero
정의: 지정한 16비트 구간에 즉치값을 넣고, 나머지 모든 비트는 0으로 클리어하는 명령어.
문법:
MOVZ Xd, #imm16, LSL #shift // shift: 0, 16, 32, 48 중 하나
예시:
movz x0, #0x1234 // x0 = 0x0000000000001234
movz x0, #0x1234, lsl #16 // x0 = 0x0000000012340000
movz x0, #0x1234, lsl #32 // x0 = 0x0000123400000000
핵심: MOVZ는 항상 나머지 비트를 0으로 밀어버리므로, 여러 구간을 조합하려는 시작점으로 사용됨.
MOVK
Move Wide with Keep
정의: 지정한 16비트 구간만 값을 갱신하고, 나머지 비트는 기존 값을 그대로 유지하는 명령어.
문법:
MOVK Xd, #imm16, LSL #shift
MOVZ와의 결정적 차이: MOVZ는 “덮어쓰기+나머지 클리어”, MOVK는 “덮어쓰기+나머지 유지”. 그래서 첫 구간은 MOVZ, 이후 구간들은 MOVK로 조합하는 것이 표준 패턴임.
64비트 임의 값 로드 예시:
// 목표: x0 = 0x123456789ABCDEF0
movz x0, #0xDEF0 // x0 = 0x000000000000DEF0
movk x0, #0x9ABC, lsl #16 // x0 = 0x000000009ABCDEF0 (기존 유지+갱신)
movk x0, #0x5678, lsl #32 // x0 = 0x0000567890ABCDEF0... 아래 정정
movk x0, #0x1234, lsl #48 // x0 = 0x123456789ABCDEF0
만약 MOVK 대신 MOVZ를 반복 사용하면: 매번 나머지 비트가 0으로 클리어되어 이전 단계 값이 모두 사라짐 — 잘못된 결과가 나옴. 이것이 MOVZ와 MOVK를 반드시 구분해서 써야 하는 이유임.
MOVN
Move Wide with NOT
정의: 지정한 16비트 값을 비트 반전(NOT) 시킨 후, 나머지 비트는 1로 채워서 레지스터에 로드하는 명령어.
문법:
MOVN Xd, #imm16, LSL #shift
동작: Xd = ~(imm16 << shift) — 결과적으로 imm16 이외의 모든 비트가 1로 채워짐.
존재 이유: 음수(negative) 값을 효율적으로 로드하기 위함. 64비트 음수는 상위 비트가 대부분 1로 채워지는 2의 보수 구조이므로, MOVZ(나머지 0으로 채움)보다 MOVN(나머지 1로 채움)이 훨씬 적은 명령어로 표현 가능한 경우가 많음.
예시:
movn x0, #0 // x0 = ~0x0000000000000000 = 0xFFFFFFFFFFFFFFFF = -1
movn x0, #5 // x0 = ~5 = 0xFFFFFFFFFFFFFFFA = -6
실전 활용: 컴파일러가 -1, -6 같은 작은 음수 상수를 로드할 때, MOVZ + 여러 MOVK 조합보다 MOVN 한 줄로 끝나는 경우가 많아 최적화 목적으로 자주 선택됨.
MOV
별칭(Alias)으로서의 정체
핵심: 사실 MOV라는 이름의 진짜 하드웨어 명령어는 레지스터-레지스터 이동의 경우만 존재하고, 즉치값 이동의 경우 MOV는 어셈블러가 상황에 맞는 실제 명령어로 자동 변환해주는 편의 표기(pseudo-instruction alias)임.
레지스터 간 이동 (진짜 MOV, 실제로는 ORR의 별칭)
MOV Xd, Xn
내부 진실: 이것도 사실 ORR Xd, XZR, Xn (제로 레지스터와의 OR 연산)의 별칭임. ARM64 설계상 “값 복사” 전용 명령어를 따로 만들지 않고 기존 논리연산을 재활용함.
즉치값 이동 (어셈블러가 자동 선택)
mov x0, #0x1234 // 어셈블러가 자동으로 movz x0, #0x1234 로 변환
mov x0, #0xFFFFFFFFFFFFFFF0 // 어셈블러가 자동으로 movn x0, #0xF 로 변환 (더 효율적이므로)
어셈블러의 판단 기준: 값이 16비트 한 구간에 들어가면 MOVZ, 음수 패턴에 최적화되면 MOVN, 여러 구간이 필요하면 MOVZ+MOVK 조합으로 자동 확장함. 개발자는 대부분 MOV만 쓰고, 실제 어떤 명령어로 바뀌었는지는 디스어셈블(objdump 등)로 확인 가능함.
MVN
Move NOT, 별개 명령어
주의: MOVN과 이름이 비슷해 혼동하기 쉬우나 완전히 다른 명령어임.
MVN (Move Not, 레지스터 버전):
MVN Xd, Xn {, shift #amount}
동작: Xd = NOT(Xn) — 즉치값이 아니라 레지스터 값 전체를 비트 반전함.
mov x0, #0x0F
mvn x1, x0 // x1 = ~0x0F = 0xFFFFFFFFFFFFFFF0
MOVN과의 차이:
| 명령어 | 오퍼랜드 | 대상 |
|---|---|---|
MOVN |
16비트 즉치값 | 즉치값을 반전 후 나머지 1로 채워 로드 |
MVN |
레지스터 | 이미 레지스터에 있는 값 전체를 반전 |
MVN의 실체: 이것도 사실 ORN Xd, XZR, Xn (제로 레지스터와의 OR-NOT)의 별칭임 — MOV가 ORR의 별칭인 것과 같은 설계 철학.
예제 코드 — 전체 조합 실습
.global _start
.align 2
.text
_start:
// 1. 작은 양수 값 (MOVZ 자동 선택)
mov x0, #100 // → movz x0, #100
// 2. 작은 음수 값 (MOVN 자동 선택, 어셈블러 최적화)
mov x1, #-1 // → movn x1, #0
// 3. 64비트 임의 값 (MOVZ+MOVK 조합, 수동 구성 필요 시)
movz x2, #0xDEF0
movk x2, #0x9ABC, lsl #16
movk x2, #0x5678, lsl #32
movk x2, #0x1234, lsl #48
// x2 = 0x123456789ABCDEF0
// 4. 레지스터 간 이동
mov x3, x2 // x3 = x2 (실제로는 orr x3, xzr, x2)
// 5. 레지스터 비트 반전
mvn x4, x3 // x4 = ~x3
mov x0, #0
mov x16, #1
svc #0x80
종합 비교표
| 명령어 | 오퍼랜드 종류 | 동작 | 나머지 비트 처리 | 실체(별칭 관계) |
|---|---|---|---|---|
MOVZ |
16비트 즉치값 | 지정 구간 설정 | 0으로 클리어 | 독립 명령어 |
MOVK |
16비트 즉치값 | 지정 구간만 갱신 | 기존값 유지 | 독립 명령어 |
MOVN |
16비트 즉치값 | 반전 후 로드 | 1로 채움 | 독립 명령어 |
MOV (즉치) |
즉치값 | 상황별 자동 변환 | - | MOVZ/MOVN/MOVK 조합의 별칭 |
MOV (레지스터) |
레지스터 | 값 복사 | - | ORR의 별칭 |
MVN |
레지스터 | 비트 반전 | - | ORN의 별칭 |
ADRP+ADD 문서와의 연결 — 실전에서 함께 등장하는 패턴
이전에 다룬 문자열/전역 데이터 주소 로드 패턴(ADRP+ADD)과 달리, 컴파일 시점에 알 수 없는 임의의 64비트 상수값(암호화 키, 매직 넘버, 큰 정수 리터럴 등)을 레지스터에 직접 로드해야 할 때 바로 이 MOVZ+MOVK 4단계 조합이 사용됨. 실제 macOS 바이너리 디스어셈블 시, 함수 프롤로그 근처에서 MOVZ+MOVK 3~4줄이 연속으로 나오면 대부분 이런 상수 로딩 패턴이라고 판단하면 됨.