2.1 서론
- ISA(Instruction Set Architecture) = 컴퓨터(정확히는 CPU)가 이해하는 명령어의 집합(레퍼토리). "이번 시험에 냄." 교과서 앞의 그린 카드 = ARM의 ISA.
- ARM = Advanced RISC Machine(ARM Holdings, 영국). RISC = Reduced Instruction Set Computing vs CISC = Complex Instruction Set Computing(Intel x86). Reduced는 CISC 대비 상대적 표현.
- ARMv8 = 64비트, v7까지 32비트. 임베디드 코어 시장 대부분.
- LEGv8 = ARMv8의 부분집합(교육용), LEG = Less Extrinsic Garrulity. 시험에서도 LEGv8 기준.
- word = 32비트(4바이트), doubleword = 64비트(8바이트). byte 1, halfword 2 — 시험에서 안 알려줌.
2.2 하드웨어 연산
- 산술 명령 = 피연산자 3개(소스 2 + 목적지 1): ADD a, b, c → a = b + c.
- 설계 원칙 1: Simplicity favors regularity (규칙성 → 구현 단순 → 저비용 고성능).
- 용어: operation(연산, OP = operation) + operand(피연산자). 연산 종류 = 산술(arithmetic) · 논리(logical) · 조건/분기(branch) · 메모리(load/store).
2.3 피연산자
레지스터 (32 × 64비트)
레지스터 용도
| X0~X7 |
프로시저 인자/결과 |
| X8 |
간접 결과 위치 |
| X9~X15 |
temporary (callee가 보존 안 함) |
| X16~X17 (IP0·IP1) |
링커 scratch 또는 temporary |
| X18 |
platform register 또는 temporary |
| X19~X27 |
saved (쓰면 callee가 저장·복원) |
| X28 (SP) |
Stack Pointer |
| X29 (FP) |
Frame Pointer |
| X30 (LR) |
Link Register = 복귀 주소 |
| XZR (31번) |
항상 0 |
- W0~W30 = X의 하위 32비트. 레지스터 용도 규약(X9~ temporary, X19~ saved)은 시험에서 주어짐. XZR = 11111 = 31은 암기.
- 설계 원칙 2: Smaller is faster (레지스터 수가 적을수록 빠름. 메모리는 수백만 위치).
- 레지스터가 메모리보다 빠르므로 컴파일러는 변수를 최대한 레지스터에.
🆕 예: f = (g + h) − (i + j), f~j = X19~X23
ADD X9, X20, X21 // t0 = g + h
ADD X10, X22, X23 // t1 = i + j
SUB X19, X9, X10 // f = t0 - t1
메모리 피연산자
- 배열·구조체·동적 데이터는 메모리. 연산하려면 load → 연산 → store.
- 메모리는 바이트 주소. LEGv8은 명령어와 스택을 제외하면 정렬(alignment)을 요구하지 않음.
- Big endian = 최상위 바이트(MSB 쪽)를 가장 낮은 주소에. Little endian = 최하위 바이트를 가장 낮은 주소에.
- 예: doubleword ABCDEF0123456789를 16번지(16~23)에 저장 → big endian이면 16번지 = AB, little endian이면 16번지 = 89.
예: A[12] = h + A[8], h = X21, A 시작 주소 = X22
LDUR X9, [X22, #64] // A[8]: 8 × 8바이트 = 64. U = unscaled
ADD X9, X21, X9
STUR X9, [X22, #96] // A[12]: 12 × 8 = 96
즉치 피연산자
- ADDI X22, X22, #4. 작은 상수가 흔하므로 load 없이 명령어에 넣음.
- 강의자료의 표기: "Design Principle 3: Make the common case fast". 교과서의 설계 원칙 3은 "좋은 설계는 적당한 절충이 필요하다"(p89, 명령어 형식). 둘 다 알아둘 것.
2.4 부호 있는 수와 부호 없는 수
- Unsigned n비트 범위: 0 ~ 2^n − 1 (32비트: 0 ~ 4,294,967,295).
- 2's complement n비트 범위: −2^(n−1) ~ 2^(n−1) − 1 (32비트: −2,147,483,648 ~ 2,147,483,647). MSB = 부호 비트.
- 특수 값: 0 = 000…0, −1 = 111…1, 최소 = 100…0, 최대 = 011…1. 모두 1인 비트 = unsigned에서는 최댓값("외워요").
- −(−2^(n−1))은 표현 불가. 음이 아닌 수는 signed·unsigned 표현이 같음.
- 부호 반전 = 보수 + 1. 예: +2 = 0…010 → −2 = 1…101 + 1 = 1…110.
- 부호 확장(Sign extension) = 부호 비트를 왼쪽으로 복사. 8→16비트: +2 = 0000 0010 → 0000 0000 0000 0010, −2 = 1111 1110 → 1111 1111 1111 1110.
- LDURSB = 부호 확장해 load, LDURB = 0으로 확장해 load.
2.5 명령어의 컴퓨터 내부 표현
- 명령어 = 32비트 고정 길이 이진 코드(기계어). 형식은 적게, opcode로 구분.
- 16진수 한 자리 = 4비트. 예: eca8 6420 = 1110 1100 1010 1000 0110 0100 0010 0000.
형식 필드 (비트 수) 쓰임새
| R |
opcode 11 |
Rm 5 |
| I |
opcode 10 |
immediate 12 |
| D |
opcode 11 |
address 9 |
| B |
opcode 6 |
address 26 |
| CB |
opcode 8 |
address 19 |
- Rn = 첫 번째 소스, Rm = 두 번째 소스, Rd = 목적지, shamt = 시프트 양, Rt = load의 목적지/store의 소스.
- opcode : ADD 1112, SUB 1624, ADDI 580, SUBI 836, LDUR 1986, STUR 1984.
인코딩 예
ADD X9, X20, X21
opcode 1112 | Rm 21 | shamt 0 | Rn 20 | Rd 9
10001011000 | 10101 | 000000 | 10100 | 01001
= 1000 1011 0001 0101 0000 0010 1000 1001 = 0x8B150289
- 내장 프로그램: 명령어도 데이터처럼 이진수로 메모리에 저장 → 프로그램이 프로그램을 다룸(컴파일러, 링커). Binary compatibility = 호환성(암기 지시).
2.6 논리 연산 명령어
연산 C LEGv8 쓰임새
| Shift left |
<< |
LSL |
0으로 채움, i비트 = ×2^i |
| Shift right |
>> |
LSR |
0으로 채움, ÷2^i (unsigned만) |
| AND |
& |
AND, ANDI |
마스크(일부 비트만 남기기) |
| OR |
|
|
ORR, ORRI |
| NOT |
~ |
EOR, EORI |
모두 1인 값과 EOR = NOT |
- 예: X19 = 9 → LSL X11, X19, #4 → X11 = 0x90 = 144. 시프트는 R 형식의 shamt 사용.
2.7 판단을 위한 명령어
- CBZ reg, L1(reg == 0이면 분기), CBNZ reg, L1(reg ≠ 0이면), B L1(무조건). 슬라이드 제목은 conditional operation이지만 실체는 branch.
if–else: f~j = X19~X23
// if (i == j) f = g + h; else f = g - h;
SUB X9, X22, X23 // X9 = i - j
CBNZ X9, Else // i != j 이면 Else
ADD X19, X20, X21 // f = g + h
B Exit
Else: SUB X19, X20, X21 // f = g - h
Exit:
while: i = X22, k = X24, save 시작 주소 = X25
// while (save[i] == k) i += 1;
Loop: LSL X10, X22, #3 // X10 = i * 8
ADD X10, X10, X25 // X10 = &save[i]
LDUR X9, [X10, #0] // X9 = save[i]
SUB X11, X9, X24 // save[i] - k
CBNZ X11, Exit
ADDI X22, X22, #1 // i = i + 1
B Loop
Exit:
- Basic block = 중간에 분기가 없고(끝 제외) 분기 대상도 없는(시작 제외) 명령어 묶음. 컴파일러 최적화 단위.
조건 코드 (NZCV) — S 접미 명령(ADDS, ADDIS, ANDS, ANDIS, SUBS, SUBIS)이 설정
- N(negative): 결과 MSB = 1 / Z(zero): 결과 = 0 / C(carry): MSB에서 올림 / V(overflow): 오버플로.
비교 signed unsigned
// if (a > b) a += 1; a = X22, b = X23
SUBS X9, X22, X23
B.LE Exit
ADDI X22, X22, #1
Exit:
- X22 = 1111…1111, X23 = 0000…0001 → signed면 −1 < +1, unsigned면 4,294,967,295 > 1.
2.8 하드웨어의 프로시저 지원
호출 6단계
- 인자를 X0~X7에 넣음(caller)
- 프로시저로 제어 이동(BL)
- 프로시저용 저장공간(지역변수) 확보
- 프로시저 연산 수행
- 결과를 caller가 읽을 레지스터에(기본 X0)
- 호출한 곳으로 복귀(X30의 주소)
- Argument(부를 때 넘기는 값, 예 1·2·3·4) vs Parameter(정의에 적힌 이름, 예 g·h·i·j). 교수님이 구분을 강조.
- BL label: 다음 명령어 주소(PC + 4)를 X30(LR)에 넣고 분기. BR LR: LR의 주소를 PC로 복사(switch–case의 계산된 분기에도 사용). LR과 X30은 같은 레지스터.
- B vs BL vs BR: B = 레이블로 그냥 분기, BL = 복귀 주소를 저장하고 분기, BR = 레지스터 안 주소로 분기.
호출하는 쪽 예 : a = leaf_example(1, 2, 3, 4); a = X21, 리턴 = X0
ADDI X0, XZR, #1
ADDI X1, XZR, #2
ADDI X2, XZR, #3
ADDI X3, XZR, #4 // 여기까지 1단계
BL leaf_example // 2단계, LR = 다음 줄 주소
ADD X21, XZR, X0 // 리턴값 받기
- BL에 들어가는 주소는 (목표 주소 − 현재 주소) / 4 만큼의 PC-relative 오프셋.
Leaf procedure — g~j = X0~X3, f = X19, 임시 X9·X10
leaf_example:
SUBI SP, SP, #24 // 스택 3칸 확보
STUR X10, [SP, #16] // X10, X9, X19 저장
STUR X9, [SP, #8]
STUR X19, [SP, #0]
ADD X9, X0, X1 // g + h
ADD X10, X2, X3 // i + j
SUB X19, X9, X10 // f
ADD X0, X19, XZR // 리턴값 = f
LDUR X10, [SP, #16] // 복원
LDUR X9, [SP, #8]
LDUR X19, [SP, #0]
ADDI SP, SP, #24 // 스택 반납
BR LR
- 규약: X9~X17 temporary = callee가 보존 안 함, X19~X28 saved = 쓰면 callee가 저장·복원. temporary는 C에 없는 임시값, saved는 C 변수.
Non-leaf procedure — 중첩 호출이라 복귀 주소와 호출 후 필요한 인자·임시값을 스택에 저장, 호출 후 복원.
// int64 fact(int64 n) { if (n < 1) return 1; else return n * fact(n - 1); }
// 인자 n = X0, 결과 = X1 (이 예에서는 X1, 시험에서도 알려줌)
fact: SUBI SP, SP, #16
STUR LR, [SP, #8] // 복귀 주소 저장
STUR X0, [SP, #0] // n 저장
SUBIS XZR, X0, #1 // n - 1 비교 (교과서 p110 ZXR → XZR)
B.GE L1 // n >= 1 이면 L1
ADDI X1, XZR, #1 // return 1
ADDI SP, SP, #16
BR LR
L1: SUBI X0, X0, #1 // n - 1
BL fact
LDUR X0, [SP, #0] // n 복원
LDUR LR, [SP, #8] // 복귀 주소 복원
ADDI SP, SP, #16
MUL X1, X0, X1 // n * fact(n-1)
BR LR
- 스택 그림은 위가 높은 주소인지 낮은 주소인지 문제마다 확인("거꾸로 그릴 때도 있음").
메모리 배치
- Text(프로그램 코드 = 기계어 명령어, 저주소) → Static data(전역·static 변수, 상수 배열·문자열) → Heap(malloc, Java new, 위로 자람) ↔ Stack(지역변수, 고주소에서 아래로).
- Dynamic data = Heap + Stack.
- 실행 파일에는 명령어 + static data만 있고, 스택·힙은 로딩되어 실행될 때 생김.
- 스택 = 지역변수를 위한 메모리 영역, 함수를 부를 때마다 커지고 끝나면 줄어듦. 메모리에 올라온 프로그램 = 프로세스
- Pitfall: 함수가 끝난 뒤 지역변수(스택) 포인터를 계속 쓰면 무효.
2.9 문자와 문자열
- ASCII(American Standard Code for Information Interchange) 128자(그래픽 95 + 제어 33), Latin-1 256자, Unicode 32비트(UTF-8·UTF-16 가변 길이).
- LDURB Rt, [Rn, offset](바이트 load), STURB(오른쪽 1바이트만 저장), LDURH/STURH(16비트 하프워드).
2.10 큰 수치와 주소를 위한 주소지정
- 대부분 상수는 12비트로 충분. 큰 상수는 MOVZ(나머지 0) + MOVK(나머지 유지), 시프트 #0·#16·#32·#48.
MOVZ X9, #255, LSL #16 // 0x0000_0000_00FF_0000
MOVK X9, #255, LSL #0 // 0x0000_0000_00FF_00FF
- B-type: B 10000 → opcode 5(6비트) + address 26비트. CB-type: CBNZ X19, Exit → opcode 181(8비트) + address 19비트 + Rt 5비트.
- PC-relative: 목표 주소 = PC + 4 × offset. 명령어가 4바이트(word)라 ×4.
- 주소지정 방식 4가지(그림 2.19 = 강의 L2 p51): ① Immediate ② Register ③ Base(레지스터 + 주소 필드) ④ PC-relative. 🆕 그림 오류: ③ 화살표 출발점은 Address(op 삭제), ④ Memory 안 상자는 Doubleword가 아니라 Word.
기계어 해독
0x8B0F0013 = 1000 1011 0000 1111 0000 0000 0001 0011
= 10001011000 | 01111 | 000000 | 00000 | 10011
= opcode 1112(ADD) | Rm 15 | shamt 0 | Rn 0 | Rd 19
= ADD X19, X0, X15
2.11 병렬성과 명령어: 동기화 🆕
- Data race: 두 프로세서가 메모리를 공유하며 동기화하지 않으면 결과가 접근 순서에 따라 달라짐 → 원자적(atomic) read/write 하드웨어 지원 필요.
- LDXR(Load exclusive register) → STXR(Store exclusive register)를 같은 주소에 쌍으로 사용. 사이에 그 주소가 바뀌면 store 실패. 사이에는 레지스터 명령만.
- STXR의 상태 레지스터: 성공 0 / 실패 1
// atomic swap: X23 <-> [X20]
again: LDXR X10, [X20, #0]
STXR X23, X9, [X20] // X9 = status
CBNZ X9, again
ADD X23, XZR, X10 // X23 = 읽은 값
// lock
ADDI X11, XZR, #1 // 잠금 값
again: LDXR X10, [X20, #0] // lock 읽기
CBNZ X10, again // 0이 될 때까지
STXR X11, X9, [X20] // 저장 시도
CBNZ X9, again // 실패하면 다시
unlock: STUR XZR, [X20, #0] // lock 해제
- 비유: 물건 100개 중 5명만 가져갈 때, 남은 개수를 보는 것 = LDXR, 채 가는 것 = STXR.
- 연습문제 2.38·2.39와 같은 유형(같은 문제 공개 예정). 어려운 버전은 안 냄. LDUR(unscaled)과 LDXR(exclusive) 구분.
2.12 프로그램 번역과 실행
- C → 컴파일러 → 어셈블리 → 어셈블러 → 목적 모듈 → 링커 → 실행 파일 → 로더 → 메모리.
- gcc 옵션: -E(.i 전처리), -S(.s 컴파일만), -c(.o 어셈블까지), -static(정적 링크).
- 목적 파일: Header / Text segment / Static data segment / Relocation info / Symbol table / Debug info.
- 링커: ① 세그먼트 병합 ② 레이블 주소 결정 ③ 위치 의존·외부 참조 수정. 가상 메모리 덕에 절대 주소로 로드 가능.
- 로더: ① 헤더로 크기 확인 ② 가상 주소 공간 생성 ③ text·데이터 복사 ④ 스택에 인자 ⑤ 레지스터 초기화(SP·FP) ⑥ startup 루틴 → main 호출, 끝나면 exit.
- Static vs Dynamic linking(과제로 낸 내용, 10/14 재강조): Dynamic = 라이브러리를 호출될 때만 링크·로드, 재배치 가능 코드 필요, 이미지 비대화 방지, 새 버전 자동 반영. DLL = Dynamically Linked Library.
- Lazy linkage(간접 테이블·stub) 세부와 Java(바이트코드·JVM·JIT) 경로는 "외울 필요 없음".
x86 명령어와 오류·함정
- x86 = CISC. 하위 호환성을 지키며 명령어가 계속 늘어남: 8080(1974, 8비트) → 8086(1978, 16비트) → 80386(32비트, IA-32) → Pentium(MMX, SSE) → AMD64(2003, 64비트) → AVX. "Technical elegance ≠ market success."
- ARM은 비교적 늦게(80~90년대) 나왔고 명령어를 최소화 → 저전력·병렬화에 유리.
- Fallacy: 강력한 명령어 = 고성능(아님, 복잡하면 단순 명령까지 느려짐) / 고성능을 위해 어셈블리 작성(아님, 컴파일러가 더 잘함) / 하위 호환 = ISA 불변(아님, 계속 늘어남).
- Pitfall: 연속 워드는 연속 주소가 아님(1이 아니라 워드 크기만큼 증가) / 반환 후 지역변수 포인터 사용.
Lecture 2a — AI를 위한 명령어 (10% 파트)
- 형 변환 비용: 부호 확장·BF16→FP32 = 배선만 / FP16→FP32 = 덧셈기+시프터 / FP4→FP16 = LUT(Look-Up Table)+곱셈기. 4비트 가중치: shift·mask → LUT → scale 곱 → MAC.
- Overflow 대응 4가지: wrap(ADD) / flag(ADDS → NZCV) / exception(LEGv8만, 실제 ARMv8 없음) / saturate(SQADD). INT8 120 + 30 → wrap −106, saturate 127. SIMD는 lane별 플래그·예외 없음 → saturate 또는 넓은 누산기.
- 레지스터 파일: X 31 × 64 = 1,984비트, V(V0~V31) 32 × 128 = 4,096비트(Q·D·S·H·B, lane 16B·8H·4S·2D). 개수 그대로, 폭만 늘려 필드 5비트 유지 = DP2.
- SIMD(Single Instruction, Multiple Data). SDOT Vd.4S, Vn.16B, Vm.16B = 곱셈 16개(스칼라 32 연산 대체). int8×int8 최대 16,384 → int16에는 2개째에 overflow, int32에는 약 131,000개.
- SVE(Scalable Vector Extension) 128~2048비트, WHILELT로 꼬리 루프 없음(n = 102: 고정 SIMD 25회 + 꼬리 2회 vs SVE 26회). SME = 2D 타일 ZA. x86 대응: SDOT↔VNNI, SVE↔AVX-512, SME↔AMX.
- NCHW 주소 = ((n·C+c)·H+h)·W+w, NHWC = ((n·H+h)·W+w)·C+c. 마지막 글자가 연속 축.
- NVIDIA 경로: CUDA C++ → nvcc → PTX(Parallel Thread eXecution), 가상 ISA·공개 → ptxas → SASS, 실제 기계어·비공개. Fat binary + 드라이버 JIT(Just-In-Time). Fallback = 최적 경로가 없을 때 느리지만 동작하는 경로("외워야").
- IR(Intermediate Representation), LLVM(N×M → N+M: 언어 4·HW 4이면 16 → 8), MLIR(Multi-Level IR). Fusion은 고수준, Tiling은 중간 수준.
- "커널"의 3가지 뜻: OS 커널(프로그램) / 계산 커널(함수) / 합성곱 커널(데이터).
- ISA = HW–SW 계약, 공개 범위도 설계 결정