와 오늘 저녁까지 수업하고, 밥먹고 다시 정리하려고 하니까 졸려 죽겠다.

낮에 수업 중간중간에 8장 PPT 볼때는 그래도 괜찬았는데

밥먹은 직후라 그런지 원래 매일 이쯤에는 이랬으니까.

 

 

 

아무튼 오늘 안에는 8장 가상 머신을 이용한 제어 파트를 마무리 하는게 목표인데,

어제도 봤었지만 이번 장 분량이 장난아니기도 하고, 어제 짚고 넘어갔어야 했는데 빨리 진행한다고 안보고 넘어간게 있어서 오늘 안에 마무리 할수 있을지는 모르겠다.

 

 

오늘 오전에 마이컴 인터럽트 다루고, 오후에는 밀린 과제 숙제하느라 중간에 여유있을때 PPT 보면서 오늘 어떤 내용을 다뤄야할지 좀 보기는 했는데 하다보면 졸음이 가든가하겠지 생각하고 정신차려야지.

 

 

함수

 모든 고급 프로그래밍 언어에서 일정한 연산 과정을 하나로 모은 것을 함수라고 한다. CS 공부하다보면 마주치는 서브루틴, 프로시저, 메서드 같은것들도 결국에는 함수같은거라 할수 있다.

 

 

 

 

잠깐 함수 파트를 하기전에 지난 시간에 정리를 제대로 안한

HACK 플랫폼에서의 표준 가상머신 맵핑을 잠깐 짚고 넘어어가야 할거같다.

 

이걸 뭐라고 해야할까. 그냥 메모리 맵처럼 변수가 RAM의 어디에 위치하게 되는지를 나타낸다고 하면 될거같은데 좀 더 잘표현할수 있을거같긴한데 아직은 잘모르겠다.

 

 local, argument, this, that이야 이전 글에서 봤다시피 각 가상 메모리 세그먼트의 베이스 주소값을 보관하고 있다. 위에는 없는데 SP야 스텍 베이스 어드레스에서 시작해서 push, pop에 됨에 따라 바뀌는 주소를 보관하는 곳이고, constant는 어짜피 연산을 할때 담을 곳이 줘서 그런가 별 말이없다. 

 

 static 변수 경우의 설명이 좀 특이한데 Foo.vm에 있는 static i에 접근하기 위해서는 파일 이름을 붙여서 Foo.i라는 변수를 사용해야한다고 한다. 정적 변수이다보니 초기화는 되는데, 동일한 변수명으로 덮어씌워지는걸 방지하기 위해서 저런가 싶다. temp는 5-12 번지를 차지하고 있다 하고, 아 THIS와 THAT이 뭐하는건가 싶었는데 그냥 포인터 역활을 하는거였구나 이제 이해했다.

 결국에는 HACK 플랫폼에서 돌아가는 가상머신, 가상머신 번역기를 만들어내려면 이런 선정의된 심볼들을 쓸수 있도록 고려해서 구현해야한다. 이 내용도 어셈블러 파트에서 봤는데 정적 세그먼트 부분이랑 스택이 추가된게 다르네.

 

 다시 돌아와서

 

