전공/컴퓨터 구조

4. Calling Convention(Caller-save, Callee-save, Hybrid)

문정훈 2021. 10. 7. 02:28

1. The Stack

명령어를 통해 프로그램 작성할 때 필요한 메모리 구조에 대해 정리하였습니다.

MIPS에서 32개의 레지스터를 가지고 있습니다. 한 프로그램 내에서 사용하는 모든 데이터를 레지스터에 로드하기 때문에 레지스터의 개수는 늘 부족합니다.

따라서 load store 명령을 통해 연산이 필요한 순간에 메모리와 레지스터가 소통하며 연산하게 됩니다.

이때 메모리의 어떤 저장 공간에 레지스터 값을 할당해야할까? 라는 정리가 필요합니다.

 

레지스터에 값을 메모리로 쫒아내는 과정을 “spil” 이라고합니다. 반대로 메모리에서 레지스터로 값을 읽어 들이는 과정을 “restore”라고 합니다.

이 두 과정을 수행하기 위한 메모리 공간을 stack 구조로 관리되게 됩니다.

 

일반적으로 OS가 프로세스에 데이터를 공급하는 방법, 즉 프로세스가 할당하는 메모리 구조를 4가지 영역으로 나누게 됩니다.

Stack (growing down)
<완충지역>
Dynamic data (growing up)
Static data
Code(text)
Reserved

운영체제에서는 각각의 프로세스마다 가상의 메모리 공간을 제공합니다. 이것을 virtual memoy space라고 하는데 각각의 OS에 따라 메모리 사이즈가 결정됩니다. 만약 32bit 운영체제를 사용한다면 4byte가 할당됩니다.

Reserved위에 Codetext영역이 있고 그 위에 static영역 그 위에 Dynamic dataheap영역이 존재합니다. 
heap
Stack 사이에는 완충 지역이 생기는데

heep영역은 주소가 low address부터 high address로 계속해서 증가합니다. 힙은 growing up을 하고 스택은 growing down을 하면서 점차 늘려 나가기 때문에 완충지역 통해 스택과 힙이 서로 확장해도 겹치지 않게 됩니다.

 

이러한 스택 구조는 아키텍쳐마다 모두 다르다. high level 언어로 작성된 코드는 컴파일러가 알아서 데이터 구조를 맞춰주기 때문에 신경쓰지 않아도 되지만 어셈블리 언어 프로그래밍을 할때는 각 아케텍쳐마다 스택 구조를 이해하고 프로그래밍 해야한다...

2. Stack 이란

스택은 프로세스 별로 할당되며 하나의 리스트인데 동일한 입구를 통해 데이터를 pop, push하는 구조입니다.

데이터를 저장하는 것을 push 데이터를 가져오는 것을 pop이라고 합니다.

만약 1,2,3을 순서대로 스택에 저장하였다고 하면 아래부터 1이 쌓이고 그 위에 2 그리고 3이 그 위로 push 되게 되며 pop시에는 들어간 순서가 아닌 스택의 젤 위의 3 의 값부터 pop하게 됩니다.

따라서 stackLIFO(Last In, First Out) 라고 합니다.

운영체제에서 메모리를 관리할 때 스택이라는 영역이 있어 스택을 통해서 CPU가 레지스터를 좀 더 효율적으로 사용할 수 있게 지원합니다.

 

MPIShigh Address에서 low Addresspush 되는 구조를 가집니다.

그리고 함수호출이나 리턴 동작에 따라 스택 프레임 단위로 push, pop이 됩니다.

 

 

3. Calling convention이 필요한 이유

스택은 주로 함수를 관리하는데 많이 사용됩니다.

프로그램은 여러 함수로 구성을 하게 됩니다만약 한 함수에서 다른 함수의 호출이 있을 때 문제가 발생합니다예를 들어

내가 만든 프로그램이 사용할 수 있는 레지스터의 개수가 8개이고프로그램에는

A라는 함수안에서 B라는 함수를 호출하는 구조로 되어있습니다.

A라는 함수가 8개의 레지스터를 모두 사용하는중에 B라는 함수를 호출하게 되면 B함수의 구현으로 B함수의 내용이 레지스터 공간을 필요로 하게 되며이때 B함수의 내용이 이미 8개가 사용되고 있는 상태에서 레지스터를 사용하게 되면 B의 실행이 다 끝나고 A의 내용이 이어질 때 이미 레지스터의 내용은 손상되게 되는 문제가 발생하게 됩니다.

 

이러한 문제점을 Calling convention 이라는 과정을 통해 해결하게 됩니다.

