ADR
Summary
ADR (Address of Relative) 은 PC(Program Counter)에 상대적인 주소를 레지스터에 로드하는 명령어야. 즉, 현재 명령어 위치를 기준으로 가까운 곳에 있는 데이터나 라벨의 주소값을 계산해서 레지스터에 넣어줘.
핵심 특징
- 범위 제한: ADR은 ±1MB 범위 내의 주소만 참조할 수 있어. (PC 기준 상대 offset이 21비트로 인코딩되기 때문)
- 위치 독립적(PIC): 절대주소가 아니라 PC 기준 상대주소를 계산하기 때문에, 코드가 메모리 어디에 로드되든 정상 작동해. macOS는 ASLR(주소 랜덤화)을 쓰기 때문에 이런 방식이 필수적이야.
- 더 먼 거리: 만약 ±1MB를 벗어나는 주소를 참조해야 하면
ADRP+ADD조합을 써야 해. (ADRP는 4KB 페이지 단위로 ±4GB 범위를 커버)
문법
ADR Xd, label
Xd: 결과를 저장할 64비트 레지스터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 = “다른 섹션에 있는 데이터(문자열, 전역 변수)나 외부 심볼 참조할 때” (사실상 대부분의 경우)
그래서 실전 어셈블리에서 ADR을 순수하게 보는 경우는 생각보다 드물고, ADRP+ADD가 표준 패턴처럼 보이는 이유가 바로 이거야. 코드 줄 수보다는 “참조하는 대상이 어느 섹션에 있는지”가 핵심 기준이라고 보면 돼!