CS/Computer Organization Design (2023-1)

[컴퓨터 구조] 2. 명령어: 컴퓨터 언어

샤아이인 2023. 4. 17.

 

2.2 하드웨어 연산

다음 MIPS 어셈블리 언어는 두 변수 b, c를 더해서 그 합을 a에 넣으라고 컴퓨터에게 지시하는 것 이다.

add a, b, c

MIPS 산술 명령어는 반드시 한 종류의 연산자만 지시하며 항상 변수 3개를 갖는 형식을 엄격하게 지킨다.

 

변수 b, c, d, e의 합을 구하여 a에 집어 넣는 예를 생각해보자.

add a, b, c   // a에 (b + c)를 대입한다
add a, a, d   // a에 d를 더한다
add a, a, e   // a에 e를 더한다

따라서 네 변수의 합을 구하려면 명령어 3개가 필요하다.

 

덧셈과 같은 연산자의 피연산자(operand)는 더해질 숫자 2개와 합을 기억할 장소 하나, 장소가 모두 3개인 것 이 자연스럽다.

이렇게 모든 명령어가 피연산자를 반드시 3개씩 갖도록 제한하는 것은 하드웨어를 단순하게 하자는 원칙과 부합한다.

 

▶ 설계 원칙 1 : 간단하게 하기 위해서는 규칙적인 것이 좋다.

 

2.3 피연산자

산술명령어의 피연산자에는 제약이 있다. 레지스터(register)라고 하는 하드웨어로 직접 구현된 특수 위치 몇 곳에 있는 것만을 사용할 수 있다.

 

MIPS 구조에서 레지스터의 크기는 32bit이다. MIPS에서는 32비트가 한 덩어리로 처리되는 일이 매우 빈번하므로 이것을 word라고 부른다.

현대 컴퓨터는 MIPS 처럼 보통 32개의 레지스터가 있다. 그러므로 기호 형태로 표현된 MIPS 언어를 단계적으로 구체화 할 때 산술 명령어의 각 피연산자는 32개의 32bit 레지스터 중 하나이어야 한다는 제약이 추가된다.

 

레지스터 개수를 32개로 제한하는 이유는 하드웨어 기술의 바탕이 되는 3가지 설계 원칙 중 두번째 원칙에서 찾을 수 있다.

 

▶ 설계 원칙 2 : 작은 것이 더 빠르다.

 

레지스터가 아주 많아지면 전기 신호를 더 멀리까지 전달되어야 하므로 클럭 사이클 시간이 길어진다.

 

명령어를 작성할 때 단순하게 레지스터 번호 0 ~ 31 까지를 사용할 수도 있지만, 레지스터를 표현하는 MIPS의 관례는 $기호 뒤에 두 글자가 따라 나오는 이름을 사용하는 것 이다. (뒤에서 그 이유를 설명하겠다)

일단은 C, Java의 변수에 해당하는 레지스터들은 $s0, $s1 등으로 표기하고, 컴파일 과정에서 필요한 임시 레지스터들은 $t0, $t1 등으로 표기하겠다.

 

2.3.1) 메모리 피연산자

프로그래밍 언어에는 array나 structure같은 복잡한 자료구조가 있다. 이러한 복잡한 자료구조 하나에는 레지스터 개수보다 훨씬 많은 데이터 원소가 있을 수 있다. 그럼 이런 큰 구조는 어떻게 컴퓨터에서 표현되고 사용될까?

 

바로 메모리 이다!

위에서 설명한 MIPS의 산술 연산은 레지스터에서만 실행되므로 메모리와 레지스터 간에 데이터를 주고 받는 명령어가 있어야 한다.

이러한 명령어를 데이터 전송 명령어(data transfer instruction)라 한다.

 

메모리에 기억된 데이터 워드에 접근하려면 명령어가 메모리 주소(memory address)를 지정해야 한다.

메모리는 주소가 인덱스 역할을 하는 큰 1차원 배열이다.

 

메모리에서 레지스터로 데이터를 복사해오는 데이터 전송 명령을 적재(load)라 한다.

load 명령은 (연산자 이름, 메모리에서 읽어온 값을 저장할 레지스터, 메모리 접근에 사용할 상수와 레지스터)로 구성된다.

메모리 주소는 명령어의 상수 부분과 두 번째 레지스터 값의 합으로 구해진다.

MIPS에서 실제 이 명령어의 이름은 lw(load word)이다.

 

▶ 메모리 피연산자를 사용하는 치환문의 번역

