<밑바닥부터 만드는 컴퓨팅 시스템> 후기

<밑바닥부터 만드는 컴퓨팅 시스템> 후기
Photo by Infralist.com / Unsplash

<밑바닥부터 만드는 컴퓨팅 시스템> (aka Nand2tetris)는 nand 게이트부터 시작하여 하나의 컴퓨팅 시스템을 만들어보는 책이다.

무언가를 이해하기 위한 가장 좋은 방법은 밑바닥까지 뜯어보는 것임을 잘 알고는 있지만, 컴퓨팅 시스템은 워낙 방대하다보니 갈피조차 잡기 쉽지 않았는데 이 책은 훌륭한 길잡이가 되어준다.

특히 나와 같이 대학에서 컴퓨터 공학을 전공하지 않아 운영체제 / 필수 CS 과목을 수강해보지 못한 사람은 필히 정독해야하지 않을까.

CH 1~3 - 하드웨어

nand 게이트를 활용하여 다른 논리 게이트를 구현하며, 가산기 및 ALU를 만든다.

챕터 1부터 3까지만 완료한 시점에서도 큰 전율을 느낄 수 있다. 단순한 논리 게이트를 조합해 CPU까지 만들며 왜 논리 게이트를 배우는지 조금이나마 이해할 수 있었다.

아쉬운 부분은 상태회로 구현시에 사용되는 플립플롭에 대한 자세한 내용은 서술되어 있지 않다.

CPU 회로도

CH 4~6 - 기계어와 어셈블러

어셈블러를 구현하는 과정은 VM, 컴파일러 구현 파트에 비해 직관적인 편이다.

명세를 따라서 구현하다 보면 어셈블러는 쉽게 구현된다.

챕터를 완료하면 CPU 아키텍쳐별로 왜 어셈블리어 문법이 달라지는지, 명령어 셋 디자인이 왜 중요한지에 대해 몸소 깨닫게 된다. 이 챕터를 진행하고 나서 현재 상용화된 ISA에 대해 찾아보았다. 특히 그 중에서도 오픈소스인 RISC-V에 대해 많은 관심이 생겨 RISC-V가 만들고 있는 생태계에 관심이 생겼다.

RISC-V instruction set. Load / Store과 관련된 instruction만 해도 굉장히 많다.

역시 실제로 사용되고 있는 Instruction Set은 굉장히 방대했는데, 어떤 과정으로 현재 사용되고 있는 ISA가 설계되었는지, ISA 설계가 전력 소모량이나 실제 성능에 어떤 영향을 미치는지 궁금증이 생겼다. 이는 추후에 따로 정리해보려 한다.

CH7~8 - VM

스택 기반의 VM을 구현한다.

Java, .NET을 다루는 자료들에서 자주 언급하는 “스택 기반 VM”의 의미를 몸소 깨닫게 된다.

VM을 직접 만들다보면 그동안 Java를 사용하면서 생각해보지 못했던, Java가 컴파일 된 후 어떤 과정을 거쳐 실행되는지에 대해 많은 고민을 하게 된다.

이 챕터를 진행하면서 스택 기반이 아닌 VM이 있는가에 대해 궁금증도 생겼다. 흥미롭게도 레지스터 기반의 VM 또한 존재하며, 과거 안드로이드 개발시 접하게되었던 Dalvik VM이 그 예시라 할 수 있다.

레지스터 기반 VM

레지스터 기반의 VM은 스택을 사용하지 않고 피연산자를 레지스터에 저장해서 사용하게 되며, 이로 인해 스택 기반의 VM에서 불가능한 최적화가 가능해진다.

이 챕터를 진행하면서 기존에는 이해하지 못했던 영역도 살펴보는 계기가 되었는데, 고급언어로 주로 개발하는 사람들에게는 꽤나 흥미로운 생각을 던져주는 챕터인듯 하다.

CH10~11 - 컴파일러

단언컨대 가장 많은 시간을 쏟은 챕터이다. 기존에는 컴파일러 동작 원리에 대해 큰 관심을 가지지 않았고, 책에서도 대략적인 방향을 제시할 뿐 세세한 구현방법에 대해서 서술하지는 않아 많은 시행착오가 있었다.

컴파일러 이론에 대해 다루는 자료나 컴파일러를 직접 만들어보는 다른 자료가 많기는 하지만 다른 자료를 참고하지 않고 직접 만들어보고자 하였다.

