상세 컨텐츠

본문 제목

메모리와 명령어의 상호작용, 왜 알아야할까??

CS지식 학습

by Tabris4547 2023. 10. 10. 15:36

본문

728x90

우리가 C,Java,Python등의 컴퓨터언어로 코드를 작성하면
컴퓨터는 해당 코드를 그대로 실행하지 않습니다.
컴퓨터가 이해할 수 있는 언어로 해석한 뒤에 실행합니다.
이 과정을 컴파일이라고 부르며
코드를 명령어 형태로 저장하고
명령어가 실행되는 중간에 메모리에 저장되고 실행됨을 반복합니다.
메모리와 명령어의 상호작용, 왜 알아야하고 어떻게 수행될까요??
 


컴파일과정과 기계어변환

C언어로 작성된 프로그램은 
다음의 과정을 거쳐 메모리에 저장이 됩니다.
먼저 컴파일러로 어쎔블리 언어 프로그램으로 저장한 뒤에
Assembler로 기계언어로 바뀝니다.
그 뒤에 Linker를 통해 기계 언어 프로그램을 만들고
최종적으로 Loader를 통해 메모리에 저장합니다.

 


Instruction Form
 

메모리에 저장된 명령어들은 2진수로 표현된
기계언어로 저장이 됩니다.
위의 형태가 기계어 저장의 예시이며
명령어 구조(Instruction Form)이라고 부릅니다.
Instruction Form은 크게 2가지가 있습니다.
 


(1)R-Type

레지스터끼리 계산하고 해당 값을 저장하는 타입입니다.
레지스터를 위한 form이라 R-Type이라고 부릅니다.
각각 항목을 살펴보면
 
opcode->어떤 명령어 form인지,어떤 동작을 하는지 보여줌
rd->연산결과를 저장하는 레지스터
funct3->추가 opcode
rs1->계산의 첫번째 연산자
rs2->계산의 두번쨰 연산자
funct7->추가 opcode
 

add x9, x20, x21

해당 어쎔블리코드는 Risc-V기준으로 어떤 명령어 form으로 만들어질까요?

다음의 케이스로 만들어질 수 있습니다.
rd에 해당하는 값이 9
rs1에 해당하는 값이 20
rs2에 해당하는 값이 21
나머지 opcode들은 0 0 51로
이 경우에는 해당 숫자가
Risc-V에서 add라고 인식하는 케이스입니다.
 


(2)I-Type

다음은 load를 위한 명령어 형태입니다.
여기서 immediate는 load하려는 메모리의 주소값을 의미합니다.

ld x9, 240(x10)

다음의 load명령은 어떤 식으로 저장이 될까요?

메모리 240번지의 값을 rs1(x10)에 임시저장하고
해당 값을 rd에 적힌 x9번에 load시킵니다.
 


(3)S-Type

register에 load를 했다면
반대로 memory에 store하는 명령도 있어야겠죠?
I-Type과 반대로 동작하는 S-Type입니다.
이 form에서는 immdiate를 2개로 쪼갰기 때문에
저장하는 주소값을 계산할 경우
앞과 뒤의 이진수를 합쳐야합니다.
이렇게 구성한 이유는 rs1,rs2가 있는 R-Type과의 유사성을 주어
관리를 용이하게 만들 수 있기 때문입니다.

sd x9, 240(x10)

 
 
 
Stored Program Concept

오늘날 컴퓨터는 2가지 개념에 근간을 둡니다.
1. 명령어들은 숫자로 표현된다
2. 프로그램은 데이터처럼 메모리에 저장되어 동작한다.
이런 개념으로 "Stored Program Concept"이 만들어졌으며
위의 구조가 만들어집니다.
메모리단에
머신코드를 위한 에디터프로그램
C컴파일러
컴파일된 프로그램이 사용되는 text영역
컴파일된 코드를 일치시키는 프로그램
등등이 저장되어있습니다.

그래서 C코드가 저장이 되면
다음과 같이 명령어와 변수들이 각각 저장이 되어 
프로그램이 동작할 때 각각에 맞는 메모리를 참조합니다.
 


CPU주요 레지스터
 