A가 100워드 배열이고, 변수 g, h는 레지스터 $s1, $s2에 할당되었다고 가정한다. 또 배열 A의 시작주소(base address)가 $s3에 기억되어 있다고 할 때 다음 C문장을 컴파일 하라.

g = h + A[8];

 

우선 피연산자 중 하나가 메모리에 있으므로, 먼저 A[8]을 레지스터로 옮긴 후 연산을 시작해야 한다.

우선 원소의 주소는 ($s3에 있는 배열의 시작 주소 + index 8)을 한 값이다. 이 원소를 다음 명령어가 사용할 수 있도록 임시 레지스터에 넣어야 한다. 첫 명령어는 다음과 같다. (물론 사실은 변위는 8*4이다. 이는 뒤에서 설명)

lw $t0, 8($s3)

이러면 필요한 값을 레지스터 $t0에 넣었으므로 덧셈을 수행할 수 있다.

add $s1, $s2, $t0

데이터 전송 명령어의 상수 부분(8)을 변위(offset)이라 하고, 주소 계산을 위해 여기에 더해지는 레지스터를($s3) base register 라고 한다.

 

MIPS에서는 워드의 시작 주소는 항상 4의 배수이어야 한다. 이러한 요구 사항을 정렬 제약(alignment restriction)이라고 하며, 많은 컴퓨터에서 이 방법을 사용한다.

컴퓨터는 제일 왼쪽, 즉 최상위(big end) 바이트 주소를 워드 주소로 사용하는 것과, 제일 오른쪽, 즉 최하위(little end) 바이트 주소를 워드 주소로 사용하는 것 두 종류로 나뉜다. MIPS는 big endian 계열에 속한다.

 

바이트 주소의 사용은 배열의 인덱스에도 영향을 미친다. 앞의 코드에서 바이트 주소를 제대로 구하려면 베이스 레지스터 $s3에 변위 4x8을 더해야 한다.

 

적대와 반대로 레지스터에서 메모리로 데이터를 보내는 명령을 저장(store)라고 한다.

저장 명령어의 형태는 적재와 같다. 즉 연산지의 이름, 저장할 데이터를 갖고 있는 레지스터, 변위, 베이스 레지스터로 구성된다.

MIPS에서 이 명령어의 실제 이름은 sw(store word)이다.

 

컴퓨터는 갖고 있는 레지스터보다 프로그램에서 사용하는 변수가 더 많은 경우가 자주 있다.
컴파일러는 자주 사용되는 변수를 가능한 한 많이 레지스터에 넣고 나머지 변수는 메모리에 저장했다가 필요할 때 꺼내서 레지스터에 넣는다.
이렇게 자주 사용하지 않는 변수를 메모리에 넣는 일을 spilling register이라고 말한다.

 

2.3.2) 상수 또는 수치 피연산자

프로그램의 연산에서 상수를 사용하는 경우가 많다.

이제까지 배운 명령어만으로 상수를 사용하려면 메모리에서 상수를 읽어 와야 한다.

예를 들어 레지스터 $s3에 상수 4를 더하는 코드는 다음과 같다.

lw $t0, AddrConstant4($s1)
add $s3, $s3, $t0

여기서 ($s1 + AddrConstant4)는 상수 4가 저장되어 있는 메모리 주소라 가정하자.

 

만약 적재 명령을 사용하지 않으려면, 피연산자 중 하나가 상수인 산술 연산 명령어를 제공하는 것이다.

이 상수를 수치(immediate) 피연산자 라고 한다.

 

수치 피연산자를 갖는 덧셈 명령어는 addi인데 레지스터 $s3에 4를 더하려면 다음과 같이 쓰면 된다.

addi $s3, $s3, 4

 

또한 상수 0은 특별한 역할을 한다. 명령어에 여러 가지 유용한 변형을 제공함으로써 명령어 집합을 단순하게 하는 것 이다.

예를 들어 복사(move) 연산은 피연산자 중 하나가 0인 add 명령어이다.

따라서 MIPS는 레지스터 $zero를 값 0으로 묶어 두도록 회로가 구현되어 있다. (0번 레지스터다)

 

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

거의 모든 명령어가 레지스터를 사용하기 때문에 레지스터 이름을 숫자로 매핑하는 규칙이 있어야 한다.

MIPS에서는 레지스터 $t0 ~ $t07 까지는 레지스터 번호 8에서 15번 까지로, 레지스터 $s0에서 $s7까지는 레지스터 번호 16에서 23번 으로 매핑한다.

 

