CS

[컴퓨터 구조] chapter2-1 정리(컴퓨터 언어 : 명령어)

SeungbeomKim 2023. 4. 11. 21:38

2장은 양도 많고, 기록할 것도 많아서 2번에 걸쳐서 포스팅하려고 합니다.

2장에서 다뤄볼 부분은 명령어입니다.

 

chapter2

2.1 서론

2.2 하드웨어 연산

2.3 피연산자

2.4 부호 있는 수와 부호 없는 수

2.5 명령어의 컴퓨터 내부 표현

2.6 논리 연산 명령어

2.7 판단을 위한 명령어

 

 

컴퓨터 하드웨에게 일을 시키려면 하드웨어가 알아들을 수 있는 언어로 말을 해야 합니다. 컴퓨터 언어에서 단어를 명령어(instruction)이라고 하고, 그 어휘를 명령어 집합(instruction set)이라고 합니다. 

 

오늘 배워볼 명령어 집합은 MIPS입니다. 

- MIPS(Microprocessor without Interlocked PipeLine Stages)란 MIPS Technologies 기반의 명령어 집합 체계입니다. 

- RISC 방식 중에 많이 사용되는 ISA중 하나입니다. 

- RISC(Reduced Instruction Set Computer)는 CPU 명령어의 개수를 줄여 명령어 해석시간을 줄임으로써, 개별 명령어의 실행속도를 개선시킨 컴퓨터입니다.

 

산술 연산 예시 

다음과 같이 세 개의 피 연산자가 있는 덧셈 연산을 예시로 들어보겠습니다.

add a, b, c

MIPS 산술 명령어는 반드시 한 종류의 연산만 지시하며 항상 변수 3개를 갖는 형식을 엄격히 지킵니다. 이 연산은 b+c를 a에 저장하는 연산입니다. 설계 원칙은 간단하게 하기 위해서 규칙적인 것이 좋습니다. (규칙성 : 적용을 단순하게 만듦, 단순성 : 고성능, 저비용)

 

복잡한 C 치환문의 번역

다음과 같은 C문장에 대한 컴파일러 출력은 어떻게 될까요?

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

MIPS 명령어는 한 번에 하나의 연산만을 하기 때문에, 컴파일러는 여러 개의 어셈블리 명령어로 나누어야 합니다. 그래서 g+h, i+j의 합을 구하고, 결과를 임시 변수에 저장하고  계산한 값들을 뺄셈 연산을 f 변수에 저장해야 합니다.

 

컴파일된 MIPS Code

add t0, g, h # temp t0 = g + h

add t1, i, j # temp t1 = i + j

sub f, t0, t1 # f = t0 - t1

 

하지만 이렇게 나타내면 안되고, 산술 명령어는 레지스터에 저장되어 있는 피연산자를 사용해야 합니다. MIPS 구조에서는 레지스터의 크기는 32비트입니다. MIPS에서는 32비트가 한 덩어리로 처리되는 일이 많으므로, 이를 워드라고 부릅니다.

레지스터의 개수를 32개로 제한하는 이유는 설계원칙 중 2번째에 해당합니다. (작은 것이 더 빠르다)

 

명령어를 작성할 때 단순하게 레지스터 번호 0 ~ 31을 사용할 수 있지만, MIPS의 관례에서는 $기호를 레지스터 값 앞에 써주는 것입니다. 그래서 다음과 같이 코드를 바꿔줘야 합니다.

add $t0, $s1, $s2

add $t1, $s3, &s4

sub $s0, $t0, $t1

 

레지스터 어셈블러 이름 

$t0, $t1, ..., $t9 for temporary values

