728x90

어셈블러에서 함수를 어떻게 처리하는지 살펴보자

 

  • Supporting Procedures

함수를 호출하는 절차는 다음과 같다

1. 함수의 매개변수들을 넘긴다.

2. memory space에 필요한 공간을 마련하고 그곳으로 제어를 옮긴다.

3. 함수를 실행한다.

4. return value를 저장하고 돌아갈 위치를 찾는다.

5. 원래 지점으로 돌아간다.

 

int main(){ //caller
	if(a == 0)
    	b = f1(g, h);
    else
    	b = f1(k, i);
    return 0;
}

int f1(x, y){ //callee, caller
	a = (x + y) * (x + 2) * f2(y);
    return a;
}

int f2(y){ //callee
	return y / 2;
}

여기서 main 함수가 f1을 호출한 caller가 되고

f1은 main 함수에 의해 호출당한 callee이며, f2를 호출한 caller가 된다.

f2는 f1에 의해 호출당한 callee이다.

 

 

그러면 RISC-V에서는 매개변수와 return value, return address를 어떻게 주고받을까?

 

RISC-V는 관습적으로 정해진 레지스터 번호에 해당하는 레지스터를 함수의 호출에 사용한다.

 

x10~x17: eight parameter registers in which to pass parameters or return values

x10~x17에 해당하는 레지스터를 매개변수를 저장할 때 사용하거나 return value를 저장할 때 사용한다.

 

x1: one return address register

x1은 함수가 return 할 때 돌아갈 지점을 저장한다.

 

또 함수를 다루기 위해서 jal이라는 명령어를 사용한다.

jal: "jump-and-link"

jal x1, ProcedureAddress

Branch unconditionally and save the address of the next instruction(return address) to the designated register

무조건 지정해 둔 주소로 분기를 하며, 레지스터에 다음번 실행해야 할 instruction을 저장하고 분기한다.

보통 return address를 저장하는 x1 레지스터를 사용한다.

 

만약 돌아올 주소를 저장하지 않고 분기하고 싶다면

jal x0, Label

로 주소를 저장하지 않고 분기할 수 있다.

 

jal에서 더 확장하여 jalr 명령어가 있는데

jalr: "jump and link register" instruction (I-type)

jalr: x0, 0(x1)

x0 주소에 return address를 저장하고 0(x1) 주소로 분기한다는 명령어이다.

 

주소값을 표현하는 데에 jal에 비해 더 많은 비트를 사용하기 때문에 더 자세한 주소값으로 분기할 수 있다.

 

  • Using More Registers

함수를 실행하기 위해서는 보통 8개의 레지스터보다 더 많은 메모리를 필요로 한다.

그렇기에 컴파일러는 레지스터 이외에 추가로 메모리를 사용한다.(당연히 속도는 더 느려진다)

이렇게 레지스터가 모두 차서 메모리에 저장하는 현상을 spill이라고 한다.

 

spill이 일어났을 때 저장하기에 이상적인 공간은 stack이다.

x2 레지스터를 stack pointer라고 하는데

이 stack pointer는 데이터가 저장되어 있는 memory의 주소이다.

이 stack은 높은 주소부터 낮은 주소로 데이터를 저장하기 시작한다.

 

 

stack pointer를 활용하는 예제이다.

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

//g -> x10
//h -> x11
//i -> x12
//j -> x13
//f -> x20

이 C 코드를

leaf_example:
addi sp, sp, -24 //adjust stack to make room for 3 items
sd x5, 16(sp)    //save register x5 for use afterwards
sd x6, 8(sp)     //save register x6 for use afterwards
sd x20, 0(sp)    //save register x20 for use afterwards
add x5, x10, x11 //register x5 contains g + h
add x6, x12, x12 //register x6 contains i + j
sub x20, x5, x6
addi x10, x20, 0 //move value f to x10
ld x20, 0(sp)    //restore register x20 for caller
ld x6, 8(sp)     //restore register x6 for caller
ld x5, 16(sp)    //restore register x5 for caller
addi sp, sp, 24  //adjust stack to delete 3 items
jalr x0, 0(x1)

x5, x6, x20에 있는 값을 sp를 이동시킨 후 memory에 저장하는 모습이다.