C언어와 같이 high level언어를 사용한다면 컴파일러가 이 문제를 해결해주기 때문에
Calling convention에 대해 고려하지 않아도 되지만 어셈블리 언어로 직접 프로그래밍을 한다면 프로그래머가 이 Calling convention와 관련된 모든 코드를 삽입해야합니다.

ISA를 공부하기 때문에 어셈블리 언어 레벨에서 생각해야합니다.

 

 

4. Calling convention 기본적 절차

A라는 함수와 B라는 함수가 있다고 가정하겠습니다만약 A라는 함수가 호출되다 Jump되어 B함수의 내용을 실행하고 B함수의 내용이 종료되고 다시 A 함수의 내용으로 돌아가는 상황을 가정할 수 있습니다.

 

순서1)

A라는 함수에서 B라는 함수로 함수 호출이 일어날 때 B의 파라미터를 설정해줘야합니다.

B의 파라미터 값을 스택 또는 레지스터에 저장합니다.

 

순서2)

A에서 B PC(program counter) B의 첫 명령으로 변경해줘야합니다그럼 B는 메모리 영역에서 필요한 만큼 스택 프레임을 형성하고 명령이 실행됩니다그리고 끝나면 A의 함수로 돌아오는데 return 값을 스택또는 레지스터를 통해 전달해줘야합니다.

 

순서3)

그리고 끝나면 A의 함수로 돌아오는데 return 값을 스택또는 레지스터를 통해 전달해줘야합니다그리고 A의 실행이 계속해서 진행됩니다.

이러한 함수와 함수간의 호출 약속을 Calling convention 라고 합니다.

 

 

5. Calling convention : Caller-Save

<Caller-save가 필요한 이유>

위 예문에서 A라는 함수는 Caller라고 하고 B함수는 Callee라고 합니다.

Caller-Save라는 것은 Caller에서 live register(Caller에서 사용하는 유효한 레지스터) save 하는 것을 의미합니다.

 Caller에서의 값이 Callee의 작업이 끝나도 유효해야하는 값을 의미합니다이것의 과정은 다음과 같습니다.

 

