Skip to the content.

ADR

Summary

ADR (Address of Relative) 은 PC(Program Counter)에 상대적인 주소를 레지스터에 로드하는 명령어야. 즉, 현재 명령어 위치를 기준으로 가까운 곳에 있는 데이터나 라벨의 주소값을 계산해서 레지스터에 넣어줘.

핵심 특징

문법

ADR Xd, label

간단한 예제 (macOS ARM64)

hello.s 라는 파일로 저장해봐:

.global _start
.align 2

.text
_start:
    // ADR로 message 라벨의 주소를 x0에 로드
    adr x0, message

    // strlen 계산을 위해 x1에 메시지 길이 저장 (예시로 하드코딩)
    mov x1, #14

    // write(1, message, 14) 시스템 콜 호출
    mov x2, x1          // length
    mov x1, x0          // buffer (message 주소)
    mov x0, #1           // fd = stdout
    mov x16, #4           // syscall number for write (macOS)
    svc #0x80

    // exit(0)
    mov x0, #0
    mov x16, #1           // syscall number for exit
    svc #0x80

.data
message:
    .ascii "Hello, ARM64!\n"

컴파일 & 실행 (macOS, Apple Silicon)

as -o hello.o hello.s
ld -o hello hello.o -lSystem -syslibroot `xcrun -sdk macosx --show-sdk-path` -e _start -arch arm64
./hello

ADR vs ADRP 비교

명령어 범위 용도
ADR ±1MB 같은 함수/근처 데이터 참조
ADRP + ADD ±4GB (페이지 단위) 멀리 떨어진 전역 데이터, 문자열 상수 등

실제로 macOS에서 Clang이 생성하는 어셈블리를 보면, 데이터 섹션이 코드와 멀리 떨어져 있는 경우가 많아서 ADR보다 ADRP + ADD 조합이 훨씬 자주 쓰이는 걸 볼 수 있을 거야. 예를 들면:

adrp x0, message@PAGE
add  x0, x0, message@PAGEOFF

이 조합이 실제 macOS 바이너리에서 문자열 상수 로드할 때 표준 패턴이야.


기타 사항

“1MB”라는 숫자가 실제로 몇 줄(명령어 개수)로 환산해보기

바이트 → 명령어 개수로 환산

ARM64에서 모든 명령어는 고정 4바이트야 (Thumb 모드 같은 가변 길이가 없어). 그래서:

1MB = 1,048,576 바이트
1,048,576 바이트 ÷ 4바이트 = 262,144 개의 명령어

즉, ADR은 앞뒤로 약 26만 개의 명령어까지 도달할 수 있어. 방향 포함하면 총 ±262,144개, 대략 52만 줄 정도의 범위인 셈이지.

그래서 실전에서는?

사실상 거의 모든 “일반적인 코드”는 이 범위 안에 들어와. 100줄, 1000줄, 심지어 1만 줄짜리 함수가 있어도 ADR로 커버가 돼. 웬만한 단일 실행 파일(하나의 .o 파일, 하나의 작은 프로그램) 내에서는 거의 문제가 안 생겨.

그럼 언제 문제가 되나?

실무에서 ADR이 아니라 ADRP+ADD를 써야 하는 이유는 “코드가 길어서”가 아니라 메모리 레이아웃 때문이야:

상황 설명
코드 섹션과 데이터 섹션이 멀리 떨어짐 링커가 .text(코드)와 .data(데이터)를 서로 다른 메모리 영역에 배치하는데, 이 거리가 1MB를 훌쩍 넘는 경우가 흔함
동적 라이브러리 참조 외부 라이브러리(libSystem 등)의 심볼은 아예 다른 바이너리에 있어서 거리를 예측 불가
큰 프로그램의 여러 오브젝트 파일 링크 여러 .o 파일이 합쳐지면 섹션 배치가 커져서 1MB를 넘기 쉬움

그래서 컴파일러(Clang)는 “이 참조가 1MB 안에 들어올지 아닐지”를 코드 크기가 아니라 “같은 함수 안 지역 라벨인가, 아니면 전역 데이터/외부 심볼인가”로 판단해서 거의 기계적으로 ADRP+ADD를 기본값으로 써버려. 즉, “안전하게 항상 큰 범위를 커버하는 쪽”을 택하는 거지.

직관적으로 정리하면

그래서 실전 어셈블리에서 ADR을 순수하게 보는 경우는 생각보다 드물고, ADRP+ADD가 표준 패턴처럼 보이는 이유가 바로 이거야. 코드 줄 수보다는 “참조하는 대상이 어느 섹션에 있는지”가 핵심 기준이라고 보면 돼!