또한, 컴파일러에 빠질 수 없는 AST와 관련해서는 AST에 대해 대략적으로는 알고 있었지만 AST의 구현체를 직접 만들어보거나 뜯어본적은 없었기에 프로젝트를 진행하며 내가 간단히 트리 구조를 만들어 진행하였다. 다만 내가 직접 만든 구조로는 토크나이징 과정에서 활용하기에는 큰 문제가 없었지만, 실질적인 컴파일 과정을 구현하는 과정에서는 여러 애로사항이 있었다. 힘든 과정이었지만 이를 통해서 왜 AST를 컴파일러에서 사용하는지 깨닫게 되었다.

조금 아쉬운 점도 있었지만, 나름대로 만족한 부분도 있다. 책에서 제시하는 문법외에도 타입을 추가한다거나, 새로운 문법을 정의한다거나 등 언어 규칙을 확장할 수 있도록 규칙을 구성하는 요소들을 클래스로 정의하고 토크나이징 과정에서 정의한 객체를 바탕으로 파싱할 수 있도록 디자인하였다.

TYPE_RULE_ELEMENTS = RefRuleElement( 
     desc="type", 
     ref=OrRuleElement( 
         desc="'int' | 'char' | 'boolean' | className", 
         or_elements=[ 
             RuleElement(RuleElementType.FIXED_TERMINAL, "int"), 
             RuleElement(RuleElementType.FIXED_TERMINAL, "char"), 
             RuleElement(RuleElementType.FIXED_TERMINAL, "boolean"), 
             RuleElement(RuleElementType.VAR_TERMINAL, "className"), 
         ], 
     ), 
 ) 
  
 KEYWORD_CONSTANT_RULE_ELEMENTS = RefRuleElement( 
     desc="keywordConstant", 
     ref=OrRuleElement( 
         desc="'true' | 'false' | 'null' | 'this'", 
         or_elements=[ 
             RuleElement(RuleElementType.FIXED_TERMINAL, "true"), 
             RuleElement(RuleElementType.FIXED_TERMINAL, "false"), 
             RuleElement(RuleElementType.FIXED_TERMINAL, "null"), 
             RuleElement(RuleElementType.FIXED_TERMINAL, "this"), 
         ], 
     ), 
 ) 

rule을 정의하는 예시

다만 클래스를 통해 정의하다보니 복잡해지는데 추후 ANTLR 처럼 DSL를 통해 rule을 정의할 수 있도록 개선해보고자 한다.

CH12 OS

사실 운영체제라기보다는 기본적인 builtin 라이브러리를 만들어보는 느낌이다. OS를 만들어보는것은 아니지만 응용쪽 개발만 하다보면 놓치는 포인트를 간략하게나마 접할 수 있다. 특히 단순하게 생각한 덧셈, 곱셈 등의 연산을 어떻게 하면 최소한의 연산력으로 구현할 수 있을지 고민하며 구현해야하는 것이 인상깊은 포인트다.

아주 간단한 그래픽 라이브러리를 만들고 책에서 제시하는 테스트 코드를 돌려보게 되면 속도가 느린것이 체감이 되는데, 이 부분을 개선해보고자 나름의 시도를 하기도 했다.

더 나아가서…

책에서 제시하는 프로젝트를 모두 완료한 이후에는 직접 기능을 추가하고 개선해보고 싶은 욕구가 생긴다.

내가 관심을 가지는 대표적인 부분은 아래와 같다.

  • 내가 직접 구현한 컴파일러는 최적화가 고려되어 있지 않은데 gcc와 같은 상용 컴파일러에서는 어떤 방식으로 최적화를 진행할까?
  • 컴파일러로 hack언어를 컴파일하면 순차적으로 실행되는 vm 언어가 생성되고 최종적으로 기계어는 순차적으로 실행되는데 멀티쓰레드 기능을 추가하려면 어떻게 해야할까? 실제 OS에서는 어떻게 이를 가능케 하는가?

이런 고민을 할 수 있도록 만들어준 이 책을 왜 더 일찍 접하지 못했을까 아쉽다.

비록 대학에서 심도있게 4년간 공부하는 전공자들의 깊이를 따라가지는 못하지만 나의 시야가 더 넓어짐은 틀림없다.

이 책을 권한 해군 SW 개발병 모 수병에게 감사의 말을 전한다.