on
어셈블러
어셈블러
기호 변환(Symbol Resolution)
기호가 있는 코드를 기호가 없는 코드로 변환하는 방법에는 기호 테이블(Symbol Table)이 있다. 우선, 번역된 코드는 주소 0부터 시작하는 메모리에 저장하고, 변수들은 주소 1024부터 할당하도록 한다(이 규칙은 하드웨어 플랫폼에 따라서 다르다). 다음으로, 소스코드에 새로운 기호 xxx가 나타날 때마다 (xxx, n) 라인을 테이블에 추가하는 식으로 Symbol Table을 구성한다(n은 기호와 연결된 메모리의 주소이다). 테이블이 다 만들어지면 이 테이블을 이용해서 기호가 없는 코드로 옮긴다.
어셈블러(Assembler)
어셈블리 프로그램은 2진 기계어로 번역해야만 컴퓨터에서 실행할 수 있다. 번역 작업은 어셈블러라는 프로그램이 담당한다. 어셈블러는 어셈블리 명령 열을 입력 받아, 동일한 의미의 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이다.
-
라인 단위로 처음부터 끝까지 훑으면서, 코드는 생성하지 않고 Symbol Table만 구성한다. 그리고 라인을 훑어 가는 동안, 최종적으로 현재 명령이 로드될 ROM 주소를 기록하는 숫자를 둔다. 이 숫자는 0에서 시작하며 레이블이나 주석이 나오면 증가하지 않는다.
-
프로그램을 다시 훑으며 각 라인을 분석하고 기호가 있는 명령어를 만날 때마다 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()