이 글은 필자가 수업시간에 들은 내용과 강의록을 토대로 정리한 글입니다.
수업 필기이다 보니, 오류가 있거나 설명이 부족한 부분이 있을 수 있습니다.
궁금하신 점이나 지적하실 점이 있다면 댓글로 달아주세요! 확인 후 내용을 추가하거나 답변해드리도록 하겠습니다 :)
$v0
, $v1
: 결과값 (레지스터 2, 3번)$a0
~ $a3
: 매개변수 (레지스터 4 ~ 7번)$t0
~ $t9
: 임시 값$s0
~ $s7
: 저장된 값$gp
: 정적 데이터(static data)를 위한 전역 포인터 (레지스터 28번)$sp
: 스택 포인터 (레지스터 29번)$fp
: 프레임(frame) 포인터 (레지스터 30번)$ra
: 반환 주소(return address) (레지스터 31번)$sp
) : 스택에서 가장 최근에 할당된 주소를 저장하는 레지스터int leaf_example (int g, h, i, j)
{
int f;
f = (g + h) - ( i + j);
return f;
}
매개변수 g, … , j는 $a0
, … , $a3
에 저장된다.
f는 $s0
에 저장된다. 따라서, $s0
은 스택에 저장해야 한다.
결과는 $v0
에 저장한다.
MIPS 코드
leaf_example:
addi $sp, $sp, -4 # Save $s0
sw $s0, 0($sp) # on stack (push)
add $t0, $a0, $a1 #
add $t1, $a2, $a3 # Procedure body
sub $s0, $t0, $t1 #
add $v0, $s0, $zero # Result
lw $s0, 0($sp) # Restore
addi $sp, $sp, 4 # $s0
jr $ra # Return
int fact (int n)
{
if (n < 1) return 1;
else return n * fact(n - 1);
}
매개변수 n은 $a0
에 저장된다.
결과는 $v0
에 저장한다.
MIPS 코드
fact:
addi $sp, $sp, -8 # adjust stack for 2 items
sw $ra, 4($sp) # save return address to caller
sw $a0, 0($sp) # save argument of this procedure
slti $t0, $a0, 1 # test for n < 1
beq $t0, $zero, L1
addi $v0, $zero, 1 # if so, result is 1
addi $sp, $sp, 8 # pop 2 items from stack
jr $ra # and return
L1: addi $a0, $a0, -1 # else decrement n
jal fact # recursive call
lw $a0, 0($sp) # restore original n
lw $ra, 4($sp) # and return address
addi $sp, $sp, 8 # pop 2 items from stack
mul $v0, $a0, $v0 # multiply to get result
jr $ra # and return
Non-Leaf 프로시저의 경우, 피호출자가 호출자의 반환 주소(return address)를 스택에 저장해야 한다. 특히 재귀일 경우 더 그런데, 중첩적으로 호출이 일어날 경우 스택에 반환 주소가 있어야만 스택에서 pop을 하며 제대로 원래 위치로 돌아갈 수 있기 때문이다.
위의 그림에서 볼 수 있듯, 매개변수가 4개보다 많을 경우, 스택의 프레임(stack frame) 보다 더 아래(Bottom)쪽에 매개변수를 저장한다. 스택의 프레임은 스택에 할당되는 해당 프로시저의 메모리 블럭 정도로 생각하면 된다. 즉, 스택에 할당되는 프로시저 블록의 바깥에 있다.
지역 변수는 저장 레지스터보다 더 위(Top)쪽에 저장하도록 한다.
스택은 큰 주소값을 가지는 쪽이 Bottom이 되도록 만들어지는데, 그리하여 앞의 예제로 사용했던 팩토리얼 예제의 경우 우측 사진과 같이 스택에 할당되게 된다.
$gp
는 오프셋(offset)을 이용해서 이 부분으로 접근할 수 있게 초기화된다.lb rt, offset(rs)
/ lh rt, offset(rs)
lbu rt, offset(rs)
/ lhu rt, offset(rs)
sb rt, offset(rs)
/ sh rt, offset(rs)
offset(rs)
에 저장한다.void strcpy (char x[], char y[])
{
int i;
i = 0;
while ((x[i]=y[i]) != '\0')
i += 1;
}
x와 y의 주소는 $a0
과 $a1
에 저장된다.
i는 $s0
에 저장된다.
MIPS 코드
strcpy:
addi $sp, $sp, -4 # adjust stack for 1 item
sw $s0, 0($sp) # save $s0
add $s0, $zero, $zero # i = 0
L1: add $t1, $s0, $a1 # $t1 = &y[i]
lbu $t2, 0($t1) # $t2 = y[i]
add $t3, $s0, $a0 # $t3 = &x[i]
sb $t2, 0($t3) # x[i] = $t2 = y[i]
beq $t2, $zero, L2 # exit loop if y[i] == 0
addi $s0, $s0, 1 # i = i + 1
j L1 # next iteration of loop
L2: lw $s0, 0($sp) # restore saved $s0
addi $sp, $sp, 4 # pop 1 item from stack
jr $ra # and return
lui rt, constant
를 사용하면 된다.
load-upper-immediate : 16비트 상수를 rt의 왼쪽(윗쪽) 16비트에 복사하고, 오른쪽 16비트는 0으로 초기화한다.
ori
를 사용하여 오른쪽(아랫쪽) 16비트를 설정해주도록 한다.(addi
를 사용하지 말것!)
예시를 보면, addi
, ori
이 두 명령어가 무슨 차이가 있는가 의문이 들 수 있다. 하지만 떠올려보라, addi
는 I 포맷 명령어이다. 즉, 32비트 상수 값을 제대로 처리할 수 없다.
beq
나 bne
같은 명령들은 I 포맷 명령이기 때문에 다음 요소들에 대한 정보를 가지고 있어야 한다.
대부분의 분기의 목적지는 분기 명령 근처에 위치한다.
“PC에 상대적인 주소 지정(PC-Relative addressing)”
점프(j
, jal
)의 목적지는 텍스트 부분 내에서 어디든 될 수 있다.
주소를 저장할 때, 오른쪽으로 두 번 시프트 한 값이 저장되기 때문에, 결국 주소의 28비트를 기억하고 있다.
의사 직접 점프 주소 지정((Pseudo) “Direct jump addressing”)
그렇다면 28비트의 정보는 알 수 있는데, 나머지 최상위(Top) 4비트는 어떻게 처리하는가?
위의 그림에서, PC 주변의 칠해져 있는 부분이 이동 가능한 범위이다. 즉, PC의 최상위 4비트가 고정된 범위 이내에서 자유로이 이동이 가능하다.
점프 레지스터 명령(jr
)
6강의 반복문 코드를 참고하자.
위의 그림에서, 화살표가 시작되는 bne
의 2는 오프셋(offset)이다. 즉, PC 상대적인 주소 지정을 사용한 것이다.
또한, j
의 Loop의 주소를 살펴보면, 20000이다. 상기했듯, 주소를 오른쪽으로 2번 시프트하여 저장하기 때문에, 실제 주소를 구하려면 4를 곱해야 하고, 목적지 주소가 80000임을 알 수 있다.
beq $s0, $s1, L1
이라는 코드가 있는데, L1이 너무나도 멀리 있다면, 어셈블러는 이 코드를 다음과 같이 재작성하여 문제를 해결한다. bne $s0, $s1, L2
j L1
L2: ...
JR
LW
SW
BEQ
BNE
J
JAL
참고로, : 는 합친다는 뜻이다.
해독하기 위해서 이를 먼저 32비트의 이진수로 변환하고, 그린카드를 참고해서 포맷에 맞추어 천천히 해결하면 된다. 참고로, opcode가 첫 6비트이기 때문에 이에 맞추어 해독하면 된다.
move $t0, $t1
은 add $t0, $zero, $t1
이 되고, blt $t0, $t1, L
은 slt $at, $t0, $t1
bne $at, $zero, L
이 되는 식이다.
$at
는 레지스터 1번으로써, 어셈블러 임시 변수이다.bgt
, bge
, ble
에도 똑같이 적용된다.C 프로그램들은 실행까지 다음 과정을 거친다.