만약 A함수에서 r8(레지스터8)을 사용하고 있다가 B의 함수의 호출이 되는데 B함수에서도 r8레지스터를 사용해야할 수 있습니다이런 상황을 대비해 B함수를 호출하기 전에 A함수의 유효한 레지스터 값들을 메모리 스택 영역에 저장합니다.(store 과정) (일종의 백업그리고 돌아왔을 때 r8에 스택에 들어있던 값을 restore하게 됩니다.

이러한 save 과정을 위 예시에서는 A함수인 Caller가 하기 때문에 Caller-Svae라고 합니다.

 

<Caller-Svae 과정>

Caller에서 live한 레지스터를 store 한다. (메모리에 저장한다.) 그리고 Callee함수의 매개변수를 함수의 매개변수를 저장하는 레지스터에 할당한다.

그리고 Caller return 주소를 $ra(return address 31번 레지스터)에 저장한다그리고 Callee Jump하게 된다.

 

Callee에서는 스택 프레임을 할당하고 Callee의 내용을 실행하고 실행이 끝나면 Callee retruen 값을 레지스터에 셋팅 해준다.

그리고 Caller의 스택 프레임을 해제하고 Caller $ra 레지스터에 저장되었던 return address로 다시 jump 하게 된다그럼 Caller에서 restore 과정을 하고 다시 Caller의 명령을 재개한다.

 

<마무리..>

프로그램 실행 흐름을 스택과 레지스터 관점에서 정리하면,

프로세스의 명령어는 메모리 영역 중 기계코드 영역에 어셈블리 언어가 기계코드로 변환된 기계어로 저장됩니다이때 저장되는 어셈블리 언어는 변수를 레지스터에 할당하도록 레지스터의 변수에 저장하도록 어셈블리 언어는 구성되어 있습니다.

(전역 변수는 그 위 전역 변수 메모리 공간에 할당됩니다.)

함수 스택 프레임은 함수의 내용이 실행되기 전까지 아직 스택엔 존재하 않습니다.

기계코드 영역에 있는 명령어가 실행될 때는 메인 메모리에서 레지스터로 명령어를 불러와 실행해야하며 이때 메인 함수에서 함수가 호출되거나 함수 실행중 다른 함수가 호출될 때

레지스터에서 Caller는 유효한 값들을 저장하기 위해 store과정을 거쳐 메모리의 스택 영역에 함수 스택 프레임을 생성하여 값들을 저장합니다.

이때 메모리의 함수 스택 프레임에 유효한 값들이 저장되기 위해 함수 스택 프레임이 생기는 것입니다.

 

 

6. Calling convention : Callee-Save

Callee-save 방식에서는 Caller에서 레지스터의 store, restore 작업을 하지 않는다.

Caller에서는 Callee의 매개변수를 레지스터에 셋팅해주고 $ra(return address 31번 레지스터)에 저장하고 Callee jump하게 된다.

 

Callee에서는 먼저 스택 영역을 할당한다그리고 Caller에서 live되야하는 변수들을 Callee에서 먼저 store 하게 된다예를 들어 Caller의 매개변수를 Callee-save Callee의 스택 프레임에 저장한다 Caller에서 헀던 store 작업을 Callee에서 Callee의 내용을 실행 하기 전에 먼저 한 것임.

 

그리고 Callee의 실행이 모두 이루어지면 Callee return 값을 레지스터에 place 시켜주고

Caller에서 live한 레지스터를 살리기 위해 restore saved registers 작업을 해준다.

그리고 Caller return address jump 하게 된다.

 

 

7. Calling convention : Hybird

Caller Callee를 혼합한 Hybid 방식을 사용해 Calling Convention 방식을 더욱 효율적으로 한다.

MIPS에서 32bit의 레지스터가 있으면 32개중 일부 예를 들어 16(실제로는 16개가 아님) Caller에서 Save를 담당하게 하고 나머지 16개에서는 Callee에서 Svae를 담당하게 하는 개념으로 처음 이해하면 된다.

 

MIPS 같은 경우 Caller에서 레지스터 $s0~$s3, $t10~$t9, $v0~$v1  Caller-save 방식으로 저장하는 레지스터이다.

예를 들어 기존의 Caller의 매개변수가 저장된 레지스터인 $a0~$a3의 값은 Caller-save되고 Callee의 매개변수가 이 레지스터 총 4개에 저장된다만약 Callee의 매개변수가 4개가 초과하면 초과된 매개변수는 Caller-save Caller 스택에 저장한다.

그리고 $t0~$t9 tempoary 변수, $v0~$v1return 변수 역시 Caller save Caller 스택 프레임에 값들이 저장된다그리고 jal 명령어를 통해 Callee Control을 넘기게 된다.

 

Callee에서는 처음 스택 프레임을 할당하는데 기존에 Caller에서 사용했던 스택 프레임의 주소값이 들어있는 $sp 레지스터에 이제 Callee 스택 프레임을 할당해야하므로

$sp = $sp  frame size 만큼 주소 값을 빼줘야한다이때 스택 프레임의 크기를 빼는 이유는 스택은 push 될 때 growing down 방식이기 때문이다.

그 다음 $s0~$s7, $ra, $fp 레지스터가 Callee save되는 레지스터인데 이 레지스터에 값들은 원래 Caller에 의한 값들이었다하지만 Callee가 이 레지스터를 사용해야겠어!! 하면 Callee-save Callee 스택 프레임 영역에 store하고 Callee의 실행이 끝나면 다시 Caller live 했던 값들을 사용할 수 있도록 레지스터로 restore 해주는 것이다.

 

<마무리 정리>

Caller에서는 기존의 자신의 매개변수를 Caller-save하고 Callee의 매개변수를 $a에 저장한다그리고 Caller live한 변수들을 스택 영역으로 save 한다.

Callee에서는 Caller에서의 상수값이 저장된 $s 레지스터들이 만약 Callee가 사용할 것이라면 Callee에서 이 값들을 Callee 스택에 save한다. ( Caller-save였다면 Caller tempoary 변수 $t들은 Caller-save하면 되지만 상수값을 저장한 레지스터인 $s 들은 Callee에서 사용될지 안될지 모른다따라서 Hybird 방식에선 Callee가 이 상수 $s 레지스터를 사용한다면 Callee-save하기 때문에 Caller-save의 단점을 해결할 수 있게 되는 것이다. )

 

사실 Caller-save 만 사용하거나 Callee-save만 사용하는 방식은 실제로 사용되지 않으며 이는 많은 문제점이 있다. Caller-save Callee-save 방식은 Hybird 방식을 이해하는데 보저적인 개념으로 받아들이자.

실제 MIPS에서는 하이브리드 방식의 Calling Convention만을 생각하면 되는 것임.

또한 어떤 레지스터를 Caller-save로 사용하고 또 어떤 레지스터를 Callee-save로 사용해야 효율이 좋다 라는 것에 대한 대답은 아직 정해진 정답이 없다고 한다..
어떤 어플리케이션을 실행하냐에 따라 이것은 달라지기 때문이다.

'전공 > 컴퓨터 구조' 카테고리의 다른 글

5. CPI 계산과 CPU time 구하기  (0) 2021.12.27
6. Processor Design  (0) 2021.10.29
3. MIPS 문법 정리  (0) 2021.10.07
2. ISA란?  (0) 2021.10.07
1. 프로그램의 번역과 실행 과정  (0) 2021.10.07