▶ MIPS 어셈블리 언어를 기계어로 변환

다음 어셈블리 명령어의 실제 MIPS 언어 버전을 10진수 형태로 표현해보자.

add $t0, $s1, $s2

십진수 표현은 다음과 같다.

명령어의 각 부분을 field라 부른다.

 

처음과 마지막 필드 0과 32에 해당하는 부분은 컴퓨터에게 덧셈을 하라고 지시하는 부분이다.

두번째 필드는 덧셈에 사용할 첫 번째 피연산자 레지스터의 번호 이다(17 = $s1),

세번째 필드는 두번째 피연산자 레지스터 번호(18 = $s2)를 나타낸다.

네번째 필드는 계산 결과가 들어갈 레지스터의 번호(8 = $t0)이다.

이 명령어에서 다섯 번째 필드는 사용되지 않으므로 0으로 표시했다.

 

종합하면, 레지스터 $s1을 레지스터 $s2에 더해서 그 합을 레지스터 $t0에 넣으라는 뜻이다.

 

위 예제에서 보인 레이아웃을 명령어 형식(instruction format)이라고 한다.

MIPS 명령어의 길이는 데이터 워드와 마찬가지로 32비트이다.

 

어셈블리 언어와 구별하기 위하여 명령어를 숫자로 표현한 것 을 기계어 라도 한다. 이런 명령어들의 시퀀스를 기계코드(machine code)라 한다.

 

2.5.1) MIPS 명령어의 필드

MIPS 명령어의 각 필드에는 다음과 같은 이름이 붙어있다.

  • op : 명령어가 실행할 연산자의 종류, 연산자(opcode)라고 부른다
  • rs : 첫 번째 근원지(source) 피연산자 레지스터
  • rt : 두 번째 근원지 피연산자 레지스터
  • rd : 목적지(destination) 레지스터. 연산 결과가 기억된다.
  • shamt: 자리이동(shift)량
  • funct : 기능(function). op필드에서 연산의 종류를 표시하고 funct 필드에서는 그중의 한 연산을 구체적으로 지정한다. function code라고 부르기도 한다.

이것보다 하나의 field 사이즈가 더 커야하는 경우는 문제가 생길 수 있다.

만일 위의 5비트 필드 중 하나를 주소로 쓴다면 2^5 = 32보다 작은 값만 사용할 수 있다. 따라서 32보다 큰 값이 필요한 경우가 많으므로 5비트 필드로는 부족하다.

 

이때문에 모든 명령어의 길이를 같게 하고 싶은 생각과 명령어 형식을 한가지로 통일하고 싶은 생각 사이에서 충돌이 생긴다.

 

▶ 설계 원칙 3 : 좋은 설계에는 적당한 절충이 필요하다.

 

MIPS 설계자들은 모든 명령어의 길이를 같게 하되, 명령어 종류에 따라 형식은 다르게 하는 것 이었다.

예를 들어 직전에 설명한 명령어는 R타입 이라 하는데, 이것만으로는 불충분 하여 I타입이라는 두 번째 명령어 형식을 만들었다.

 

I타입은 수치 연산과 데이터 전송 명령어에서 사용되며 다음과 같다.

16bit 주소를 사용하므로 lw명령은 base register rs에 저장된 주소를 기준으로 +, - 2^15 바이트를 지정할 수 있다.

마찬가지로 addi 에서 사용할 수 있는 상수는 +- 2^15보다 더 클수 없다.

 

위에서 살펴봤던 명령을 보자

lw $t0, 32($s3)

rs필드에 19($s3의 번호), rt 필드에는 8($t0의 번호), 주소필드에는 32가 들어간다.

즉 위 명령어 형식에서는 rt필드의 의미가 바뀌어 적재 결과가 들어갈 목적지 레지스터 번호를 표시하는 것으로 바뀌었다.

 

명령어의 형식은 첫번째 필드(op)의 값을 보고 구분할 수 있다.

하드웨어는 op필드를 보고 명령어의 오른쪽 절반을 필드 3개로 볼지(R type), 하나로 볼지(I type) 결정한다. 

2.5.2) MIPS 어셈블리 언어를 기계어로 번역하기

$t1에 배열 A의 시작 주소가 기억되어 있고 $s2는 변수 h에 대응된다고 할 때 다음 문장은

아래와 같이 컴파일 된다.

이를 MIPS 기계어로 바꾸어 보자!

 

편의를 위해 우선 10진수로 만든후에 2진수로 표현하자.

1) lw

