※"컴퓨터 구조 및 설계 6판 MIPS EDITION" 책을 간단하게 정리한 내용의 글입니다.※
2.6 논리 연산 명령어
지금까지 워드 단위로 처리하는 명령어를 보았다. 하지만 워드 내 일부 비트들 뿐만 아니라 개개 비트에 대한 연산이 필요한 경우도 있다. 비트를 워드로 묶거나(packing), 워드를 비트 단위로 나누는(unpacking) 작업을 하는 명령어가 명령어 집합(instruction set)에 추가되었다. 이런 명령어를 논리 연산 명령어라고 한다.
[ Logical operations ] | [ C operators ] | [ MIPS instructions ] |
---|---|---|
Shift left | << | sll |
Shift right | >> | srl |
Bit-by-bit AND | & | and, andi |
Bit-by-bit OR | | | or, ori |
Bit-by-bit NOT | ~ | nor |
위의 명령어를 통해 비트 단위로 연산을 할 수 있다.
nor은 '|' 연산을 하고 그 결과에 '~'을 한 것과 같다.
2.7 판단을 위한 명령어
beq
beq register1, register2, L1
여기서 beq는 "branch if equal"을 의미한다. 즉, register1과 register2의 값이 같으면 L1으로 가라는 의미이다.bne
bne register1, register2, L1
여기서 bne는 "branch if not equal"을 의미한다. 즉, register1과 register2의 값이 같지 않으면 L1으로 가라는 의미이다.
이 두 명령어 beq와 bne를 조건부 분기(conditional branch)라고 한다.
< 예시 1 >
if (i == j) f = g + h; else f = g - h;
위의 C언어 코드가 컴파일된 코드를 짜보자. f, g, h, i, j는 순서대로 $s0부터 $s4 레지스터이다.
bne $s3 $s4 Else
add $s0, $s1, $s2
j Exit
Else: sub $s0, $s1, $s2
Exit:
같지 않으면 Else로 보낸다. 같지 경우 바로 아래에서 add를 수행 후에 Exit로 jump한다. (jump 명령어는 추후에 j타입 명령어로 배운다.)
< 예시 2 >
while (save[i] == k)
i += 1;
위의 C언어 코드가 컴파일된 코드를 짜보자. i는 $s3에, k는 $s5에 할당되었다. save 배열의 시작 주소는 $s6에 저장되어 있다.
Loop: sll $t1, $s3, 2
add $t1, $t1, $s6
lw $t0, 0($t1)
bne $t0, $s5, Exit
addi $s3, $s3, 1
j Loop
Exit:
2를 sll을 하는 이유는 워드(4바이트)에 맞게 4를 곱하기 위함이다.
(※ 컴파일러의 초기 단계 작업 중 하나는 기본 블록(basic block) 단위로 프로그램을 나누는 일이다. 기본 블록은 맨 뒤를 제외하고 분기 명령이 없고, 맨 앞을 제외하고 분기 목적지나 분기 레이블이 없는 시퀀스이다.)
< 그 외 분기 명령 >
- slt
slt $t0, $s3, $s4
$s3가 $s4보다 작으면 $t0의 값을 1로, 아니면 0으로 한다. - slti
slti $t0, $s2, 10
$s2가 10보다 작으면 $t0의 값을 1로, 아니면 0으로 한다.
MIPS 컴파일러는 slt, slti, beq, bne와 레지스터 $zero에 있는 상수 0을 이용해서 모든 비교 조건을 만들 수 있다. ($zero는 0번 레지스터이다.)
(sltu도 있다. sltu는 끝의 값을 unsigned 값으로 부호없는 수로 보고 대소를 비교한다.)
2.8 하드웨어의 프로시저 지원
프로시저
함수처럼 이해하기 쉽고 재사용이 가능하도록 프로그램을 구조화하는 방법 중 하나이다.프로시저를 위한 레지스터 할당 관례
레지스터는 데이터를 저장하는 가장 빠른 장소이기 때문에 가능한 많이 사용하는 것이 바람직하다.- $a0 ~ $a3: 전달할 인수를 가지고 있는 인수 레지스터 4개
- $v0 ~ $v1: 반환되는 값을 갖게 되는 값 레지스터 2개
- $ra: 호출한 곳으로 되돌아가기 위한 복귀 주소를 가지고 있는 레지스터 1개
프로지서를 위한 명령어
jal (jump-and-link)
지정된 주소로 점프하면서 동시에 다음 명령어의 주소를 $ra 레지스터(31번 레지스터)에 저장한다.jal ProcedureAddress
위의 명령어 실행 시, 원래 다음 실행할 명령어의 주소를 $ra 레지스터에 저장하고 ProcedureAddress의 주소로 점프합니다.
여기서 $ra에 저장되는 주소를 복귀 주소(return address)라고 한다.
PC(program counter)라고 하는 레지스터에는 현재 실행중인 명령어의 주소가 들어있다. jal 명령은 프로시저가 끝나고 다음 명령어를 이어서 실행하도록 PC 값에 4를 더한 PC + 4를 $ra 레지스터에 저장한다.jr (jump register)
프로시저 수행 후에 $ra 레지스터에 저장된 주소로 복귀하기 위해 사용하는 명령이다.jr $ra
위와 같이 사용한다.
< 더 많은 레지스터의 사용 >
레지스터 스필링
프로시저 호출을 할 때 프로시저 코드에서 레지스터를 사용할 수 있다. 그리고 프로시저를 마치고 나서 원래 코드로 돌아갈 때 이 레지스터는 프로스저를 수행하기 전의 상태여야 한다. 그렇기 때문에 레지스터가 부족할 때 기존의 레지스터 값을 백업해야 한다. 이 백업을 메모리에 스택으로 백업을한다. 이를 레지스터 스필링이라고 한다.스택
스택 포인터(stack pointer)는 레지스터 값 하나가 스택에 저장되거나 스택에서 복구될 때마다 한 워드씩 조정된다. 스택은 메모리에서 높은 주소에서 낮은 주소 쪽으로 "성장"한다. 그렇기 때문에 스택에 푸시할 때는 스택 포인터 값이 감소하고, 팝할 때는 스택 포인터 값이 증가한다.
MIPS 아키텍처에서 29번 레지스터인 $sp 레지스터에 스택 포인터를 저장한다.
< 중첩된 프로시저 >
프로시저에서 프로시저를 호출할 수 있다. 재귀 호출로 스스로를 호출할 수도 있다. 이때 인수 레지스터($a0 ~ $a3)나 $sp, $ra 레지스터의 값이 덮어씌워지면 안되고, 따로 관리를 해줘야 한다. 그래서 이를 스택에 넣어 놓는다.
스택에 보존되어야 하는 레지스터 값을 넣고, $sp 값은 그게 맞게 조정된다.
중첩 프로시저 수행을 완료한 후에 원래의 레지스터 값을 스택에서 복구한다. 그리고 이에 맞춰서 스택 포인터를 다시 조정한다.
(※ 정적(static) 변수의 경우에는 전역 포인터인 $gp 레지스터를 활용해서 정적 변수에 접근할 수 있게 한다.)
< 새 데이터를 위한 스택 공간의 할당 >
- 프로시저 프레임(procedure frame) 또는 액티베이션 레코드(activation record)
프로시저의 저장된 레지스터들과 지역 변수를 가지고 있는 스택 부분을 말한다.
스택 포인터가 프로시저 내부에서 바뀔 수도 있다. 그렇기 때문에 베이스 레지스터 역할을 하는 레지스터가 필요하다.
프로시저가 호출될 때마다 해당 프로시저의 실행을 위한 스택 프레임(프로시저 프레임)이 생성된다. 스택 프레임은 함수의 매개변수, 리턴 주소, 지역 변수 등을 저장하는 공간이다. 또한 스택 프레임에는 프로시저를 호출한 곳에서 사용하던 레지스터 값도 저장된다. - 프레임 포인터와 스택 포인터
프레임 포인터는 스택에서 해당 프로시저의 시작 지점을 가리키고 있는 포인터이다. 프로시저의 호출 시점의 스택 프레임의 기준 위치를 가리킨다.
프레임 포인터는 해당 프로시저에 대해 베이스 레지스터 역할을 하기 때문에 프레임 포인터를 통해 스택의 값에 접근한다.
스택 포인터는 스택의 가장 마지막 워드를 가리키고 있는 포인터이다. 프레임 포인터는 프로시저 중에 변하지 않지만, 스택 포인터는 스택이 변화할 때마다 자동으로 업데이트 된다.
< 새 데이터를 위한 힙 공간의 할당 >
위의 이미지를 보면 메모리의 구조를 볼 수 있다.
- Reserved
사용이 유보되어 있는 공간이다. - Text
텍스트 세그먼트(text segment)라고 부르는 공간으로, MIPS 기계어 코드(명령어)가 들어가는 부분이다. - Static data
상수와 기타 정적(static) 변수들이 들어간다. 배열은 그 크기가 고정되어 있어서 정적 데이터 세그먼트에 잘 맞는다. - Dynamic data 또는 힙(heap)
링크 리스트 같은 자료구조는 여기에 들어간다. C언어의 malloc()이나 자바의 new(객체)로 생성되는 것들이 여기 들어간다.
이 공간은 malloc()으로 할당받고, free()로 해제한다. 여기서 많은 오류가 발생한다. 그렇기 때문에 자바에서는 자동 메모리 할당과 가비지 컬렉션을 활용한다. 그 주 목적은 수동 메모리 할당과 해제로 인한 오류를 방지하기 위함이다.
2.9 문자와 문자열
< 문자를 표현하는 방식 >
컴퓨터는 모든 것을 이진수로 나타낸다. 문자도 이진수로 표현되는데, 오늘날 대부분의 컴퓨터는 8비트 바이트로 문자를 표현하고, 거의 모든 컴퓨터가 ASCII(American Standard Code for Information Interchange)를 사용한다.
즉, 문자를 표현할 때는 바이트 단위로 표현한다.
< 바이트 전송 명령 >
lw나 sw로도 바이트를 전송할 수 있지만, 텍스트 데이터를 다루는 경우가 매우 많기 때문에 MIPS에서는 바이트 전송 명령을 지원한다.
lb $t0, 0($sp)
sb $t0, 0($gp)
위의 lb(load byte)는 메모리에서 바이트를 읽어서 레지스터의 오른쪽 8비트에 체우는 명령이다.
위의 sb(store byte)는 레지스터의 오른쪽 8비트를 읽어서 메모리로 보내는 명령이다.
< 문자열을 표현하는 방식 >
컴퓨터가 문자열을 표현하는 방법은 다음 세 가지가 있다.
- 문자열의 맨 앞에 문자열의 길이를 표시하는 방법
- 구조체처럼 같이 사용되는 변수에 그 길이를 표시하는 방법
- 문자열의 끝을 표시하는 특수 문자를 문자열의 끝에 두는 방법
C언어에서는 3번째 방법을 사용한다. 문자열의 끝에 ASCII 0번인 null을 둬서 문자열의 끝을 표시한다.
< Java의 문자와 문자열 >
Java에서는 포괄성을 위해 문자를 유니코드로 표현한다. 즉, 문자 표현에 16비트를 사용하는 것이 디폴트이다.
이를 위해 MIPS 명령에 집합에는 하프워드(halfword)라 불리는 16비트 데이터에 대한 적재와 저장 명령어가 있다. lh(load half)는 메모리에서 16비트를 읽어서 레지스터의 우측 16비트에 넣는 명령이고, sh(store half)는 레지스터의 우측 16비트를 메모리에 쓰는 명령이다. lhu(load halfword unsigned)를 사용하면 부호없는 수를 적재한다. 따라서 lhu가 lh보다 좀 더 많이 쓰인다.(부호가 필요가 없다.)
자바에서는 C와 다르게 문자열의 길이를 표시하는 워드를 가지고 있어서 이를 통해 문자열을 표현한다.
2.10 32비트 수치와 주소를 위한 MIPS의 주소지정 방식
MIPS 명령어는 길이를 32비트로 고정해서 하드웨어가 간단해졌다. 하지만 명령어 내에 상수나 32비트 주소를 표시할 수 없어 불편한 점도 있다.
< 32비트 수치 피연산자 >
16비트로 표현할 수 있는 상수보다 더 큰 상수가 필요할 때가 있다. 이럴 경우를 위해 MIPS에서는 레지스터의 상위 16비트에 상수를 넣는 lui(load upper immediate) 명령어를 제공한다.
그리고 ori 명령을 통해 하위 16비트를 채울 수 있다. addi 명령의 경우 MSB를 16비트에서 복사해서 32비트 상수를 만들어서 연산에 사용한다. 근데 ori 명령은 상위 16비트를 0으로 채운 후에 연산을 한다. 그래서 lui에 ori 명령어를 쓰는 것이 적합하다.
< 분기와 점프 명령에서의 주소지정 >
MIPS에서 가장 간단한 주소지정 방식은 점프 명령을 사용하는 것이다.
J타입 명령어는 6비트의 op코드와 26비트의 주소 필드로 구성되어 있다.
j 10000
코드로는 위와 같이 표현한다.
조건부 분기 명령어는 op코드와 분기 주소 외에도 두 개의 피연산자(조건 비교를 위한)를 사용한다. 이러한 이유로 조건부 분기 명령어는 I-타입 형식을 가지며, 분기 주소는 최대 16비트로 표현된다. 그러나 이렇게 되면 프로그램의 크기가 최대 216 바이트, 즉 64KB를 초과할 수 없다. 이를 해결하기 위해 MIPS는 PC 상대 주소 지정(PC-relative addressing) 방식을 사용한다. 이 방식은 현재 PC(Program Counter) 레지스터의 값에 16비트 분기 주소를 더해 실제 분기 주소를 계산한다. 대부분의 경우, 분기 명령의 목적지는 가까운 위치에 있기 때문에 이 방식은 효과적이다.
그러나 함수 호출과 같은 경우, 목적지 주소가 먼 곳에 있을 수 있다. 따라서 jal 명령어(jump and link)는 다른 주소 지정 방식을 사용한다. MIPS 아키텍처에서는 j 명령어와 jal 명령어가 26비트의 긴 주소를 사용하기 위해 J-타입 형식을 사용한다. 또한, MIPS는 바이트 단위가 아닌 워드 단위로 주소를 표현하여, 분기 가능한 범위를 4배로 확장할 수 있게 한다. 이렇게 함으로써 MIPS는 더 넓은 범위로 분기할 수 있게 되어, 프로그램의 구조가 유연해진다.
< 조건부 분기 명령으로 16비트보다 멀리 분기해야 하는 경우 >
beq $s0, $s1, L1
어셈블러는 위의 코드를 아래와 같이 바꾼다.
bne $s0, $s1, L2
j L1
L2:
< MIPS 주소지정 방식 요약 >
- 수치(immediate) 주소지정: 피연산자는 명령어 내에 있는 상수이다.
- 레지스터 주소지정: 피연산자는 레지스터이다.
- 베이스(base) 또는 변위(displacement) 주소지정: 메모리 내용이 피연산자이다. 메모리 주소는 레지스터와 명령어 내의 상수를 더해서 구한다.
- PC 상대 주소지정: PC 값과 명령어 내 상수의 합을 더해서 주소를 구한다.
16비트 주소를 2비트 왼쪽 자리이동한 후(워드 단위로 주소를 표현하기 때문) PC의 상위 4비트와 연접한다. - 의사직접(pseudodirect) 주소지정: 명령어 내의 26비트를 PC의 상위 비트들과 연접하여 점프 주소를 구한다.
26비트 주소를 2비트 왼쪽 자리이동한 후 PC의 상위 4비트와 연접한다.
1번은 레지스터의 값이 아닌 명령어 자체에 피연산자가 있는 것이다.
2번은 레지스터가 피연산자가 되는 것이다.
3번은 피연산자가 메모리에 있는 경우이다.
4번과 5번은 명령어 주소를 지정하기 위한 방식이다.
< 기계어의 해독 >
어셈블리어를 기계어로 바꾼 것 처럼 기계어를 어셈블리 명령으로 바꿀 수 있다.
00af 802016 가 있으면
0000 0000 1010 1111 1000 0000 0010 0000으로 바꿀 수 있고 이는
000000 00101 01111 10000 00000 100000 으로 R타입 명령으로 바꿀 수 있다.
add $s0, $a1, $t7
이 명령은 위의 어셈블리 코드에 해당한다.
'CS > 컴퓨터구조' 카테고리의 다른 글
[컴퓨터구조] 3 컴퓨터 연산 (3.5 부동 소수점 ~) (0) | 2024.11.05 |
---|---|
[컴퓨터구조] 3 컴퓨터 연산 3.1 ~ 3.4 (1) | 2024.10.15 |
[컴퓨터구조] 2 명령어: 컴퓨터 언어 2.11 ~ (0) | 2024.10.15 |
[컴퓨터구조] 2 명령어: 컴퓨터 언어 2.1~2.5 (0) | 2024.10.01 |
[컴퓨터구조] 1 컴퓨터 추상화 및 관련 기술 (5) | 2024.09.17 |