- (임시 저장소(temporary), 큰 연산을 하기 위한 작은 연산을 위해서만 사용, 따로 저장해 줄 필요가 없음. 값이 덮어 씌워져도 상관 없음.

$s0, $s1, ..., $s7 for saved variables(값을 저장하기 위함)

- 사라지지 않기 위해 메모리에 옮겨 둬야 함. 덮어 씌워질 때 값을 옮겨 놔야 함.

 

메모리 피연산자

복잡한 자료구조의 데이터를 위해서는 메인 메모리가 사용됩니다.

 

메인 메모리의 데이터들에 대해서 산술 연산을 하기 위해서는 

data load(lw)(메모리 -> 레지스터로 데이터 복사), result store(sw)(레지스터 -> 메모리로 결과 저장)와 같이 적재 및 저장이 필요합니다.

이와 같은 연산을 하기 위해서는 메모리 주소가 필요합니다. 

 

메모리 피연산자 예시 1

g = h + A[8]

g, h -> 레지스터 $s1, $s2에 할당

A의 시작 주소(base address) -> 레지스터 $s3에 저장

 

Compiled MIPS code

lw $t0 32($s3) # A[8]의 값을 을 레지스터로 옮기는 작업 ( 1워드 : 4바이트로 구성되기 때문에 바이트의 주소를 구하기 위해서는 Offset에 4를 곱해줘야 함)

add $s1, $s2, $t0 # g = h + A[8] 덧셈 연산 수행

A[8]의 값은 메모리에 있으므로, 레지스터로 옮긴 후 연산을 시작해야 합니다. 8은 index이자 offset(변위)라고 하고 주소 계산을 위해 더해지는 $s3를 베이스 레지스터라고 합니다. 메모리에 있던 A[8]에 값을 레지스터 $t0에 넣어줬으므로 덧셈을 수행할 수 있게 됩니다.

 

적재(lw)와 저장(sw)를 사용한 번역

A[12] = h + A[8]

h가 $s2에 할당, A의 시작 주소($s3)

 

lw $t0, 32($s3) : A[8]의 값을 임시 변수 $t0에 저장

add $t0, $s2, $t0 : h + A[8]의 연산 값을 임시 변수 $t0에 저장

sw $t0, 48($s3) : $s3를 베이스 레지스터로 하여 값을 합을 A[12]에 저장

 

 

레지스터 vs 메모리

레지스터는 메모리보다 접근하기 빠름

더 빈번하게 사용되는 데이터들을 레지스터에 사전에 저장해야 합니다.

메모리 데이터를 사용하는 것은 적재(lw)와 저장(sw) 명령을 필요로 함

  • 더 많은 명령어 실행
  • 컴파일러는 레지스터를 가능한 한 많이 사용
  • 덜 자주 사용되는 데이터에 대해서만 메모리로 스필링 해야함
  • 레지스터 최적화가 중요

수치 피연산자

상수에 대해서 적재를 사용하지 않으려면 상수 산술 연산 명령어가 있어야 합니다(16bit 내에서 표현 가능)

addi $s3, $s3, 5 (add와의 차이점은 add는 레지스터 3개가 피연산자로 들어가기 때문에 상수 연산을 하기 위해서는 addi를 사용)

수치 명령여에서 뺄셈은 없기에 음수 상수를 이용합니다.(addi $s2, $s1, -1)

 

설계 원칙 3 : 많이 쓰이는 것은 빠르게

  • 작은 상수는 자주 사용됨
  • 수치 피연산자는 적재 명령어를 피할 수 있게 함
  • 이에 대한 장점을 살리기 위해 상수 산술 명령어 등장

상수로서 0

  • MIPS register 0($zero)는 상수 0
  • 덮어 쓰여질 수 없음
  • 자주 쓰이는 연산에서 효과적
  • add $t2, $s1, $zero(s1에 있는 데이터 값을 t2로 move, s1값을 t2에 저장)

부호 있는 수(0 ~ 4294967295) or 부호 없는 수(-2147483648 ~ 2147483647)

 

음수의 표현

부호와 크기(sign and magnitude)

가장 직관적인 방법이고, 별도의 부호를 덧붙이는 방법입니다. 부호를 1비트를 사용해서 나타내지만 단점이 있습니다. 1비트를 어디에 둬야 할지에 대한 생각과 덧셈기의 부호를 결정할 때, 최종 부호가 무엇인지 파악할 수 없습니다. 양의 0과 음의 0을 갖기 때문에 쓰이지 않게 됩니다.

2의 보수법을 사용한 부호 있는 수 표현(two's complement)

MSB 0이면 양수이고, 1이면 음수입니다. 

역부호화 : 보수화(1->0, 0->1)+ 1 

 

부호확장 

더 많은 비트로 표현하는 것 -> 레지스터 길이에 맞춰 그 값을 정확하게 표현하기 위해서 부호확장 수행

  • 수의 값은 유지
  • 명령어 add, branch, load, store의 수치값 필드
  • 32비트 레지스터와 더하려면 16비트 수의 값을 32비트로 확장해야 함

addi : 32비트 아키텍처에서 16비트 부호 있는 값인 경우 32비트로 부호확장

lb, lh : Byte/Halfword 단위로 로드 후 32비트로 부호확장

beq, bne :  32비트로 부호확장 

 

부호 비트를 왼쪽으로 복제하고 부호 없는 수는 앞을 0으로 확장합니다.

 

명령어 표현

명령어는 2진수로 표현되고, 기계어로 표현됩니다.

MIPS 명령어는 32-bit instruction words로 코딩되고, 32비트는 각 명령어의 필드에 대해 분배되고 어떤 Operation을 하는지, 레지스터 넘버 등으로 구별됩니다.

 

MIPS R-format(3개의 레지스터)

 

op : operation code(6bits, 명령어가 실행한 연산의 종류, 연산자)

rs : first source register number(5bits, 피연산자 번호1)

rt : second source register number(5bits, 피연산자 번호 2)

rd : destination register number(5bits, 연산 결과 저장)

shamt : shift amount(5bits, 자리이동량)

funct : function code(6bits, op필드에서 연산의 종류, funct 필드에서는 그중의 한 연산을 구체적으로 지정, functional code(기능 코드)

R-format 예시

 

MIPS I-format(2개의 레지스터 1개의 상수)

5비트로는 상수값을 표현할 수 없기에 이를 보완하기 위해 I-format 명령어 사용

I-format을 적용한다면, 5비트보다 큰 16비트의 값을 표현할 수 있습니다.

(설계원칙 4: 좋은 설계는 적당한 절충이 필요하다)

op : operation code, 명령어가 실행할 연산의 종류(6bits)

rs : source register(5bits, rs 과 constant 연산)

rt : source register or destination register number(5bits, 저장될 레지스터)

constant : -2^15 ~ 2^15 -1

address : rs에서 더해지는 offset

 

내장 프로그램 컴퓨터(메모리에 여러 개의 프로그램이 담겨 있음)

  • 컴퓨터의 성능을 혁신적으로 개선시킴
  • 명령어는 데이터와 같이 이진 숫자로 표현
  • 명령어와 데이터는 메모리에 저장
  • 프로그램은 다른 프로그램 위에서 실행(ex : complier, linker)
  • 이진 호환성은 컴파일된 프로그램을 다른 컴퓨터에서 사용할 수 있게 함(ex : Standardized ISAs)

논리연산 명령어 (sll, srl, and, andi, or, ori, nor)

 

자리 이동(Shift Operations)

shamt : 자리 이동량(how many positions to shift)

sll(shift left logical) : 왼쪽 자리 이동

  • 왼쪽으로 shift하고 나머지 0으로 채운다.
  • 왼쪽에 0이 있어야 오버플로우가 발생하지 않는다.
  • 2^비트수로 곱하는 과정  

srl(shift left logical) : 오른쪽 자리 이동

  • 오른쪽으로 shift하고 나머지 수들은 0으로 채운다
  • 오른쪽에 0이 있어야 오버플로우가 발생하지 않는다.
  • 2^비트수로 나누는 과정

NOT Operations example

nor $t0, $t1, $zero

0 -> 1, 1 -> 0 교체, $t1과 $zero에 OR연산을 취한 후 NOT 연산을 취해주고 $t0에 저장

 

조건 명령어(Conditional Operations) 

조건이 참이면 labeled instruction으로 분기(그렇지 않으면 연속적으로 진행)

beq rs, rt, L1 (rs==rt -> L1으로 분기)

bne rs, rt, L1 (rs!=rt -> L1으로 분기)

j L1 (조건에 상관없이 L1으로 분기)

 

if-then-else를 조건부 분기로 번역

if(i==j) f=g+h; else f=g-h;를 컴파일한 코드

f, g, h는 레지스터 $s0, $s1, $s2에 저장되어 있는 값

bne $s3, $s4, Else

add $s0, $s1, $s2

j Exit

Else : sub $s0, $s1, $s2

Exit

 

while문 번역

while(save[i]==k) i+=1; # i in $s3, k in $s5, address of save in $s6

MIPS 언어로 컴파일한 코드

  1. Loop : sll $t1, $s3, 2 (save[i]는 메모리에 있는 값이므로 레지스터에 lw하기 위해 주소를 구해야 합니다. 그래서 i(인덱스)에 4를 곱함(Byte 주소를 사용하기에 4를 곱함)
  2. add $t1, $t1, $s6 (t1에 save에 시작 주소를 더해줘서 저장)
  3. lw $t0, 0($t1) (임시 레지스터에 save[i]의 값을 넣음) 
  4. save[0], save[1]... save[i]는 각각 주소값 + 4*i 만큼 떨어져 있습니다.
  5. bne $t0, $s5, Exit (save[i]!=k 이면 순환에서 빠져나감)
  6. addi $s3, #s3, 1 (i에 1을 더해줌)
  7. j Loop(Loop로 분기)

조건 명령어

조건이 참이면 결과를 1로 두는 명령어

slt : set on less than

slt rd, rs, rt

-> if(rs < rt) rd = 1 else rd = 0

slti rt, rs, constant

-> if(rs < constant) rt = 1 else rt =0

 

분기 명령어에서 이하, 이상은 사용하지 않는데, 연산 속도가 느려지고, Branch와 다른 조건 명령어를 합치면 명령어당 더 많은 일이 필요하고, 클럭이 느려지기 때문에 beq/bne를 사용합니다.

 

부호 있는 비교 vs 부호 없는 비교

부호 있음 : slt, slti

부호 없음 : sltu, sltui

부호 있음 slt $t0, $s0, $s1 ( -1 < +1) $t0 = 1

부호 없음 sltu $t0, $s0, $s1 (+4294967295 > 1) $t0 = 0