Skip to the content.

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줄이 연속으로 나오면 대부분 이런 상수 로딩 패턴이라고 판단하면 됨.