이전 글에서 가상 머신이 런타임에서 어떻게 동작하는지를 대강 봤었고

이제 구현하기에 앞서 잠깐 VM 프로그램에 대해서 짚고 넘어가야할 부분이 있다.

 

 

 

VM 프로그램 구현 시 유의사항

책에서 이미 나온 내용들은 대충 패스하고

아 JACK 언어로 만든 프로그램들을 컴파일하면 vm 파일이 나온다했는데

FileName.jack -> FileName.vm 같은 식을 된다.

 

 VM 프로그램에서의 함수명 : 그런데 FileName.jack 에 bar라고 하는 생성자든, 함수가 있다고 하자

VM 함수 명은 전역적이다 보니까 그러면 이런 함수는 여러 vm 파일들을 합쳐서 어샘블리어로 만들 때 동일한 함수명이 존재하는 경우 충돌나게 되므로 서로 다른 vm 파일에 있는 동일한 이름의 함수를 구분해주기 위해서

VM 프로그램의 함수는 FileName.functionName과 같은 식으로 되어야 한다.

foo.jack에 mult라는 함수가 있는 경우 컴파일 하면 foo.vm에는 foo.mult 같은 식으로 함수 이름이 만들어진다.

 

 

 프로그램 시작점은 Main.jack 파일의 main 함수가 된다 그러므로 VM 프로그램 상에서는 Main.vm에 위치한 Main.main이란 함수가 프로그램의 시작하는 위치가 되겠다. 그리고 이 함수에서는 이제 프로그램이 시작되면서 운영체제의 초기화를 위한 함수인 Sys.init 을 호출하면서 시작된다. 다시 말하면 Main.vm 에 있는 Main.main 함수에서 프로그램이 시작하며 가장 먼저 초기화 함수인 Sys.init을 실행한다.

 

 프로그램 실행 과정 :제공되는 VM emulator로 돌리려면 프로그램 폴더에다가 하나든 여러개든 .vm 파일을 넣고 에뮬레이터로 로드하면된다.

 

 

 

VM 명령어와 VM 번역기로 번역한 어셈블리어 (슈도) 코드 

 VM 명령어로 함수를 호출하고 반환하는걸 어떻게 어셈블리어로 쓰나 싶었는데 이렇게 슈도 코드로 알려준다.

 

 

HACK 컴퓨터 가상 머신 표준

1. 스택

 앞에서 여러번 말한거지만 RAM 0 ~ 15번지까지는 가상 레지스터/포인터들이 차지하고 있고, 16 ~ 255번지까지는 정적 변수, 그리고 256번지 부터는 스택 영역으로 차지하고 있다. 그래서 일단 VM 번역기는 0 ~ 256까지 초기화 하는 내용으로 시작해야 하고, 값이 스택에 값이 추가되거나 뺄때 SP가 변경되도록 해야한다.

 

2. 특별한 심볼들

  어셈블리어와 마찬가지로 가상 머신이 다뤄야하는 특별한 심볼 두 가지가 있는데, 하나는 어셈블리어에서 봤던 선정의된 심볼들. 두번째는 return address와 함수 시작점을 표기하기 위한 심볼릭 라벨이다.

 

 내가 앞에서 생략한건데 소프트웨어 파트 시작할때 PointDemo.jack이라는 프로그램을 잠깐 소개했었다. 이 프로그램은 Main.jack과 Point.jack이라는 파일 두개가 PointDemo라는 폴더에 들어있었는데, 컴파일러를 돌리면 파일이 두개니까 두 vm 파일이 나온다. 컴파일 하면 서로 다른 파일의 함수를 구분 시켜주기 위해 기존의 jack 함수 명앞에 파일 명을 붙인다고 했었으므로, Main.vm에는 Main.main이, Point.vm에는 Point.new, Point.get 등으로 바뀐다.

 

 이 폴더를 VM 번역기로 돌리면 같은 폴더에 있는 vm 프로그램들을 읽어서 하나의 어셈블리어 파일인 PointDemo.asm을 만들어내며 이 asm 까지오면 더이상 추상화 된것 없이 심볼릭으로 전체가 구현된다. 

 

 

 