lw 명령어의 op필드 값은 35이다. rs에는 base register 번호 9가, rt 필드에는 목적지 레지스터 번호 8($t0)이 지정되어 있다.

A[300]을 선택하기 위한 변위값(1200)은 마지막 필드인 address 필드에 있다.

 

2) add

add명령어는 op 필드 값이 0, funct 필드 값이 32이다. 레지스터 피연산자 3개(18, 8, 8)가 각각 두 번째, 세 번째, 네 번째 필드에 있으며 각각이 $s2, $t0, $t0에 대응된다.

 

3) sw

sw명령어의 op필드 값은 43이며, 나머지 부분은 lw와 같다.

 

위 명령어를 2진수로 나타내면 다음과 같다.

 

2.5.3) 요점 정리

오늘날의 컴퓨터는 두가지 중요한 원리에 바탕을 두고 있다.

1. 명령어는 숫자로 표현된다.

2. 프로그램은 메모리에 기억되어 있어서 데이터처럼 읽고 쓸 수 있다.

 

이것이 내장 프로그램의 개념이다. 이 개념을 발명한 덕택에 컴퓨터가 눈부시게 발전할 수 있었다. 다음 그림은 내장 프로그램의 장점을 보여준다.

메모리에는 편집기가 편집중인 소스코드, 컴파일된 기계어 프로그램, 실행 프로그램이 사용하는 텍스트 데이터, 심지어는 기계어를 생성하는 컴파일러까지 기억될 수 있다.

 

명령어를 숫자처럼 취급하게 된 결과, 프로그램이 이진수 파일 형태로 판매되게 되었다.

이러한 이진 호환성(binary compatibility)

 

2.6 논리 연산 명령어

비트들을 워드로 묶는(packing) 작업과 워드를 비트 단위로 나누는(unpacking) 작업을 간단하게 하는 명령어들이 프로그래밍 언어와 명령어 집합에 추가되어다. 이러한 명령어들을 논리연산 명령어라 부른다.

 

MIPS 자리이동 명령어의 실제 이름은 sll(Shift left logical)srl(shift right logical)이다.

다음은 왼쪽으로 4칸 shift하는 MIPS 명령어이다. 단, 원래 값은 $s0에 있고 결과는 $t2 레지스터에 저장된다고 가정하자.

위에서는 R형식 명령어의 shamt 필드에 대한 설명을 미루어 왔는데, 이제 설명할 시기가 되었다.

이는 자리이동량 shift amount를 나타내는 것 이다. 따라서 위 명령어의 기계어 형식은 다음과 같다.

sll은 op와 funct가 0, rd는 10($t2), rt는 16($t0), shamt는 4를 갖도록 인코딩되었다.

 

그 외에도, AND, OR, NOT, NOR 가 있다.

MIPS설계자 들은 3-피연산자 형식을 유지하기 위해서 NOT대신 NOR명령어를 포함시켰다.

피연산자가 0이면 NOT과 같아진다. 예를 들어 A NOR 0 = NOT (A OR 0) = NOT (A)

 

앞에서 사용한 레지스터 $t1이 바뀌지 않았고, 레지스터 $t3에 0이 있다면,다음 MIPS 명령어의 실행 결과

상수는 산술 연산에서 뿐만 아니라 AND, OR 연산에서도 유용하다. 그래서 MIPS는 andi(and immediate)와 ori(or immediate)명령어도 제공한다.

NOR의 주용도는 NOT대신 사용하는 것이기 때문에, 상수는 잘 사용하지 않는다.

 

2.7 판단을 위한 명령어

다음 명령어를 살펴보자.

beq register1, register2, L1

이는 register1 과 register2의 값이 같으면 L1에 해당하는 문장으로 가라는 뜻이다.

beq는 branch if equal을 의미한다.

 

bne register1, register2, L1

위 명령어는 1과 2의 값이 같지 않은경우에 L1으로 가라는 뜻이다. 즉, branch if not equal을 의미한다.

 

beq, bne 두 명령어를 조건부 분기(conditional branch)라 부른다.

 

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

다음 코드에서 f, g, h, i, j는 변수이고, 각각은 레지스터 $s0 ~ $s4 까지에 해당된다.

다음 C언어 if문장을 컴파일한 코드는?

if (i == j)
    f = g + h;
else:
    f = g - h;

왼쪽 사각형은 if문의 then 부분, 오른쪽은 else 부분에 해당된다.

조건을 bne로 검사해서 then 부분을 건너뛰게 하는것이 더 효율적이다.

