컴퓨터구조 1편

MIPS 명령어 형식과 데이터 전송 — R-type, I-type, lw·sw

suhyun·2026년 9월 22일·읽는 데 약 9분
한 줄로: MIPS 명령어는 전부 32비트 한 줄이다. 그 32비트를 어디서 자르는지만 알면 명령어를 전부 읽을 수 있다.

1. 32비트 한 줄

add $t0, $s1, $s2. MIPS 명령어를 처음 만나면 이렇게 생겼습니다. 달러 기호와 숫자가 섞여 있어 처음 보면 읽기 어렵습니다. 그런데 이 한 줄은 0과 1로 된 32비트 한 줄로 정확히 바뀝니다. 그 32비트를 어디서 자르는지만 알면, MIPS 명령어를 전부 읽을 수 있습니다. 이 글에서는 그 자르는 방법 두 가지를 보고, 문제 네 개를 직접 풉니다.

2. ISA와 레지스터

먼저 ISA입니다. 명령어 집합이 어떻게 해석되고 실행되는지 정한 약속입니다. 프로세서가 어떤 명령어를 알아듣고, 레지스터가 몇 개이고, 메모리를 어떻게 읽는지가 여기서 정해집니다. x86, ARM, RISC-V, MIPS가 전부 이런 약속의 이름입니다. 이 시리즈는 MIPS로 갑니다. 명령어 형식이 세 가지뿐이라 가장 단순하기 때문입니다. ARM은 다섯 가지입니다.

약속의 첫 번째는 레지스터입니다. 프로세서 안에 있는 작은 저장 칸이고, 계산은 전부 여기서 일어납니다. MIPS에는 이 칸이 32개 있고, 하나가 32비트입니다. 32비트는 4바이트이고, 이것을 워드라고 부릅니다. 하드웨어는 이 32개를 0번부터 31번까지 번호로만 봅니다. 이름은 사람이 읽기 쉬우라고 붙인 관례입니다.

  • $s0~$s7 (16~23번) — 값을 오래 들고 있을 때 씁니다.
  • $t0~$t7 (8~15번) — 잠깐 쓰고 버릴 때 씁니다.
  • $zero (0번) — 항상 0이 들어 있습니다.

이 세 가지만 알면 이 글의 명령어는 전부 읽을 수 있습니다. 번호는 잠시 뒤 그대로 씁니다.

3. R-type — 레지스터 사이의 연산

약속의 둘째 항목은 명령어 형식입니다. MIPS 명령어는 길이가 전부 32비트이고, 이 32비트를 어디서 자르느냐가 형식입니다. 첫 번째가 R-type, 레지스터 사이의 연산에 쓰는 형식입니다. 앞뒤가 6비트, 가운데 네 칸이 5비트씩, 여섯 조각입니다.

형식필드 (비트 수)
R-typeop (6)rs (5)rt (5)rd (5)shamt (5)funct (6)
I-typeop (6)rs (5)rt (5)상수 (16)

앞의 op는 연산 코드입니다. rs와 rt는 읽어 올 레지스터 두 개, rd는 결과를 넣을 레지스터입니다. 5비트면 0부터 31까지, 레지스터 32개를 정확히 가리킵니다. shamt에는 비트를 미는 양이 들어가고, funct가 실제 연산을 정합니다.

4. 문제 1 — add를 32비트로

add $t0, $s1, $s2를 32비트로 바꿉니다. 덧셈이니 op에 덧셈 코드가 들어갈 것 같습니다. 그런데 R-type의 op는 항상 0입니다. 덧셈이라는 정보는 맨 뒤 funct에 32로 들어갑니다. 레지스터는 아까 본 번호로 바꿉니다. $s1은 17, $s2는 18, 결과를 받는 $t0는 8입니다. 미는 연산이 아니니 shamt는 0입니다.

op     rs     rt     rd     shamt  funct
0      17     18     8      0      32
000000 10001  10010  01000  00000  100000

십진수를 각 칸의 비트 수에 맞게 이진수로 펴면 32비트 한 줄이 완성됩니다. 뺄셈 sub는 funct만 34로 바뀝니다.

op가 0인 이유

왜 op에 0만 넣고 연산은 뒤에서 정할까요. R-type은 반드시 레지스터 두 개를 읽어야 하고, 읽는 데 시간이 걸립니다. op에 0이 들어 있으면 프로세서는 R-type이라고 바로 압니다. 무슨 연산인지 아직 몰라도, 가운데 rs와 rt를 먼저 읽기 시작합니다. 해독과 레지스터 읽기가 동시에 진행됩니다. 연산의 종류는 레지스터 값이 도착할 때 funct로 알면 늦지 않습니다. 필드를 어디에 두느냐가 속도로 이어집니다.

5. I-type — 명령어 안에 상수를

$s2 대신 $s1에 4를 더하고 싶으면 어떻게 할까요. 4라는 값 자체를 명령어 안에 넣어야 합니다. 값이 레지스터가 아니라 명령어 안에 직접 박혀 있는 방식을 즉시 어드레싱이라고 부릅니다.

문제는 자리입니다. 5비트 칸에는 0부터 31까지밖에 안 들어갑니다. 그래서 뒤의 세 칸을 하나로 합쳐 16비트를 만듭니다. 이것이 두 번째 형식, I-type입니다. op가 6비트, rs와 rt가 5비트씩, 상수가 16비트입니다. 이 형식에 addi $s0, $s1, 4를 넣어 보면 op는 8, rs는 17, rt는 16, 상수는 4입니다. I-type에서는 rt가 결과를 받는 자리입니다. op가 0이 아니므로 funct 없이 op만으로 연산이 정해집니다.

6. 산술과 논리 명령어