가상 머신 번역기의 함수 호출과 반환 어셈블리어 생성

 그러면 어셈블리어에서 함수를 호출하면서 점프하려면 함수 시작점을 어떻게 표시하고, 함수의 결과를 반환하는건 어떻게 표현할까? 아까 위에서 함수 호출, 결과 반환시에 대한 VM 명령어와 VM 번역기가 생성한 어셈블리 코드 표를 붙여넣기는 했었지만 이 PointDemo.asm에서는 아래와 같은 식으로 어셈블리어가 만들어진다.

 

 VM 코드의 Main.main에서 call Point.new 를 호출한게 어셈블리어 상에서 점프문으로 'goto 함수명', 점프할 위치는 심볼릭 라벨인 (함수명)의 형태하여 goto Point.new와 (Point.new)로 함수의 호출과 함수의 시작점을 표현하였고, (Point.new)아래 부분에 이 함수에서 어셈블리어로 구현된 연산 명령어들이 위치한다.

 

 이번에 리턴의 경우 vmcode function Point.new 의 맨 끝에 return이 위치하고 있다. 이 리턴문에는 반환 값은 없어 보이지만 goto Main.main$ret0라고, (Main.main$ret0)의 위치로 점프하라고 적혀있으며 이 심볼릭 라벨은 goto Point.new 바로 다음 줄에 위치하므로 함수를 실행 한 후에 마치면 메인 함수의 함수 호출문 바로 다음줄로 돌아와서 나머지 메인 함수의 명령어를 실행하게 되었다!

 

 

 

가상머신 번역기가 생성하는 특별한 심볼들

 앞에서 나온 .vm 파일의 명령어가 .asm 파일에서 어떻게 가상머신 번역기를 통해 생성되는지는 위 표를 참고하면 될거같다.

 

- SP, LCL, ARG, THIS, THAT이야 스택 포인터, 로컬 변수 메모리 세그먼트 베이스 주소, 매개변수 메모리 세그먼스 베이스 주소, 포인터 2개인걸 앞에서 여러번 봤었다.

- Xxx.i : Xxx.vm 이란 vm 파일에 존재하는 i번째 정적변수는 어셈블리어 상에서 Xxx.i로 생성된다.

- functionName$label : 이건 내가 놓쳣는지 앞에서 본적이 없었는데, 달라 앞부분만 보면 Xxx.vm 파일에 foo 라는 함수가 있는 경우 어셈블리어 상에서 Xxx.foo라는 심볼로 나오는건 알지만 이 foo 함수 안에 여기선 라벨이라고 하는데 변수라고 이해하면 될지 좀 햇갈리지만 ... 아 조건문일때 내용이구나 조건문을 만족하면 함수 내의 목적지를 Xxx.foo$bar라는 식으로 표기하는거 같다.

- functionName : 이거야 앞에서 봤지만 Xxx.vm 파일의 Xxx.foo 함수는 어셈블리어 상에서 함수의 시작점으로 (Xxx.foo)형태의 심볼릭 라벨로 표기된다.

- functionName$ret.i : 이것도 앞의 예시에서 본거지만 functionName 이라는 함수에서 i 번째 리턴후 시작하는 지점을 나타내며 (functionName$ret.i) 의 심볼릭 라벨로 어셈블리어로 생성된다. 아까의 경우 Main.main 함수의 첫번째 함수 호출 바로 다음줄에 위치하여, 첫번째 함수를 다 실행하면 이 심볼릭 라벨로 점프하여 다시 메인 함수 내용을 실행하도록 되어있었다.

- R13-R15 : 이거는 필요할때 쓰라고 남겨둔거라고 한다.

 

 

 

 와!! 아직 가상 머신 파트 조금 남기는 했지만 거의 다와갔다. ppt 자료는 장난아니게 많아서 걱정좀 했었는데 보다 보니까 전역 스택에서 어떻게 상태를 저장하고 왔다갔다 하는지를 한페이지씩 보여주다 보니까 이런식으로 페이지 양이 장난아니게 많았더라. 조금만 더하면 실제 VM 구현을 제외한 내용이 끝난다.

 

 잠깐 쉬었더니 마무리하기 진짜 귀찬네 ㅋㅋ

일단 다시 돌아와서 VM 번역기가 하는 일을 이제 제대로 정리하자

 VM 번역기는 hack 플랫폼의 가상 머신으로 컴파일러가 번역하여 생성한 VM 코드를 hack에서 실행가능한 어셈블리어로 번역해주는 역활을 한다. 이번 장에서 배운 VM 명령어는 산술논리연산 명령어, 메모리 제어 명령어, 분기 명령, 함수 명령어로 이뤄져있었으며, 이게 우측 하단의 타겟 언어의 내용처럼 되어있는 hack 어셈블리어로 번역된다.

 

 

 조금 더 자세히 보자

 

VM 번역기를 이용한 어셈블리어 생성