bne $s3, $s4, Else

 

그 다음으로 연산하나를 수행하면

add $s0, $s1, $s2

 

위 명령을 실행한 후에는 if 문장의 끝부분으로 가야한다.

이를 무조건 분기(unconditional branch)라는 새로운 종류의 분기 명령으로 해결한다.

MIPS에서는 이 같은 명령어에 jump라는 이름을 붙이고, 간략하게 j로 사용한다.

j Exit

 

 

else 부분의 치환문도 역시 명령어 하나로 번역된다. 단 이 명령어에는 Else라는 레이블을 추가해야 한다.

그리고 이 명령어 뒤에는 if-then-else 문장의 끝을 표시하는 Exit란 레이블을 둔다.

Else: sub $s0, $s1, $s2
Exit:

 

이를 하나로 보면

bne $s3, $s4, Else
add $s0, $s1, $s2
j Exit
Else: sub $s0, $s1, $s2
Exit:

 

2.7.2) 순환문

다음 전형적인 while문 코드를 살펴보자.

while (save[i] == k)
    i += 1;

i와 k가 register $s3와 $s5에 할당되었고 배열 save의 시작 주소가 $s6에 저장되어 있다고 해보자.

이때 MIPS 어셈블리 코드를 구해보자!

 

1) 반복문 구조

인덱스 i에 4를 곱해서 save의 시작주소에 더해야 주소가 만들어진다.

2비트씩 왼쪽자리 이동을 하면 4를 곱한것과 같은 효과다. 즉, sll 연산을 사용하자.

Loop: sll $t1, $s3, 2 // Temp reg $t1 = i * 4

 

2) save[i]의 값을 임시 레지스터로 불러오기

$t1 값에다 $s6에 있는 save의 시작 주소값을 더한다.

add $t1, $t1, $s6

 

이제 이 주소를 사용해서 save[i]를 임시 레지스터에 저장할수 있다.

lw $t0, 0($t1)

 

3) 반복 검사를 수행하여 save[i] != k 시 순환문 빠져나가기

bne $t0, $s5, Exit

 

4) i에 1을 더하기

addi $s3, $s3, 1

 

5) 순환문의 끝

j    Loop
Exit:

 

▶ slt

경우에 따라서는 두 변수간의 대소 비교가 필요할수도 있다. for(save[i] < 0) 같은 상황일수도 있다.

MIPS에서는 slt(set on less than) 명령어로 이런 일을 처리한다.

slt는 두 레지스터의 값을 비교한 후, 처번째 레지스터 값이 두번째 레지스터의 값보다 작으면 세 번째 레지스터 값을 1로, 아니면 0으로 지정한다.

slt $t0, $s3, $s4 // $t0 = 1 if $s3 < $s4

상수 피연산자는 비교에도 사용된다.

slti $t0, $s2, 10  // $t0 = 1 if $s2 < 10

 

▶ jr(jump register)

이 명령어는 레지스터에 저장된 주소로 무조건 점프한다.

 

2.8 하드웨어의 프로시저 지원

프로시저(procedure)나 함수는 이해하기 쉽고 재사용이 가능하도록 프로그램을 구조화하는 방법 중의 하나다.

parameter는 프로시저에 값을 보내고 결과를 받아오는 일을 하므로, 프로그램의 다른 부분 및 데이터와 프로시저 사이의 인터페이스 역할을 한다.

 

프로시저를 실행할 때 다음 6단계를 거친다.

  1. 프로시저가 접근할 수 있는 곳에 인수를 넣는다.
  2. 프로시저로 제어를 넘긴다.
  3. 프로시저가 필요로 하는 메모리 자원을 획득한다.
  4. 필요한 작업을 수행한다.
  5. 호출한 프로그램이 접근할 수 있는 장소에 결과값을 넣는다.
  6. 프로시저는 프로그램 내의 여러 곳에서 호출될 수 있으므로 원래 위치로 제어를 돌려준다

MIPS 소프트웨어는 다음의 프로시저 호출 관례에 따라서 레지스터를 할당한다.

MIPS 어셈블리 언어는 레지스터를 할당할 뿐 아니라, 프로시저를 위한 명령어도 제공한다.

jal(jump-and-link) 명령어는 지정된 주소로 점프하면서 동시에 다음 명령어(jal ProcedureAddress 명령어)의 주소를 $ra 레지스터에 저장하며 모양은 다음과 같다.

jal ProcedureAddress

 