지금까지 본 add, sub, addi가 산술 명령어입니다. 곱셈 mult는 결과가 64비트라 레지스터 하나에 안 들어갑니다. 그래서 Hi와 Lo 두 레지스터에 나눠 담습니다. 이 둘은 아까 센 32개 바깥에 따로 있는 칸입니다. 번호로 가리킬 수 없으니 전용 명령어 mfhimflo로 꺼냅니다.

논리 명령어 and, or, nor는 같은 자리의 비트끼리 짝지어 계산합니다. 32비트 두 개를 넣으면 32비트 하나가 나옵니다. C 언어에서 &&&가 다른 이유가 이것입니다. sllsrl은 비트를 왼쪽, 오른쪽으로 밉니다.

7. 문제 2·3 — 레지스터 아끼기와 NOT

f = (g+h) − (i+j)

g, h, i, j, f는 순서대로 $s0부터 $s4에 들어 있습니다. 더하기 둘, 빼기 하나이니 명령어 세 줄이면 될 것 같습니다.

add $s4, $s0, $s1   # g + h
add $s1, $s2, $s3   # i + j, 다 쓴 h 자리에 덮어씀
sub $s4, $s4, $s1   # 앞의 합 − 뒤의 합

둘째 줄에서 결과를 어디에 둘지가 문제입니다. 새 레지스터를 꺼내 쓰고 싶어지지만, h는 이미 다 썼습니다. 다 쓴 자리는 덮어써도 됩니다. 레지스터를 더 줄일 수 있느냐고 물으면, 이렇게 다 쓴 자리를 찾아 덮어쓰면 됩니다.

비트 반전

32비트의 0과 1을 전부 뒤집고 싶습니다. 그런데 MIPS 명령어 목록에 not이 없습니다. 있는 것으로 만들어야 합니다. 여기서 쓰는 것이 nor$zero입니다. nor는 or 결과를 뒤집은 것입니다. 어떤 비트에 0을 or하면 그 비트 그대로이고, 그것을 뒤집으면 반전입니다. 그래서 nor $s1, $s2, $zero 한 줄이 비트 반전입니다. 항상 0인 레지스터가 이런 데 쓰입니다.

8. 메모리 — 바이트와 워드

약속의 셋째 항목은 메모리입니다. 메모리는 거대한 1차원 배열이고, 주소는 그 배열의 인덱스입니다. 주소 하나에 1바이트가 들어 있습니다. 그런데 레지스터는 4바이트 워드입니다. 워드 하나는 바이트 네 칸을 차지합니다. 0번부터 3번 바이트가 워드 0, 4번부터 7번이 워드 1입니다. 그래서 워드 단위로 움직일 때는 주소가 4씩 커집니다.

주소는 32비트입니다. 그러면 셀 수 있는 바이트는 232, 4기가바이트입니다. 워드 수는 그것을 4로 나눈 230입니다. 바이트로 세느냐 워드로 세느냐만 맞추면 되는 계산입니다.

9. lw와 sw

배열 같은 자료구조는 메모리에 있습니다. 그런데 add나 and는 레지스터에서만 일어납니다. 그러니 메모리와 레지스터 사이에 값을 옮기는 명령어가 필요합니다. MIPS에는 딱 두 개, lwsw입니다.

lw $s1, 8($s0). 괄호 안의 $s0가 기준 주소이고, 앞의 8이 거기서 떨어진 거리입니다. 기준 주소에 거리를 더해 실제 주소를 구하는 이 방식을 베이스 더하기 오프셋 어드레싱이라고 부릅니다. $s0에 8을 더한 주소부터 바이트 네 개를 한 번에 읽어 $s1에 넣습니다. sw $s1, 12($s0)는 반대 방향입니다. $s1의 4바이트를 $s0 더하기 12부터 네 칸에 저장합니다.

읽고 쓰는 방향은 프로세서 기준입니다. 프로세서가 가져오면 load, 내보내면 store입니다. 둘 다 I-type이고, 16비트 상수 칸에 오프셋이 들어갑니다.

10. 문제 4 — 배열 원소 다루기

A[8] = h + A[8]

$s3가 배열 A의 시작 주소, $s2가 h입니다. 인덱스가 8이니 오프셋도 8일 것 같습니다. 그런데 원소 하나가 4바이트입니다. 인덱스 8은 시작에서 8 곱하기 4, 32바이트 뒤에 있습니다.

lw  $t0, 32($s3)    # A[8]을 가져옴
add $t0, $s2, $t0   # h를 더함
sw  $t0, 32($s3)    # 같은 자리에 되돌려 놓음

메모리에서 꺼내고, 레지스터에서 계산하고, 메모리에 넣는 세 박자입니다.

연습 — A[1] = A[3] + 3

이번에는 $s0가 시작 주소입니다. A[3]의 오프셋은 3 곱하기 4, 12이고, A[1]의 오프셋은 4입니다. 3을 더하는 것은 상수 덧셈이니 앞에서 본 I-type addi를 씁니다.

lw   $t0, 12($s0)
addi $t0, $t0, 3
sw   $t0, 4($s0)

인덱스에 4를 곱하는 것만 잊지 않으면 됩니다.

정리

형식 두 가지를 봤습니다. 레지스터 사이의 계산은 R-type, 명령어 안에 상수나 오프셋이 들어가면 I-type입니다. 산술, 논리, 그리고 lw와 sw까지 이 둘로 전부 읽었습니다. 남은 형식 하나, J-type은 프로그램의 흐름을 바꾸는 제어 명령어에서 나옵니다. 분기와 점프는 다음 글에서 다룹니다. 개념 자체는 어렵지 않습니다. 문법이 낯설 뿐입니다.

dev-news학습 노트소개개인정보 처리