가상 머신 언어에서의 함수

 가상 머신에서의 연산이라 하면 기본 연산인 add, sub 같은 것들과 추상화된 연산(함수) multiply, sqrt같은 것들이 있다. 이 기본 연산과 함수는 둘다 스택의 맨 위에 있는 값들을 매개변수로 쓰고 연산 결과를 스택의 위에다가 놓는건 똑같다. 

 

 

 한번 세 함수로 이뤄진 VM 언어가 런타임에서 어떻게 돌아가는지 보자 우측의 스택은 전역 스택이 아닌 각 함수만의 공간을 나타내고 있다. main 함수의 call hypot을 통해 hypot 함수로 매개변수인 3, 4를 가지고 갔다. 

 

 hypot의 첫번째 곱샘 연산인 call mult의 경우 x를 스택에 2번 넣고 곱샘 연산을 수행하게 되는데 곱샘 연산한 뒤에는 3 두개는 더이상 없고 결과인 9만 남는다. 그 다음에 yy를 스택에다가 넣고 곱 연산을 하게 되어 결국에는 9, 16이 스택에 들어와있다. 

 

 그러면 mult의 private 스택에서는 어떨까. mult(3, 3)을 호출했을때 19번 줄 시점에서는 막 시작하면서 스택은 비어있고, 로컬 메모리 세그먼트에 sum, i 변수가 0으로 초기화 되어있다. 이 함수가 연산 수행 되고 나면 끝날때 쯤인 36번 명령 실행 후를 보면 mult 함수의 private 스택에는 곱샘 연산 결과인 9가 나오고 이 값이 hypot으로 반환된다.호출한다고 하자. 그러면  그 결과 hypot의  8번 라인 실행 후 private stack에는 9들어오게 된다.

 

 콜링 체인 : 위의 예시 처럼 한 실행 시점에서 main -> hypot -> mult 같은 함수들의 관계가 나타나는대 이를 콜링체인이라고 부른다. 위에서 main, hypot, mult 이외에도 sqrt가 있었지만 현재는 실행되지 않았듯이, 컴퓨터 프로그램은 수 많은 함수로 이뤄지고 있지만 실행 시점에는 콜링 체인으로 이뤄진 일부 함수들만 일을하고 있는 상황이 된다. 근데 실제로는 콜릭 체인의 함수들은 자기가 호출한 함수가 동작이 종료되고 값을 반환할 때까지 기다리기 때문에 실제로는 콜링 체인의 맨 끝에 있는 함수가 활동하며 이 함수를 현재 함수 current function(현재 실행중인 함수)이라 한다.

 

 

 위 main -> hypot -> mult 하는 예시에서 나왔드시 각 함수는 함수마다 고유의 지역 변수와 매개 변수를 가지고 있는데, 이 변수들은 함수가 호출되고 반활할때까지만 유지된다. 그러면 콜링 체인이 깊어지고, 재귀적이면 엄청 복잡할탠대 어떻게 메모리 관리를 해줄수 있을까? 

 

호출과 반환 로직

 이를 구현한 방법이 호출과 반환 로직인데 call and return logic 스택에서 이게 잘 동작한다. 이 호출과 반환 로직을 사용하기 위해서 포인터인 LCL, ARG, THIS, THAT을 사용한다. 책이랑은 조금 다르지만 위 예시의 hypot 함수에서 yy를 매개변수로 mult 함수를 호출한다고 생각해보자.

 

 mult 함수로 넘어가기전에 hypot 함수를 중지하고 넘어간다고 했는데 mult 함수의 동작이야 hypot의 작업 스택에 간섭없이 mult 함수만의 작업 스택을 이용한다. 위 그림에서 나와있듯이 hypot의 작업 스택과 mult의 작업스택은 완전히 분리되어있다. 그러니 hypot함수의 내용들을 덮어씌울 일이 없으니 걱정할 필요가 없고 중지한 함수의 내용을 잘 보관만 하고 있으면 된다.

 

 

 

 전역 스택에서의 호출 제어 handling call 과정

 

 그러면 호출과 반환이 어떻게 스택에서 동작하는지 조금 더 자세히 보자. 전역 스택에서 프로그램이 돌아가다가 값들이 쌓아 놓고 아래의 값 몇개를 매개변수로 지정해서 foo 라는 함수를 호출했다. 그러면 어디서 부터가 매개변수인지 콜리에서 알수 있도록 표기해두어야 한다.

 

 

  어디서부터가 매개변수인지는 포인터인 ARG에다가 매개변수 시작점의 주소를 담아두면 된다. 즉, 현재 호출하는 함수 foo의 매개변수 메모리 세그먼트를 만들었다!  그런데 이 다음에는 콜리에서 콜리만의 지역 변수, 콜리안에서 새로운 매개변수, this, that 같은 포인터가 사용되면서 콜러의 것을 덮어씌우면 안되니 콜러만의 정보를 저장해 두어야 한다. 콜러의 정보인 프레임을 스택에다가 추가하여 저장하면 아래와 같이 된다.

 

 매개변수 뒤에다가 돌아와야 할곳의 (스택이자 메모리상의)주소인 return address, 다음에는 현재 함수의 LCL 어드레스, 현재 지정한 ARG 어드레스, 그외 THIS와 THAT 포인터까지 5개의 값(프레임)을 저장해두면 호출 제어는 끝나고, 이제 함수를 제어할 차례이다.

 

 전역 스택에서의 함수 제어 handling function 과정

 먼저 콜리의 로컬 세그먼트부터 설정해주고,  호출된 함수 동작을 하며 워킹 스택을쌓아나간다. 그러다가 반환해야할 값을 만들었다. 이제 반환 제어를 하면 된다. 반환이 되면 콜리의 내용은 필요 없으므로 나중에 재사용한다. 반환하려면 결과 값을 어디에다가 넣으면 될까?

 

 

 전역 스택에서의 반환 제어 handling return 과정

