어셈블러

어셈블러

기호 변환(Symbol Resolution)

ASM

기호가 있는 코드를 기호가 없는 코드로 변환하는 방법에는 기호 테이블(Symbol Table)이 있다. 우선, 번역된 코드는 주소 0부터 시작하는 메모리에 저장하고, 변수들은 주소 1024부터 할당하도록 한다(이 규칙은 하드웨어 플랫폼에 따라서 다르다). 다음으로, 소스코드에 새로운 기호 xxx가 나타날 때마다 (xxx, n) 라인을 테이블에 추가하는 식으로 Symbol Table을 구성한다(n은 기호와 연결된 메모리의 주소이다). 테이블이 다 만들어지면 이 테이블을 이용해서 기호가 없는 코드로 옮긴다.

어셈블러(Assembler)

어셈블리 프로그램은 2진 기계어로 번역해야만 컴퓨터에서 실행할 수 있다. 번역 작업은 어셈블러라는 프로그램이 담당한다. 어셈블러는 어셈블리 명령 열을 입력 받아, 동일한 의미의 2진 명령어 열로 출력한다. 어셈블러는 다음과 같은 작업을 한다.

Parser

Parser는 어셈블리 명령을 읽어 들여 구문 분석하고, 명령 세부요소(필드와 기호)에 편리하게 접근할 수 있게 한다. 추가로 모든 공백과 주석을 제거한다.

from enum import Enum

class Command(Enum):
    A_COMMAND = 0
    C_COMMAND = 1
    L_COMMAND = 2

class Parser:
    def __init__(self, file_name: str):
        self.file_name: str = file_name
        self.current_line: int = 0
        self.current_command: str = None
        self._command_type: Command = None
        self.address = 0

        try:
            with open(file_name, "r") as data:
                self.data = data.readlines()
        except FileNotFoundError:
            exit(f"File {file_name} not found.")

    def has_more_commands(self) -> bool:
        if self.current_line >= len(self.data):
            return False
        
        return True

    def advance(self) -> None:
        raw_data = self.data[self.current_line]
        self.current_command = raw_data.split("//")[0].strip()
        self.current_line += 1

        if len(self.current_command) == 0:
            self._command_type = None
            return

        if self.current_command[0] == "@":
            self._command_type = Command.A_COMMAND
            self.address += 1
        elif self.current_command[0] == "(" and self.current_command[-1] == ")":
            self._command_type = Command.L_COMMAND
        elif len(self.current_command) == 0 or self.current_command[0:2] == "//":
            self._command_type = None
        else:
            self._command_type = Command.C_COMMAND
            self.address += 1
    
    def command_type(self) -> Command:
        return self._command_type
    
    def symbol(self) -> str:
        if self._command_type == Command.A_COMMAND:
            return self.current_command[1:]
        elif self._command_type == Command.L_COMMAND:
            command_len = len(self.current_command)
            return self.current_command[1: command_len - 1]
        else:
            exit("symbol function only can be called in A_COMMAND or L_COMMAND.")
    
    def dest(self) -> str:
        if self._command_type != Command.C_COMMAND:
            exit("dest function only can be called in C_COMMAND")

        if self.current_command.find("=") == -1:
            return ""
        
        return self.current_command.split("=")[0]

    def comp(self) -> str:
        if self._command_type != Command.C_COMMAND:
            exit("comp function only can be called in C_COMMAND")

        if self.current_command.find("=") == -1:
            return self.current_command.split(";")[0]

        return self.current_command.split(";")[0].split("=")[1]
        
    
    def jump(self) -> str:
        if self._command_type != Command.C_COMMAND:
            exit("jump function only can be called in C_COMMAND")
        
        if self.current_command.find(";") == -1:
            return None
        
        return self.current_command.split(";")[1]

    def reset(self) -> None:
        self.current_line: int = 0
        self.current_command: str = None
        self._command_type: Command = None
        self.address = 0

Code

어셈블리의 연상기호를 2진 코드로 번역한다.

