본문 바로가기

23년 2학기 학교공부/컴파일러개론

[CP] Stack Machine Code, Tree Code

목차

    728x90
    반응형
    SMALL
    2023학년도 2학기 충남대학교 조은선 교수님의 컴파일러개론 수업 정리자료입니다.

     

     

     

     

    📁 가상 기계 코드 (Bytecode, MSIL)

    *

    컴파일러는 전반부와 후반부로 나뉨. 전반부가 끝나면 중간코드가 나오고, 중간코드가 후반부를 거쳐서 타겟 머신 코드로 바뀐다.

    전반부는 주로 하이레벨 언어에만 관련이 있고, 후반부는 주로 머신코드와 관계가 되어있다.

    그래서 IR을 잘 정의해야함.

    이렇게 잘 나눠놓고 후반부를 interpreter로 만드는 경우가 있다. 이렇게 interpreter로 만들면 그거 자체가 가짜 기계가 되고, 중간코드는 인터프리터 위에서 실행되는 구조.

    이런 구조에서 사용되는 머신이 스택머신인 경우가 많고, IR이 스택머신 위에서 돌아가는 스택머신인 경우가 많음.

     

    왜 후반부를 거쳐서 머신코드까지 만들지 않고, 전반부만 끝나면 가짜기계를 만들어서 돌리는걸까?

    대표적으로 이식성과 호환성을 목적으로함. 자바바이트코드는 인터넷 환경에서 잘 수행될수 있는 애플릿?이 특징이었음. 서버 컴퓨터 A, 클라이언트 컴퓨터 B가 있을 때, B가 어떤 운영체제인지를 몰라도 자바 버츄얼 머신만 깔면 애플릿을 B에게 편하게 보낼 수 있다는 개념이 중요해서, 이식성이 중요.

    이식성이란 곧 타겟 머신 호환성.

    abstract machine interpreter자체만 운영체제를 맞춰서 잘 깔아주면, 어떤 IR이라도 내 컴퓨터의 운영체제와 무관하게 돌아갈 수 있음.

     

    언어 호환성. 마이크로소프트에서 여러 언어를 지원해왔는데, 이런 언어들이 서로 라이브러리 등을 공유하기 어려웠음. 그래서 어떻게 하면 한번 만든 걸 다른 언어에서 쓸 수 있을까? 고민하다가 C#이라는 언어의 구조에 맞게 로우레벨한 형태를 통일하고, 이 형태는 중간코드화 되어서 통일됨. 이 통일된 중간코드가 가상머신 위에서 돌아가는 형태를 띄게 됨.

    *

     

    컴파일러의 전반부와 후반부를 나누는 잘 정의된 인터페이스를 정의함.

    전반부는 high level language에만 관계가 있고, 후반부는 machine code에만 관계가 있음.

     

    가상기계에서 동작하도록 한다. stack machine이 많고, 일반 IR로도 혼용되는 경우도 많다.

     

    이식성, 호환성이 목적이다. Java bytecode는 target machine 호환성, C# MSIL은 langauge 호환성.

     

     

     

     

    *

    자바 바이트 코드를 살펴보자.

    왼쪽 사진은 메모리 구조. 스택머신 코드이므로 스택 위치를 한번 살펴보면, 함수 호출하면 스택이 하나 생기고 거기에 호출된 함수에 해당하는 사용하는 메모리들이 스택으로 쌓이는, 그래서 맨 마지막에 호출된게 pop up으로 처음 리턴되는 그런 구조가 있는데 이게 call stack.

    call stack 속에 각각의 메모리는 현재 호출된 함수 또는 메소드의 로컬변수같은 필요한 정보를 가지고 있음. 스택머신코드의 하나의 네모가 이런 형태.

    스택머신코드 네모속에는 다시 오퍼랜스 스택이 있음. 이건 임시변수가 아니라 스택을 이용해서 계산을 주고받겠다는 용도. 3addr 코드에서 많이 발생하는 임시변수가 여기선 안발생하고, 명령길이가 더 짧음. t1에 넣어라 할때 t1같은걸 굳이 명시하지 않아도 push만 해도 스택에 들어가기 때문.

    자바를 컴파일해보면 javac test.java라고 커맨드라인에 쓰면 .class 파일이 나옴. 그 파일이 자바 바이트 코드 파일. 아래같은 스택과 로컬변수들을 운용하는 코드가 들어있고, 일반적으론 인코딩되어있어서 제대로 사람이 읽으려면 javap -c 해야 볼수있음.

    스택은 아래부터 위로 증가함. 로컬변수가 오른쪽 아래 자리하고있는데, 네모의 오른쪽 아래 네모는 글로벌변수나 컨스탄트들. 이 전체 네모가 하나의 메소드가 필요로하는 메모리.

    이게 call stack에서 쌓이는거임.

    각각 속에 oprand stack을 가지고 있는거임.

    *

     

     

    🌱 JVM Bytecode

     

     

     

    *

    javac를 해보자. 자바파일이 javac를 통해 .class 파일을 생성한다. 이걸 메모장으로 열면 이상한 글자들이 들어있음. 보통 ide에서 코딩을 하기 때문에 커맨드라인에서 자바를 컴파일해본적 없을거임.

    *

     

    *

    자바 버츄얼 머신 코드, 바이트 코드를 보자.

    아래는 가장 간단한 형태.

    검은글씨 3줄은 커멘트. javap해서 봣을 때 이게 어떤 메소드의 부분이다 라고 보여주는 부분

    진짜 코드는 4번째줄부터.

     

    자바가 stack machine이니까 push, pop을 하고, 별도의 local 변수 array가 있음

    aload_0란, 어떤 오브젝트를 스택에 푸쉬하라는 뜻. 근데 로컬변수 0번은 보통 this값. this값의 oid가 스택에 push되는게 0번줄 명령.

    1번줄 명령에서 invokespecial #3까지가 명령. <> 내용은 보기 좋게 여기다가 나열하는 곳이다 라고 나타내줌. 3번을 invokespecial해라. 이건 3번 함수를 불러달라는 뜻. constant pool이라고 따로 뽑아져있는 데이터영역이 글로벌 변수를 담당한다고 했는데, 글로벌 변수 뿐만아니라 글로벌하게 성립되는 모든 정보들이 들어있다고 보면됨. 즉 invokespecial #3란 java.lang.Object의 constructor가 고유의 번호 3번을 가지고있음. 이걸 invoke하라는 의미. 본인의 super클래스의 생성자를 불러주는것. 이 바이트 코드가 Employee라는 클래스의 생성자라서 처음에 자기 super클래스의 생성자를 불러주는 과정임. extend나 그런게 없는거 보니까 super클래스가 Object이니까 Object의 생성자를 부르는 코드인것.

    invokespecial은 자바 버츄얼 머신 코드에서 보통 생성자나 private을 호출할때 사용.

    constructor를 불러왔으면, operand stack이 popup 되어서 사라진 상황일것. 즉 super class 컨스트럭터를 부를 때 그 직전에 넣었던 this가 사라짐.

    다음 4번줄 명령에서 aload_0으로 this를 스택에 load.

    5번줄 명령에서 aload_1로 스택에 푸쉬. 이때 1번이 strName이라는 변수. 첫번째 인자. this 푸쉬 이후 strName이 푸쉬된 상태.

    이렇게 로컬변수에서 스택으로 가져오는 명령이 load. 반대로 결과를 로컬변수에 저장할때는 보통 store 사용

    this하고 strName을 이렇게 준비한 후, 6번 줄 코드의 putfield #5. 필드 5번 자리에 해당 값을 넣으라는 의미. 뒤에 <> 커멘트를 보니까 Employee의 name이란 필드가 있음. 이걸 5번이라고 지칭하는거임. 그래서 스택 탑에 있는 걸(strName) name이라는 필드에 넣도록 지정하는걸 의미. 여기까지가 name = strName; 명령을 실행한거.

    9-11번줄 코드도 위랑 비슷

     

    명령어 앞에 써 있는 저 숫자들을 보자. 0, 1 다음에 4가 오는게 이상하지않냐? 인자가 없는 명령어는 1byte크기, 어떤 명령은 인자 크기에 따라 명령 길이가 늘어난다. 예를들어 invokespecial의 인자 #3을 보면 constant pool에 개수가 많기 때문에 넉넉한 자리를 잡아줘야한다. 즉 3번이라고만 써있지만 실제로는 00000003 이런식. 즉 앞에있는 번호는 명령이 시작하는 바이트 주소 번호. invokespecial #3은 3바이트. 명령 하나에 1byte에 번호에 2byte 사용.

    그래서 바이트코드라고 불림.

     

    aload랑 iload의 차이점. aload는 로컬변수의 아이디가 스택에 푸쉬되는것.  ILOAD는 boolean, byte, char, short 또는 int 로컬 변수를 로드하는 데 사용됩니다. LLOADFLOAD 및 DLOAD는 각각 long, float 또는 double 값을 로드하는 데 사용됩니다 (LLOAD 및 DLOAD는 실제로 두 개의 슬롯 i 및 i + 1을 로드합니다). 마지막으로 ALOAD는 non primitive 값, 즉 객체 및 배열 참조를 로드하는 데 사용됩니다. 

     

    aload_0 명령어를 보면, 이 명령어에서 진짜 명령은 aload 뿐이다. 그런데 로컬변수 수가 많지 않으므로 aload_0부터 aload_3까지는 1byte로 인코딩됨. 만약 aload 0 이렇게 따로 쓰면 3byte를 쓰게됨.

     

    메소드를 invoke하거나 putfield하는경우 스택에 꼭 this를 먼저 깔고 진행함. 이렇게 깐 this는 invoke하거나 putfield할 때 다 popup됨. 그래서 만약에 코드 전체에서 다섯번정도 invoke하거나 putfield한다면 aload_0을 다섯번 해버리고 시작하는 경우도 있음.

    *

     

     

     

     

    🌱 CIL (Common Intermediate Langauge)

    *

    .NET에서 생성된 중간코드를 보자. 옛날엔 MSIL이라고 불렸다. 지금은 CIL이라고 부름. 처음에는 IR이라고 불렸는데 보통명사라서 MS를 붙이거나 CLI이라고 부름.

    여러 종류의 C#, VB.NET, J# 등을 컴파일러를 통해 머신 코드로 가는게 아니라 일단 CIL형태로 바뀌고 나서 CIL에 Common Language Runtime이라고 불리는 인터프리터가 있다. 여기서 동작하는 시스템.

    기본적으로는 C#에 맞춰서 정의되어있음.

    CLI CLR 등이 MS에서 많이 사용됨. 여기서 버츄얼머신만 똑 떼면 VES라고 부름.

    여기서 중간코드를 실행시킬수 도있는데, 기계어로 번역하는 비중이 더 큼.

    인터프리팅을 하면서 수행하도록 하지만 자주 사용되는건 기계어로 번역해두고 두번째 사용부턴 기계어로 바로 동작시킴. JIT 컴파일이라고 부름. 자바도 JIT 컴파일을 하는데, 자바는 일반적으로는 그냥 인터프리팅을 하다가 옵션을 걸어주면 JIT 컴파일을 하는거고, MS에서는 기본이 JIT 컴파일이고 옵션을 빼면 인터프리팅만 하도록 구성됨.

    *

     

    MS .NET 프레임워크에서 사용되고, 예전 이름은 MSIL이었다.

    여러 종류의 프로그래밍 언어가 공유하는 LIR.

    C#에 맞추어 정의되었다.

    Common Language Infrastructure (CLI) – CTS (Common Type System) – Metadata – CLS (Common Language Specification) – VES (Virtual Execution System)

    VES에서 CIL을 기계어로 번역한다.

     

     

    *

    CIL 코드를 보자.

    add eax, edx라는 어셈코드가 아래처럼 CIL코드로 번역될 수 있음. 똑같이 스택머신코드라서 ldloc.0, ldloc.1은 0번, 1번째 것을 가져와서 load하고, 더한다음, 이걸 stloc.0으로 0번째에 넣어주는 작업.

     

    아래 코드를 가지고 c언어로 변경해보자. main()을 생각해보자. .entrypoint부터. "Hello, world!"를 operand stack에 저장한다. 그럼 이제 연산자는 스택에 기반한 계산기처럼 stack top을 인자로 해서 작업하는데, 그 작업이 WriteLine, 즉 printf.

    .assembly라는 게 뭐냐면 자바에 버전 미스매치 문제가 있음. 옛날 컴파일했던 파일을 지금 런해보면 잘 안되는 경우가 있음. 런타임에 멈추는 경우가 있음. 이런 오류를 줄이기 위해서 연관되는 코드를 묶어놓고 특정 버전으로 맞춘 후 새로 collection 클래스가 업데이트됐다고 하더라도 이걸 받아오지 마라 등 건들지말라고 하는 걸 이렇게 표시함.

    .NET에서 보면 여러 클래스가 assembly라는 단위로 묶여있고, 그 안에 리소스도 들어있음. 이거랑 유사한 개념이 자바의 모듈 개념. 

    *

     

     

     

     

    📁 Tree code

    *

    트리구조로 된 코드는 기계어를 만들때 좋음. AST나 HIR에 비해서는 로우레벨. 메모리 로드 등의 명시적 표현이 다있음. 좀더 복잡. 어디까지 하나의 명령으로 보고 번역할것인지 트리를 보고 결정. 왼쪽 트리는 메모리의 어떤 값을 구하기 위해 메모리에서 값을 읽어오고 있고, 오른쪽은 메모리에서 메모리로 카피하는 연산. 코드 제너레이션 하는 과정이 복잡 트리같은게 있어서 이걸 가지고 어떻게 묶느냐에 따라 명령어를 만들 수 있게됨.

    *

    AST나 HIR과 비슷한 면도 있지만 보다 자세하다. 메모리 로드 등이 명시적으로 표현된다.

    기계어코드 selection을 위해 사용된다.

     

     

    🌱 GCC RTL(Register Transfer Language)

    *

    대표적인 걸로 GCC RTL이 있음. 로우레벨한 언어고, 트리기반의 언어. 트리를 만들 때 S-expression을 사용하여 만듬. 이게 뭐냐면 레지스터 138, 139 두개가 있을때 이걸 plus 더하고, 이걸 레지스터 140에 set.

    *

    로우레벨하고, 트리를 기반으로 하는 언어이다.

    Lisp S-expression을 사용한다.

    728x90
    반응형
    LIST