※ 해당 게시글은 주제를 탐구하면서 주관적인 생각을 정리 한 글입니다.
이전 글들을 통해 ALU 명령어와 Non-ALU 명령어에 대해 알아보았다.
그러면서 명령어가 무엇인지,
명령어는 어떻게 처리되는지 그 원리도 파악할 수 있었다.
※ 2024-12-02추가적으로 Non-ALU를 포함시키기 위해새로운 배선 배치도를 창안했으며 그로 인해기본적인 메모리로부터 명령어를 가져오고, 해석하는 과정부터ALU 명령어 뿐만 아니라, Non-ALU 명령어까지 처리할 수 있는8BIT 컴퓨터를 완성할 수 있었다.
그러면 이어서 앞서 구현한 컴퓨터가 제공하는
명령어와 어셈블리어의 관계를 다시 한번 살펴보면서
어떻게 어셈블리어로 프로그래밍하고, 또 그렇게 제작된 프로그램을
어떻게 해석 및 처리하는지 어셈블러의 원리를 탐구 해 보겠다.
< 초기 어셈블리어와 어셈블러 구현 및 원리 >
< 명령어의 원리와 어셈블리어의 등장 >
명령어의 원리에 대해 핵심적인 부분을 설명하면 다음과 같다.
우선 입력이란 스위치 소자를 통해
전류가 흐르도록 하거나, 흐르지 않도록 하는 행위와 같다.
출력은 입력 신호에 따른 결과로,
입력 신호에 따라 특정 장치나 회로가 반응하여 나타나는 신호이다.
이에 따라 디코더라는 장치는 n개의 입력 스위치가 존재할 때
입력 신호 조합에 따라 2ⁿ개의 출력을 생성하여
2ⁿ개의 출력 중 하나의 신호만이 활성화되도록 설계되었다.
이처럼 디코더를 이용하면 입력조합에 따라 여러 출력들 중
특정 하나의 신호만 활성화하여
특정 장치만 동작시킬 수 있도록 활용할 수 있다.
8bit 명령어 레지스터를 예를 들면,
왼쪽 상위 4bit 중 최상위 비트는 1x2 디코더와 연결하고,
다시 1x2 디코더 출력은 [0] : Non-ALU 연산 활성화 / [1] : ALU 연산 활성화
신호라는 의미를 부여하여 활용했다.
그리고 나머지 상위 3bit는 3x8 디코더와 연결하고,
다시 3x8 디코더 출력은 각 출력마다
여러 연산 장치들과 특수 레지스터들을 연결하여
ALU / Non-ALU 연산들 중에서 어떤 연산을 이용할 것인지
선택할 수 있도록 활용하였다.
그리고 나머지 하위 4bit는 둘로 나누어, 각각 2x4디코더와 연결하고,
각 2x4 디코더 출력은 4개의 범용(데이터) 레지스터와 각각 연결하여
상위비트에서 선택한 연산자의 피연산자로 사용할
범용(데이터) 레지스터를 선택할 수 있도록 활용하였다.
이처럼 입력과 출력 시스템을 기반으로
제작한 디코더를 통해 특정 연산 장치나 레지스터를 활성화하여
작동하도록 한다.
따라서
스위치를 누르고, 떼는 행위를 입력으로 하여금
입력 조합을 통해
특정 장치들이 동작함으로써,
의미 있는 값을 출력하는 할 때의 그 입력 조합을
각 이진수에 대응시켜 이진수 코드로 표현한 것이 '명령어'이다.
그러나 사용자가 명령어를 입력하기 위해
스위치를 직접 입력해야 하는 문제가 있다.
하지만 그것 보다 더 큰 문제는 명령어는
이진수 코드, 즉, 0과 1로 표현하기 때문에
여러 명령어를 사용할 경우 가독성이 매우 떨어진다.
이를 해결하기 위해 특정 이진수 코드 즉, 명령어를
문자에 대응시켜 표현하는 방식이 도입되었다.
이전 글들을 통해 구현한 CPU 명령어 체계를 예시로 들면
1000 0102 이진수 코드는
R1, R2 레지스터에 저장되어 있는 값을 피연산자로 하여
ADD 연산기를 통해 덧셈을 진행하고 그 결과를 다시
R1에 저장하는 ALU 연산 명령어 코드이다.
그리고 해당 코드를 문자(알파벳/숫자)에 대응시켜
1000 0102 = ADD R1, R2로 표현하기를 약속할 수 있다.
이처럼 CPU 설계에 따라 제공되는 명령어(이진수 코드)들을
문자에 대응시켜 하나의 언어체계를 구축하였고,
그 언어 체계를 "어셈블리어"라고 불렀다.
그러나 이전 글들을 통해 구현한 컴퓨터는
문자 자체를 인식하거나, 처리할 수 없기 때문에
어셈블리어로 작성된 명령어집합(프로그램)을
0과 1로 변환하고 조립하는 어셈블러(Assembler) 과정이 필요했다.
따라서 이번 글을 통해
어셈블리어가 이진수로 변환되는,
어셈블러 과정과 그 원리를 탐구해 보겠다.
< 초기 어셈블리어와 어셈블러 >
어셈블리어(Assemble language)는 앞서 언급하였던 것처럼
CPU 설계에 따라 제공되는 명령어:이진수 코드들을
문자에 대응시켜
사람들이 보다 빠르고 쉽게 명령어를 작성하고 이해할 수 있도록
구축한 하나의 컴퓨터 언어 체계이다.
이는 초기 컴퓨터에게 명령어를 입력하는 개발자들에게
명령어를 쉽게 조립(assemble)하여 프로그램을 작성하고,
읽을 수 있도록 도움을 주었다.
ex) 이전 글들을 통해 구현한 CPU 설계에 따른 명령어 및 어셈블리어
< Non-ALU 명령어 >
어셈블리어 | 명령어 코드 |
LOAD RA, RB (= LAD RA, RB) |
0 000 RA, RB |
STORE RA, RB (= STR RA, RB) |
0 001 RA, RB |
DATA XX, RB | 0 010 XX, RB |
JMPR XX, RB | 0 011 XX, RB |
JMP XX, XX [0000 0000] |
0 100 XX, XX [0000 0000] |
JC [0000 0000] JA [0000 0000] JE [0000 0000] JZ [0000 0000] 등등.. |
0 101 C|A|E|Z [0000 0000] |
CLF XX, XX | 0 110 XX, XX |
NOP XX, XX | 0 111 XX, XX |
< ALU 명령어 >
어셈블리어 | 명령어 코드 |
ADD RA, RB | 1 000 RA, RB |
SHR RA, XX | 1 001 RA, XX |
SHL RA, XX | 1 010 RA, XX |
NOT RA, XX | 1 011 RA, XX |
AND RA, RB | 1 100 RA, RB |
OR RA, RB | 1 101 RA, RB |
XOR RA, RB | 1 110 RA, RB |
CMP RA, RB | 1 111 RA, RB |
< 범용(데이터) 레지스터 >
RA 명령어 코드 | RB 명령어 코드 | |
R0 | X XXX 00 XX | X XXX XX 00 |
R1 | X XXX 01 XX | X XXX XX 01 |
R2 | X XXX 10 XX | X XXX XX 10 |
R3 | X XXX 11 XX | X XXX XX 11 |
어셈블리어의 도입에 따라
개발자들은 실행하고자 하는 프로그램을 이진수가 아닌
어셈블리어로 보다 쉽게 작성할 수 있었지만,
초기의 컴퓨터는 여전히 이진수만 처리할 수 있었다.
이에 따라 초기 개발자들은 어셈블리어로 프로그램을 작성한 후
각 명령어를 직접 이진수로 변환하였고,
이렇게 변환한 이진수를 컴퓨터의 RAM에 입력할 때에도.
스위치를 누르고 떼는 방식으로 수동 입력 하였다.
즉, 최초의 어셈블러는 사람과 종이 그리고 펜이었다.
이후 천공카드(Punched card)와 타자기 그리고 천공카드 판독기가
컴퓨터와 결합되면서, 보다 빠르고 쉽게 어셈블리어를
이진수로 변환하고 컴퓨터 RAM에 입력할 수 있게 되었다.
< 천공 카드 : Punched card >
천공 카드는 알파벳, 숫자, 특수문자, 기호 등을 8bit로 표현할 수 있는
ASCII 코드에 따라 세로줄마다 하나의 문자 또는 데이터를
표현할 수 있었다.
※초기에는 ASCII 코드 외에도 제조사 별로 사용하는 코드가 달랐다.
천공카드와 천공카드 타자기를 사용하면
사람이 직접 어셈블러 하는 것보다 오류와 시간을 줄일 수 있었고,
프로그래밍된 천공 카드를
물리적으로 보관하여 저장장치로써 사용되기도 했으며,
뿐만 아니라 재사용도 할 수 있었다.
그리고 타자기는 문자와 숫자를 기계적 설계 회로에 따라서
각 키(Key)를 누르면 해당 키에 대응되는 이진수 값에 따라
천공카드를 뚫어 주었다.
또, 천공 카드에 문자 또는 데이터를 표현하는 8bit에
특정 제어 비트 추가하여 입력한 문자 또는 데이터가
명령어를 나타내는지, 데이터(값)를 나타내는지
구분할 수 있었다.
이후 키보드가 도입됨으로써
숫자와 문자 등이 물리적 회로를 통해 ASCII코드로 변환하였고,
키보드 또한,
명령어 모드와 데이터 모드를 구분할 수 있는
전환 키 또는 특수 키 등을 사용하여
명령어 모드가 활성화되었을 경우에만
키보드에 의해 ASCII 코드로 입력된 이진수를 명령어 코드를
ASCII 코드로 변환하였을 때와 비교하여
일치하는 명령어로 변환하여 RAM에 저장할 수 있도록 하였다..
이에 따라 물리적인 요소와 소프트웨어적인 요소가
융합되어 어셈블러 역할을 하였다.
< 어셈블러 : Assembler >
타자기 또는 키보드는 문자 또는 숫자 등을
ASCII 코드에 대응시켜 이진수로 변환해 주고,
해당 이진수가 명령어를 의미하는 문자인지,
데이터(값) 의미하는 문지인지만 컴퓨터에게 신호로 알려줄 뿐
명령어 코드로 변환해 주는 역할은 하지 않는다.
예를 들어 명령어 코드 1000 0001 : ADD R0 R1를 실행하기 위해
사용자가 키보드 또는 천공카드&타자기를 통해
ADD R0 R1라고 입력하였을 경우
키보드와 타자기는 해당 문자를 다음과 같이 변환해 준다.
'A' : 0100 0001
'D' : 0100 0100
'D' : 0100 0100
' ' : 0010 0000
'R' : 0101 0010
'0' : 0011 0000
' ' : 0010 0000
'R' : 0101 0010
'1' : 0011 0001
' ' : 0010 0000
해당 결과는 데이터(값) 일뿐 명령어가 아님으로
해당 이진수들을 조합(Assemble)하여
명령어 코드 1000 0001로 변환해 주는 소프트웨어적인
어셈블러 프로그램이 필요하다.
그렇다면 최초의 소프트웨어적인 어셈블러 프로그램은 어떻게 구현할까?
초기의 컴퓨터는 기본적인 ALU 연산과 Non-ALU 연산들만 제공되는데
이를 통해 어떻게 위와 같은 기능을 하는 어셈블러를 제작할 수 있을까?
이에 대해 필자가 작성한 어셈블러는 다음과 같다.
ex) 키보드 또는 타자기를 통해 ADD R0 R1 입력받았을 때 처리
※ 해당 어셈블러는 필자의 주관에 따라 작성된 코드로, 오류가 발생할 수 있다.
※ 아스키코드에 따라 모든 문자 및 숫자를 8BIT로 표현할 수 있다고 가정한다.
※ 모든 명령어는 아스키코드에 따라 24BIT+8BIT+8BIT로 표현할 수 있다고 가정한다.
※ [24BIT : OPcode] + [8BIT : RA] + [8BIT : RB]
※ 아스키코드표
8-BIT ASCII CODE TABLE
# Control Characters
0000 0000 (0x00) - NUL (Null)
0000 0001 (0x01) - SOH (Start of Heading)
0000 0010 (0x02) - STX (Start of Text)
0000 0011 (0x03) - ETX (End of Text)
0000 0100 (0x04) - EOT (End of Transmission)
0000 0101 (0x05) - ENQ (Enquiry)
0000 0110 (0x06) - ACK (Acknowledge)
0000 0111 (0x07) - BEL (Bell)
0000 1000 (0x08) - BS (Backspace)
0000 1001 (0x09) - HT (Horizontal Tab)
0000 1010 (0x0A) - LF (Line Feed)
0000 1011 (0x0B) - VT (Vertical Tab)
0000 1100 (0x0C) - FF (Form Feed)
0000 1101 (0x0D) - CR (Carriage Return)
0000 1110 (0x0E) - SO (Shift Out)
0000 1111 (0x0F) - SI (Shift In)
# Printable Characters
0010 0000 (0x20) - Space
0010 0001 (0x21) - !
0010 0010 (0x22) - "
0010 0011 (0x23) - #
0010 0100 (0x24) - $
0010 0101 (0x25) - %
0010 0110 (0x26) - &
0010 0111 (0x27) - '
0010 1000 (0x28) - (
0010 1001 (0x29) - )
0010 1010 (0x2A) - *
0010 1011 (0x2B) - +
0010 1100 (0x2C) - ,
0010 1101 (0x2D) - -
0010 1110 (0x2E) - .
0010 1111 (0x2F) - /
# Digits
0011 0000 (0x30) - 0
0011 0001 (0x31) - 1
0011 0010 (0x32) - 2
0011 0011 (0x33) - 3
0011 0100 (0x34) - 4
0011 0101 (0x35) - 5
0011 0110 (0x36) - 6
0011 0111 (0x37) - 7
0011 1000 (0x38) - 8
0011 1001 (0x39) - 9
# Uppercase Letters
0100 0001 (0x41) - A
0100 0010 (0x42) - B
0100 0011 (0x43) - C
0100 0100 (0x44) - D
0100 0101 (0x45) - E
0100 0110 (0x46) - F
0100 0111 (0x47) - G
0100 1000 (0x48) - H
0100 1001 (0x49) - I
0100 1010 (0x4A) - J
0100 1011 (0x4B) - K
0100 1100 (0x4C) - L
0100 1101 (0x4D) - M
0100 1110 (0x4E) - N
0100 1111 (0x4F) - O
0101 0000 (0x50) - P
0101 0001 (0x51) - Q
0101 0010 (0x52) - R
0101 0011 (0x53) - S
0101 0100 (0x54) - T
0101 0101 (0x55) - U
0101 0110 (0x56) - V
0101 0111 (0x57) - W
0101 1000 (0x58) - X
0101 1001 (0x59) - Y
0101 1010 (0x5A) - Z
# Lowercase Letters
0110 0001 (0x61) - a
0110 0010 (0x62) - b
0110 0011 (0x63) - c
0110 0100 (0x64) - d
0110 0101 (0x65) - e
0110 0110 (0x66) - f
0110 0111 (0x67) - g
0110 1000 (0x68) - h
0110 1001 (0x69) - i
0110 1010 (0x6A) - j
0110 1011 (0x6B) - k
0110 1100 (0x6C) - l
0110 1101 (0x6D) - m
0110 1110 (0x6E) - n
0110 1111 (0x6F) - o
0111 0000 (0x70) - p
0111 0001 (0x71) - q
0111 0010 (0x72) - r
0111 0011 (0x73) - s
0111 0100 (0x74) - t
0111 0101 (0x75) - u
0111 0110 (0x76) - v
0111 0111 (0x77) - w
0111 1000 (0x78) - x
0111 1001 (0x79) - y
0111 1010 (0x7A) - z
※ [24BIT] : ALU - OPcode 아스키 코드
어셈블리어 | 명령어 코드 | ASCII Code |
ADD | 1 000 0000 | A : 0100 0001 D : 0100 0100 D : 0100 0100 |
SHR | 1 001 0000 | S : 0101 0011 H : 0100 1000 R : 0101 0010 |
SHL | 1 010 0000 | S : 0101 0011 H : 0100 1000 L : 0100 1100 |
NOT | 1 011 0000 | N : 0100 1110 O : 0100 1111 T : 0101 0100 |
AND | 1 100 0000 | A : 0100 0001 N : 0100 1110 D : 0100 0100 |
OR | 1 101 0000 | O : 0100 1111 R : 0101 0010 |
XOR | 1 110 0000 | X : 0101 1000 O : 0100 1111 R : 0101 0010 |
CMP | 1 111 0000 | C : 0100 0011 M : 0100 1101 P : 0101 0000 |
※ [24BIT] : Non-ALU OPcode 아스키코드
어셈블리어 | 명령어 코드 | ASCII Code |
LAD (LOAD) |
0 000 0000 | L : 0100 1100 A : 0100 0001 D : 0100 0100 |
STR (STORE) |
0 001 0000 | S : 0101 0011 T : 0101 0100 R : 0101 0010 |
DTA (DATA) |
0 010 0000 | D : 0100 0100 T : 0101 0100 A : 0100 0001 |
JPR (JMPR) |
0 011 0000 | J : 0100 1010 P : 0101 0000 R : 0101 0010 |
JMP | 0 100 0000 | J : 0100 1010 M : 0100 1101 P : 0101 0000 |
JC JA JE JZ |
0 101 1000 0 101 0100 0 101 0010 0 101 0001 |
J : 0100 1010 C : 0100 0011 A : 0100 0001 E : 0100 0101 Z : 0101 1010 |
CLF | 0 110 0000 | C : 0100 0011 L : 0100 1100 F : 0100 0110 |
NOP | 0 111 0000 | N : 0100 1110 O : 0100 1111 P : 0101 0000 |
※ [8BIT] : RA/RB 레지스터 아스키코드
< 레지스터 RA >
어셈블리어 | 명령어 코드 | ASCII Code |
RA : R0 | X XXX 00 XX | R : 0101 0010 0 : 0011 0000 |
RA : R1 | X XXX 01 XX | R : 0101 0010 1 : 0011 0001 |
RA : R2 | X XXX 10 XX | R : 0101 0010 2 : 0011 0010 |
RA : R3 | X XXX 11 XX | R : 0101 0010 3 : 0011 0011 |
< 레지스터 RB >
어셈블리어 | 명령어 코드 | ASCII Code |
RA : R0 | X XXX XX 00 | R : 0101 0010 0 : 0011 0000 |
RA : R1 | X XXX XX 01 | R : 0101 0010 1 : 0011 0001 |
RA : R2 | X XXX XX 10 | R : 0101 0010 2 : 0011 0010 |
RA : R3 | X XXX XX 11 | R : 0101 0010 3 : 0011 0011 |
[ 메모리 구역 설정 ]
메모리 용량 : 256x8 = 2048bit
[0x00 ~ 0x4F] = 어셈블러 프로그램
...
[0x50 ~ 0x7F] = 사용자가 입력한 ASCII 문자
…
[0x80 ~ 0xBF] = 사용자가 입력한 ASCII 문자 저장 영역
-- [0x80] 0110 0101; ‘A’
-- [0x81] 0110 1000; ‘D’
-- [0x82] 0110 1000; ‘D’ → ‘ADD’
-- [0x83] 0010 0000; ‘ ’
-- [0x84] 0110 1000; ‘R’
-- [0x85] 0110 1000; ‘0’ → ‘R0’
-- [0x86] 0010 0000; ‘ ’
-- [0x87] 0110 1000; ‘R’
-- [0x88] 0110 1000; ‘1’ → ‘R1’
-- [0x89] 0010 0000; ‘ ’
…
[0xC0 ~ 0xEF] = 명령어 ASCII 코드 테이블
-- [0xC0] 0110 0101; ‘A’
-- [0xC1] 0110 1000; ‘D’
-- [0xC2] 0110 1000; ‘D’ → ‘ADD’
-- [0xC3] 0010 0000; ‘ ’
-- [0xC4] 0101 0011; ‘S’
-- [0xC5] 0100 1000; ‘H’
-- [0xC6] 0101 0010; ‘R’ → ‘SHR’
-- [0xC7] 0010 0000; ‘ ’
...
[0xF0 ~ 0xFF] = 명령어 이진수 코드 테이블
-- [0xF0] 1000 0000; ADD 명령어
-- [0xF1] 1001 0000; SHR 명령어
…
[어셈블러 코드]
※ 해당 어셈블러는 필자의 주관에 따라 작성된 코드로, 오류가 발생할 수 있다.
[0]
먼저 명령어 또는 데이터를 저장할 특정 메모리주소에
이미 명령어가 입력되어 있을 경우가 있으므로
LOAD명령어와 비교 명령어를 통해 특정 메모리 주소의 값이
0000 0000인 경우만 저장할 수 있도록 한다.
R0 : 명령어를 저장할 특정 메모리 주소
R1 : 비교할 메모리 주소
R2 : 0000 0000 (R1과 비교할 0000 0000)
R3 : 0000 0001 (R1을 +1 해 줄 0000 0001)
[Hex]
[0x00] 0000 0001;
[0x01] 0101 0000; 명령어 코드 변환 결과 저장 영역 시작 주소 [0x50] ~ [0x7F] 저장
[0x02] XOR R0, R0; R0 = 0000 0000으로 초기화
[0x03] DATA R1,
[0x04] 0000 0001:[0x01]; R1 = 0101 0000 으로 초기화
[0x05] XOR R2, R2; R2 = 0000 0000 으로 초기화
[0x06] DATA R3, XX
[0x07] 0000 0000:[0x00]; R3 = 0000 0001으로 초기화 (RAM 0번지에 0000 0001 저장)
[0x08] LAD R0, R1; R1 :[0x50] 주소에 저장되어 있는 값을 R0에 저장한다.
[0x09] ADD R1, R3; R1 : [0x50] 주소를 +1 해준다. (R1 = 0101 0000 + 1)
[0x0A] CMP R0, R2; R1 주소[0x50]에 저장되어 있는 값(R0)과 0000 0000(R2)을 비교
[0x0B] JE
[0x0C] 0000 1101:[0x0F]; 만약 R0값이 0000 0000인 경우 [0x0F]로 분기한다.
[0x0D] JNE
[0x0E] 0000 0008:[0x08]; 만약 R0값이 0000 0000과 같지 않으면 [0x08]로 돌아간다.
…
[0x50] 0000 0000
…
[1]
1X2 디코더를 이용하여 키보드 또는 천공카드로부터
‘명령어 모드(1)’를 입력받았을 때만
사용자가 입력한 ASCII 문자를 저장한 이후
미리 메모리에 저장한 명령어 ASCII 코드 값과
비교연산(CMP)을 진행한다.
※ 이때 사용자가 입력한 ASCII 문자는 0x80부터 저장된다.
※ 사용자가 입력한 ASCII 문자와 비교할 명령어 ASCII코드는 0xC0부터 저장된다.
[2]
ADD R0 R1에 대해서
이후 입력받은 OPcode ADD : 24BIT를 8BIT 끊어
명령어 ASCII 코드와 비교한다.
ex)
ADD의 'A' = 65 = 0100 0001 이므로,
명령어의 ASCII 코드와 비교하여 해당 이진수와 같은
문자만 다음 8bit 문자를 비교할 수 있도록 조건 분기한다.
ADD의 'D' = 68 = 0100 0100 이므로,
명령어의 ASCII 코드와 비교하여 해당 이진수와 같은
문자만 다음 8bit 문자를 비교할 수 있도록 조건 분기한다.
ADD의 두 번째 'D' = 68 = 0100 0100 이므로,
명령어의 ASCII Code와 비교하여 해당 이진수와 같으면
명령어 ADD를 뜻하므로
조건 분기하여 앞서 구한
변환된 명령어를 저장할 특정 메모리 주소(R0)의 값을
레지스터에 LOAD 하여
ADD : 1000 0000을 더한 후
그 결과를 다시 특정 메모리 주소(R0)에 저장한다.
※ 해당 기능을 수월하게 진행하기 위해서는 16개 정도의 레지스터가 요구된다.
※ 16개의 레지스터를 다루기 위해서는 8BIT 레지스터를 모두 16BIT로 변환해야 한다.
그러나 어셈블러의 원리만 파악하고자 하므로 무시한다.
※ JNE = Jump Not Eault 명령어가 추가로 필요하다.
※ JNE (Jump Not Equal) : CMP RA, RB에 대해서 RA, RB결과가 같지 않으면 분기한다.
※ JNE 명령어에 대해서, 명령어 레지스터 용량을 늘리면 명령어를 추가할 수 있다.
※ JNE 명령어에 대해서, 기존 JE 회로에 NOT게이트를 추가하면 JNE 명령어로 사용할 수 있다.
※ SUB (Subtraction) : SUB RA, RB에 대해서 RA = RA - RB
R0 : 명령어 저장할 특정 메모리 주소
R1 : 비교할 메모리 주소,
R2 : 0000 0000 (R1과 비교할 0000 0000)
R3 : 0000 0001 (R1을 +1 해 줄 0000 0001)
R4 : 0010 0000; ‘ ’ 해당 문자를 통해 명령어 비교 종료시점을 파악한다.
R5 : 사용자 입력 ASCII 코드 시작 주소; [0x80 ~ 0xBF]
R6 : 명령어 ASCII 코드 테이블 시작 주소; [0xC0 ~ 0xEF]
R7 : 명령어 코드 테이블 시작 주소; [0xF0 ~ 0xFF]
R8 : 비교할 사용자 입력 ASCII 코드 값
R9 : 비교 할 명령어 ASCII 코드 값
RA : 현재 비교 중인 명령어 코드
RB : 변환된 명령어를 저장할 특정 메모리 주소값을 임시 저장 할 레지스터
RC : 사용자 입력 ASCII 코드 끝 주소; [0x80 ~ 0xBF]
RD : 명령어 ASCII 코드 테이블 끝 주소; [0xC0 ~ 0xEF]
RE : 명령어 코드 테이블 끝 주소; [0xF0 ~ 0xFF]
RF : 0000 0003; ASCII 코드 테이블 원소들 사이의 간격
[0x0F] 0010 0000
[0x10] 1000 0000; 사용자 입력 ASCII 코드 시작 주소 [0x80]
[0x11] 1100 0000; 명령어 ASCII 코드 테이블 시작주소[0xC0]
[0x12] 1111 0000; 명령어 코드 테이블 시작 주소[0xF0]
[0x13] 1011 1111; 사용자 입력 ASCII 코드 끝 주소 [0xBF]
[0x14] 1110 1111; 명령어 ASCII 코드 테이블 끝주소[0xEF]
[0x15] 1111 1111; 명령어 코드 테이블 끝 주소[0xFF]
[0x16] 0000 0003
[0x17] DATA R4, XX
[0x18] 0000 1111:[0x0F]; R4 레지스터에 [0x0F]번지 값 = 0010 0000 = ‘ ‘ 저장
<끝 주소 초기화 >
[0x19] DATA RC, XX
[0x1A] 0001 0011:[0x13]; RC에 사용자 입력 ASCII 코드 끝 주소 저장
[0x1B] DATA RD, XX
[0x1C] 0001 0100:[0x14]; RD에 명령어 ASCII 코드 테이블 끝 주소 저장
[0x1D] DATA RE, XX
[0x1E] 0001 0101:[0x15]; RE에 명령어 코드 테이블 끝 주소 저장
< 시작 주소 초기화 >
[0x1F] DATA R5, XX
[0x20] 0001 0000:[0x10]; 사용자 입력 ASCII 코드 시작 주소를 R5에 저장
[0x21] DATA R6, XX
[0x22] 0001 0001:[0x11]; 명령어 ASCII 코드 테이블 시작주소 R6에 저장
[0x23] DATA R7, XX
[0x24] 0001 0010:[0x12]; 명령어 코드 테이블 시작 주소 R7에 저장
< 명령어 ASCII 테이블 내에서 명령어들 사이의 간격 값 초기화 >
[0x25] DATA RF, XX
[0x26] 0001 0110:[0x16]; RF 레지스터에 [0x16] 번지 값 = 0000 0003 저장
< 시작 주소 값 설정 >
[0x27] LAD R8, R5; R5가 저장하고 있는 주소에 있는 사용자 입력 ASCII 코드 값을 R8에 LOAD
[0x28] LAD R9, R6; R6가 저장하고있는 주소에 있는 명령어 ASCII 코드값을 R9에 LOAD
[0x29] LAD RA, R7; R7가 저장하고있는 주소에 있는 명령어 코드값을 RA에 LOAD
< 끝주소 범위 확인 >
[0x2A] CMP R5, RC
[0x2B] JA
[0x2C] 0001 1111:[0x1F];
[0x2D] CMP R6, RD
[0x2E] JA
[0x2F] 0010 0001:[0x21];
< 명령어가 세 글자인 경우와 두 글자인 경우에 따라 다음 명령어 시이의 간격 맞춤 >
[0x30] CMP R9, R4; 명령어 ASCII 값이 ' ' = 0100 0000 일 경우 R6+1, 명령어 간격을 맞춘다.
[0x31] JNE
[0x32] 0011 0101:[0x35]
[0x33] ADD R6, R3; R9 = R9 + 0000 0001(R3)
[0x34] LAD R9. R6
<사용자 입력 ASCII 코드 값과 명령어 ASCII 값이 같은 경우>
[0x35] CMP R8, R9
[0x36] JE
[0x37] 0100 0001:[0x41]
<사용자 입력 ASCII 코드 값과 명령어 ASCII 테이블 값이 다른 경우>
[0x38] CMP R9, RF; < + 만약 명령어 ASCII 테이블 값이 = ' ' 일경우>
[0x39] JE
[0x3A] 0011 1111:[0x3F]
[0x3B] ADD R6, RF; 같지 않은 경우 명령어 ASCII 코드에서 다음 명령어 까지의 간격을 더한다.
[0x2C] DATA RF, XX
[0x2D] 0001 0110:[0x16]; RF 레지스터에 [0x16]번지 값 = 0000 0003 저장 및 명령어 간격 초기화
[0x3E] ADD R7, R3; 명령어 코드 주소 + 1
[0x3F] JMP XX, XX
[0x40] 0010 1000:[0x28]
< 사용자 입력 ASCII 코드 값과 명령어 ASCII 값이 같은 데, 심지어 ' '로 같은 경우 (1) >
[0x41] CMP R4, R8
[0x42] JE
[0x43] 0100 1001:[0x49]
< 사용자 입력 ASCII 코드 값과 명령어 ASCII 값이 같지만, ' '는 아닌 경우 >
[0x44] ADD R5, R3; 사용자 입력 ASCII 주소 + 1
[0x45] ADD R6, R3; 명령어 ASCII 주소 + 1
[0x46] SUB RF, R3; 명령어 간격 - 1
[0x47] JMP
[0x48] 0010 0111:[0x27]
< 사용자 입력 ASCII 코드 값과 명령어 ASCII 값이 같은 데, 심지어 ' '로 같은 경우 (2) >
[0x49] ADD R5, R3; 사용자 입력 ASCII 주소 + 1
[0x4A] LAD RB, R0
[0x4B] ADD RB, RA
[0x4C] STR R0, RB
[0x4D] XOR RB, RB
; 다음 단어로 비교하기 전에,
변환된 명령어를 저장할 주소(R0) 값을 RB로 LOAD 하여
현재 완성된 단어에 해당되는 명령어 코드를 RB에 더하여
다시 R0에 저장(SOTRE)한다.
[0x4E] JMP XX, XX
[0x4F] 0001 1111 :[0x1F]
따라서 위의 코드를 반복하면 이어서 ADD R0 R1의
R0와 R1을 명령어로 변환하는 과정을 진행할 수 있다.
[3]
이후 ADD R0의 'R' = 0101 0010 이므로,
명령어의 ASCII 코드와 비교하여 해당 이진수와 같은
문자만 다음 8bit 문자를 비교할 수 있도록 조건 분기한다.
ADD R0의 ’ 0’ = 0011 0000 이므로,
명령어의 ASCII Code와 비교하여 해당 이진수와 같으면
명령어 ADD R0를 뜻하므로
조건 분기하여 앞서 구한
변환된 명령어를 저장할 특정 메모리 주소(R0)의 값을
RB로부터 LOAD 하여
R0 : 00000000을 더한 후
그 결과를 다시 특정 메모리 주소(R0)에 저장한다.
[4]
이후 ADD R0 R1의 'R' = 0101 0010 이므로,
명령어의 ASCII 코드와 비교하여 해당 이진수와 같은
문자만 다음 8bit 문자를 비교할 수 있도록 조건 분기한다.
ADD R1의 ’ 1’ = 0011 0001 이므로,
명령어의 ASCII Code와 비교하여 해당 이진수와 같으면
명령어 ADD R0 R1를 뜻하므로
조건 분기하여 앞서 구한
변환된 명령어를 저장할 특정 메모리 주소(R0)의 값을
RB로부터 LOAD 하여
R1 : 0000 0001을 더한 후
그 결과를 다시 특정 메모리 주소(R0)에 저장한다.
[5]
이에 따라 최종적으로 특정 메모리 주소
0101 0000:[0x50]에는
ADD R0 R1 명령어 코드 : 1000 0001가 저장되어 있으며,
이후 해당 메모리 주소로 분기하면 해당 명령어가 실행된다.
이처럼 초기의 소프트웨어 어셈블러를 기반으로 동작하는 어셈블리어를 통해서
복잡한 문법 규칙과 여러 명령어를 처리할 수 있는 어셈블러를 개발하고,
또 그러한 어셈블러를 통해 더 발전된 어셈블러를 만들어가면서
현재의 어셈블러까지 도달하게 되었다
< 주관적 깨달음 >
[1]
사용자가 입력한 ASCII 코드를 명령어 코드로 변환하기 위해서는
우선 명령어 코드를 특정 메모리 구역에 입력할 필요가 있었다.
그리고 명령어 코드를 ASCII 코드로 변환한 값들을
따로 또 저장해야 할 필요가 있었다.
이에 따라
초기 어셈블러는 명령어를 문자와 숫자를 이용해 표현하기로 약속한 언어 체계인
어셈블리어를 이용하여 사용자가 입력한 ASCII 코드와
메모리에 저장된명령어 ASCII 코드와 비교하여 일치한 경우에
대응되는 명령어 이진수 코드를 사용하는 원리임을 짐작으로 알게 되었다.
[2]
이에 따라 실질적으로 프로그램을 구현하고,
실행하기 위해서는메모리를 구역 및 테이블 형태로 구
성할 필요가 있다는 것을 알게 되었다.
다음은 어셈블러 프로그램을 작성하기 위해
주관적으로 메모리를 구역 및 테이블 형태로 구성한 예시이다.
(1) 어셈블러 프로그램
(2) 사용자가 입력한 ASCII 문자
(3) 사용자가 입력한 ASCII 문자 저장 영역
(4) 명령어 ASCII 코드 테이블
(5) 명령어 이진수 코드 테이블
위를 기준으로 메모리 용량 : 256x8 = 2048bit 경우
다음과 같이 메모리 구역을 분리하였다.
1) [0x00 ~ 0x4F] = 어셈블러 프로그램
2) [0x50 ~ 0x7F] = 사용자가 입력한 ASCII 문자
3) [0x80 ~ 0xBF] = 사용자가 입력한 ASCII 문자 저장 영역
4) [0xC0 ~ 0xEF] = 명령어 ASCII 코드 테이블
5) [0xF0 ~ 0xFF] = 명령어 이진수 코드 테이블
[3]
그리고 각 구역 및 테이블에 있는 값/원소들을
특정 순서대로 참고하거나, 오류를 방지하는 등
효율적으로 참조하기 위해서는
특정 값과 각 구역 및 테이블의 시작 주소와 끝 주소를
메모리와 레지스터에 입력하여 사용할 필요가 있다는 것을
알게 되었다.
[4]
위와 같이 각 구역과 테이블의 시작 주소와 끝 주소
그리고 비교연산에서 사용할 특정 값 등을 모두
저장하고 사용하기 위해서는
앞서 구현한 레지스터의 개수 4개보다
더 많은 레지스터가 요구되었다.
이에 따라 최소 16bit 컴퓨터 체계가 필요함을 알 수 있었다.
[5]
또, 앞서 구현한 8bit 컴퓨터 체계에서 제공하는
ALU 연산과 Non-ALU 연산 이외에도
JNE , SUB와 같이 좀 더 다양하고, 복잡한 명령어가필요했다.
때문에 더욱이 왜 16bit 컴퓨터 체계를 구축하여
명령어 레지스터의 용량을 증가시켜 더 다양하고,
복잡한 명령어 체계가 요구되는지 알 수 있었다.
[6]
분기문과 반복문에 대해서
JE , JNE를 주로 사용하였는데
고급프로그래밍언어와 달리
JE [0000 0000] 이후에 작성되는 명령어들은
JE가 성립되었을 때가 아닌, 그 반대(JNE) 상황에 대해서
작성해야 했으며,
JNE [0000 0000] 이후에 작성되는 명령어들은
JNE가 성립되었을 때가 아닌, 그 반대(JE) 상황에 대해서
작성해야 했다.
그리고 본 내용 즉 조건이 성립하였을 때 실행 할 명령어들은
분기한 주소부터 작성해야 했음을 알게 되었다.
[7]
사용자 입력 ASCII 코드와 명령어 ASCII 코드를 비교연산 할 때
값이 다른 경우 다음 명령어 ASCII 코드와 비교하기 위해서는
해당 원소들이 몇 번째 원소인지 파악하기 위한
카운트 값이 필요함을 알게 되었다.
이를 해결하고자,
명령어는 최대한 두 글자 또는 세 글자로 통일하였고,
레지스터를 추가하여
각 명령어 ASCII 코드 사이에 ' ' = 0010 0000을 배치하여
구분지점을 두었다.
그래서 다음 명령어까지의 주소간격이
3 (두 글자 명령어) 또는 4 (세 글자 명령어)
차이 나도록 구현하였고,
사용자 입력 ASCII 코드의 첫 번째 문자와
명령어 ASCII 코드의 첫 번째 문자부터 다를 경우
명령어 ASCII 코드 주소에 +3을 하여 다음 명령어와 비교하도록 하였다.
이때 세 글자 명령어의 경우 +3을 하면 ' ' = 0010 0000에
도달하게 됨으로 해당 값이 ' ' = 0010 0000 경우에는
+1을 더해주도록 구성하였다.
하지만
만약 메모리 용량이 더 크고, 레지스터를 좀 더 많이 사용할 수 있다면
각 명령어의 길이를 모두 메모리에 입력하여
명령어의 길이에 대한 테이블을 구성하여
각 명령어와 대응시키는 것이 좀 더 효율적임을 알 수 있었다.
[8]
그리고 다시 처음 질문으로 돌아가면
어셈블리어는
스위치와 릴레이 소자로 구현된 회로 및 장치들이 제공하는 명령어를
문자로 표현한 하나의 언어체계이며,
초기에는 어셈블리어를 종이와 펜을 이용해 작성하고
다시 이진수로 변환하는 과정을 사람이 직접 하였지만,
이후 천공카드와 타자기 그리고 원초적인 어셈블리어를 통해
원초적인 소프트웨어적인 어셈블러 프로그램을 제작하여
연쇄적으로 더 발전된 어셈블러 프로그램이 지속적으로
구현되었음을 알게 되었다.
이때 가장 궁금했었던 부분은
"결국 완전 초기에는 어떻게 이진수로 어셈블러를 구현하는가?"였다.
이에 대한 답은 위에 언급하였듯이
완전 초기 어셈블러는 이미 '사람'으로 구현되어 있었다.
사람에 의해 작성된 어셈블리어는
사람을 통해 좀 더 발전된 어셈블러
천공 카드와 타자기가 등장했고,
좀 더 발전해서
소프트웨어적인 어셈블러 프로그램을 제작할 수 있었다.
[9]
더 나아가 어셈블러와 같이 컴퓨터를 동작시키는데
필수적이고 중요한 프로그램인 경우
컴퓨터의 전원을 꺼도 해당 프로그램이
유지되어 있으면, 전원을 킬 때마다
해당 프로그램을 입력하지 않아도 됨에 따라
휘발성 메모리가 아닌,
비휘발성 메모리 즉,
전기로 물리적인 표시를 읽고 쓸 수 있는 저장장치가
필요함을 알 수 있었다.
[10]
※ 2024-12-02
추가적으로 조건분기명령어를 사용 후에
플래그를 초기화 시켜주는 명령어를 추가해야
,다음 ALU 연산과, 조건분기 명령어를
정상적으로 처리할 수 있다
하지만
이전 글들을 통해 구현한 컴퓨터는
뒤늦게 그러한 사실을 알게 된것도 있지만
메모리 용량이 2048BIT 한계가 있어서
추가 및 수정하지 못했다.
다음은 이어서 어셈블리어를 좀 더 편리하게 작성하고,
시각적으로 확인할 수 있는 키보드와 모니터에 대해 탐구해 보겠다.
※ 해당 게시글은 주제를 탐구하면서 주관적인 생각을 정리 한 글입니다.