CPU가 명령어를 수행하기 위해서는
이에 필요한 내부 레지스터들이 있습니다.
각각을 살펴보면
PC(Program Counter):다음 수행할 명령어의 주소를 가지고있는 레지스터.
각 명령어가 수행된 이후에는 명령어길이만큼 증가.
분기 명령어가 실행되면 해당 주소지로 이동
AC(Accumulator):데이터를 일시적으로 저장하는 레지스터.
CPU가 한번에 연산을 처리할 수 있는 데이터비트수(word)와 같다.
IR(Instruction Register):가장 최근 수행된 명령어가 저장된 레지스터
MAR(Memory Address Register):PC에 저장된 주소가 출력되기 전에 
일시적으로 저장되는 주소레지스터
MBR(Memory Buffer Register):기억장치에 저장될 데이터 혹은 기억장치로부터 읽은 데이터가
일시적으로 젖아되는 버퍼 레지스터.
LR(Link Register):수행한 이전 주소값을 저장하는 레지스터
 
 


Stack Pointer

앞서 메모리단에 명령어가 저장되고 수행된다는 걸 알았습니다.
각각의 명령어가 차례대로 수행이 되는데
어떤 원리에 의해서 수행이 될까요?
컴퓨터는 Stack Pointer를 사용해
메모리 단의 명령들을 Stack처럼 push,pop를 해서 명령을 수행합니다.

long long int leaf_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;
}
leaf_example:
	addi sp, sp, -24 // adjust stack to make room for 3 items
	sdx5, 16(sp) // save register x5 for use afterwards
	sdx6, 8(sp) // save register x6 for use afterwards
	sdx20, 0(sp) // save register x20 for use afterwards
	add x5, x10, x11// register x5 contains g + h
	add x6, x12, x13// register x6 contains i + j
	sub x20, x5, x6// f = x5 − x6, which is (g + h)−(i + j)
	addi x10, x20, 0 // returns f (x10 = x20 + 0)
	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) // branch back to calling routine

위의 C코드는 다음과 같이 어쎔블리어로 변환이 됩니다
이럴 때 메모리 단에서는 어떻게 동작을 할까요?

 
먼저, 메모리 주소번지 하나가 8이라는 가정하에
3개를 할당해야하기 때문에 그에 맞는 할당포인터를 마련합니다.
그 후, 각 레지스터값을 push해주어
메모리를 관리합니다.

 
동작이 끝난 이후에는 모두 pop을 시켜줍니다.
이제 더이상 사용할 일이 없기 때문에
pop을 해주고 새로운 메모리를 할당해줍니다.
 


재귀함수를 통해 메모리 동작을 자세히 알아보자

long long int fact (long long int n)
{
	if (n < 1) return (1);
	else return (n * fact(n −1));
}
fact:
	addi sp, sp, -16// adjust stack for 2 items
	sd x1, 8(sp)// save the return address
	sd x10, 0(sp)// save the argument n
    addi x5, x10, -1 // x5 = n - 1
    bge x5, x0, L1 // if (n - 1) >= 0, go to L1
    addi x10, x0, 1 // return 1
    addi sp, sp, 16 // pop 2 items off stack
    jalr // return to caller
    
L1: addi x10, x10, -1 // n >= 1: argument gets (n −1)
	jalx1, fact // call fact with (n −1)
    addi x6, x10, 0 // return from jal: move result of fact(n -1) to x6:
	ld x10, 0(sp) // restore argument n
    ld x1, 8(sp) // restore the return address
    addi sp, sp, 16 // adjust stack pointer to pop 2 items
    mul x10, x10, x6 // return n * fact (n −1)
    jalr x0, 0(x1) // return to the caller

프로그래밍을 하다보면 다음과 같이 재귀함수를 사용하는 경우가 많습니다.
가장 기본적인 팩토리얼 재귀함수를 통해
어떻게 메모리단에 저장이 되어서 동작하는지 보겠습니다.
(fact(2)를 동작한다는 가정)

처음 fact(2)를 수행할 때의 모습.
아직 Stack단에는 수행된 것이 없기에 아무것도 없습니다.
LR에는 fact(2)를 마친 후에 리턴할 주소가 저장되어있고
PC에는 이제 수행해야할 fact()함수의 주소가 저장되어있습니다.

fact(2)를 수행합니다.
sp에 fact(2)의 리턴주소와 현재 n값을 저장합니다.
x10에는 현재의 n값
x5에는 현재의 n값에 -1을 해준 값이 있습니다.
x5에 저장된 값이
0보다 작은 지 비교하고
0보다 작다면 L1으로 이동해서 동작을 수행합니다.
그 후 n*fact(n-1)을 수행하기 위애
fack을 호출합니다.

호출하기 전에 PC에 다음으로 수행할 fact 함수가 저장된
0x900을 저장합니다.
그리고 LR에는 fact의 결과를 사용할 0x924를 저장하고
추후에 리턴될 때 해당 주소지로 돌아갈 것을 염두합니다.