사용 후에는 caller의 데이터를 복원하기 위해 레지스터에 원래 데이터를 두고 sp를 원위치시킨다.

stack을 사용하던 도중 저 sp는 처음 위치보다 더 올라가서 읽으면 안된다.

그렇기에 시작 주소를 fp에 저장하고 그 위로는 데이터를 읽을 수 없도록 한다.

sp는 함수 실행 도중 변할 수 있지만 fp는 그렇지 못하다.

 

 

  • Register saving convention

하지만 모든 상황에서 caller의 데이터를 복구해 두는 것은 아니다.

No need for callee to save callee must preserve if used
x5-x7, x28-x31 x8, x9, x18-x27

저 표에 있듯 x8, x9, x18-x27의 데이터는 복구를 해두어야 하지만 x5-x7, x28-x31의 값은 복구할 필요가 없다.

다시 말해 caller는 callee를 호출 한 후 x5-x7, x28-x31의 값이 원래와 같지 않을 수 있다고 생각을 해야한다는 것이다.

 

저 표에 sp, fp 등의 레지스터를 추가해보면

Preserved Not preserved
Saved registers: x8-x9, x18-x27 Temporary registers: x5-x7, x28-x31
Stack pointer register: x2(sp) Argument/result registers: x10-x17
Frame Pointer: x8(fp)  
Return address: x1(ra)  

다음과 같다.

 

  • Allocating Memory on the Heap

프로그램 실행 중에 데이터를 메모리에 저장하는 방법이다.

Text Segment: instructions, program binaries

Static data segment: constants and static variables

Heap(for dynamic data): place where dynamically allocated(malloc and free in C)

stack은 위에 나왔던 stack 과 같다.

 

처음 실행 할 때 Text, Static data 영역에 데이터를 저장해두고 프로그램 실행 중에 Heap 영역만 변하게 된다.

 

  • Instructions

lui

immediate에 저장되어 있는 12비트의 수를 64비트로 확장하는 instruction

우선 31~12에 저장되어 있는 20비트의 수를 load한 후

저장하려는 64비트의 왼쪽에 32비트를 sign 비트로 채운다.

그 뒤에 20비트의 수를 넣고 나머지 오른쪽의 12비트를 0으로 채운다.

 

SB-type

beq, bne등의 instruction이 해당하며 분기하는 instruction이다.

immediate를 이용해서 -4096 ~ 4094 범위로 분기 할 수 있다.

imm[0]은 언제나 0이기 때문에 따로 저장하지 않는다.(항상 짝수이기 때문)

 

UJ-type

조건에 상관없이 무조건 분기하는 jal 계열의 instruction이다.

조건을 확인하지 않기에 immediate가 더 긴 것을 볼 수 있으며, 여기도 imm[0]이 항상 짝수이기 때문에 따로 저장하지 않는다.

 

여기서 보면 conditional branch는 13bit, unconditional branch는 21bit의 immediate field를 가진다.

부호를 표현하는 데 1bit, 4byte instruction이기에 2bit가 빠지면

conditional branch는 2의 10제곱 만큼의 위치를

unconditional branch는 2의 18제곱 만큼의 위치로 분기 할 수 있다.

'학교 생활 > 컴퓨터구조' 카테고리의 다른 글

컴퓨터구조 3주차  (0) 2023.01.13
728x90

컴퓨터구조 3주차 정리이다.

 

전에 다 알아보지 못했던 2의 보수법과 CPU의 기본 명령어들을 알아보도록 하자

 

  • Signed and Unsigned Numbers

컴퓨터는 단순 2진수로만 수를 저장할 수 있지만 양수만이 아닌 음수도 저장할 수 있다.

그렇다면 과연 컴퓨터 내에서는 음수를 어떻게 저장할까?

 

수를 저장할 때 부호 비트의 자리를 따로 만들고 해당 비트가 0이면 양수, 1이면 음수로 표현한다.

부호 비트를 앞쪽에 저장하는지, 뒤쪽에 저장하는지에 따라서 LSB와 MSB로 나뉘는데

 

LSB(least significant bit): the right most bit, bit position 0

부호 비트를 0번에 표시하면 LSB

 

MSB(most significant bit): the left most bit, bit position 63

부호 비트를 63번에 표시하면 MSB