1. 호출 제어 어셈블리어 생성

 맨 좌측의 VM 코드를 보면 먼저 call Bar.mult 2라는 명령어가 있는데, 이 명령어는 위 상수 19와 지역변수 3을 매개변수을 가지고 Bar.mult 라는 함수를 호출하게 된다. 우측의 어셈블리어 코드를 보면 메인 함수 프레임을 저장하는거나 다른 해야되는 연산 내용은 없지만 goto Bar.mult로 호출한 함수로 이동하는거랑 콜리의 연산을 마치고 돌아올 수 있도록 Foo 함수 내부의 함수 호출 후 1번째 반윈 위치를 나타내는 (Foo$ret.1)이 있다.

 

 이 과정은 우측의 어셈블리 코드와 전역 스택을 보면 더 자세히 나와있는데, 함수를 호출하는 vm 코드는 슈도 어셈블리어로 맨 먼저 리턴주소와 다른 정보들을 스택에다가 넣어두고 foo,main 함수 프레임 저장을 완료하면, 호출된 함수의 매개변수 세그먼트 베이스 어드레스를 ARG = SP-5-nArgs로 지정해준다. 그 다음에는 현재 스택 포인터를 로컬 메모리 세그먼트 베이스 주소로 잡아주고

 

 이 그림과 같이 Bar.mult 함수 어셈블리어 시작점으로 넘어와 이 함수에 관한 명령어들이 실행된다.

 

 

 

 

2. 함수 제어 어셈블리어 생성

 함수 제어 어셈블리어의 경우에는 함수 내용에 따라서 만들어야 하는 어셈블리어는 다르지만 일단 공통적으로는 이 함수의 지역변수 만큼 공간을 잡고 0으로 초기화 시켜준 다음, 필요한 연산을 수행하게 되고 리턴문 그러니까 goto Foo$ret.1까지 진행하게 된다. 연산 결과를 어떻게 foo.main 함수로 넘어갈지는 다음의 반환 제어 어셈블리어 파트를 보자

 

3. 반환 제어 어셈블리어 생성

 이제 진짜 VM 번역기 동작 끝에 거의 다왔다. 이제 반환 연산을 어셈블리어로 나타낼 차례인데 좌측의 코드를 보면 LCL-5로 리턴 어드래스를 구하는데, 이 리턴 어드레스가 (Foo$ret.1) 심볼릭 라벨의 명령어 메모리상의 주소를 의미하는거 같다.

 

 아무튼 이렇게 돌아가야할 명령어 주소를 구해낸뒤, *ARG = pop()을 통해 글로벌 스택 최상단에 위치한 연산 결과를 매개변수 포인터 위치 즉, 콜러의 워킹 스택 최상단에다가 넣음으로서 콜리의 연산 결과를 콜러의 스택에다가 옮겼다! 그리고 연산 결과를 담았으니 스택 포인터도 그 위로 옮겨주고, 스택 포인터 뒤는 메인 함수 프레임을 가져와 저장하고 나서는 이재 재활용해서 쓴다.

 

 이제 콜러의 프레임들얼 다시 되찾아올수 있도록 endFrame(=LCL) - 1, 2, 3, 4를 하여 나머지 포인터 값들도 다시 읽어들인 뒤에 goto retAddr 으로 (Foo$ret.1)일로 점프해서 함수 호출 후 남은 main 함수의 명령어들을 수행하는게 VM 번역기가 하는 호출, 함수 처리, 반환하면서 어셈블리어 생성하는 과정이 되겠다.

 

 

프로그램 컴파일과 부팅 과정

 

 프로그램 컴파일 및 번역 과정을 정리하자면 한 폴더 myProg에 있는 모든 jack 파일들을 컴파일러가 vm 파일로 바꾸고, vm 파일의 함수 명은 앞에 파일명.함수로 바뀌어서 만들어 진다. 그리고, VM 번역기에 의해 vm code 함수명은 심볼릭 라벨로 생성되어 함수의 시작할 주소를 의미한다.

 

 

 VM 프로그램이 부팅하기 위해서는 VM 프로그램 중에 하나는 프로그램 시작점을 나타낼 수 있도록 Main.vm과 이 파일 안에 Main.main이라는 함수가 존재해야한다.

 

 그리고 hack 표준 맵핑에 나왔듯이 스택은 256번지부터 시작했었는데, 여기서 Sys.init 그러니까 Sys.vm의 Sys.init 함수를 호출하면 초기화 동작과 Main.main 함수를 호출에 무한 루프를 돌며 동작하게 된다!

 

 

 

 

8장 정리

 이번 장에서는 추상적인 분기화 함수 명령어를 hack 플랫폼의 어셈블리어로 구현하는 과정을 거치면서 VM 번역기가 어떻게 어셈블리어를 생성하는지 이해할 수 있었다. 이 내용은 2단계 컴파일 모델의 백앤드 파트로 이제 다음장에서 JACK을 배운 후 10, 11장에서 프론트 엔드 파트인 컴파일러에 대해서 정리하자!

 

 

 wa 드디여 8장을 마무리했다. 코드 구현하지 못한건 아쉽긴한데 그래도 이걸 보면서 가상 머신이 어떻게 된거고 중간 언어를 가지고 어셈블리어를 생성하고, 상세한 함수 호출, 동작, 반환 루틴을 알게 되니까 참 멀리왔다 ㅠㅜ

이번주 금요일까지 코드 구현까지는 아니더라도 OS 내용까지 마무리하면 진짜 뿌듯할거같다 

 

 

 

 

 

 

+ Recent posts