Introduction
LEGv8 ISA는 Computer Organization And Design: ARM Edition에서 소개하는 ISA (Instruction Set Architecture)이다. 이 책이 Computer Organization And Design의 ARM 버전이지만 ARMv8의 전체 명령어 집합을 교육용으로 다루기에는 복잡하다. 그래서 책에서 LEGv8이라고 하는 ARMv8 ISA의 부분 집합을 정의한다. 부분 집합이라고는 하지만 일부 명령어는 ARMv8와 다르다. 일부 ARMv8 스펙은 불필요하게 프로세서를 복잡하게 만들기 때문에 LEGv8은 이런 명령어들을 좀더 단순화한다.
이 글에서는 LEGv8의 특징과 명령어 포맷에 대해 다룬다. 이 글의 모든 사진은 Computer Organization And Design: ARM Edition에서 발췌한 것이다.
LEGv8/ARMv8의 특징
LEGv8/ARMv8의 특징을 알아보자.
RISC architecture
RISC는 Reduced Instruction Set Computers의 약자다. 보통 단순한 ISA나 단순한 ISA를 사용하는 ISA를 지칭하는데 사용되는 말이다. 어떤 ISA가 단순하다는 것이 어떤 느낌인지 묘사해보기 위해 복잡한 명령어들을 사용하는 CISC (Complex Instruction Set Computers)의 예를 들어보자. 예를 들어서 행렬곱과 같은 연산을 RISC로 구현한다면 LOAD, STORE, ADD, SUB, ... 이러한 단순한 명령어들을 나열해서 구현해야 하지만 CISC는 행렬곱 전용 명령어를 하나 만드는 식이다.
따라서 CISC는 복잡한 연산을 처리하는 명령어를 구현함으로써 프로그램의 크기를 줄인다. 같은 프로그램을 더 적은 명령어로 만들 수 있다면 명령어를 불러오는 오버헤드도 적고, 캐시 히트율에서 이득을 볼 수 있다. 하지만 명령어가 복잡하면 그만큼 더 많은 사이클이 필요하다. 반대로 RISC는 더 단순한 명령어들을 사용해서 명령어의 수는 많아지지만, 명령어당 더 적은 사이클을 사용한다.
아무튼 ARMv8/LEGv8은 RISC 아키텍처이다.
Fixed length instructions
프로세서에게 명령어는 0과 1의 나열이다. 명령어를 해석하려면 명령어의 형식을 미리 정의해두어야 한다. 이때 명령어가 차지하는 길이가 가변적인 경우와 고정된 경우가 있다. x86이 명령어가 가변적인 대표적인 경우다. x86에서는 1 ~ 15바이트까지 명령어의 길이가 다양하다. 명령어의 길이를 가변적이게 하면, 허프만 코드처럼 자주 쓰이는 명령어의 길이를 짧게 함으로써 프로그램의 크기를 줄일 수 있다.
이에 비해 ARMv8/LEGv8은 명령어의 길이가 항상 4바이트로 크기가 고정이다. 이 경우에는 크기가 고정이라서 발생하는 여러가지 제약이 생긴다. 명령어의 크기가 4바이트라면 4바이트/8바이트짜리 직접값(Immediate)은 어떻게 레지스터로 불러오겠는가? 대신 고정 길이인 경우에는 가변 길이보다 명령어의 해석이 더 단순해진다. 참고로 ARMv8, MIPS과 같은 아키텍처는 더 짧은 버전인 2바이트 고정 길이를 사용하는 ISA(Thumb/MIPS 16e)도 정의한다. 대신 길이가 작아진 만큼 더 제약이 많아지겠지?
load-store, or register-register architecture
x86처럼 피연산자가 메모리가 될 수 있는 아키텍처를 register-memory architecture라고 한다. 예를 들어서 x86 assembly의 add에서는 "add <레지스터>,<메모리 주소>"처럼 덧셈 연산임에도 불구하고 하나의 피연산자는 레지스터, 다른 하나의 피연산자는 메모리 주소로 지정할 수 있다.
하지만 ARMv8, LEGv8과 같은 register-register architecture 또는 load-store architecture에서는 모든 피연산자가 레지스터다. 따라서 add를 할 때는 레지스터끼리만 덧셈을 할 수 있고 메모리에 접근하려면 별도의 load/store 명령어를 실행해야한다.
Registers
LEGv8에서는 총 32개의 레지스터를 정의한다.
X0-X17: caller-save registers
X0부터 X17까지는 함수 호출 시에 호출 전과 값이 같을 것이라는 보장이 없다. 따라서 함수 호출 이후에도 필요한 레지스터는 저장해야한다.
X19-X27: callee-save registers
X19부터 X27까지는 함수 호출 이후에도 보장이 되기 때문에, 호출되는 함수 쪽에서 이 레지스터를 사용한다면 미리 스택에 저장한 후에 함수가 끝나기 전에 되돌려놓아야 한다.
SP (X28):
SP는 스택의 TOP 주소를 저장하는 레지스터이다.
FP (X29): Frame Pointer Register
FP는 프레임의 주소를 가리킨다. 함수 호출을 하다보면 변수를 사용하기 위해서 SP를 빼면서 스택에 공간을 할당하는데, 이러한 정보들을 스택 프레임이라고 하며, FP는 해당 프레임의 주소를 가리킨다.
FP는 프로그램 작성에 반드시 필요한 존재는 아니지만, SP 같은 경우에는 스택의 공간을 할당하고 해제함에 따라서 바뀌기 때문에 SP로 어떤 변수를 가리키면 알아보기가 매우 어려울 것이다. 반면에 프레임의 주소는 변하지 않기 때문에 FP에 대한 상대 주소로 변수를 나타내면 더 이해하기 쉽다.
LR (X30): Link Register
LR은 리턴 주소를 저장하는 레지스터다. x86 ISA에서는 리턴 주소를 항상 스택에 저장하지만 ARMv8/LEGv8은 레지스터에 저장한다.
XZR (X31) : Register for zero value
XZR은 0을 나타내기 위한 레지스터이다. 이 레지스터는 항상 0의 값을 갖는다.
Instruction Format
명령어는 종류에 따라서 필요한 피연산자가 다양하기 때문에 종류에 따라서 형식도 달라진다.
R-format
ADD X9, X0, X1
R-format은 모든 피연산자가 레지스터이며, 첫 번째 레지스터가 destination, 두 번째와 세 번째 레지스터가 모두 source이다. 위의 ADD 명령어는 X9 = X0 + X1이라는 의미를 갖는다.
R-format은 다음으로 구성된다.
opcode: 명령어의 종류 (ADD, SUB, ...)
Rm: 두 번째 source 레지스터
shamt: Shift amount ㅡ 말 그대로 얼마나 shift를 할지를 나타낸다.
Rn: 첫 번째 source 레지스터
Rd: destination 레지스터
위의 예시에서는 Rm이 X1, Rn이 X2, Rd가 X9이고 shamt는 0이다.
D-format
LDUR X9, [X22, #64]
D-format은 load/store 명령에 사용된다. 이때 특정 레지스터 + 오프셋으로 주소를 표현한다. 여기서 Rn은 시작 주소를 담고있는 레지스터를 가리키고, address는 해당 레지스터로부터 떨어진 오프셋이다. 9비트이므로 레지스터로부터 +256, -256바이트가 떨어진 오프셋까지를 지정할 수 있다.
Rt는 destination일 수도 있고 source일 수도 있다. load 명령어 에서는 destination이, store 명령어 에서는 source가 된다.
I-format
I-format은 immediate (직접값)을 사용하는 명령어 포맷이다. 직접 값이 무엇인지를 잠깐 언급하자면, 명령어에서 상수를 사용할 때는 컴파일러가 생성하는 .rodata 섹션 같은 곳에 상수를 넣은 후 load 명령어로 불러오는 방법이 있을 것이다. 하지만 명령어 인코딩 자체에 그 상수 값을 넣을 수 있다면 load 명령어를 실행하지 않아도 된다. 상수가 명령어 포맷에 포함되어있으면 상수 자체가 명령어 페치 작업의 일부로써 불러와지기 때문이다. 따라서 이러한 값을 ㅡ 레지스터나 메모리에 있지 않은 ㅡ 직접 값이라고 한다.
아무튼 간에 I-format에서 immediate는 12비트 직접값을 나타내며 Rn은 source 레지스터, Rd는 destination 레지스터를 나타낸다.
B-format
B-format은 branch 명령어에 사용된다. 여기서 BR_address는 점프할 주소를 나타내며 opcode가 5비트, BR_address가 26비트이다. 하지만 BR_address는 바이트 단위가 아니라 워드(4바이트) 단위이므로 실제로는 28비트 크기의 주소를 표현할 수 있다. 이때 BR_address는 메모리 상의 절대주소가 아니라 PC-relative, 즉 프로그램 카운터에 대한 상대주소이다. 따라서 B-format에서는 PC + 128MiB, PC - 128MiB 범위의 주소로 점프할 수 있다. 그리고 BR_address가 워드 단위이기 때문에 점프할 주소는 반드시 워드 기준으로 정렬되어야(word-aligned) 한다.
CB-format
CB-format은 conditional branch 명령어에 사용된다. 이때 opcode는 8비트, COND_BR_address는 19비트, Rt는 5비트이다. COND_BR_address도 마찬가지로 워드 단위이다.
CBNZ X19, Exit
위의 명령어 같은 경우에는 Rt가 X19이며, X19 != 0인 경우 COND_BR_address가 가리키는 주소로 점프한다. COND_BR_address도 마찬가지로 프로그램 카운터에 대한 상대주소이다.
IW-format
IW-format은 Wide Immediate/Address를 위한 포맷이다. I-format의 immediate 필드나 D-format의 address 필드로는 부족할 때 더 큰 값을 만들기 위해서 사용할 수 있다. 이 포맷에 해당하는 명령어는 MOVK, MOVZ 두 개이다.
이때 opcode는 11비트, MOV_immediate는 16비트, Rd는 5비트이다.
MOVZ, MOVK 명령어는 Rd라는 destination 레지스터에, MOV_immediate는 logical shift left를 하여 저장한다. 이때 MOVZ는 저장하는 16비트를 제외한 48비트를 0으로 덮어쓰고, MOVK는 0으로 덮어쓰지 않고 기존의 값을 유지한다.
MOVK/MOVZ의 특이한 점은, shift할 크기가 opcode의 마지막 2비트로 표시된다는 점이다. 마지막 2비트가 00이면 0만큼 shift, 01이면 16만큼, 10이면 32만큼, 11이면 48만큼 shift하는 것이다.
왜 포맷마다 opcode의 길이가 다른가?
처음 Instruction Format을 보자마자 의문이 든 건 format에 따라서 opcode의 길이가 다르다는 것이다.물론 필요에 의해서 길이가 다른 거지만 이러면 어떤 format인지 알아내기가 좀 번거롭지 않을까?
그래서 opcode에는 간단한 규칙이 존재한다. 길이가 긴 opcode는 길이가 더 짧은 opcode로 시작할 수 없다. 예를 들어서 어떤 6비트짜리 명령어의 opcode가 010101이라고 할 때, 6비트보다 더 긴 opcode들은 010101로 시작하지 않아야 한다. 이 규칙을 추가함으로써 명령어의 디코딩이 단순해진다.
References
'Computer Architecture' 카테고리의 다른 글
PIPT / VIVT / VIPT 캐시와 flush_dcache_folio() (0) | 2022.06.25 |
---|---|
다시 정리하는 NUMA (1) | 2022.01.21 |
Instruction Set Architecture vs Microarchitecture (0) | 2021.12.01 |
메모리 계층별 대략적인 성능 비교 (0) | 2021.11.07 |
The Elements of Cache Programming Style (0) | 2021.10.07 |
댓글