MIPS 명령어셋 레퍼런스
MIPS 아키텍처의 명령어를 GAS 문법 기준으로 종합 정리합니다. 전통 RISC 설계 철학과 고정 32-bit 명령어, 32개 범용 레지스터(Register)와 HI/LO 특수 레지스터, 지연(Latency) 슬롯 메커니즘, MIPS32/64 Release 2와 Release 6의 차이, R/I/J 3가지 인코딩 타입, 데이터 전송·산술·논리·분기·스택·시스템·원자적(Atomic)·MSA SIMD 명령어 카테고리 표, 인코딩 다이어그램, 커널 핵심 명령어(SYSCALL, ERET, LL/SC, MFC0/MTC0, TLBWI) 고급까지 Linux 커널 개발에 필요한 MIPS ISA 전체를 다룹니다.
핵심 요약
- 전통 RISC — 고정 32-bit 명령어, Load/Store 아키텍처, 균일한 레지스터 파일.
- 32개 범용 레지스터 — $0=zero(항상 0), $31=ra, $29=sp, $4-$7=a0-a3(인자).
- 지연 슬롯 — 분기/점프 다음 명령어가 항상 실행됨 (R6에서 compact branch로 개선).
- HI/LO 레지스터 — 곱셈/나눗셈 결과 저장 (R6에서 제거, GPR 직접 사용).
- R2 → R6 진화 — Release 6에서 지연 슬롯 없는 분기, HI/LO 제거, 새 인코딩 도입.
단계별 이해
- 레지스터 구조 파악
$0-$31의 ABI 이름($zero, $at, $v0-$v1, $a0-$a3, $t0-$t9, $s0-$s7 등)을 먼저 익힙니다. - 지연 슬롯 이해
분기/점프 직후 명령어가 항상 실행되는 MIPS 고유 특성을 반드시 이해합니다. - R/I/J 인코딩
3가지 고정 인코딩 포맷의 필드 배치를 학습합니다. - CP0와 특권 명령어
MFC0/MTC0으로 접근하는 CP0 레지스터와 SYSCALL/ERET를 학습합니다.
아키텍처 개요
MIPS(Microprocessor without Interlocked Pipelined Stages)는 1981년 Stanford에서 John Hennessy가 설계한 RISC 아키텍처입니다. MIPS Computer Systems(1984) → SGI(1992) → Imagination Technologies(2012) → Wave Computing(2018) → MIPS(2021)를 거쳤습니다.
| 특성 | MIPS |
|---|---|
| 설계 철학 | 전통 RISC (교과서적 파이프라인 설계) |
| 명령어 길이 | 고정 32-bit |
| 엔디언 | 바이-엔디언 (Big 또는 Little) |
| 범용 레지스터 | 32개 ($0=hardwired zero) |
| 특수 레지스터 | HI/LO (곱셈/나눗셈, R2), PC |
| 지연 슬롯 | 분기/점프 다음 1개 명령어 항상 실행 (R6: compact branch 도입) |
| 주요 릴리스 | MIPS32/64 R2 (2003), R5 (2012), R6 (2014) |
| 동작 모드 | 커널 모드, 슈퍼바이저 모드, 유저 모드 |
| 주소 공간(Address Space) | 32-bit (MIPS32) 또는 64-bit (MIPS64) |
MIPS 클래식 5단계 파이프라인
MIPS 가상 주소 공간 (32-bit)
레지스터 셋
범용 레지스터 ($0-$31)
| 번호 | ABI 이름 | 용도 | 보존 |
|---|---|---|---|
| $0 | $zero | 하드와이어 제로 (항상 0) | — |
| $1 | $at | 어셈블러 임시 (Assembler Temporary) | — |
| $2-$3 | $v0-$v1 | 함수 반환값 | Caller |
| $4-$7 | $a0-$a3 | 함수 인자 (o32 ABI) | Caller |
| $8-$15 | $t0-$t7 | 임시 레지스터 | Caller |
| $16-$23 | $s0-$s7 | Saved 레지스터 | Callee |
| $24-$25 | $t8-$t9 | 임시 레지스터 | Caller |
| $26-$27 | $k0-$k1 | 커널 전용 (예외 핸들러용) | — |
| $28 | $gp | 전역 포인터 (Global Pointer) | Caller |
| $29 | $sp | 스택 포인터 | Callee |
| $30 | $fp ($s8) | 프레임 포인터 / Saved 레지스터 | Callee |
| $31 | $ra | 복귀 주소 (Return Address) | Caller |
$k0으로 Context 레지스터를, $k1으로 PTE 값을 임시 저장하는 패턴이 대표적입니다.
특수 레지스터
| 레지스터 | 설명 |
|---|---|
| HI / LO | 곱셈 상위/하위, 나눗셈 나머지/몫 (R2, R6에서 제거) |
| PC | 프로그램 카운터 (직접 접근 불가) |
CP0 (Coprocessor 0) 레지스터 — 주요
| 번호 | 이름 | 설명 |
|---|---|---|
| $8 | BadVAddr | 가장 최근 주소 오류의 가상 주소(Virtual Address) |
| $9 | Count | 카운터 (타이머(Timer)) |
| $11 | Compare | 카운터 비교값 (타이머 인터럽트(Interrupt)) |
| $12 | Status | 프로세서 상태 (IE, EXL, ERL, KSU, IM 등) |
| $13 | Cause | 예외 원인 (ExcCode, IP 등) |
| $14 | EPC | 예외 PC (복귀 주소) |
| $15 | PRId | 프로세서 ID |
| $16 | Config | 구성 레지스터 |
| $0 | Index | TLB 인덱스 |
| $1 | Random | TLB 랜덤 인덱스 |
| $2-$3 | EntryLo0/1 | TLB 엔트리 하위 (짝수/홀수 페이지(Page)) |
| $4 | Context | TLB 미스 컨텍스트 |
| $5 | PageMask | TLB 페이지 마스크 |
| $6 | Wired | 고정 TLB 엔트리 수 |
| $10 | EntryHi | TLB 엔트리 상위 (VPN2, ASID) |
| $15.1 | EBase | 예외 베이스 주소 (R2+) |
| $30 | ErrorEPC | 에러 예외 PC |
CP0 Status 레지스터 비트 필드 ($12)
CP0 Cause 레지스터 비트 필드 ($13)
MIPS64 n64 ABI 차이
주소 지정 모드
MIPS는 매우 제한적인 주소 모드를 사용합니다: 베이스 + 16-bit 부호 확장 오프셋(Offset)만 지원합니다.
| 패턴 | 문법 | 설명 | 예제 |
|---|---|---|---|
| 베이스+오프셋 | offset(base) | [base + sign_ext(offset)] | lw $t0, 8($sp) |
| 32-bit 즉시값 | lui + ori | 상위 16 + 하위 16 조합 | lui $t0, %hi(sym); ori $t0, $t0, %lo(sym) |
| 의사 명령어 li | li $t0, imm | 어셈블러가 lui+ori로 확장 | li $t0, 0x12345678 |
| 의사 명령어 la | la $t0, symbol | 주소 로드 | la $t0, my_var |
addiu는 분기 결과와 무관하게 항상 실행됩니다:
beq $t0, $t1, target /* $t0 == $t1이면 target으로 분기 */
addiu $v0, $v0, 1 /* ← 지연 슬롯: 항상 실행됨! */
/* 여기는 분기하지 않았을 때만 실행 */
...
target:
/* $v0는 이미 +1된 상태 (지연 슬롯이 실행되었으므로) */
주의: 지연 슬롯에서 분기 조건의 소스 레지스터를 수정하면 안 됩니다. 또한 지연 슬롯에 또 다른 분기 명령어를 넣는 것은 미정의 동작입니다.
.set noreorder— 어셈블러가 명령어 재배치(Relocation)/NOP 삽입을 하지 않음. 프로그래머가 지연 슬롯을 직접 채워야 함. 커널 핵심 경로(예외 핸들러, TLB refill)에서 사용..set reorder— 어셈블러가 필요 시 NOP을 자동 삽입하고 명령어 순서를 최적화. 일반 C 호출 함수에서 사용.
.set noreorder
beq $t0, $zero, done
addiu $v0, $v0, 1 /* 유용한 명령어를 지연 슬롯에 배치 */
.set reorder
Linux 커널의 arch/mips/ 어셈블리 코드는 대부분 .set noreorder 블록 내에서 작성됩니다.
데이터 전송 명령어
| 명령어 | 문법 | 설명 | 비고 |
|---|---|---|---|
| LB / LBU | lb $t0, 0($a0) | 바이트 로드 (부호/제로 확장) | |
| LH / LHU | lh $t0, 0($a0) | 하프워드 로드 | |
| LW | lw $t0, 0($a0) | 워드 로드 | |
| LWU | lwu $t0, 0($a0) | 워드 제로 확장 로드 | MIPS64 |
| LD | ld $t0, 0($a0) | 더블워드 로드 | MIPS64 |
| SB / SH / SW / SD | sw $t0, 0($a0) | 바이트/하프/워드/더블 저장 | |
| LUI | lui $t0, 0x1234 | 상위 16-bit 즉시값 로드 | |
| LWL / LWR | lwl $t0, 3($a0) | 비정렬 워드 로드 (좌/우) | R2 전용 |
| MFC0 / MTC0 | mfc0 $t0, $12 | CP0 레지스터 읽기/쓰기 | 특권 |
| MFHI / MFLO | mfhi $t0 | HI/LO 레지스터 읽기 | R2 |
| MTHI / MTLO | mthi $t0 | HI/LO 레지스터 쓰기 | R2 |
| MFC1 / MTC1 | mfc1 $t0, $f0 | FPU 레지스터 ↔ GPR | |
| LWC1 / LDC1 | lwc1 $f0, 0($a0) | FPU 메모리 로드 | |
| SWC1 / SDC1 | swc1 $f0, 0($a0) | FPU 메모리 저장 |
LWL(Load Word Left)과 LWR(Load Word Right)을 쌍으로 사용해야 합니다. R6에서는 이 문제가 해결되어 LW/SW가 비정렬 접근을 하드웨어적으로 지원하며, LWL/LWR은 제거되었습니다.
/* R2: 비정렬 32-bit 로드 (LWL/LWR 패턴) */
/* $a0에 비정렬 주소가 있다고 가정 (예: 0x10003) */
/* Big-Endian 기준 */
lwl $t0, 0($a0) /* 주소의 워드 좌측 바이트들 로드 */
lwr $t0, 3($a0) /* 주소의 워드 우측 바이트들 로드 */
/* 이제 $t0에 4바이트가 올바르게 로드됨 */
/* R2: 비정렬 32-bit 저장 (SWL/SWR 패턴) */
swl $t0, 0($a0) /* 워드 좌측 바이트들 저장 */
swr $t0, 3($a0) /* 워드 우측 바이트들 저장 */
/* R6: 단순화됨 — LW/SW가 비정렬 지원 */
lw $t0, 0($a0) /* R6에서는 비정렬이어도 정상 동작 */
arch/mips/kernel/unaligned.c는 비정렬 접근 예외(AdEL/AdES)를 에뮬레이션합니다. R2 커널 코드에서 비정렬 접근이 필요한 경우 get_unaligned() / put_unaligned() 매크로(Macro)를 사용하며, 이 매크로는 내부적으로 LWL/LWR 패턴을 사용합니다.
산술 명령어
| 명령어 | 문법 | 설명 | 비고 |
|---|---|---|---|
| ADD / ADDU | addu $t0, $t1, $t2 | 덧셈 (ADD=오버플로 트랩, ADDU=무시) | |
| ADDI / ADDIU | addiu $t0, $t1, 42 | 즉시값 덧셈 | |
| SUB / SUBU | subu $t0, $t1, $t2 | 뺄셈 | |
| MULT / MULTU | mult $t0, $t1 | 곱셈 → HI:LO | R2 |
| DIV / DIVU | div $t0, $t1 | 나눗셈 → LO=몫, HI=나머지 | R2 |
| MUL (R6) | mul $t0, $t1, $t2 | 곱셈 → GPR (하위) | R6 |
| MUH / MULU / MUHU (R6) | muh $t0, $t1, $t2 | 곱셈 상위 / 부호없는 | R6 |
| DIV / MOD (R6) | div $t0, $t1, $t2 | 나눗셈 / 나머지 → GPR | R6 |
| MADD / MADDU | madd $t0, $t1 | 곱셈-누적: HI:LO += t0*t1 | R2 |
| CLO / CLZ | clz $t0, $t1 | 선행 1/0 카운트 | R2+ |
| DADD / DADDU / DSUB | daddu $t0, $t1, $t2 | 64-bit 덧셈/뺄셈 | MIPS64 |
| DMULT / DDIV | dmult $t0, $t1 | 64-bit 곱셈/나눗셈 | MIPS64 |
- R2 방식:
MULT rs, rt→ 결과가 HI:LO에 저장됨 →MFHI/MFLO로 읽어야 함. 3개 명령어 필요. - R6 방식:
MUL rd, rs, rt(하위 32-bit) /MUH rd, rs, rt(상위 32-bit) → GPR에 직접 저장. 1개 명령어로 완료. - R6에서 HI/LO 레지스터와 MFHI/MFLO/MTHI/MTLO/MADD/MSUB 모두 제거되었습니다.
/* R2: 곱셈 패턴 — result = $a0 * $a1 */
mult $a0, $a1 /* HI:LO = $a0 * $a1 */
mflo $v0 /* $v0 = 결과 하위 32-bit (곱의 하위) */
mfhi $v1 /* $v1 = 결과 상위 32-bit */
/* R2: 나눗셈 패턴 — quotient = $a0 / $a1, remainder = $a0 % $a1 */
div $zero, $a0, $a1 /* LO=몫, HI=나머지 */
mflo $v0 /* $v0 = 몫 */
mfhi $v1 /* $v1 = 나머지 */
/* R6: 곱셈 — 결과가 GPR에 직접 */
mul $v0, $a0, $a1 /* $v0 = ($a0 * $a1) 하위 32-bit */
muh $v1, $a0, $a1 /* $v1 = ($a0 * $a1) 상위 32-bit */
/* R6: 나눗셈 — 몫과 나머지 각각 별도 명령어 */
div $v0, $a0, $a1 /* $v0 = $a0 / $a1 (몫) */
mod $v1, $a0, $a1 /* $v1 = $a0 % $a1 (나머지) */
논리/시프트/비트 조작 명령어
| 명령어 | 문법 | 설명 | 비고 |
|---|---|---|---|
| AND / ANDI | and $t0, $t1, $t2 | 비트 AND | |
| OR / ORI | or $t0, $t1, $t2 | 비트 OR | |
| XOR / XORI | xor $t0, $t1, $t2 | 비트 XOR | |
| NOR | nor $t0, $t1, $t2 | 비트 NOR (NOT = nor $t0,$t1,$zero) | |
| SLL | sll $t0, $t1, 4 | 논리 좌측 시프트 (즉시값) | |
| SRL | srl $t0, $t1, 4 | 논리 우측 시프트 | |
| SRA | sra $t0, $t1, 4 | 산술 우측 시프트 | |
| SLLV / SRLV / SRAV | sllv $t0, $t1, $t2 | 변수 시프트 (레지스터 양) | |
| INS | ins $t0, $t1, pos, size | 비트 필드 삽입 | R2 |
| EXT | ext $t0, $t1, pos, size | 비트 필드 추출 | R2 |
| WSBH | wsbh $t0, $t1 | 하프워드 내 바이트 스왑(Swap) | R2 |
| SEB / SEH | seb $t0, $t1 | 바이트/하프워드 부호 확장 | R2 |
| ROTR / ROTRV | rotr $t0, $t1, 8 | 우측 순환 시프트 | R2 |
| BITSWAP (R6) | bitswap $t0, $t1 | 바이트 내 비트 반전 | R6 |
| ALIGN (R6) | align $t0, $t1, $t2, 2 | 바이트 정렬 추출 | R6 |
| DSLL / DSRL / DSRA | dsll $t0, $t1, 4 | 64-bit 시프트 | MIPS64 |
| DSLL32 / DSRL32 / DSRA32 | dsll32 $t0, $t1, 4 | 64-bit 시프트 (32+ 위치) | MIPS64 |
비교/분기 명령어
| 명령어 | 문법 | 설명 | 지연 슬롯 |
|---|---|---|---|
| SLT / SLTI | slt $t0, $t1, $t2 | 부호 있는 < 비교 (1/0) | — |
| SLTU / SLTIU | sltu $t0, $t1, $t2 | 부호 없는 < 비교 | — |
| BEQ | beq $t0, $t1, label | 같으면 분기 | 있음 (R2) |
| BNE | bne $t0, $t1, label | 다르면 분기 | 있음 (R2) |
| BGTZ / BLEZ | bgtz $t0, label | >0 / <=0 분기 | 있음 (R2) |
| BLTZ / BGEZ | bltz $t0, label | <0 / >=0 분기 | 있음 (R2) |
| BLTZAL / BGEZAL | bltzal $t0, label | 조건부 분기 + 링크 | 있음 (R2) |
| J | j label | 무조건 점프 (26-bit 타겟) | 있음 (R2) |
| JAL | jal func | 함수 호출 ($ra ← PC+8) | 있음 (R2) |
| JR | jr $ra | 간접 점프 (레지스터) | 있음 (R2) |
| JALR | jalr $ra, $t0 | 간접 호출 | 있음 (R2) |
| BC (R6) | bc label | 무조건 분기 (compact) | 없음 |
| BALC (R6) | balc label | 무조건 호출 (compact) | 없음 |
| BEQZC / BNEZC (R6) | beqzc $t0, label | 제로/비제로 compact 분기 | 없음 |
| BEQC / BNEC (R6) | beqc $t0, $t1, label | 비교 compact 분기 | 없음 |
| MOVZ / MOVN | movz $t0, $t1, $t2 | 조건부 이동 (R2) | — |
| SELEQZ / SELNEZ (R6) | seleqz $t0, $t1, $t2 | 조건부 선택 (R6 대체) | — |
/* 비최적화: NOP 낭비 */
addiu $v0, $v0, 1
bne $v0, $a0, loop
nop /* 낭비되는 사이클 */
/* 최적화: 유용한 명령어를 지연 슬롯에 이동 */
bne $v0, $a0, loop
addiu $v0, $v0, 1 /* 지연 슬롯에서 실행 — 1 사이클 절약 */
/* 함수 호출 최적화: 인자 설정을 지연 슬롯에 */
jal some_func
move $a0, $s0 /* 지연 슬롯: 인자를 $a0에 설정 */
스택/함수 호출 명령어
MIPS에는 PUSH/POP 명령어가 없습니다. addiu $sp와 sw/lw로 수동 관리합니다.
/* 함수 프롤로그/에필로그 (o32 ABI) */
my_func:
addiu $sp, $sp, -32 /* 스택 프레임 할당 */
sw $ra, 28($sp) /* 복귀 주소 저장 */
sw $fp, 24($sp) /* 프레임 포인터 저장 */
move $fp, $sp /* 프레임 포인터 설정 */
/* ... 함수 본문 ... */
move $sp, $fp /* 스택 복원 */
lw $fp, 24($sp) /* 프레임 포인터 복원 */
lw $ra, 28($sp) /* 복귀 주소 복원 */
addiu $sp, $sp, 32 /* 스택 프레임 해제 */
jr $ra /* 복귀 (지연 슬롯) */
nop /* 지연 슬롯 */
- 인자: $a0-$a3 (4개, 나머지 스택)
- 반환값: $v0 (+ $v1)
- Callee-saved: $s0-$s7, $fp, $sp
- Caller-saved: $t0-$t9, $a0-$a3, $v0-$v1, $ra
- n64 ABI: $a0-$a7 (8개 인자 레지스터)
시스템/특권 명령어
| 명령어 | 설명 | 비고 |
|---|---|---|
| SYSCALL | 시스템 콜(System Call) 트랩 | |
| BREAK | 디버그 브레이크포인트 트랩 | |
| ERET | 예외 복귀 (EPC → PC, Status 복원) | |
| MFC0 / MTC0 | CP0 레지스터 읽기/쓰기 | |
| DMFC0 / DMTC0 | 64-bit CP0 접근 | MIPS64 |
| WAIT | 저전력 대기 (인터럽트까지) | |
| EI / DI | 인터럽트 활성화/비활성화 | R2+ |
| TLBR | TLB 읽기 (Index → EntryHi/Lo) | |
| TLBWI | TLB 쓰기 (Index 지정) | |
| TLBWR | TLB 쓰기 (Random 인덱스) | |
| TLBP | TLB 탐색 (EntryHi → Index) | |
| CACHE | 캐시(Cache) 조작 (I-cache/D-cache) | |
| SYNC | 메모리 배리어(Memory Barrier) | |
| RDHWR | 하드웨어 레지스터 읽기 (ULR 등) | R2+ |
| GINVI / GINVT (R6) | 전역 TLB 무효화 | R6 |
예외 벡터 레이아웃
EI(Enable Interrupts)과 DI(Disable Interrupts)는 CP0 Status의 IE 비트를 원자적으로 설정/해제합니다. 이전 R1에서는 MFC0/MTC0으로 Status 레지스터를 읽고-수정-쓰기 해야 했으며, 이 사이에 인터럽트가 발생할 수 있는 경쟁 조건(Race Condition)이 있었습니다.
/* R2+: 원자적 인터럽트 비활성화 */
di $t0 /* $t0 = 이전 Status, IE←0 */
ehb /* Execution Hazard Barrier */
/* ... 크리티컬 섹션 ... */
ei /* IE←1, 인터럽트 재활성화 */
/* R1 (구형): MFC0/MTC0 패턴 — 경쟁 조건 가능 */
mfc0 $t0, $12 /* Status 읽기 */
ori $t1, $t0, 1 /* IE 비트 세트 */
mtc0 $t1, $12 /* Status 쓰기 — 여기서 인터럽트 경쟁 가능 */
원자적/동기화 명령어
| 명령어 | 문법 | 설명 |
|---|---|---|
| LL | ll $t0, 0($a0) | Load Linked (32-bit 독점 로드) |
| SC | sc $t1, 0($a0) | Store Conditional ($t1=0이면 실패, 1이면 성공) |
| LLD | lld $t0, 0($a0) | 64-bit Load Linked (MIPS64) |
| SCD | scd $t1, 0($a0) | 64-bit Store Conditional (MIPS64) |
| SYNC | sync 0 | 메모리 배리어 (0=full barrier) |
LL/SC CAS 루프 패턴
/* compare_and_swap(addr=$a0, expected=$a1, desired=$a2) → old in $v0 */
cas_loop:
ll $v0, 0($a0) /* Load Linked */
bne $v0, $a1, 1f /* expected와 다르면 실패 */
nop /* 지연 슬롯 */
move $t0, $a2 /* desired 값 복사 */
sc $t0, 0($a0) /* Store Conditional */
beqz $t0, cas_loop /* SC 실패(0) 시 재시도 */
nop /* 지연 슬롯 */
1:
jr $ra /* $v0에 이전 값 반환 */
nop /* 지연 슬롯 */
- LL과 SC 사이에 다른 메모리 접근(load/store) 금지 — SC 실패를 유발할 수 있음
- LL과 SC 사이의 명령어 수를 최소화 (구현에 따라 제한 있음, 일반적으로 수십 개 이내)
- LL과 SC 사이에 분기/점프 금지 (조건부 이동 MOVZ/MOVN은 허용)
- LL과 SC는 동일한 주소를 대상으로 해야 함
- SC 결과 레지스터를 반드시 확인하여 실패 시 재시도해야 함
- 다른 코어의 SC가 동일 캐시 라인(Cache Line)에 쓰면 LL의 링크가 해제되어 SC 실패
LL/SC 스핀락(Spinlock) 구현
/* arch_spin_lock(lock=$a0) — 간소화된 MIPS 스핀락 */
arch_spin_lock:
.set noreorder
1:
ll $t0, 0($a0) /* 락 값 Load Linked */
bnez $t0, 1b /* 이미 잠겨있으면 재시도 */
li $t1, 1 /* 지연 슬롯: 잠금 값 준비 */
sc $t1, 0($a0) /* Store Conditional (잠금 시도) */
beqz $t1, 1b /* SC 실패 시 재시도 */
nop /* 지연 슬롯 */
sync /* 메모리 배리어 (acquire) */
jr $ra
nop
.set reorder
/* arch_spin_unlock(lock=$a0) */
arch_spin_unlock:
.set noreorder
sync /* 메모리 배리어 (release) */
sw $zero, 0($a0) /* 락 해제 (일반 store로 충분) */
jr $ra
nop
.set reorder
stype 필드(비트 10:6)로 배리어 범위를 지정합니다:
| stype | 이름 | 설명 |
|---|---|---|
| 0x00 | SYNC | 완전 배리어 — 모든 이전 load/store 완료 후 이후 접근 시작 |
| 0x10 | SYNC_WMB | 쓰기 배리어 — 이전 store 완료 후 이후 store 시작 (Linux wmb()) |
| 0x13 | SYNC_MB | 읽기-쓰기 배리어 (Linux mb()) |
| 0x12 | SYNC_RMB | 읽기 배리어 — 이전 load 완료 후 이후 load 시작 (Linux rmb()) |
arch/mips/include/asm/barrier.h에서 mb(), wmb(), rmb() 매크로가 이 stype 값을 사용합니다.
MSA (MIPS SIMD Architecture) 명령어
MSA는 128-bit 벡터 레지스터 $w0-$w31을 사용하며, 바이트(.b)/하프워드(.h)/워드(.w)/더블워드(.d) 요소 타입을 지원합니다.
| 명령어 | 설명 |
|---|---|
| LD.B/H/W/D $w0, 0($a0) | 벡터 로드 (128-bit) |
| ST.B/H/W/D $w0, 0($a0) | 벡터 저장 |
| ADDV.B/H/W/D $w0, $w1, $w2 | 벡터 정수 덧셈 |
| SUBV.B/H/W/D $w0, $w1, $w2 | 벡터 정수 뺄셈 |
| MULV.B/H/W/D $w0, $w1, $w2 | 벡터 정수 곱셈 |
| MADDV / MSUBV | 벡터 곱셈-누적 / 곱셈-뺄셈 |
| AND.V / OR.V / XOR.V / NOR.V | 벡터 비트 논리 연산 |
| SLL.B/H/W/D / SRL / SRA | 벡터 시프트 |
| CEQ.B/H/W/D / CLT / CLE | 벡터 비교 (같음/작음/이하) |
| FADD.W/D / FSUB / FMUL / FDIV | 벡터 부동소수점 연산 |
| FMADD.W/D | 벡터 FMA |
| SHF.B/H/W $w0, $w1, imm | 4-요소 셔플 |
| VSHF.B/H/W/D $w0, $w1, $w2 | 벡터 셔플 |
| PCKEV / PCKOD | 짝수/홀수 요소 팩 |
| ILVL / ILVR | 좌/우 인터리브 |
| HADD_S/U / HSUB_S/U | 수평 덧셈/뺄셈 |
커널 핵심 명령어
예외 핸들러 디스패치 (Cause.ExcCode)
MIPS에서 General Exception 벡터(0x80000180)에 도달하면, 커널은 Cause.ExcCode 필드를 확인하여 적절한 핸들러로 분기합니다. Linux 커널의 arch/mips/kernel/genex.S에서 이 디스패치 로직을 구현합니다.
/* General Exception 벡터 진입점 (0x80000180) */
/* arch/mips/kernel/genex.S — except_vec3_generic */
except_vec3_generic:
.set noreorder
mfc0 $k1, $13 /* Cause 레지스터 읽기 */
andi $k1, $k1, 0x7c /* ExcCode 필드 추출 (비트 6:2) */
/* ExcCode * 4 = exception_handlers 테이블 인덱스 (이미 <<2 위치) */
la $k0, exception_handlers
addu $k0, $k0, $k1 /* 테이블 + ExcCode*4 */
lw $k0, 0($k0) /* 핸들러 주소 로드 */
jr $k0 /* 핸들러로 점프 */
nop /* 지연 슬롯 */
.set reorder
/* exception_handlers 테이블 (ExcCode별 핸들러 포인터) */
/* 0: handle_int — 인터럽트 */
/* 1: handle_tlbm — TLB Modified */
/* 2: handle_tlbl — TLB Load 미스 */
/* 3: handle_tlbs — TLB Store 미스 */
/* 4: handle_adel — Address Error (Load) */
/* 5: handle_ades — Address Error (Store) */
/* 8: handle_sys — System Call */
/* 9: handle_bp — Breakpoint */
/* 10: handle_ri — Reserved Instruction */
/* 11: handle_cpu — Coprocessor Unusable */
/* 13: handle_ov — Overflow */
예외 진입/복귀 전체 시퀀스
/* 예외 진입 — 레지스터 저장 (arch/mips/kernel/genex.S SAVE_ALL 매크로) */
exception_entry:
.set noreorder
.set noat
mfc0 $k0, $12 /* Status 레지스터 */
mfc0 $k1, $14 /* EPC (복귀 주소) */
/* 커널 스택 포인터 확보 */
addiu $sp, $sp, -320 /* pt_regs 구조체 크기만큼 할당 */
/* 모든 범용 레지스터 저장 */
sw $zero, 0($sp) /* $0 (형식상) */
sw $at, 4($sp) /* $at */
sw $v0, 8($sp) /* $v0 */
sw $v1, 12($sp) /* $v1 */
sw $a0, 16($sp) /* $a0-$a3 */
sw $a1, 20($sp)
sw $a2, 24($sp)
sw $a3, 28($sp)
/* ... $t0-$t7, $s0-$s7, $t8-$t9 저장 생략 ... */
sw $gp, 112($sp) /* $gp */
sw $fp, 120($sp) /* $fp */
sw $ra, 124($sp) /* $ra */
/* CP0 레지스터 저장 */
sw $k0, 128($sp) /* Status */
sw $k1, 132($sp) /* EPC */
mfc0 $t0, $13
sw $t0, 136($sp) /* Cause */
mfc0 $t0, $8
sw $t0, 140($sp) /* BadVAddr */
/* HI/LO도 저장 (R2의 경우) */
mfhi $t0
sw $t0, 144($sp) /* HI */
mflo $t0
sw $t0, 148($sp) /* LO */
.set at
.set reorder
/* 예외 복귀 — 레지스터 복원 (RESTORE_ALL 매크로) */
exception_exit:
.set noreorder
.set noat
/* HI/LO 복원 */
lw $t0, 144($sp)
mthi $t0
lw $t0, 148($sp)
mtlo $t0
/* CP0 복원 */
lw $t0, 128($sp) /* Status */
lw $t1, 132($sp) /* EPC */
mtc0 $t0, $12 /* Status 복원 */
mtc0 $t1, $14 /* EPC 복원 */
/* 모든 범용 레지스터 복원 */
lw $ra, 124($sp)
lw $fp, 120($sp)
lw $gp, 112($sp)
/* ... $s0-$s7, $t0-$t9, $a0-$a3, $v0-$v1 복원 ... */
lw $at, 4($sp)
addiu $sp, $sp, 320 /* 스택 프레임 해제 */
eret /* PC←EPC, EXL←0 (원자적) */
.set at
.set reorder
SYSCALL 진입 경로
/* 유저 공간: SYSCALL 명령어 → 예외 벡터로 트랩 */
/* 예외 벡터: 0x80000180 (General Exception, BEV=0) */
/* Cause.ExcCode = 8 (Syscall) */
/* arch/mips/kernel/scall32-o32.S 핵심 경로 */
handle_sys:
lw $v0, PT_R2($sp) /* syscall 번호 ($v0) */
sll $v0, $v0, 2 /* × 4 (테이블 인덱스) */
la $t1, sys_call_table
addu $t1, $t1, $v0
lw $t0, 0($t1) /* 시스콜 핸들러 주소 */
jalr $t0 /* 핸들러 호출 */
nop /* 지연 슬롯 */
ERET — 예외 복귀
/* Status.EXL=1 → EPC에서 복귀, EXL=0으로 복원 */
mfc0 $k0, $14 /* EPC 읽기 */
mfc0 $k1, $12 /* Status 읽기 */
/* Status 복원 처리 ... */
mtc0 $k1, $12 /* Status 쓰기 */
eret /* PC←EPC, EXL←0 */
TLB 조작 패턴
/* TLB Refill 핸들러 (0x80000000) */
tlb_refill:
mfc0 $k0, $4 /* Context 레지스터 */
lw $k1, 0($k0) /* PTE 로드 */
mtc0 $k1, $2 /* EntryLo0 */
lw $k1, 4($k0) /* 다음 PTE */
mtc0 $k1, $3 /* EntryLo1 */
tlbwr /* Random 인덱스로 TLB 쓰기 */
eret
CACHE 명령어
/* D-cache writeback + invalidate */
static inline void flush_dcache_line(unsigned long addr)
{
asm volatile(
"cache %0, 0(%1)"
:: "i"(Hit_Writeback_Inv_D), "r"(addr));
}
op 필드로 캐시 종류(I/D)와 연산을 지정합니다. 하위 2비트가 캐시 종류, 상위 3비트가 연산을 나타냅니다:
| op 값 | 매크로 | 설명 |
|---|---|---|
| 0x00 | Index_Invalidate_I | I-cache: 인덱스로 무효화 |
| 0x01 | Index_Writeback_Inv_D | D-cache: 인덱스로 기록 후 무효화 |
| 0x08 | Index_Store_Tag_I | I-cache: 태그 저장 (초기화용) |
| 0x09 | Index_Store_Tag_D | D-cache: 태그 저장 (초기화용) |
| 0x10 | Hit_Invalidate_I | I-cache: 주소 히트 시 무효화 (자주 사용) |
| 0x11 | Hit_Invalidate_D | D-cache: 주소 히트 시 무효화 (쓰기 미저장) |
| 0x15 | Hit_Writeback_Inv_D | D-cache: 주소 히트 시 기록 후 무효화 (가장 일반적) |
| 0x19 | Hit_Writeback_D | D-cache: 주소 히트 시 기록 (무효화하지 않음) |
Hit_Writeback_Inv_D로 D-cache를 메모리에 기록하고, 자체 수정 코드 후 Hit_Invalidate_I로 I-cache를 무효화합니다.
명령어 인코딩
MIPS는 3가지 기본 인코딩 타입을 사용합니다: R(레지스터), I(즉시값), J(점프).
인코딩 실제 예: addu $t0, $t1, $t2
addu $t0, $t1, $t2는 R-type 명령어입니다. 각 필드에 실제 값을 대입하면:
| 필드 | 값 | 이진수 | 설명 |
|---|---|---|---|
| op | 0 (SPECIAL) | 000000 | R-type은 op=0, funct로 구분 |
| rs | $t1 = $9 | 01001 | 소스 레지스터 1 |
| rt | $t2 = $10 | 01010 | 소스 레지스터 2 |
| rd | $t0 = $8 | 01000 | 목적지 레지스터 |
| sa | 0 | 00000 | 시프트 양 (ADDU에선 미사용) |
| funct | 0x21 (ADDU) | 100001 | ADDU 함수 코드 |
000000 01001 01010 01000 00000 100001 = 0x012A4021
R6 Compact Branch 인코딩
TLB 관리
MIPS는 소프트웨어 관리 TLB를 사용합니다. TLB 미스 시 CPU가 자동으로 페이지 테이블(Page Table)을 검색하는 x86/ARM64와 달리, MIPS는 TLB Refill 예외를 발생시키고 커널이 직접 TLB 엔트리를 채워야 합니다. 이를 위해 EntryHi, EntryLo0/1, PageMask, Index CP0 레지스터와 TLBWR/TLBWI/TLBR/TLBP 명령어를 사용합니다.
/* arch/mips/mm/tlbex.c — TLB Refill 핸들러 (빌드 시 동적 생성) */
/* 가장 성능 크리티컬한 커널 코드 — ~20 명령어로 최적화 */
/* 개념적 TLB Refill 핸들러 (R2/R6 공통 원리) */
tlb_refill_handler:
/* 1. k0/k1은 예외에서 자유롭게 사용 가능 (커널 전용) */
mfc0 k1, C0_CONTEXT /* Context.PTEBase + BadVPN2 → PTE 주소 */
lw k0, 0(k1) /* EntryLo0 = PTE[even] */
lw k1, 4(k1) /* EntryLo1 = PTE[odd] */
mtc0 k0, C0_ENTRYLO0 /* EntryLo0 설정 */
mtc0 k1, C0_ENTRYLO1 /* EntryLo1 설정 */
ehb /* Execution Hazard Barrier */
tlbwr /* Random 위치에 TLB 쓰기 */
eret /* 예외 복귀 (EPC로 점프) */
/* TLB 프로빙과 무효화 */
mtc0 t0, C0_ENTRYHI /* 검색할 VPN2|ASID 설정 */
ehb
tlbp /* TLB 검색 → Index에 결과 */
ehb
mfc0 t1, C0_INDEX /* Index.P(bit31)=1이면 미스 */
bltz t1, not_found /* 음수 → TLB에 없음 */
/* Index >= 0 → 해당 위치에 무효 엔트리 쓰기 */
mtc0 zero, C0_ENTRYLO0
mtc0 zero, C0_ENTRYLO1
ehb
tlbwi /* Index 위치에 쓰기 (무효화) */
CP0 레지스터 비트 필드 상세
CP0 (Coprocessor 0)는 MIPS의 시스템 제어 레지스터 집합입니다. x86의 CR0-CR4/MSR, ARM64의 시스템 레지스터에 대응합니다. 커널은 mfc0/mtc0로 CP0에 접근하며, 예외 처리·TLB 관리·캐시 설정·인터럽트 제어의 핵심입니다.
| CP0 레지스터 | 번호 | 핵심 필드 | 커널 사용 |
|---|---|---|---|
| Index | $0 | P(probe result), Index[5:0] | TLBWI/TLBP 대상 |
| Random | $1 | Random[5:0] | TLBWR 자동 감소 카운터 |
| EntryLo0/1 | $2/$3 | PFN, C, D, V, G | TLB 쓰기 데이터 |
| Context | $4 | PTEBase, BadVPN2 | TLB Refill 빠른 PTE 조회 |
| PageMask | $5 | Mask[28:13] | 가변 페이지 크기 (4KB-256MB) |
| Wired | $6 | Wired[5:0] | 고정 TLB 엔트리 수 |
| BadVAddr | $8 | 전체 VA | 주소 예외/TLB 미스 주소 |
| Count | $9 | 32비트 카운터 | 타이머 (CPU 클럭/2) |
| EntryHi | $10 | VPN2, ASID | TLB 매칭 키 |
| Compare | $11 | 32비트 비교값 | Count==Compare → IP7 인터럽트 |
| Status | $12 | IE, EXL, ERL, KSU, IM, CU | 인터럽트/모드 제어 |
| Cause | $13 | ExcCode, IP, BD | 예외 원인 판별 |
| EPC | $14 | 복귀 주소 | 예외 복귀 (eret) |
| PRId | $15,0 | Company, Processor | CPU 식별 |
| EBase | $15,1 | Exception Base | 예외 벡터 베이스 재배치 |
| Config | $16 | K0, AT, AR, MT | 아키텍처 설정 |
Hazard Barrier 필수: MIPS에서 CP0 레지스터를 mtc0로 수정한 후 해당 값이 실제로 반영되기까지 실행 해저드(Execution Hazard)가 있습니다. R2에서는 ehb(sll $0,$0,3) 명령어를, R6에서는 ehb 또는 충분한 nop 삽입이 필요합니다. TLBWI/TLBWR 전에 ehb를 빠뜨리면 이전 CP0 값이 반영되지 않아 잘못된 TLB 엔트리가 기록될 수 있습니다.
ABI 호출 규약 (o32/n32/n64)
MIPS에는 세 가지 주요 ABI가 존재합니다. o32는 MIPS32의 전통적 ABI로 4개의 인자 레지스터($a0-$a3)를 사용하며, n32와 n64는 MIPS64에서 8개의 인자 레지스터($a0-$a7)를 사용합니다. Linux 커널에서는 arch/mips/kernel/scall32-o32.S와 arch/mips/kernel/scall64-n64.S에서 각 ABI의 시스템 콜 진입점(Entry Point)을 분리 구현합니다.
| 특성 | o32 | n32 | n64 |
|---|---|---|---|
| 레지스터 폭 | 32-bit | 64-bit (32-bit 주소) | 64-bit |
| 인자 레지스터 | $a0-$a3 (4개) | $a0-$a7 (8개) | $a0-$a7 (8개) |
| 반환값 | $v0, $v1 | $v0, $v1 | $v0, $v1 |
| Callee-saved | $s0-$s7, $fp, $sp | $s0-$s7, $fp, $sp | $s0-$s7, $fp, $sp |
| Caller-saved | $t0-$t9, $at, $v0-$v1, $a0-$a3 | $t0-$t3, $at, $v0-$v1, $a0-$a7 | $t0-$t3, $at, $v0-$v1, $a0-$a7 |
| FP 인자 | $f12, $f14 | $f12-$f19 (8개) | $f12-$f19 (8개) |
| 스택 정렬 | 8바이트 | 16바이트 | 16바이트 |
| syscall 번호 | $v0 (4000+) | $v0 (6000+) | $v0 (5000+) |
| HI/LO 저장 | Caller 책임 | Caller 책임 | Caller 책임 |
o32 vs n64 함수 호출 비교
/* o32 ABI: 5개 인자 함수 호출 — $a0-$a3 + 스택 */
/* int func(int a, int b, int c, int d, int e); */
li $a0, 1 /* 첫 번째 인자 */
li $a1, 2 /* 두 번째 인자 */
li $a2, 3 /* 세 번째 인자 */
li $a3, 4 /* 네 번째 인자 */
li $t0, 5
sw $t0, 16($sp) /* 다섯 번째 인자 → 스택 */
jal func /* 함수 호출 */
nop /* 지연 슬롯 */
/* 반환값: $v0 */
/* n64 ABI: 동일한 5개 인자 — 모두 레지스터 */
/* long func(long a, long b, long c, long d, long e); */
li $a0, 1 /* 첫 번째 인자 */
li $a1, 2 /* 두 번째 인자 */
li $a2, 3 /* 세 번째 인자 */
li $a3, 4 /* 네 번째 인자 */
li $a4, 5 /* 다섯 번째 인자 (레지스터!) */
jal func /* 함수 호출 */
nop /* 지연 슬롯 */
/* 반환값: $v0 */
HI/LO 레지스터 저장 (o32)
/* HI/LO 레지스터 컨텍스트 저장/복원 */
/* 곱셈/나눗셈 결과가 HI:LO에 남아있으면 caller가 저장해야 함 */
save_hilo:
mfhi $t0 /* HI → $t0 */
mflo $t1 /* LO → $t1 */
sw $t0, 0($s0) /* HI 저장 */
sw $t1, 4($s0) /* LO 저장 */
restore_hilo:
lw $t0, 0($s0) /* HI 복원 */
lw $t1, 4($s0) /* LO 복원 */
mthi $t0 /* $t0 → HI */
mtlo $t1 /* $t1 → LO */
/* R6에서는 HI/LO 제거 — MUL rd,rs,rt 직접 사용 */
/* R6: MUL $v0, $a0, $a1 → $v0 = $a0 * $a1 하위 32비트 */
/* R6: MUH $v1, $a0, $a1 → $v1 = $a0 * $a1 상위 32비트 */
long 타입이 32비트입니다. MIPS64 프로세서에서 32비트 주소 공간으로 동작하므로, 64비트 레지스터를 활용하면서도 메모리 효율이 높습니다. Linux 커널에서는 CONFIG_MIPS32_O32, CONFIG_MIPS32_N32 옵션으로 ABI 호환 계층을 선택합니다.
예외/인터럽트 처리
MIPS 예외 처리는 Status 레지스터의 EXL(Exception Level) 비트를 중심으로 동작합니다. 예외 발생 시 하드웨어가 자동으로 EXL=1로 설정하여 인터럽트를 비활성화하고 커널 모드로 전환한 뒤, Cause.ExcCode에 예외 원인을 기록하고, EPC에 복귀 주소를 저장합니다. 소프트웨어는 General Exception 벡터(0x80000180)에서 ExcCode를 디코딩하여 적절한 핸들러로 분기합니다.
예외 벡터 초기화
/* arch/mips/kernel/traps.c — 예외 벡터 설치 */
void __init trap_init(void)
{
/* exception_handlers[] 테이블 초기화 */
set_handler(0 * 4, handle_int); /* ExcCode 0: 인터럽트 */
set_handler(1 * 4, handle_tlbm); /* ExcCode 1: TLB Modified */
set_handler(2 * 4, handle_tlbl); /* ExcCode 2: TLB Load */
set_handler(3 * 4, handle_tlbs); /* ExcCode 3: TLB Store */
set_handler(4 * 4, handle_adel); /* ExcCode 4: Address Error Load */
set_handler(5 * 4, handle_ades); /* ExcCode 5: Address Error Store */
set_handler(8 * 4, handle_sys); /* ExcCode 8: System Call */
set_handler(9 * 4, handle_bp); /* ExcCode 9: Breakpoint */
set_handler(10 * 4, handle_ri); /* ExcCode 10: Reserved Instruction */
/* TLB Refill 핸들러를 0x80000000에 복사 */
set_uncached_handler(0x0, &tlb_handler, 0x80);
/* General Exception 핸들러를 0x80000180에 복사 */
set_uncached_handler(0x180, &except_vec3_generic, 0x80);
}
General Exception 디스패치
/* arch/mips/kernel/genex.S — General Exception 벡터 */
NESTED(except_vec3_generic, 0, sp)
.set push
.set noat
mfc0 k1, CP0_CAUSE /* Cause 레지스터 읽기 */
andi k1, k1, 0x7c /* ExcCode 필드 추출 (bit 6:2) */
#ifdef CONFIG_64BIT
dsll k1, k1, 1 /* 64비트: 8바이트 포인터 */
#endif
PTR_L k0, exception_handlers(k1) /* 핸들러 주소 로드 */
jr k0 /* 핸들러로 점프 */
.set pop
END(except_vec3_generic)
Status/Cause 레지스터 조작
/* 인터럽트 마스크 조작 예제 */
enable_irq:
mfc0 $t0, $12 /* Status 읽기 */
ori $t0, $t0, 0x8401 /* IE=1, IM2=1, IM7=1 활성 */
mtc0 $t0, $12 /* Status 쓰기 */
ehb /* 실행 해저드 배리어 */
disable_irq_save:
di $v0 /* R2: 인터럽트 비활성 + 이전 Status 반환 */
ehb /* 해저드 배리어 */
jr $ra
nop
restore_irq:
mtc0 $a0, $12 /* 저장된 Status 복원 */
ehb /* 해저드 배리어 */
jr $ra
nop
$k0/$k1 전용 레지스터: 예외 핸들러 진입 시점에서는 아직 레지스터를 저장할 스택 포인터조차 신뢰할 수 없습니다. 그래서 MIPS ABI는 $k0($26)과 $k1($27)을 커널 전용으로 예약했습니다. 유저 프로그램은 이 레지스터를 사용하면 안 되며, 예외 핸들러가 경고 없이 덮어쓸 수 있습니다.
커널 어셈블리 실전 예제
Linux MIPS 커널은 성능 크리티컬 경로와 하드웨어 의존 코드를 어셈블리로 직접 구현합니다. 시스템 콜 진입, 컨텍스트 스위치, 원자적 연산(LL/SC), 유저 공간 메모리 접근, TLB Refill 핸들러가 대표적입니다.
시스템 콜 진입 (handle_sys)
/* arch/mips/kernel/scall32-o32.S — o32 시스템 콜 핸들러 */
NESTED(handle_sys, PT_SIZE, sp)
.set noat
SAVE_SOME /* $at, $v0-$v1, $a0-$a3, ... 저장 */
SAVE_AT
SAVE_TEMP
lw $t0, PT_R2($sp) /* 시스콜 번호 ($v0에서 저장됨) */
subu $t0, $t0, __NR_O32_Linux /* 오프셋 계산 */
sltiu $t1, $t0, __NR_O32_Linux_syscalls
beqz $t1, illegal_syscall /* 범위 초과 */
sll $t0, $t0, 2 /* × 4 (함수 포인터 크기) */
la $t1, sys_call_table /* 시스콜 테이블 주소 */
addu $t1, $t1, $t0
lw $t0, 0($t1) /* 핸들러 함수 주소 로드 */
jalr $t0 /* 시스콜 핸들러 호출 */
nop /* 지연 슬롯 */
sw $v0, PT_R2($sp) /* 반환값 저장 */
j syscall_exit
nop
END(handle_sys)
컨텍스트 스위치 (resume)
/* arch/mips/kernel/r4k_switch.S — 프로세스 전환 */
LEAF(resume)
mfc0 $t1, CP0_STATUS /* 현재 Status 저장 */
sw $t1, THREAD_STATUS($a0) /* prev→thread.cp0_status */
/* callee-saved 레지스터 저장 */
sw $s0, THREAD_REG16($a0)
sw $s1, THREAD_REG17($a0)
sw $s2, THREAD_REG18($a0)
sw $s3, THREAD_REG19($a0)
sw $s4, THREAD_REG20($a0)
sw $s5, THREAD_REG21($a0)
sw $s6, THREAD_REG22($a0)
sw $s7, THREAD_REG23($a0)
sw $sp, THREAD_REG29($a0) /* prev 스택 포인터 */
sw $fp, THREAD_REG30($a0)
sw $ra, THREAD_REG31($a0)
/* next 프로세스로 전환 */
lw $s0, THREAD_REG16($a1)
lw $s1, THREAD_REG17($a1)
lw $s2, THREAD_REG18($a1)
lw $s3, THREAD_REG19($a1)
lw $s4, THREAD_REG20($a1)
lw $s5, THREAD_REG21($a1)
lw $s6, THREAD_REG22($a1)
lw $s7, THREAD_REG23($a1)
lw $sp, THREAD_REG29($a1) /* next 스택 포인터 */
lw $fp, THREAD_REG30($a1)
lw $ra, THREAD_REG31($a1)
lw $t1, THREAD_STATUS($a1)
mtc0 $t1, CP0_STATUS /* next Status 복원 */
move $v0, $a0 /* prev 태스크 반환 */
jr $ra
nop
END(resume)
원자적 연산 (LL/SC 루프)
/* arch/mips/include/asm/atomic.h — atomic_add 어셈블리 */
/* LL/SC 루프: 하드웨어 비교-교환 없이 원자성 보장 */
atomic_add: /* $a0=값, $a1=atomic_t 주소 */
1:
ll $t0, 0($a1) /* Load Linked: 값 로드 + 링크 설정 */
addu $t0, $t0, $a0 /* 덧셈 */
sc $t0, 0($a1) /* Store Conditional: 링크 유효 시 저장 */
beqz $t0, 1b /* SC 실패(0) → 재시도 */
nop /* 지연 슬롯 */
jr $ra
nop
/* R6에서는 SC 실패 시 자동 재시도하는 llsc_wb 옵션 가능 */
/* CONFIG_WAR_R10000_LLSC: R10000 LL/SC 버그 워크어라운드 */
유저 공간 메모리 접근 (copy_to_user)
/* arch/mips/lib/memcpy.S — __copy_user 핵심 루프 */
/* 유저 공간 접근 시 페이지 폴트 가능 → fixup 섹션 필요 */
__copy_user:
.set noreorder
EXC( lw $t0, 0($a1), l_fixup) /* 유저에서 읽기 */
EXC( lw $t1, 4($a1), l_fixup)
EXC( sw $t0, 0($a0), s_fixup) /* 커널에 쓰기 */
EXC( sw $t1, 4($a0), s_fixup)
addiu $a0, $a0, 8
addiu $a1, $a1, 8
subu $a2, $a2, 8
bnez $a2, __copy_user
nop
/* EXC 매크로: __ex_table에 fixup 주소 등록 */
/* 페이지 폴트 발생 시 fixup으로 점프하여 -EFAULT 반환 */
l_fixup:
move $v0, $a2 /* 미복사 바이트 수 반환 */
jr $ra
nop
TLB Refill 핸들러
/* arch/mips/mm/tlbex.c에서 동적 생성되는 TLB Refill 핸들러 (개념) */
/* 0x80000000에 배치, 최대 32개 명령어 */
tlb_refill:
mfc0 k0, CP0_BADVADDR /* 미스된 가상 주소 */
mfc0 k1, CP0_CONTEXT /* PTEBase + BadVPN2 */
lw k0, 0(k1) /* PTE 짝수 페이지 */
lw k1, 4(k1) /* PTE 홀수 페이지 */
mtc0 k0, CP0_ENTRYLO0 /* EntryLo0 설정 */
mtc0 k1, CP0_ENTRYLO1 /* EntryLo1 설정 */
ehb /* 해저드 배리어 */
tlbwr /* Random 위치에 TLB 쓰기 */
eret /* 복귀 (EXL ← 0) */
/* Linux 실제 구현: tlbex.c에서 CPU 모델별 최적 코드를 런타임 생성 */
/* MIPS32: Context 레지스터의 PTEBase로 PGD 접근 */
/* MIPS64: XCONTEXT로 3-level 페이지 테이블 탐색 */
arch/mips/mm/tlbex.c의 build_r4000_tlb_refill_handler()에서 CPU 모델에 맞게 동적으로 생성합니다. R2/R6, 32/64비트, HugePage 지원 여부에 따라 최적화된 명령어 시퀀스가 만들어집니다.
캐시 연산
MIPS의 CACHE 명령어는 I-cache와 D-cache를 직접 제어할 수 있는 특권 명령어입니다. 명령어 형식은 cache op, offset(base)이며, op 필드가 캐시 종류(2비트)와 연산 종류(3비트)를 인코딩합니다. DMA 전송이나 자기 수정 코드(SMC) 시나리오에서 올바른 캐시 관리가 필수적입니다.
캐시 명령어 사용
/* MIPS 캐시 연산 기본 사용법 */
/* cache op, offset(base) 형식 */
/* D-cache 라인 Writeback + Invalidate (Hit) */
cache 0x15, 0($a0) /* Hit_Writeback_Inv_D (101_01) */
/* I-cache 라인 Invalidate (Hit) */
cache 0x10, 0($a0) /* Hit_Invalidate_I (100_00) */
/* D-cache 전체 Invalidate (Index) — 부팅 시 초기화 */
__init_dcache:
li $t0, 0x80000000 /* kseg0 시작 */
li $t1, DCACHE_SIZE /* D-cache 전체 크기 */
addu $t1, $t0, $t1
1:
cache 0x01, 0($t0) /* Index_Writeback_Inv_D */
addiu $t0, $t0, LINESIZE /* 다음 캐시 라인 */
bne $t0, $t1, 1b
nop
r4k_blast_dcache 구현
/* arch/mips/mm/c-r4k.c — D-cache 전체 flush */
static void r4k_blast_dcache(void)
{
unsigned long dc_lsize = cpu_dcache_line_size();
unsigned long dc_size = current_cpu_data.dcache.waysize
* current_cpu_data.dcache.ways;
unsigned long addr = CKSEG0; /* kseg0: 0x80000000 */
unsigned long end = addr + dc_size;
while (addr < end) {
cache_op(Index_Writeback_Inv_D, addr);
addr += dc_lsize;
}
}
/* DMA 동기화: 가상 주소 범위의 D-cache flush */
static void r4k_dma_cache_wback_inv(unsigned long addr, unsigned long size)
{
if (size >= dcache_size) {
r4k_blast_dcache(); /* 전체 flush가 더 빠름 */
} else {
blast_dcache_range(addr, addr + size); /* 범위 flush */
}
}
DMA 캐시 동기화
/* arch/mips/mm/dma-noncoherent.c — DMA 캐시 동기화 */
void arch_sync_dma_for_device(phys_addr_t paddr, size_t size,
enum dma_data_direction dir)
{
switch (dir) {
case DMA_TO_DEVICE:
dma_cache_wback((unsigned long)phys_to_virt(paddr), size);
break;
case DMA_FROM_DEVICE:
dma_cache_inv((unsigned long)phys_to_virt(paddr), size);
break;
case DMA_BIDIRECTIONAL:
dma_cache_wback_inv((unsigned long)phys_to_virt(paddr), size);
break;
}
}
/* MIPS 하드웨어 캐시 일관성이 없는 플랫폼에서 필수 */
/* CONFIG_DMA_NONCOHERENT: 대부분의 MIPS SoC */
I-cache/D-cache 비일관성: MIPS는 일반적으로 Harvard 캐시 구조를 사용하여 I-cache와 D-cache가 분리됩니다. 자기 수정 코드(JIT, 모듈 로딩)에서는 반드시 D-cache Writeback 후 I-cache Invalidate를 수행해야 합니다. flush_icache_range()가 이 시퀀스를 처리합니다.
마이크로아키텍처 변종
MIPS ISA는 여러 Release와 변종으로 나뉩니다. 특히 Release 6(2014)은 이전 버전과 하위 호환이 깨지는 대규모 변경을 도입했습니다. 지연 슬롯 없는 compact branch, HI/LO 레지스터 제거, 정렬되지 않은 메모리 접근 지원, 일부 명령어 재인코딩 등이 핵심 변경 사항입니다. 또한 microMIPS와 nanoMIPS 변종은 코드 밀도를 높이기 위한 가변 길이 인코딩을 제공합니다.
| 특성 | MIPS32/64 R2 | MIPS32/64 R6 | microMIPS | nanoMIPS |
|---|---|---|---|---|
| 출시 | 2003 | 2014 | 2009 | 2018 |
| 명령어 길이 | 고정 32비트 | 고정 32비트 | 16/32비트 혼합 | 16/32/48비트 혼합 |
| 지연 슬롯 | 있음 (필수) | compact branch 추가 | 있음/없음 혼합 | 없음 |
| HI/LO 레지스터 | 사용 (MULT→HI:LO) | 제거 (MUL rd,rs,rt) | R2 기반 | R6 기반 |
| 비정렬 접근 | 예외 발생 (LWL/LWR 사용) | 하드웨어 지원 | R2 동일 | R6 동일 |
| 분기 조건 | BEQ/BNE + 지연 슬롯 | BC/BALC (compact, PC-rel) | B16/BEQ16 등 | BC/BALC |
| PC-relative | 없음 | ALUIPC, AUIPC, LWPC | ADDIUPC | 전면 지원 |
| Linux 지원 | 전면 지원 | 4.0+ 지원 | 부분 지원 | 미지원 |
| 주요 CPU | MIPS24K/34K/74K, Octeon | Warrior I6400/I6500 | M14K/M14Kc | I7200 (IoT) |
| 코드 밀도 | 기본 | 기본 | ~35% 절감 | ~40% 절감 |
R6 vs pre-R6 분기 차이
/* pre-R6 (R2): 지연 슬롯 있는 분기 */
beq $a0, $a1, target /* 분기 결정 */
addiu $v0, $zero, 1 /* 지연 슬롯: 항상 실행됨! */
/* 분기 안 됐으면 여기로 */
addiu $v0, $zero, 0
/* R6: compact branch — 지연 슬롯 없음 */
beqc $a0, $a1, target /* compact: 지연 슬롯 없음 */
/* 분기 안 됐으면 즉시 여기 실행 */
addiu $v0, $zero, 0
/* R6: PC-relative 주소 계산 (PIC 코드에 유용) */
auipc $t0, %pcrel_hi(symbol) /* PC + upper 20비트 */
addiu $t0, $t0, %pcrel_lo(symbol) /* + lower 12비트 */
/* R6: 비정렬 로드 (LWL/LWR 대체) */
lw $t0, 1($a0) /* R6: 비정렬 주소도 하드웨어 처리 */
/* pre-R6: 비정렬 4바이트 로드 */
lwl $t0, 3($a0) /* Left: 상위 바이트 로드 */
lwr $t0, 0($a0) /* Right: 하위 바이트 로드 */
Compact Branch 활용
/* R6 compact branch 종류 */
/* 모두 지연 슬롯 없음, 분기 조건 직후 다음 명령어는 비분기 시에만 실행 */
/* 레지스터 비교 분기 */
beqc $a0, $a1, equal /* $a0 == $a1 */
bnec $a0, $a1, not_equal /* $a0 != $a1 */
bltc $a0, $a1, less /* $a0 < $a1 (signed) */
bgec $a0, $a1, ge /* $a0 >= $a1 (signed) */
bltuc $a0, $a1, less_u /* $a0 < $a1 (unsigned) */
bgeuc $a0, $a1, ge_u /* $a0 >= $a1 (unsigned) */
/* 즉석 비교 분기 (0과 비교) */
beqzc $a0, is_zero /* $a0 == 0 */
bnezc $a0, not_zero /* $a0 != 0 */
bltzc $a0, negative /* $a0 < 0 */
bgezc $a0, non_neg /* $a0 >= 0 */
/* 무조건 compact 분기 */
bc target /* 무조건 점프 (26비트 오프셋) */
balc target /* 무조건 함수 호출 (link) */
/* 커널 영향: R6 지원 시 지연 슬롯 nop 제거 → 코드 크기 감소 */
CONFIG_CPU_MIPSR2와 CONFIG_CPU_MIPSR6로 빌드 타임에 구분합니다. 일부 R2 전용 명령어(LWL/LWR, MULT→HI:LO)를 R6에서 사용하면 Reserved Instruction(RI) 예외가 발생합니다.
EVA (Enhanced Virtual Addressing)
MIPS EVA(Enhanced Virtual Addressing)는 MIPS32 Release 3(2010)에서 도입된 확장으로, 커널과 유저 공간이 동일한 가상 주소 범위를 사용할 수 있게 합니다. 기존 MIPS에서는 kseg0/kseg1(0x80000000~0xBFFFFFFF)이 커널 전용이었지만, EVA는 SegCtl CP0 레지스터를 통해 세그먼트 매핑(Mapping)을 재구성하여 커널이 유저 주소 공간의 전체 4GB를 활용할 수 있게 합니다.
EVA 전용 로드/스토어 매크로
/* arch/mips/include/asm/eva.h — EVA 유저 접근 매크로 */
/* EVA 활성화 시: 커널 모드에서 유저 VA 접근에 특수 명령어 사용 */
/* 유저 공간 바이트 로드 */
#ifdef CONFIG_EVA
#define user_lw(reg, addr) "lwe " reg ", " addr /* EVA: lwe 명령어 */
#define user_sw(reg, addr) "swe " reg ", " addr /* EVA: swe 명령어 */
#define user_lb(reg, addr) "lbe " reg ", " addr
#define user_sb(reg, addr) "sbe " reg ", " addr
#else
#define user_lw(reg, addr) "lw " reg ", " addr /* 일반: lw 명령어 */
#define user_sw(reg, addr) "sw " reg ", " addr /* 일반: sw 명령어 */
#define user_lb(reg, addr) "lb " reg ", " addr
#define user_sb(reg, addr) "sb " reg ", " addr
#endif
/* copy_from_user에서 사용 예시 */
/* user_lw("$t0", "0($a1)") → EVA: "lwe $t0, 0($a1)" */
/* → 일반: "lw $t0, 0($a1)" */
/* EVA 장점: 유저 3.5GB 가능 (기존 2GB 한계 극복) */
/* 임베디드 MIPS SoC (Ci40, Malta) 에서 주로 사용 */
CONFIG_EVA 옵션으로 활성화하며, arch/mips/include/asm/mach-malta/spaces.h에서 세그먼트 배치를 정의합니다.
MT (Multi-Threading) / VPE
MIPS MT(Multi-Threading) ASE는 하드웨어 수준의 멀티스레딩을 지원하는 확장입니다. 하나의 물리 코어 내에 여러 개의 TC(Thread Context)와 VPE(Virtual Processing Element)를 배치하여, 각 VPE가 독립적인 프로세서처럼 동작합니다. Intel Hyper-Threading과 유사한 개념이지만, MIPS MT는 더 세밀한 하드웨어 자원 공유 제어를 제공합니다.
VPE/TC 구성과 SMTC 스케줄러(Scheduler) 통합
/* arch/mips/kernel/mips-mt.c — MIPS MT 초기화 */
void __init mips_mt_init(void)
{
unsigned int mvpconf0 = read_c0_mvpconf0();
unsigned int nvpe = (mvpconf0 & MVPCONF0_PVPE) >> MVPCONF0_PVPE_SHIFT;
unsigned int ntc = (mvpconf0 & MVPCONF0_PTC) >> MVPCONF0_PTC_SHIFT;
pr_info("MIPS MT: %d VPEs, %d TCs\n", nvpe + 1, ntc + 1);
/* VSMP 모드: 각 VPE에 하나의 TC 바인딩 */
/* VPE 0 = Boot CPU, VPE 1+ = Secondary CPUs */
}
/* arch/mips/kernel/smp-mt.c — VSMP 부팅 */
static void vsmp_boot_secondary(int cpu, struct task_struct *idle)
{
/* DVP: 다른 VPE 비활성화 */
dvpe();
/* 대상 TC의 TCRestart에 부팅 함수 주소 설정 */
write_tc_c0_tcrestart((unsigned long)&smp_bootstrap);
/* TC 활성화: TCStatus.A = 1 */
write_tc_c0_tcstatus(
read_tc_c0_tcstatus() | TCSTATUS_A);
/* EVP: VPE 실행 재개 */
evpe(EVPE_ENABLE);
}
/* MT 전용 CP0 레지스터: MVPControl, MVPConf0/1, VPEControl, */
/* VPEConf0/1, TCStatus, TCBind, TCRestart, TCHalt, TCContext */
/* 명령어: dvpe/evpe (VPE 제어), dmt/emt (MT 제어), fork/yield */
NR_CPUS=2로 동작합니다. 74Kf는 2코어 각 1VPE 구조입니다. /proc/cpuinfo에서 VPE 번호를 확인할 수 있으며, 스케줄러는 VPE 간 캐시/TLB 공유를 고려한 토폴로지 정보를 활용합니다.
alternatives 패칭
MIPS Linux 커널은 alternatives 프레임워크를 사용하여 부팅 시 CPU 기능에 따라 명령어를 동적으로 패칭합니다. 예를 들어 R6 CPU에서는 지연 슬롯 없는 compact branch로 교체하고, LL/SC 버그가 있는 R10000에서는 워크어라운드 코드를 삽입합니다. x86의 alternatives와 유사한 개념으로, cpu_has_* 매크로로 CPU 기능을 감지하고 해당하는 최적 코드 경로를 선택합니다.
MIPS alternatives 매크로
/* arch/mips/include/asm/alternative.h — alternatives 프레임워크 */
/* 부팅 시 CPU 기능에 따라 명령어 동적 교체 */
#define ALTERNATIVE(old, new, feature) \
"661:\n" \
old "\n" \
"662:\n" \
".section .altinstructions, \"a\"\n" \
".word 661b\n" /* 원본 위치 */ \
".word 663f\n" /* 대체 코드 */ \
".hword " __stringify(feature) "\n" \
".hword 662b - 661b\n" /* 원본 길이 */ \
".hword 664f - 663f\n" /* 대체 길이 */ \
".previous\n" \
".section .altinstr_replacement, \"ax\"\n" \
"663:\n" new "\n664:\n" \
".previous\n"
/* 사용 예: R6에서 compact branch 사용 */
static inline void some_function(void)
{
asm volatile(
ALTERNATIVE(
"beqz %0, 1f\n nop", /* 기본: R2 지연 슬롯 */
"beqzc %0, 1f", /* R6: compact, nop 불필요 */
cpu_has_mips_r6
)
: : "r"(val)
);
}
/* apply_alternatives(): 부팅 시 .altinstructions 섹션 순회 */
/* cpu_has_* 확인 후 원본 코드를 대체 코드로 memcpy + I-cache flush */
cpu_has_* 기능 감지와 런타임 패칭
/* arch/mips/include/asm/cpu-features.h — CPU 기능 매크로 */
/* Config 레지스터 체인 (Config→Config1→Config2→...) 파싱 결과 */
#define cpu_has_mips_r2 (cpu_data[0].isa_level & MIPS_CPU_ISA_M32R2)
#define cpu_has_mips_r6 (cpu_data[0].isa_level & MIPS_CPU_ISA_M32R6)
#define cpu_has_tlb (cpu_data[0].options & MIPS_CPU_TLB)
#define cpu_has_llsc (cpu_data[0].options & MIPS_CPU_LLSC)
#define cpu_has_msa (cpu_data[0].ases & MIPS_ASE_MSA)
#define cpu_has_eva (cpu_data[0].options & MIPS_CPU_EVA)
#define cpu_has_mt (cpu_data[0].ases & MIPS_ASE_MIPS_MT)
#define cpu_has_vz (cpu_data[0].ases & MIPS_ASE_VZ)
#define cpu_has_dsp (cpu_data[0].ases & MIPS_ASE_DSP)
#define cpu_has_smartmips (cpu_data[0].ases & MIPS_ASE_SMARTMIPS)
/* arch/mips/kernel/cpu-probe.c — 부팅 시 Config 체인 파싱 */
void cpu_probe(void)
{
unsigned int config0 = read_c0_config();
unsigned int config1 = read_c0_config1();
/* Config.AT: 아키텍처 타입 (0=R1, 1=R2, 2=R6) */
switch (config0 & MIPS_CONF_AT) {
case 0: c->isa_level |= MIPS_CPU_ISA_M32R1; break;
case 1: c->isa_level |= MIPS_CPU_ISA_M32R2; break;
case 2: c->isa_level |= MIPS_CPU_ISA_M32R6; break;
}
/* Config1: TLB 엔트리 수, I/D 캐시 설정 */
if (config1 & MIPS_CONF1_FP) c->options |= MIPS_CPU_FPU;
if (config1 & MIPS_CONF1_WR) c->options |= MIPS_CPU_WATCH;
/* Config3: ASE (MT, MSA, EVA, VZ 등) */
unsigned int config3 = read_c0_config3();
if (config3 & MIPS_CONF3_MT) c->ases |= MIPS_ASE_MIPS_MT;
if (config3 & MIPS_CONF3_MSA) c->ases |= MIPS_ASE_MSA;
/* alternatives 적용 */
apply_alternatives();
}
CONFIG_WAR_R10000_LLSC는 R10000의 LL/SC 하드웨어 버그를 우회하기 위해 LL/SC 루프 주변에 추가 배리어 명령어를 삽입합니다. arch/mips/include/asm/war.h에 플랫폼별 워크어라운드가 정의되어 있습니다.
참고 자료
- MIPS 공식 아키텍처 페이지 — MIPS Architecture Products
- MIPS32 명령어셋 레퍼런스 — MIPS32 Instruction Set Architecture Reference (Volume II)
- MIPS64 명령어셋 레퍼런스 — MIPS64 Instruction Set Architecture Reference (Volume II)
- 커널 공식 문서 — Linux kernel MIPS architecture documentation
- 커널 소스 — arch/mips 디렉터리 (Bootlin Elixir)
- 커널 소스 — arch/mips/kernel/genex.S 예외 핸들러 (Bootlin Elixir)
- 커널 소스 — arch/mips/kernel/head.S 부팅 엔트리 (Bootlin Elixir)
- 커널 소스 — arch/mips/include/asm/mipsregs.h CP0 레지스터 정의 (Bootlin Elixir)
- LWN.net MIPS 아키텍처 기사 모음 — LWN Kernel Index: MIPS
- LWN.net — The end of the MIPS architecture?
- 커널 소스 — arch/mips (GitHub torvalds/linux)
- Linux/MIPS 커뮤니티 위키 — Linux/MIPS project
- MIPS 아키텍처 개요 — Wikipedia: MIPS architecture
관련 문서
- 어셈블리 종합 — GCC 인라인 어셈블리, 호출 규약
- SIMD 명령과 커널 개발 — 커널 SIMD 사용
- GNU Assembler (as) — GAS 지시자
- x86_64 명령어셋 (ISA) — x86_64 CISC 비교
- ARM64 명령어셋 (ISA) — ARM64 RISC 비교
- RISC-V 명령어셋 (ISA) — RISC-V 모듈형 ISA