class Code:
    _dest_table = {
        "": "000",
        "M": "001",
        "D": "010",
        "MD": "011",
        "A": "100",
        "AM": "101",
        "AD": "110",
        "AMD": "111",
    }

    def dest(self, dest: str):
        return self._dest_table[dest]

    _comp_table = {
        '0':'0101010',  
        '1':'0111111',
        '-1':'0111010', 
        'D':'0001100', 
        'A':'0110000',  
        '!D':'0001101', 
        '!A':'0110001', 
        '-D':'0001111', 
        '-A':'0110011', 
        'D+1':'0011111',
        'A+1':'0110111',
        'D-1':'0001110', 
        'A-1':'0110010',
        'D+A':'0000010',
        'D-A':'0010011',
        'A-D':'0000111', 
        'D&A':'0000000',
        'D|A':'0010101',
        'M':'1110000',  
        '!M':'1110001', 
        '-M':'1110011', 
        'M+1':'1110111',
        'M-1':'1110010',
        'D+M':'1000010',
        'D-M':'1010011',
        'M-D':'1000111', 
        'D&M':'1000000', 
        'D|M':'1010101',
    }

    def comp(self, comp: str):
        return self._comp_table[comp]

    _jump_table = {
        "": "000",
        "JGT": "001",
        "JEQ": "010",
        "JGE": "011",
        "JLT": "100",
        "JNE": "101",
        "JLE": "110",
        "JMP": "111",
    }

    def jump(self, jump: str):
        return self._jump_table[jump]

Symbol Table

번역 과정에서 기호들을 실제 주소로 바꾸어야 하는데, 어셈블러가 Symbol Table을 통해서 이 작업을 처리한다.

class SymbolTable:
    def __init__(self):
        self.entries = {
            "R0": "0",
            "R1": "1",
            "R2": "2",
            "R3": "3",
            "R4": "4",
            "R5": "5",
            "R6": "6",
            "R7": "7",
            "R8": "8",
            "R9": "9",
            "R10": "10",
            "R11": "11",
            "R12": "12",
            "R13": "13",
            "R14": "14",
            "R15": "15",
            "SP": "0",
            "LCL": "1",
            "ARG": "2",
            "THIS": "3",
            "THAT": "4",
            "SCREEN": "16384",
            "KBD": "24576",
        }

    def add_entry(self, symbol: str, address: int) -> bool:
        if self.entries.get(symbol):
            return False
        
        self.entries[symbol] = f"{address}"
        return True
    
    def contains(self, symbol: str) -> bool:
        if self.entries.get(symbol):
            return True
        return False
    
    def get_address(self, symbol: str) -> int:
        if self.entries.get(symbol):
            return int(self.entries[symbol])
        
        return -1

Assembler

이 어셈블러는 2-pass assembler이다.

  1. 라인 단위로 처음부터 끝까지 훑으면서, 코드는 생성하지 않고 Symbol Table만 구성한다. 그리고 라인을 훑어 가는 동안, 최종적으로 현재 명령이 로드될 ROM 주소를 기록하는 숫자를 둔다. 이 숫자는 0에서 시작하며 레이블이나 주석이 나오면 증가하지 않는다.

  2. 프로그램을 다시 훑으며 각 라인을 분석하고 기호가 있는 명령어를 만날 때마다 Symbol Table에서 기호를 조회한다. 테이블에 그 기호가 있으면 기 기호에 대응되는 숫자 값으로 교체하고 해당 명령의 번역을 완료한다. 기호가 테이블에 없다면 그 기호는 새로운 변수라는 뜻이다.

from argparse import ArgumentParser
from parser import Parser, Command
from code import Code
from symbol_table import SymbolTable
from os import path

def parse_args() -> str:
    arg_parser = ArgumentParser(
        description="python3 assembler.py <file_name>"
    )

    arg_parser.add_argument(
        "file_name",
    )

    return arg_parser.parse_args().file_name

def main():
    file_name = parse_args()
    parser = Parser(file_name)
    code = Code()
    symbol_table = SymbolTable()

    result = ""

    while parser.has_more_commands():
        parser.advance()
        
        if parser.command_type() == Command.L_COMMAND:
            symbol_table.add_entry(parser.symbol(), parser.address)

    parser.reset()

    while parser.has_more_commands():
        parser.advance()
        if parser.command_type() == Command.A_COMMAND:
            try:
                tmp_result = bin(int(parser.symbol()))[2:].zfill(16) + "\n"
                result += tmp_result
            except ValueError:
                if symbol_table.contains(parser.symbol()):
                    address = symbol_table.get_address(parser.symbol())
                    result += bin(address)[2:].zfill(16) + "\n"
                else:
                    exit(f"Symbol table doesn't contain symbol {parser.symbol()}")
        elif parser.command_type() == Command.C_COMMAND:
            dest = code.dest(
                parser.dest()
            )
            comp = code.comp(
                parser.comp()
            )
            parser_jump = parser.jump()
            jump = "000"
            if parser_jump != None:
                jump = code.jump(
                    parser_jump
                )
            
            result += "111" + comp + dest + jump + "\n"
        else:
            pass

    file_name, _ = path.splitext(parser.file_name)

    with open(f"{file_name}.out", "w") as file_data:
        file_data.write(result)

if __name__ == "__main__":
    main()