이제 fact(1)을 수행합니다.
fact(2)와 유사한 과정을 거치고
fact(0)를 호출합니다.

fact(0)에서는
x5가 0보다 작기 때문에
L1분기로 이동하지 않습니다.
x10을 1로 계산하고
stack의 2개를 pop합니다.

이후에는 fact(1)로 되돌아갑니다.
LR에 저장된 0x924로 되돌아가서
1*fact(0)를 수행합니다.
이후 Stack을 pop해주고
저장된 값을 fact(2)에 수행하기 위해
LR에 저장된 값으로 리턴합니다.

드디어 fact(2)로 되돌아왔습니다.
fact(1)의 결과를 리턴해
fact(2)를 리턴해준 후
fact(2)를 호출한 함수의 주소값으로 리턴합니다.

 

 

메모리 구조

메모리구조는 다음과 같이 되어있습니다.

stack영역에는 현재 수행하는 명령및 지역변수가

저장되어있습니다.

Dynamic Data는 heap이라고도 불리며 

정적변수를 저장합니다.

Static Data에는 프로그램 전반적으로 동작할

전역변수가 저장되어있습니다.

Text에는 작성한 프로그램코드가 저장되어있으며stack등으로 추후에 꺼냅니다.

 


알아야하는 이유
 


1.stack Overflow 방지

 
재귀함수를 잘못 짜다보면 stack overflow가 발생합니다.
메모리 stack 공간은 한계가 있는데 
계속 stack을 호출하면서 리턴되지 않아 pop이 되지 않아
stack이 넘쳐나게 됩니다.
이런 stack over flow에 대한
에러를 사전에 잡기 위해
해당 개념을 알아야할 필요가 있습니다

 


2.메모리참조


2023.09.23 - [CS지식 학습] - Call by value vs Call by reference??? (C언어 고찰)

 

Call by value vs Call by reference??? (C언어 고찰)

C언어는 다른 언어보다 C배 어렵다고 합니다. 그 이유로 포인터를 이야기하죠. 자바,파이썬 등의 대중적인 언어와 다르게 메모리참조를 직접 할 수 있는 기능이 있고 이 기능 때문에 개발자들의

door-of-tabris.tistory.com

 
C언어에서는 포인터에 의해
값을 참조하고 쓰는 동작이 가능합니다
이를 call by reference라고 하죠
반대로 stack 에서 이뤄지는 값의 변환은
Call by value라고 합니다

void swap1(int a, int b){
	int tmp=a;
    a=b;
    b=tmp;
}

void swap2(int *a, int *b){
	int tmp=*a;
    *a=*b;
    *b=tmp;
}


int main(){
	int a=3;
    int b=4;
    
    swap1(a,b);
    
    printf("%d %d",a,b); //3 4 출력
    
    swap2(&a,&b);
    printf("%d %d",a,b); //4 3 출력
}

swap1과 swap2는 모두 값을 교체하는 함수이지만

main의 결과는 다릅니다.

swap1은 main에서 3,4를 받고 

swap1이 수행한 주소값에 대해

3 4 의 값을 교환한 후 

리턴이 됩니다.

리턴되면서 저장했던 값이 pop되기 때문에

main으로 와서는 a b의 값이 그대로 입니다.

하지만 swap2는 main에 할당한

a b의 주소값을 인자로 받습니다.

그리고 a b의 주소값을 따라가 

해당 주소값의 값을 바꿉니다.

그래서 swap2가 종료되도

swap에 사용된 tmp만 pop됩니다.

아직 main이 끝나지 않았기에

a b는 pop되지 않았고

swap2에서 주소값을 참조해 교체했으므로

main에 교체한 값이 유효합니다.

int* ex(){
	int arr[3];
    for(int i=0;i<3;i++)
    	arr[i]=i;
    return arr;
}

다음 함수는 형태만 보면 맞는거같습니다.

int*를 반환하는 함수고

그에 때라 arr이라는 배열을 반환하는 함수입니다.

하지만 실제로 동작하면 error가 발생할 수 있습니다.

ex()의 동작이 종료가 되면 arr은 stack에서 pop이 됩니다.

그러니 arr이 리턴이 되더라도

해당 주소번지의 값은 이미 삭제가 된 것이죠.

그래서 어떤 int*가 ex()의 값을 리턴받아도

그 값은 arr의 주소값이 아닌

삭제된 nullpt를 받게 됩니다.

 

 

참고자료

Computer Organization and Design RISC-V edition

숭실대학교 박민호 교수님 강의자료

728x90

관련글 더보기

댓글 영역