$ra에 저장되는 이 "링크"를 복귀 주소(return address)라고 부른다.

MIPS는 프로시저에 작업을 마친 후 복귀할때 jr명령을 이용한다.

jr $ra

 

호출 프로그램(caller)은 $a0-$a3에 전달할 인수값을 넣은 후 jal X 명령을 이용해서 프로시저 X로 점프한다.

프로시저는 계산이 끝난 후 결과를 $v0 - $v1에 넣은 후 jr $ra 명령을 실행하여 복귀한다.

 

내장 프로그램 개념은 현재 실행 중인 명령어의 주소를 기억하는 레지스터를 필요로 한다.

레지스터의 이름은 명령어 주소 레지스터(instruction address register)라고 하는 것이 타당하지만, 역사적인 이유로 보통 Program Counter(PC)라고 부른다.

 

jal 명령은 프로시저에서 복귀할 때 다음 명령어부터 실행하도록 PC + 4를 레지스터 $ra에 저장한다.

 

2.8.1) 더 많은 레지스터의 사용

컴파일러가 프로시저를 번역하는데 레지스터의 수가 부족한 경우를 생각해보자.

프로시저의 호출이 다른 부분에 영향을 미쳐서는 안되므로, 호출 프로그램이 사용하는 모든 레지스터는 복귀하기 전에 프로시저 호출 전의 상태로 되돌려 놓아야 한다.

 

이때 레지스터 스필링이 필요하다. 스필링에 이상적인 자료구조는 Stack이다.

 

Stack에는 다음 프로시저가 스필할 레지스터를 저장할 장소나 레지스터의 옛날 값이 저장된 장소를 표시하기 위해 최근에 할당된 주소를 가리키는 포인터가 필요하다. 이 stack pointer는 레지스터 값 하나가 stack에 저장되거나, stack으로부터 복구될때마다 한 워드씩 조정된다.

 

MIPS는 스택 포인터를 위한 29번 레지스터를 할당해 놓고 있는데, 이름이 $sp이다.

 

역사적인 이유로 stack은 높은 주소에서 낮은 주소 쪽으로 "성장"한다. 그러므로 stack에 push할때는 stack point값을 감소시키고, pop할때는 stack pointer값을 증가시킨다.

 

2.8.2) 다른 프로시저를 호출하지 않는(leaf) C 프로시저의 컴파일

다음 C언어 함수를 하나 생각해보자!

int leaf_example (int g, int h, int i, int j) {
    int f;
    
    f = (g + h) - (i + j)l
    return f;
}

argument g, h, i , j 는 $a0, $a1, $a2, $a3에 해당되고, f는 $s0에 해당된다.

컴파일된 프로그램은 다음과 같이 프로시저의 레이블로부터 시작된다.

leaf_example:

 

그 다음으로는 프로시저가 레지스터를 사용할 것 이기 때문에, 원래 레지스터에 저장되있던 값들을 잠시 스필링 해줘야한다.

stack에 다음과 같이 3개의 word를 저장할 자리를 만든 후 값을 저장한다.

 

다음 그림은 프로시저 호출 전후와 프로시저 실행 중의 stack 상태를 보여준다.

 

프로시저의 본문은 명령어 3개로 번역된다. 또한 임시 레지스터 2개 ($t0, $t1)이 사용된다.

계산 결과 f를 반환해주기 위해서 f를 결과값 레지스터에 복사한다.

add $v0, $s0, $zero    # returns f ($v0 = $s0 + 0)

호출 프로그램으로 되돌아가 전에 저장해 두었던 값을 스택에서 꺼내 레지스터를 원상복구한다.

이 프로시저는 복귀 주소를 사용하는 점프 명령으로 끝난다.

jr   $ra

 

위 예제에서는 임시 레지스터의 값도 저장했다가 원상복구하는 방식으로 되어있다.

그러나 사용하지도 않는 레지스터의 값을 쓸데없이 저장했다 복구하는 일을 방지하기 위해서 MIPS 소프트웨어는 레지스터 18개를 두 종류로 나누게 된다.

  • $t0-$t9: 프로시저 호출 시, 피호출 프로그램이 값을 보존해 주지 않는 임시 레지스터
  • $s0-$s7: 프로시저 호출 전과 후의 값이 같게 유지되어야 하는 변수 레지스터

 

2.8.3) 중첩된 프로시저

다른 프로시저를 호출하지 않는 프로시저를 leaf프로시저 라고 부른다.

하지만, 프로시저는 다른 프로시저를 호출할수도 있으며, 심지어 자기 자신을 호출하기도 한다.

 

