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

=, ≠ B.EQ, B.NE B.EQ, B.NE
< B.LT B.LO
≤ B.LE B.LS
> B.GT B.HI
≥ B.GE B.HS
// 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단계

  1. 인자를 X0~X7에 넣음(caller)
  2. 프로시저로 제어 이동(BL)
  3. 프로시저용 저장공간(지역변수) 확보
  4. 프로시저 연산 수행
  5. 결과를 caller가 읽을 레지스터에(기본 X0)
  6. 호출한 곳으로 복귀(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 계약, 공개 범위도 설계 결정

'전공 공부 정리 > 컴퓨터구조' 카테고리의 다른 글

1. 컴퓨터 추상화&성능  (0) 2026.10.17

+ Recent posts