이다.

 

보통은 MSB를 사용한다.

 


 

  • signed numbers

음수를 부호 비트 1로 표현하는 것은 알았다.

그렇다면 그냥 단순히 부호비트만 1로 바꾸면 음수가 되는 것일까?

 

0000 = 0      -> 1000 = -0

0001 = 1       -> 1001 = -1

0010 = 2       -> 1010 = -2... 이런식으로 양수에서 부호비트만 바꾸는 방법을 sign and magnitude(부호-크기)방식이라고 한다.

하지만 해당 방식의 문제는 뺄셈의 계산을 할 때 발생한다.

뺄셈은 음수의 더하기로 계산을 한다.

그렇다면 이 식을 계산해보자

 

6 - 3 = 0110 + 1011 = ..0001 대충 계산을 해보아도 다른 답이 나오는 것을 볼 수 있다.

 

뺄셈을 위해 컴퓨터에서는 이 부호-크기 방식을 사용하지 않고 보수법을 사용하게 된다.

 

1's complement(1의 보수법)

0000 = 0      -> 1111 = -0

0001 = 1       -> 1110 = -1

0010 = 2       -> 1101 = -2 ... 이런식으로 양수와 음수 비트를 역전시키는 방법을 1's complement(1의 보수법)이라고 한다.

이 방법을 사용하면 뺄셈을 수행할 수 있다.

 

6 - 3 = 0110 + 1100 = 0010 = 2, 비록 3이 나오지는 않지만 이 수에 1을 더해주면 결과가 나오게 된다.

뺄셈 후에는 모두 일관되게 1만 더해주면 되기 때문에 뺄셈 연산이 가능하다.

 

하지만 이 방법의 단점은 무엇일까?

0을 표현하는 방법이 2가지 이다.(0000, 1111)

이렇게 된다면 표현할 수 있는 수가 하나 줄게 된다.

 

2's complement(2의 보수법)

0000 = 0      -> 1111 = -1

0001 = 1       -> 1110 = -2

0010 = 2       -> 1101 = -3 ... 이런식으로 1의 보수법에서 아예 미리 1을 더해놓는 방법을 2's complement(2의 보수법)이라고 한다.

이 방법을 사용하면

 

6 - 3 = 0110 + 1101 = 0011 = 3, 이렇게 바로 원하는 결과를 얻을 수 있으며

0이 중복으로 표현되지 않기 때문에 표현할 수 있는 수가 손실되지 않는다.

 

양수와 음수를 변환 할 때, 비트를 역전시킨 후에 1만 더해주면 된다.

2 -> -2 는 0010 -> 1101 -> add1 -> 1110으로

-2 -> 2 는 1110 -> 0001 -> add2 -> 0010으로 상호변환이 가능하다.

이렇게 양수와 음수를 표현하는 방법에 대해 알아보았다.

그렇다면 sign extension(부호 확장)에 대해 알아보자

 

sign extension

이번엔 부호의 확장에 대해 알아보자.

n bit였던 수를 m bit로 확장한다면

해당 수의 부호를 보존하기 위해 MSB 비트를 확장하는 만큼 앞에 채워주면 된다.

그래도 만약 원한다면 0으로 채워줄 수는 있다.

 

Instructions : lb(load byte), lh(load halfword), lw(load word)

if you don't want to sign extension, use lbu(load byte unsigned).

 


 

  • Representing instructions

컴퓨터에서 수를 어떻게 저장하는지 살펴보았으니 이제 명령어들을 어떻게 표현하는지 알아보자.

가장 대표적인 명령어 타입으로 R-type, I-type, S-type 이 있다.

 

R-type

R-type 명령어는 이렇게 이루어져 있다.

opcode: 대략적인 명령어의 종류

rd: destination register(값을 저장할 레지스터)

funct3: additional opcode

rs1: 1st source register

rs2: 2nd source register

funct7: additional opcode 

 

x20과 x21을 더해서 x9에 저장하는 add x9, x20, x21을 표현한다면

0000000/10101/10100/000/01001/0110011로 표현이 될 것이다.

 

I-type

i가 붙은 명령어들을 수행하는 I-type instruction이며

이렇게 표현이 된다.