예를 들어 main 프로그램이 인수값 3을 가지고 프로시저 A를 호출했다고 가정하자. 이때 레지스터 $a0에 3을 넣고 jal A 명령을 실행할 것 이다.

 

프로시저 A가 다시 인수 7을 가지고 jal B를 통해 프로시저 B를 호출했다고 하자. 아직 A가 다 끝난 거이 아니기 때문에 레지스터 $a0 사용에서 충돌이 발생한다.

 

마찬가지로 레지스터 $ra에 지금은 B의 복귀 주소가 있으므로 $ra의 복귀 주소에 대해서도 충돌이 생긴다.

 

한가지 해결 방법은 값이 보존되어야 할 모든 레지스터를 stack에 넣는 것이다. 호출 프로그램은 인수 레지스터($a0-$a3)와 임시 레지스터($t0-$t9) 중 프로시저 호출 후에도 계속 사용해야 하는 것은 모두 스택에 넣는다.

복귀한 후에는 메모리에서 값을 꺼내 레지스터를 원상복구 한후, 이에 맞추러 stack pointer를 다시 조정한다.

 

2.8.4) 새 데티어를 위한 stack 공간의 할당

레지스터에 들어가지 못할만큼 큰 배열이나 구조체 같은 지역변수를 저장하는 데도 stack이 사용되기 때문에 복잡해진다.

프로시저의 저장된 레지스터와 지역변수를 가지고 있는 Stack 영역을 procedure frame 또는 activation record 라고 부른다.

 

다음은 프로시저 호출 전, 중, 후 를 나타낸다.

프로시저 호출 전(a), 중(b), 후(c)

MIPS 소프트웨어 중에는 Frame pointer($fp)가 프로시저 프레임의 첫 번째 워드를 가리키도록 하는 것이 있다.

이러한 frame point를 사용하면 프레임 포인터가 변하지 않는 베이스 레지스터 역할을 하므로 지역변수 참조가  간단해진다.

 

2.8.5) 새 데이터를 위한 힙 공간의 할당

프로시저 내에서만 정의되는 자동변수 외에도 정적 변수와 동적 자료구조를 위한 메모리 공간이 필요하다.

다음 그림은 MIPS의 메모리 할당 방식이다.

stack은 최상위 주소에서 시작해서 아래로 자란다. 최하위 주소 부분은 사용할수 없으며, 그 위 부분(text)은 MIPS 기계어 코드가 들어가는 부분이다. 이 부분은 전통적으로 text segment 부른다.

 

그 위에는 정적 데이터 세그먼트(static data segment)라는 부분이 있는데, 상수와 기타 정적 변수들이 여기에 들어간다.

 

배열은 그 그키가 고정되어 있어서 정적 데이터 segment에 잘 맞는다. 그러나 Linked List같은 자료구조는 늘어났다 줄어들었다 한다.

이러한 자료구조를 위한 세그먼트를 전통적으로 heap이라 부른다.

 

다음 그림은 MIPS어셈블리 언어의 레지스터 사용 관례를 보여준다.

이는 자주 생기는 일을 빠르게 하라는 원칙의 예이다.

위 레지스터 정도면 대부분의 프로시저를 충족시키기 때문에 거의 메모리에 갈일이 없어서 빠르다.

 

2.9 문자와 문자열

MIPS에서는 효율성을 위해서 byte전송 명령어가 따로 있다.

lb(load byte)는 메모리에서 한 바이트를 읽어서 레지스터의 오른쪽 8비트에 채우는 명령이고,

sb(store byte)는 레지스터의 오른쪽 8비트를 메모리로 보내는 명령이다.

 

예를 들어 한 바이트를 복사할때는 다음과 같다.

lb $t0, 0($sp)
sb $t0, 0($gp)

 

MIPS 명령어 집합에는 하프워드(halfword)라 불리는 16비트 데이터에 대한 적재와 저장 명령어가 포함되어 있다.

lh(load half) 명령은 메모리에서 16비트를 읽어와서 레지스터의 우측 16비트에 넣는다.

lb처럼 하프워드를 부호있는 수로 취급하여 상위 16비트를 부호확장한다.

반면 lhu(load halfword unsigned)는 부호없는 수를 적재한다.

 

sh(store half)는 레지스터의 우측 16비트를 메모리에 쓴다.

lhu $t0,0($sp) # Read halfword (16 bits) from source
sh $t0,0($gp) # Write halfword (16 bits) to destination

 