1. 콜리의 리턴 값은 콜리의 매개변수 세그먼트 베이스 주소, 그러니까 콜러에서 첫번째 매개변수 자리에다가 넣으면 된다. 콜리를 호출할때 쓴 매개변수랑 콜리 내용들은 이제 필요없다.

 

2. 콜리의 결과를 기존 콜러의 워킹 스택 맨위에다가 넣었으니 콜리의 맨 위에있던 스택 포인터도 결과값 다음으로 위치를 옮겨주자.

 

 이렇게 하면 콜리에서 연산한 결과를 콜러로 잘 가져왔다! 그러면 이게 끝인거 같지만 콜러의 정보를 저장해둔 프레임이 남아있다.

 

3. SP를 원래 자리로 옮긴 뒤에 기존에 콜러의 프레임에 있던 LCL, ARG, THIS, THAT을 복원 시키면서 그림 상에 있었던 콜리의 LCL과 ARG는 사라졌다. 

 

4. 그 다음에는 return address로 점프하면 되는데, 이 부분이 정확하게 이해가 잘 되지는 않는다. 스택 포인터로 원 자리에 되돌렸으니 끝난게 아닌가 싶기는한데, 콜리의 어셈블리 명령을 마치면서 워킹 스택 상 데이터 정리는 끝났으니 콜리 호출했던 부분 다음의 어셈블리어 명렁어로 점프해서 나머지 명령어가 실행되도록 하라는 뜻인가 싶다.

 

 

 

 콜리 함수 연산을 마치고 값도 반환받고 스택 포인터와 명령어 위치도 되돌리고 나면 콜러가 다시 실행이 되는데, 기존에 매개변수 세그먼트나 프레임, 콜리 내용들은 이후 콜러 연산 과정에서 덮어씌우면서 재사용 된다. 그런 의미에서 우측 상단에서 블록이라 적어놓은거같다. 아무튼 call foo 매개변수 한 결과는 아래처럼 정리된다.

 

 

 

고급 언어 -> VM 코드 -> 런타임 상 동작과 전역 스택의 변화

 

 위 그림은 고급 언어로 구현한 팩토리얼 코드를 VM 코드로 컴파일 한 후에, 어떻게 전역 스택 상에서 동작되는지를 보여준다.

 

 동작 과정을 쭉 적었는데 이렇게 정리할수 있을거같다.

 

1. 메인 함수에서 factorial(3)을 하면서 3을 매개변수 세그먼트로 잡고 호출했다.

2. f(3)을 연산하기 전에 메인 함수의 정보들인 메인 프레임을 저장해두고, 연산하며 3 -1 한 2를 매개변수 세그먼트에 잡고 f(2)를 호출했다.

3. f(2)을 호출하면서 f(3) 프레임을 저장하고, 전역 스택이 깊어지며 매개 변수 1 세그먼트 잡은 채 f(1)을 또 호출했다.

4. f(1)에서 f(2) 프레임을 저장했지만 바로 1을 반환한다.

5. f(2) 함수에서 2와 f(1)에서 받은 1을 곱한 결과를 구하고, f(2) 프레임을 복원한뒤 값을 반환한다.

6. f(3) 함수에서 3과 f(2)로부터 전달 받은 2를 곱하여 결과를 구하여 메인 함수로 반환하고, f(3) 프레임을 복원한다.

7. 메인 함수는 f(3) 함수로부터 값을 반환받고, 메인 프레임을 복원하며 프로그램이 종료된다.

 

 

 

 

이제 이동해야하는 시간이라 여기까지 하고

지금까지 가상 머신에서 추상화된 함수 제어의 전반에 대해서 보면서

이번 글에서는 대강 이 5가지 정도 정리된거 같다.

 

1. HACK의 표준 매모리 맵핑 형태

2. 호출 제어

3. 함수 제어

4. 반환 제어

5. 런타임 때 전역 스택의 동작 과정

 

이번 장 남은 내용은 HACK 플랫폼 가상 머신 구현이다.

이걸 마무리하면 지난번에 못한 어셈블리어부터 VM 구현까지 직접 해봐야할거같긴한데 

안할수도 있고, 그냥 결과물 참고해서 따라 코딩할수도 있고

예전 처럼 계속 삽질하기에는 시간 아까워서 어떻게 할지는 좀 고민해야겠다.

+ Recent posts