R-type에서 rs2와 funct7 코드가 합쳐져 immediate가 되었으며 immediate는 연산을 수행 할 때 같이 계산이 되는 상수이다.

 

x2의 레지스터에 있는 주소값에서 8을 더한 주소의 값을 x14에 저장하는 ld x14, 8(x2)를 표현한다면

000000001000/00010/011/01111/0000011로 표현이 된다.

 

S-type

S-type에는 rd가 없는것이 특징이다. (레지스터에 데이터를 저장하는 과정이 필요하지 않기 때문)

대신 immediate가 rd의 위치까지 차지한 상태로 나뉘어 저장이 된다.

x2의 레지스터에 있는 주소값에서 8을 더한 주소의 값을 x14에 저장되어 있는 주소에 저장하는 sd x14, 8(x2)를 표현한다면

0000000/01110/00010/011/01000/0100011로 표현이 된다.

 

  • logical operations

논리적인 계산을 수행하는 명령어들을 알아보자.

Shift left와 right 부터 살펴보자.

왼쪽으로 이동하는 Shift left는 그냥 0으로 채우지만, 오른쪽으로 이동하는 Shift right는 무슨 수로 채우는지에 따라 부호가 바뀌기 때문에 신중해야 한다.

 

slli, srli - shift left (right) logical immediate

이 명령어들은 단순히 이동시킨 후 빈자리를 0으로 채워준다.

 

만약 이 문제를 해결하고 싶다면

sra, srai - shift right arithmetic(immediate): fill with sign-bit

sign bit로 채워주는 sra 계열의 명령어를 사용해야한다.

 

AND

OR

XOR

해당 명령어들은 컴퓨터를 공부했다면 직관적으로 파악할 수 있을것이다.

 

여기에 NOT연산은 따로 구현해두지 않고

1111...1111과 xor연산으로 구할 수 있다.

 


 

  • Instructions for Making Decisions

컴퓨터가 결정을 내리는 과정 코드에 대해 알아보자

쉽게 IF문으로 생각하면 된다.

 

컴퓨터는 Branch instruction을 이용해 분기를 한다.

 

Branch instruction

 

- if statement

  • beq rs1, rs2, L1

beq: branch if equal

만약 rs1의 값과 rs2의 값이 같다면 L1으로 분기한다는 조건문이다.

 

  • bne rs1, rs2, L1

bne: branch if not equal

만약 rs1의 값과 rs2의 값이 같지 않다면 L1으로 분기한다는 조건문이다.

 

bne와 beq를 이용해서 C의 코드를 변환해보자

만약 C의 이런 코드가 있다.

그리고 각각의 변수는 해당 레지스터에 값이 저장되어 있다.

f: x19, g:x20, h:x21, i:x22, j:x23

이렇게 변환이 될 것이다.

 

While문도 이렇게 조건을 이용해서 만족할 때만 탈출할 수 있도록 만든다.

i: x22, k: x24, base of save: x25

 

이렇게 계속 만족하는 조건에만 탈출하고 아니면 계속 위로 올라가는 방식으로 조건문을 구현한다.

 

- Other conditional Branches

  • blt: branch is less than

blt rs1, rs2, L1

뜻 그대로 rs1에 저장된 값이 rs2보다 작으면 L1으로 분기한다.

 

  • bge: branch if greater than or equal

bge rs1, rs2, L1

rs1에 저장된 값이 rs2보다 크거나 같으면 분기한다.

 

bltu, bgeu는 비교하는 값들이 unsigned 수라고 생각하고 비교하는 것이다.

 

이것들을 이용해 Array의 범위 체크를 구하는 명령어를 만들어보자

우리는 배열을 사용할 때 해당 인덱스가 0 이상 최대 인덱스 미만인지 확인을 해야한다.

 

당연히 조건문을 두번 사용해야 할 것 같지만

이렇게 bgeu를 한번만 사용한다.

만약 x20에 저장된 수가 음수라면 unsigned 수로는 엄청나게 큰 수로 읽게 되고 그럼 x11보다 클 것이기 때문에

저 식 한번으로도 체크할 수 있다.

'학교 생활 > 컴퓨터구조' 카테고리의 다른 글

컴퓨터구조 4주차  (0) 2023.01.15

+ Recent posts