2.10 32비트 수치와 주소를 위한 MIPS의 주소지정 방식

MIPS의 명령어 길이를 32비트로 고정한 덕택에 하드웨어가 간단해지기는 했지만, 명령어 내에 32비트 상수나 32비트 주소를 표시할 수 없어 불편한점도 있다.

 

3.10.1) 32비트 상수의 로딩

프로그램에서 사용하는 상수는 대체로 크기가 작아서 16bit면 충분하지만, 더 큰 상수가 필요한 경우도 있다.

이럴 때를 위해 MIPS는 레지스터의 상위 16비트에 상수를 넣는 lui(load upper immediate)명령어를 제공한다.

 

하위 16비트는 그 다음에 나오는 다른 명령어로 채울 수 있다.

다음 그림은 lui의 동작을 보여준다.

 

예시를 들어보자. 다음 32비트 상수를 채우는 MIPS 어셈블리 코드를 작성해보자.

0000 0000 0011 1101 0000 1001 0000 0000
lui $s0, 61         # 61 decimal = 0000 0000 0011 1101 binary
ori $s0, $s0, 2304  # 2304 decimal = 0000 1001 0000 0000

0000 0000 0011 1101 0000 1001 0000 0000

 

2.10.2) 분기와 점프 명령에서의 주소지정

MIPS에서 가장 간단한 주소지정 방식은 Jump 명령에서 사용하는 것이다.

Jump명령은 6비트의 op코드와 26비트의 주소 필드로 구성되는 J타입 명령어 형식을 사용한다.

 

그러므로

j 10000    # go to location 10000

를 어셈블하면 다음과 같다.

여기서 jump의 opcode는 2이고, 분기 주소는 10000이다.

 

점프와 달리 조건부 분기 명령에는 분기 주소외에 2개의 피연산자가 더 있다.

bne $s0,$s1,Exit   # go to Exit if $s0 ≠ $s1

만일 프로그램에서 사용하는 모든 주소가 이 16비트 필드에 들어가야 한다면, 어떤 프로그램도 2^16보다 더 커질수는 없다.

이는 너무 작은값이다. 이를 해결할 수 있는 대안으로, 어떤 레지스터를 지정해서 그 값을 분기 주소와 더하도록 하는 것이다.

 

PC = 레지스터 + 분기주소

 

이 방식은 2^32까지 커지며, 조건부 분기도 지원함으로써 크기 제한을 극복한다.

남은 문제는 어떤 레지스터를 사용하느냐 이다.

 

조건부 분기가 어떻게 사용되는지를 살펴보면 이 문제의 답을 구할 수 있다.

조건부 분기는 주로 순환이나 if문에서 사용되므로 가까이 있는 명령어로 분기하는 경향이 있다.

PC는 현 명령에의 주소를 가지고 있으므로 분기 주소를 더할 레지스터로 PC를 선택하면 현 위치에서  +-2^15워드 이내 떨어진 곳은 어디든지 분기할 수 있다.

 

이러한 주소지정 방식을 PC-relative addressing 방식이라한다.

하드웨어 입장에서는 PC를 일찍 증가시켜 다음 명령어를 가리키게 하는 것이 편하다. 그러므로 실제 MIPS주소는 현재 명령어 주소를 기준으로 하는거이 아닌, 다음 명령어 주소(PC+4)를 기준으로 하게된다.

 

MIPS명령어의 길이는 항상 4byte 이므로, 분기할 거리를 바이트 단위가 아니라 워드 단위로 나타내면 더 먼 거리까지 분기할 수 있다.

바이트 주소 대신 워드 주소를 사용함으로써 PC 상대 주소지정 방식의 분기 거리를 4배로 늘릴 수 있는 것이다.

마찬가지로 jump명령어의 26비트 필드도 워드 주소이다.

 

2.10.3) MIPS 주소지정 방식 요약

  1. 수치(immediate) 주소지정: 피연산자는 명령어 내에 있는 상수이다.
  2. 레지스터 주소지정: 피연산자는 레지스터이다.
  3. 베이스(base) 또는 변위(displacement) 주소지정: 메모리 내용이 피연산자이다. 메모리 주소는 레지스터와 명령어 내의 상수를 더해서 구한다.
  4. PC 상대 주소지정: PC 값과 명령어 내 상수의 합을 더해서 주소를 구한다.
  5. 의사직접(pseudodirect) 주소지정: 명령어 내의 26비트를 PC의 상위 비트들과 연접하여 점프 주소를 구한다.

댓글