Addressing Mode (AArch64)
AArch64 아키텍처 A64 명령셋에서 사용하는 주소 인덱싱 모드를 알아본다.
- Simple (또는Base Register Only)
- Base 레지스터에 담긴 주소를 참조한다.
- Offset (또는 Base Plus Offset)
- Base 레지스터에 오프셋을 더한 주소로 참조한다.
- Base 레지스터는 인덱스의 변화가 없다.
- Pre-Indexed
- Base 레지스터에 인덱스를 증가시킨 후 Base 레지스터의 주소를 참조한다.
- Post-Indexed
- Base 레지스터의 주소를 참조한 후 Base 레지스터에 인덱스를 증가
- Literal (또는 PC-Relative)
- 컴파일러 타임에 <label>까지의 offset 바이트 값을 <imm> 값으로 변환하여 명령을 엔코딩한다.
- offset 바이트 값과 <imm> 값은 1:1로 변환할 수도 있지만, 각 명령마다 다르다.
- 그 후 런타임에 PC + offset 주소를 참조한다.
- 컴파일러 타임에 <label>까지의 offset 바이트 값을 <imm> 값으로 변환하여 명령을 엔코딩한다.
- Immediate Addressing
- 상수로 입력한 직접(immediate) 절대 주소는 지원하지 않는다.
- 예) ldr x1, #0x1234567800000000 <- 불가능
다음 표는 정수 값을 가리키는 주소를 사용하여 참조하는 어셈블리 코드를 주소 인덱스 모드별로 예를 보여준다.
- 읽어오는 값은 int 형이므로 목적지 레지스터로 32비트 Wt 레지스터를 사용하였고, 베이스 레지스터로 64비트 X1 레지스터를 사용하였다.
주소 이동(branch)
Immediate
- 상수로 입력한 직접(immediate) 주소는 이동 뿐만이 아니라 어떠한 참조라도 AArch64 A64에서 사용할 수 없다.
- 예) b #0x1234567800000000
- Error: immediate out of range at operand 1 — `b 0x1234567800000000′
- 4바이트 명령에 8바이트 주소를 담아낼 공간이 없어 주소를 직접 지정하여 이동하는 방법은 지원하지 않는다.
Simple
- 레지스터에 담긴 주소로의 이동은 가능하다.
- 예) br x0
Literal (or PC-Relative)
- 각 명령이 지원하는 범위(Range) 이내에 위치한 <Label>을 참고할 수 있다.
- 예) b my_label
- = b #offset
다음 그림은 컴파일 타임에 branch 코드의 주소와 사용한 my_label이 위치한 주소와의 차이 값 offset을 산출하여 사용하는 모습을 보여준다.
AArch64 아키텍처에서의 A64 인코딩
AArch64 아키텍처에서 코드 재배치에 대해 정확히 이해하려면 명령(instruction) 사이즈가 고정되지 않은 CISC 구조와 고정되어 동일한 RISC 구조에 대한 이해와 인코딩 방법에 대해 알아야 한다.
AArch64 A64 명령 세트의 경우 4바이트 고정 길이 명령을 사용하고 다음과 같은 형식을 사용한다.
- 명령어, <오퍼랜드1>, <오퍼랜드2>, …
- 예) add w0, w1, #123, lsl12
4 바이트 고정길이를 사용하는 인코딩을 사용하고, 인코딩에 사용되는 각 필드들은 다음과 같다.
- op(operation)
- bits[28:25]으로 시작한다.
- operand
- 각 명령에 사용되는 인자들로 레지스터(register)나 상수(immediate)등이 있다.
- Rt 또는 Rd
- 첫 번째 오퍼랜드 레지스터이다. 타겟(target) 또는 목적지(destination)을 의미하는 레지스터로 64비트 레지스터인 경우 Xt와 32비트 레지스터인 경우 Wt를 사용한다.
- Rn, Rm
- 두 번째, 세 번째 오퍼랜드로 사용되는 레지스터이다.
Addressing 모드와 관련되어 대표적으로 자주 거론되는 명령들에 대한 자세한 인코딩 방법을 알아본다.
- 이동 명령
- B
- BL
- BR
- BLR
- 대입 명령
- MOV
- 로드 & 스토어 명령
- LDR
- STR
- 주소 참조 명령 (PC+relative)
- ADR
- ADRP
B 명령
- <label> 주소로 이동(branch) 한다.
- 컴파일러 타임에 현재 주소로부터 <label> 위치까지의 offset을 계산하여 4로 나눈 값을 imm26에 위치하게 만들어준다.
- 이와 같은 주소 인덱싱 방법을 Literal Addressing이라고 한다.
- 런타임에 PC(Program Counter) + immediate offset * 4 주소로 이동한다.
BL 명령
- <label> 주소로 서브루틴 콜을 수행한다. B 명령과 거의 같은 엔코딩 포맷을 사용하고, op만 1이다.
BR 명령
- 레지스터에 담긴 주소로 이동(Branch) 한다.
BLR 명령
- 레지스터에 담긴 주소로 서브루틴 콜을 수행한다. BR 명령과 거의 같은 엔코딩 포맷을 사용하고, op만 01이다.
MOV 명령
- Rd에 상수값을 대입한다.
- Rd에 Rm을 대입한다.
- Rd에 Rn + 상수값을 대입한다.
- Rd에 상수값을 shift 만큼 좌측 시프트한다.
- Rd에 상수값을 대입한다.
LDR 명령
- Rt에 Rn 주소가 가리키는 값을 읽어온 후, Rn 주소를 상수값만큼 증가시킨다.
- Rn 주소를 상수값만큼 증가시킨 후, Rn 주소가 가리키는 값을 Rt에 읽어온다.
- 1번과 유사하지만 +방향만 지원하며, 조금 더 큰 범위까지 지원한다.
- Rt에 <label> 주소가 가리키는 값을 읽어온다. <label> 주소는 +- 256K 범위내에서 지원한다.
- 컴파일 타임에 <lable> 까지의 offset을 산출하여 imm12에 대입한다.
- 주의: ldr <Xt>, =<label> 과 다르다.
주의: 다음 3 가지 명령은 각각 다른 결과를 가져온다.
- ldr x0, label
- label 주소에 있는 8바이트 값을 로드하여 x0에 담는다.
- ldr x0, =label
- 실제 존재하는 명령이 아니라 컴파일러가 사용하는 pesudo-instruction이다.
- 코드 인근에 8바이트 label 주소를 담을 영역을 만든다. 이 8 바이트 값에는 컴파일 타임에 label 주소 값을 담아둔다. 그리고 코드에서는 ldr x0, <8바이트 주소>에 해당하는 4바이트 코드를 만들어 사용한다. 결국 컴파일 타임에 생성된 label 주소를 로드하여 알아오는 코드를 만들어낸다.
- adr x0, label
- 런타임에 읽어들인 label 주소를 x0에 담는다.
다음 샘플 코드를 통해 각각의 결과를 확인해본다.
./test.S
.text
.globl _start
.align 2
_start:
adrp x0, msg // 1. 아래 명령과 같이 사용하여 msg의 런타임 주소를 알아온다.
add x0, x0, :lo12:msg //
adr x1, msg // 2. msg의 런타임 주소를 알아온다.
ldr x2, msg // 3. msg 주소에 있는 8바이트 값을 로드한다.
ldr x3, =msg // 3. 컴파일 타임에 계산된 msg의 주소를 알아온다.
/* sys_exit 코드 */
mov x0, 123
mov x8, 93
svc #0
.data
msg:
.quad 10
gdb 디버거를 통해 실행된 결과를 레지스터 값으로 확인한다.
$ gdb ./test (gdb) ┌──Register group: general─────────────────────────────────────────────────────────────────────────────────────────────┐ │x0 0x4100d8 4260056 x1 0x4100d8 4260056 │ │x2 0xa 10 x3 0x4100d8 4260056 │ │x4 0x0 0 x5 0x0 0 │ │ | | (...생략...) | │ | │x28 0x0 0 x29 0x0 0 │ │x30 0x0 0 sp 0x7ffffff4d0 0x7ffffff4d0 │ │pc 0x4000c4 0x4000c4 <_start+20> cpsr 0x200000 [ EL=0 SS ] │ │fpsr 0x0 0 fpcr 0x0 0 │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ B+ │0x4000b0 <_start> adrp x0, 0x410000 │ │0x4000b4 <_start+4> add x0, x0, #0xd8 │ │0x4000b8 <_start+8> adr x1, 0x4100d8 │ │0x4000bc <_start+12> ldr x2, 0x4100d8 │ │0x4000c0 <_start+16> ldr x3, 0x4000d0 <_start+32> │ >│0x4000c4 <_start+20> mov x0, #0x7b // #123 │ │0x4000c8 <_start+24> mov x8, #0x5d // #93 │ │0x4000cc <_start+28> svc #0x0 │ │0x4000d0 <_start+32> .inst 0x004100d8 ; undefined │ │0x4000d4 <_start+36> .inst 0x00000000 ; undefined | │ | | (...생략...) | │ | │0x4100d8 .inst 0x0000000a ; undefined │ │0x4100dc .inst 0x00000000 ; undefined |
STR 명령
- Rt 값을 Rn 주소가 가리키는 위치에 기록한 후, Rn 주소를 상수값만큼 증가시킨다.
- Rn 주소를 상수값만큼 증가시킨 후, Rn 주소가 가리키는 위치에 Rt 값을 기록한다.
- 1번과 유사하지만 +방향만 지원하며, 조금 더 큰 범위까지 지원한다.
- Rt 값을 <label> 위치에 기록한다. <label> 주소는 +- 256K 범위내에서 지원한다.
- 컴파일 타임에 <lable> 까지의 offset을 산출하여 imm12에 대입한다.
ADR 명령
- 런타임에 현재 동작중인 PC를 기준으로 <label>이 위치한 주소를 읽어온다.
- <label> 까지의 범위는 최대 +-1M로 제한된다.
- 컴파일 타임에 <label>까지의 offset을 계산하여 #imm(immhi:immlo)에 사용한다.
- 컴파일 타임에 생성되는 주소를 사용하지 않으므로 MMU가 off된 상태에서 <label> 물리 주소를 알아올 수 있다.
ADRP 명령
- 런타임에 현재 동작중인 PC를 기준으로 <label>이 위치한 주소 페이지(4K 단위)를 읽어온다.
- <label> 까지의 범위는 최대 +-4G로 제한된다.
- 컴파일 타임에 <label>까지의 offset을 계산하여 이를 4K로 나눈 값을 #imm(immhi:immlo)에 사용한다.
- ADR과 유사하게 동작하지만 조금 더 큰 범위를 지원하고, 4K 미만의 주소를 추가로 얻으려면 다음과 같은 명령을 추가로 사용해야 한다.
- add X0, X0, :lo12:<label>
참고
- ELF Relocations (AArch64) | 문c
- kernel/head.S – ARM64 (new for v5.10) | 문c
- Arm A64 Instruction Set Architecture (2021) | ARM – 다운로드 pdf
- Armv8-A Instruction Set Architecture (2020) | ARM – 다운로드 pdf
- ARMv8-A A64 ISA Overview (2015) | ARM – 다운로드 pdf
- ARMv8 Instruction Set Overview (2013) | ARM – 다운로드 pdf
- ARMv8 instructions set analysis | – 다운로드 pdf
- Relocations: fantastic symbols, but where to find them? (2020) | Hell Oh Entropy
- ELF for the ARM 64-bit Architecture (AArch64, 2013) | 다운로드 pdf