아아 진짜 오늘 너무 바보짓했다 ㅠㅜ

 

기계어/어셈블리어 파트 이론 마무리하고 과제하는데

 

이번 장의 과제가 어셈블리어로 곱셈 연산과

 

키보드, 화면 입출력 어셈블리어로 구현하는 예제인데

 

곱샘 연산 구현하는데만 4~5시간은 낭비해버리고 말았다.

 

// This file is part of www.nand2tetris.org
// and the book "The Elements of Computing Systems"
// by Nisan and Schocken, MIT Press.
// File name: projects/04/mult/Mult.tst

load Mult.asm,
output-file Mult.out,
compare-to Mult.cmp,
output-list RAM[0]%D2.6.2 RAM[1]%D2.6.2 RAM[2]%D2.6.2;

set RAM[0] 0,   // Set test arguments
set RAM[1] 0,
set RAM[2] -1;  // Test that program initialized product to 0
repeat 20 {
  ticktock;
}
set RAM[0] 0,   // Restore arguments in case program used them as loop counter
set RAM[1] 0,
output;

set PC 0,
set RAM[0] 1,   // Set test arguments
set RAM[1] 0,
set RAM[2] -1;  // Ensure that program initialized product to 0
repeat 50 {
  ticktock;
}
set RAM[0] 1,   // Restore arguments in case program used them as loop counter
set RAM[1] 0,
output;

set PC 0,
set RAM[0] 0,   // Set test arguments
set RAM[1] 2,
set RAM[2] -1;  // Ensure that program initialized product to 0
repeat 80 {
  ticktock;
}
set RAM[0] 0,   // Restore arguments in case program used them as loop counter
set RAM[1] 2,
output;

set PC 0,
set RAM[0] 3,   // Set test arguments
set RAM[1] 1,
set RAM[2] -1;  // Ensure that program initialized product to 0
repeat 120 {
  ticktock;
}
set RAM[0] 3,   // Restore arguments in case program used them as loop counter
set RAM[1] 1,
output;

set PC 0,
set RAM[0] 2,   // Set test arguments
set RAM[1] 4,
set RAM[2] -1;  // Ensure that program initialized product to 0
repeat 150 {
  ticktock;
}
set RAM[0] 2,   // Restore arguments in case program used them as loop counter
set RAM[1] 4,
output;

set PC 0,
set RAM[0] 6,   // Set test arguments
set RAM[1] 7,
set RAM[2] -1;  // Ensure that program initialized product to 0
repeat 210 {
  ticktock;
}
set RAM[0] 6,   // Restore arguments in case program used them as loop counter
set RAM[1] 7,
output;

사용한 테스트 스크립트는 이건데

 

 

ALU로 M=M+D 연산이 가능한걸 잊어버리고,

+1연산으로 이중루프를 만들어 곱셈 연산을 구현하느라

 

클럭 루프를 너무 많이 돌아버렸고, 

앞에 작은 수를 다루는 경우는 문제 없었지만

 

마지막 6 * 7 예제에서 210회 클럭안에 수행해야하는데

M=M+D 연산으로 하면 금방할걸

+1연산으로 구현해 놓으니 210회 클럭안에 연산을 마치지 못해서 자꾸 에러뜨길래

이게 라인 수가 너무 많아서 그런갑다 싶어 라인 수를 줄이느라 시간낭비했다.

 

그러다가 갑자기 +1 안해도 되는게 생각나서 했더니 20분도 안걸리고 해결했다 ㅜㅜ

아아아아아ㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏ

이거에서 해매지만 않았어도 진작에 입출력 제어 끝내고 다음 장으로 넘어갈텐데

 

곱셈 연산 해결한게 밤 11시 30분이라

입출력 헨들링까지 하고 글 정리하고 가면 좀 더 지나야 갈수있을거같다.

Mult_v1.asm Mult_v2.asm Mult_v3.asm
//210 클럭안에 6 * 7 곱연산을 마무리하지 못해 실패
@0 // init R2 as 0
D=A
@R2
M=D
@R0                  // if R0 or R1 == 0 -> end
D=M
@BIG_STOP
D;JEQ
@R1
D=M
@BIG_STOP 
D;JEQ
@j                    // init j(=R0)     -  j * i = result
M=1
(BIG_LOOP)        //--start of big loop--------------------------------
@j                    // make big loop end condition value
D=M
@R0
D=M-D
@BIG_STOP       // if (R0 - j < 0 )  goto BIG_STOP  r0 = 3, d = 4 => 3-4 < 0
D;JLT
@i
M=1
(SMALL_LOOP)   // -----------start of small loop--------------
@i                   //make small loop end condition value
D=M
@R1
D=M-D
@SMALL_STOP  // if (R1 - i < 0 )  goto SMALL_STOP r1 =5, i = 6    -> 5 - 6 < 0
D;JLT
@R2                 //small loop start
M=M+1
@i
M=M+1
@SMALL_LOOP // goto small loop
0;JMP
(SMALL_STOP)   // -----------end of small loop--------------
@j                   //start big loop
M=M+1
@BIG_LOOP
0;JMP               //goto big loop
(BIG_STOP)        //--end of big loop--------------------------------
@BIG_STOP
0;JMP
@R2 // init R2 as 0
M=0
@R1
D=M
@y // x * y = resuilt    <- R0 * R1 = R2, y = R1
M=D
@R0
D=M
@BIG_LOOP_END
D;JEQ
(BIG_LOOP_START)
@y
D=M
@R1
M=D
@R0
D=M
@BIG_LOOP_END
D;JEQ
(SMALL_LOOP_START)
@R1
M=M-1
@R1
D=M
@SMALL_LOOP_END
D;JLT
@R2
M=M+1
@SMALL_LOOP_START
0;JMP
(SMALL_LOOP_END)
@R0
M=M-1
@BIG_LOOP_START
0;JMP
(BIG_LOOP_END)
@BIG_LOOP_END
0;JMP
@R2
M=0
@R0
D=M
@END
D;JEQ
@R1
D=M
@END
D;JEQ
  (LOOP)
@R1
D=M
@R2
M=M+D
@R0
M=M-1
@R0
D=M
@END
D;JEQ
@LOOP
0;JMP
  (END)
@END
0;JMP

 

삽질하면서  HACK 어셈블리어 구현 연습한건 좋긴한데

이렇게 간단하게 할수있는걸 하루/4 시간을 낭비한게 너무 아깝긴하다 ㅜㅜ

마지막 Mult.asm이 마지막 구현 결과

 

 

gif 용량이 너무 커져서 0 곱셈 부분은 좀 자르고

활용한 CPU 에뮬레이터로 앞서 구현한 ALU와 메모리, 프로그램 카운터가 어떻게 동작하는지 알수 있다.

 

 

위에껀 너무 빠르니까

천천히 볼수있게 6 * 7부분만 캡처

 

 

두번째 과제로는

입출력 처리 어셈블리어 작성

1. 무한 루프를 돌며 키보드 입력을 받고, 입력되면 화면 전체를 꺼멓캐 한다.

2. 키를 놓으면 허옇게 하면된다.

 

 

이걸 어떻게 하냐

지난 글에 키보드, 스크린에 대해서 제대로 설명안했는데

각 입출력장치 메모리맵에다가 값을 읽고 써주면 될거같다.

 

키보드와 스크린은 어디있나 싶었는데

CPU 에뮬레이터에 상에 같이 있었다.

 

 

키보드 버튼을 누르면 키보드가 사용가능해져서

실제 키입력을 누르면 가상 머신에 반영되는거같다.

 

 

HACK 스크린

아무튼 HACK 컴퓨터의 스크린은

1. 256 x 512 = 2^8 * 2^9 = 2^17 = 131,072개의 픽셀을 가지고 있고

-> 레지스터가 16비트이고, 한비트가 한픽셀 값을 저장하니까 전체 픽셀을 16으로 나누면 8K의 RAM 공간이 사용된다.

ex) 131,072 / 16 = 2^17/2^4 = 2 ^ 13 = 8192 = 8K

 -> 전체 메모리 32K 중에 RAM이 16K니까, RAM 절반을 얘가 사용하네?

2. RAM의 16384번지에 있고, 이 베이스 주소는 SCREEN 이란 이름으로 미리 정의되어있다.

3. 보통 이미지 다룰때 처럼 베이스 번지가 0, 0을 나타낸다.

4. 이 화면은 그레이스케일이나 컬러가아닌 단순 흑백 화면으로 해당 픽셀에 1을 넣으면 검정, 0을 넣으면 흰색이된다.

5. RAM[SCREEN + row * 32 + col/16] : 1에서 말했듯이 레지스터에서 16개의 픽셀을 보관하고 있다보니, 픽셀 하나하나에 접근할수 없고, 접근하고자 하는 레지스터의 해당 픽셀 부분을 바꿔주면 될거같다.

 

 

 

스크린 예시1 ) 스크린 좌표 0,0 픽셀에다가 검정 표시하기

대강 스크린 베이스 레지스터에다가 1000 0000 0000 0000을 넣으면 될거같은데

A 명령어는 0부터 시작하고 값은 15비트밖에 못넣지 않았던가? 

 

아 다시 생각해보니까

A 명령어는 @SCREEN이니까 상관이 없네

 

그럼 1000 0000 0000 0000은 어떻게 만들어야 할지가 막막하다.

C명령어 테이블을 보면 C비트에다가 111010을 넣으면 -1이 나온다고 한다.

10진수 -1은   0000 0000 0000 0001에다가 2의 보수를 취해 구하면

-1 (10) = 1111 1111 1111 1111(2)이 된다.

 

우리가 구하고자 하는 값은 1000 0000 0000 0000이니

1. 어드레스 레지스터의 값은 2^15승이 최대이므로, A 명령어의 값에 2^16을 넣지 않고

2. 0111 1111 1111 1111를 컨트롤 비트로 -A든 -D든 해서

3. 목적지를 데이터 레지스터로 설정하여 담은 뒤

4. 스크린의 베이스 레지스터에다가 데이터 레지스터 값을 넣어보자

 

진짜 될진 모르겟네

0111 1111 1111 1111은

 

   1000 0000 0000 0000

-  0000 0000 0000 0001

2^16 - 1이므로 65,535가 된다.

 

@65535

D=-A

이걸 하려고 했지만 위 표를 보니 A 레지스터 최대값이 32767이라고 안돌아간다..

 

@32767
D=-A

으로 에뮬레이터에 돌려봤는데

 

데이터레지스터의 값을 2진수로 보니 내가 원했던 값이랑 거의 비슷하다?

그리고 왜 32767이 2^15 = 32768(10) = 0100 0000 0000 0000 이니까

32767의 1의보수 = 1011 1111 1111 1111이고

-32767 = 1100 0000 0000 0000 라 생각했지만

 

2진법 변환기에서 32767은 

0111 1111 1111 1111

32768이

1000 0000 0000 0000 이라고 한다.

 

 

오늘 곱샘연산에서 1/4나절 헛짓하고, 시간이 시간이라 그런가 잘 이해가 안된다

 

 

 

1 = 2^0

0000 0000 0000 0001

2 = 2^1

0000 0000 0000 0010

4 = 2^2

0000 0000 0000 0100

8 = 2^3

0000 0000 0000 1000

...

 

2^15

1000 0000 0000 000

 

하 이렇게 2진법을 쓰니까 이해가간다

2^0 = 1이 LSM이니 0에서 부터 시작해서 16비트의 MSB가 2^15 위치인데

 

2^15=32,768이고

A 명령어의 맨 앞비트만 사용 불가하니

최대 가능한 값은 0111 1111 1111 1111 = 32,768 -1 = 32,767이니까

위의 코드가 동작해서

 

-A 연산한 결과가

-32,767이 1000 0000 0000 0001이 나왔다!

 

내가 원하는 건 0, 0픽셀만 출력하면 되니

1000 0000 0000 0000으로 만들어주기 위해

-A 한 결과에다가 -1을 해주면 된다.

 

@32767
D=-A
@SCREEN
M=D-1

 

픽셀 하나만 찍다보니 뭐가 바뀌었나 싶긴한데 잘보면

화면 좌측 상단 쪽에 M=D-1 연산한 뒤에 아주 작은 검은점 하나 생겼다.

 

 

 

 

스크린 예시2 ) 첫째 행만 검은줄로 채우기

첫째 행만 검은줄을 채우기 위해 네 가지를 기억하면 될거같다.

1.  메모리 레지스터 하나당 16비트를 가지고 있어 16개의 픽셀 값을 표현한다.

2. 1111 1111 1111 1111은 -1이다.

3. 스크린의 열 길이가 512픽셀이니 512/16 = 32개의 픽셀에다가 -1을 채우면 된다.

4. @SCREEN 그러니까 스크린 베이스 어드레스는 16384이므로 16416번지까지 -1을 넣어주자

 

 

금방 할줄 알았는데 생각보다 머릴 좀 써야된다.

앞에서 어셈블리어 연습 많이 했다고 생각했는데 값을 넣었다 뺏다만 했지

값 + 1은 몰라도 주소 + 1은 안해서 어떻게 하나 고민했는데

 

 

ㅋㅋㅋ 진짜 이것도 결국에 검색해서 알았는데

꼭 A명령어 1번, C 명령어 1번 같은 식으로 할 필요가 없고,

A명령어 1번, C명령어 2번이든 여러번 하는게 가능했다.

 

A명령어

C명령어 각각 한번씩만 하면

 

도저히 RAM[A+1]에 접근할 방법이 생각이 안났는데

A=100일때, A=A+1을 시켜놔도

 

@3

M=D를 하려고하면 A의 값이 3으로 바뀌어져

아무리 생각해도 101번 번지에 담을수가 없었기 때문이다.

 

잠깐 찾아봤더니 이게 가능한거더라 .

@x
A=M+D
M=-1

 

이러면 나머지 문제도 금방 풀수 있을거같다.

 

 

 

@16416
D=A
@dest
M=D
@SCREEN
D=A
@cur_addr
M=D
    (LOOP)
@cur_addr
A=M
M=-1
@cur_addr
M=M+1
D=M
@dest
D=D-M
@LOOP
D;JLT
     (END)
@END

 

 

 

 

 

 

벌써 시간이 2시 넘었네 ;;

 

나머지는 얼른 대충 구현하고 가야겠다.

 

 

하도 앞에서 삽질을 많이 하는 바람에 

실제 과제는 금방 끝냈다.

 

 

두 번째 과제 : 어셈블리어로 키보드 스크린 입출력 제어 

1. 키보드 입력 받으면 화면 꺼멓게

2. 키보드 입력이 없으면 화면 허옇게

 

@INIT
0;JMP
     (INIT)
@SCREEN  //start screen addr
D=A
@cur_addr //cur screen addr
M=D
@8192      //total screen register num
D=A
@SCREEN
D=M+D
@end_addr // end screen addr
M=D
     (LOOP)
@KBD
D=M
@BLACK     // when key is pressed, val is greater than 0 
D;JGT
@WHITE
0;JMP
     (WHITE)
@cur_addr
D=M
@SCREEN
D=D-M
@LOOP
D;JEQ        // cur_addr ==screen  dont ram[cur_addr]=1 and  cur_addr -= -1
@cur_addr
A=M
M=0
@cur_addr
M=M-1
@LOOP
0;JMP
     (BLACK)
@cur_addr
D=M
@end_addr
D=D-M
@LOOP
D;JEQ         // cur_addr == end__addr dont ran[cur_addr]=-1 and cur_addr+1
@cur_addr
A=M
M=-1         // ram[cur_addr] = -1
@cur_addr
M=M+1      // cur_addr += 1
@LOOP
0;JMP

 

캡처한 gif는 너무 느려서 커지는 바람에 용량 줄인다고 반토막 냈다

동작은 잘되는데

하드웨어 시뮬레이션이라 그런가 클럭 속도 제한때문에 그런건지

아니면 내가 한번에 모든 픽셀을 바꿀줄 몰라서 그런걸까 

생각보다 반영되는게 좀 많이 느리다

 

 

 

 

새벽까지 하긴 해버렸지만 거의 이틀만에

난드투 테트리스 기계/어셈블리어 파트를 마무리했다 

대충 책보는거랑 정리하는거랑 걸리는 시간 차이가..

 

내일 5장 얼마나 진행할수 있을까 ㅋㅋㅋㅋㅋㅋ

어재 수업 빼고는 하루종일 hack 기계어와 어셈블리어를 정리했는데

내용은 절반? 정도 밖에 못했는데 

시간에 들인거에 비해서 분량도 얼마 되지는 않았다..

 

 

 

 

아 뭔가 보다가 조금 이상하다 싶었는데

어제 내가 C명령어를 정리하지 않았었다.

 

 

 

C 명령어

C명령어는 연산 비트(comp, 7비트), 목적지 비트(dest, 3비트), 점프 조건 비트(jmp, 3비트) 세 부분으로 111accccccdddjjj 같이 구성된다.

 

(1) acccccc : 7비트로 ALU가 수행할 연산을 결정하는 comp 비트

* 위 표에 나오다 시피 a가 0일때와, 1일때 수행하는 연산이 따로 나눠져있다.

(2) ddd : 3비트로 ALU 연산 결과를 담을 목적지를 나타내는데 저장하지 않을지(null), M에 담을지(RAM[A]), D 레지스터에 담을지, A레지스터 D레지스터 둘다에 연산 결과를 담을지 등 이 세 비트의 값으로 결정한다. 

(3) jjj : 3비트로 이뤄진 어떤 조건에 따라 점프를 할지를 의미하는 비트로 000인경우 점프 안함, 001 인경우 JGT로 연산 결과가 0보다 클때 점프, 111일때는 연산 결과에 상관없이 무조건 점프하는 JMP 명령이된다.

 

 

C 명령어의 연산 comp

 

 2장에서 ALU 구현한걸 다시 떠올리면, 두 16비트 입력을 받도록 되어있었다. 그러면 이 두 입력을 가지고 어떻게 기계어로 연산을 할수 있을까? 첫번째 입력 x는 데이터 레지스터 D에 있던 값을, 두 번째 입력 y는 어드레스 레지스터 A로 지정한 데이터 메모리 레지스터 M에 담아둔 값을 사용한다.

(이부분이 조금 햇갈리긴한데 그냥 진행한다)

 

 

 

 

 한번 comp의 입출력,  ALU 상태 비트의 입출력 테이블을 같이 보면

comp 명령에서는 a가 0이고, cccccc가 001100일때 D의 값을 가져오고

ALU 상태 입력이 comp의 c비트와 동일하게 001100일때 x를 출력으로 하는걸 볼수있다.

 

 그러니까 c가 001100 이더라도 a가 0일때는 데이터 레지스터 D, 1일때는 데이터 메모리 레지스터 M에있는 값을 가져온다.

 

 

 

ex) 심볼릭과 기계어로 D에 31, RAM[24]에 13이 들어있을때  D + M 연산해서 RAM[24]에 저장하자.

  =>  RAM[24] = 31 + 13

 

심볼릭 기계어(가독성을 위해 4비트씩 띄워쓰기함)
//1.  RAM[24]에 13넣기
@13
D=A
@24
M=D

//2. D = 31
@31
D=31

//3. D+M 후 RAM[24]에 쓰기
@24
M=D+M
0000 0000 0000 1101  // A명령어 13
1110 1100 0001 0000  // C 명령어 A를 로드해서(110000), D에다 저장(010), jmp안함(000)
0000 0000 0001 1000 // 24
1110 0011 0000 1000 // D의 값을 M에 쓰기     RAM[24]에 13넣기 끝

0000 0000 0001 1111 // 31
1110 1100 0001 0000 // A에 저장된 31을 D에다가 넣기

0000 0000 0001 1000 // 24
1111 0000 1000 1000 // RAM[24] = D + M

 

 

주석과 띄워쓰기를 지워 정리하면 hack 컴퓨터에서

RAM[24] = 31 +13 연산을 하는 기계어는 아래와 같다.

 

0000000000001101
1110110000010000
0000000000011000
1110001100001000
0000000000011111
1110110000010000
0000000000011000

1111000010001000

 

 

JMP 비트 사용하기

C 명령어의 JMP 비트는 연산 결과에 따라 어드레스 레지스터 A에 지정된 주소의 명령어를 가져오는(점프)하는 역활을 한다.

 

JMP 예시.  참고  c 명령어 : dest = comp;jmp

- D의 값이 0과 같으면 (LOOP)로 점프하라

@LOOP

D;JEQ

 

 

 

ex) 1에서 N까지의 합 계산하는 어셈블리 (RAM[1] = 1 + 2 + ... +RAM[0]

// 변수 i, sum 초기화
@i
M=1
@sum
M=0
(LOOP) // loop문 시작
@i
D=M
// D(i)-M(R0) = 0이면  i가 R0의 값과 같다 = i가 N까지 도달하였다.

@R0
D=D-M 
 // 모든 수를 더하였으므로 루프를 그만하고 (STOP) 문으로 넘어간다.

@STOP
D;JGT
// STOP으로 점프하지 않았다면 루프를 진행하면 되니 sum = sum + i 연산 하자
@sum
D=M
@i
D=D+M
@sum
D=M
// sum + i 연산을 한 뒤 i = i+1을 하자

@i
M=M+1
// 다시 루프 시작으로 가자
@LOOP
0;JMP
(STOP) // 루프 종료 조건 충족시 아래의 루틴 진행
@sum
D=M
@R1
M=D
(END) 
@END
0;JMP

주석을 지우면 아래와 같다.

@i
M=1
@sum
M=0
@i
D=M
@R0
D=D-M 
@STOP
D;JGT
@sum
D=M
@i
D=D+M
@sum
D=M

@i
M=M+1
@LOOP
0;JMP
(STOP)
@sum
D=M
@R1
M=D
(END)
@END
0;JMP

 

 

A 명령어의 값은 RAM 데이터 메모리의 주소인가? ROM 명령어 메모리의 주소인가?

진짜 A 명령어와 C명령어의 내용이 너무 햇갈린다. C 명령어의 dst와 jmp 둘다 쓸수 있지 않은지, 둘다 동시에 쓴다면 C 명령어 앞에 @R3이 있다면 R3의 값을 가져와서 1. dst 비트에 따라 M or A or D 에다가 C명령어의 연산 결과를 담고, 2. R3으로 JMP 간다고 생각하면 될까? 아니면 dst와 jmp를 동시에 쓰지 않고 dst만 하던가 jmp만 하는지. 지금까지 본 내용들을 보면 jmp와 dst 를 동시에 쓰는것 같지는 않다.

 

 또 A 명령어의 값이 RAM과 ROM의 주소를 동시에 나타대나보니  C 명령어와 함께 너무 햇갈리게 되는데, 뒤에 오는 C 명령어에 M이 존재하는지 여부에 따라 A가 RAM 혹은 ROM의 주소를 나타내는지 판단할 수 있다.

 

뒤에 오는 C 명령어가 M을 사용하는 경우 => A 명령어의 값을 RAM의 주소로 쓴다

C 명령어로 점프를 사용하는 경우 => A 명령어의 값을 ROM의 주소로 쓴다

 

위의 두가지 경우 정도로 이해하고 넘어가자

 

 

 

 

hack 어셈블리어의 심볼

지금까지 본 심볼들은 숫자, 상수와 달리 의미를 가진 알아볼수 있는 문자였다. 어느 위치에 접근할 때마다. 숫자를 주소로 하여 사용하기도 하고, 위의 예시에선 i나 sum 같이 변수를 이용해서 값에 접근하기도 하였다. 근데 지금까지 본 어셈블리어에서는 i, 4 같은 숫자와 문자 뿐만아니라 R1, R3, (LOOP), (END) 같은 다른 것들도 있었는데 이 심볼들은 무엇일까? hack 어셈블리어에는 우리가 만든 심볼 외에도 미리 정의된 심볼들이 있다.

 

미리 정의된 심볼들

1. R0, ..., R15(가상 레지스터)

 hack cpu는 16개의 레지스터를 가지고 있으며 R0은 RAM[0] ~ R15는 RAM[15]이 주소를 가지고 있다. A 레지스터가 주소 값으로 사용 될 때 @3의 경우 @R3가 동일한 동작을 한다고 할수 있긴 하지만, 미리 정의된 심볼을 사용하여 @3은 (확실하진 않지만) 데이터 레지스터로 사용되고 있구나, @R3은 데이터 메모리 주소로 사용되구나를 짐작할수 있는 장점이 있다.

 

2. SP, LCL, ARG, THIS 등

 가상 레지스터 이외에도 스텍 포인터를 나타내는 SP, LOCAL 등 다른 미리 정의된 심볼들이 있지만 가상 머신을 배울때 다루므로 일단 넘어가자.

 

3. SCREEN, KBD

 hack 컴퓨터는 IO장치로 스크린과 키보드를 사용하고 있다. 이 장치들을 사용하기 위해서 memory maped IO, 그러니까 스크린의 메모리 영역에 값을 쓰면 화면상에 표시되고, 키보드의 특정 키를 누르면 해당 키와 매핑된 메모리 주소의 값이 1이되는 식의 형태인데 이 미리 정의된 심볼들은 스크린 매모리맵, 키보드 매모리맵의 시작 주소 base address를 나타낸다. 이 내용은 5장 쯤에 다룬다.

 

4. 라벨 심볼

 이 외에도 앞서 루프문 예제에서 봤듯이 (LOOP), (STOP), (END)와 같이 (내용)과 같은 형태의 심볼을 봤는데 이러한 심볼들을 라벨 심볼이라 한다. 이 라벨 심볼은 바로 다음에 나오는 명령어의 주소를 가리키고 있어 점프 시 라벨 심볼 뒤의 명령어를 시작하게 된다. 그리고 이 라벨 심볼은 대문자로 써야한다.

 

일반 변수 심볼

 앞서 4가지의 미리 정의된 심볼에 대해서 정리하였고, 이런 심볼들 외  i, sum, x같은 것들을 변수 심볼이라 하며 @3을 사용했을때는 숫자 주소라 했지만 @i 일때는 어느 주소를 의미하는지 아직 알아보지는 않았는데 이는 어셈블리 파트에서 배울 수 있다.

 

 

 

스크린이랑 키보드에 내용은 그냥 넘어가고

간단하게 어셈블리어 작성 예시도 위에 했으니 기계어 장 이론은 여기까지 하고

다음 글에서는 이번 장 과제를 정리하자

 

글 하나 하나를 내가 아는 데로 정리하면서 쓰다보니 몇시간이 걸리는데

실제 글 내용은 생각보다 너무 적다.

차라리 교재든 강의 ppt든 번역하는게 더 빠르겠다 ㅠㅠ

3장에서 메모리 구현을 마무리하고,

이제 4장에서는 hack의 기계어와 어셈블리어에 관해서 설명한다.

 

일단 이책 7장 가상 머신 중간까지 완전히 이해했다고 하기는 힘들지만

어느정도 보기는 했는데

 

4장 부터는 어떻게 정리해야되나 싶다.

전처럼 아예 책을 통째로 번역하는짓은 하고 싶지는 않은데,

 

 

 

 

 

기계어와 고급언어

아무튼 이번 장에서는 우리가 구현할 컴퓨터 hack의 기계어와 어셈블리어가 어떻게 되는지

메모리 구조 등에서 간단하게 살펴본다.

 

일단 기계어에 대해서 설명하고 있는데,

보통 강의에서는 간단하게 컴퓨터가 알아들을 수 있는 2진수를 보고 기계어라고 하는 정도로만 설명한다.

 

하지만 우리가 할 내용들은 거기서 조금 더 나아가서

기계어란 CPU가 산술 논리 연산을 하고, 메모리에 읽고 쓰기를 할수 있도록 작성한 언어라고 이해하면 된다.

 

또 보통 강의에서는 기계어와 고급언어 차이를 설명한다면

고급언어는 사람이 보고 이해할수 있고, 기계어는 사람이 이해할수 없는 언어라는 정도로 이야기 하고 넘어가지만.

 

 

 

고급언어의 이식성? 저급언어는 하드웨어 의존적? 무슨소리인가.

기계어와 고급언어의 또 다른 차이점이라 하면

 

앞서 우리가 만들 컴퓨터 hack은 16비트 컴퓨터로 한번에 16비트 크기의 데이터를 처리하는 만큼

레지스터, 메모리, 프로그램 카운터, ALU 등 모두 16비트 입력을 받고 연산 할수있도록 만들었었다.

또, 메모리의 경우 16K 공간을 차지하도록 4K 메모리를 4개를 붙여서 만들었었는데,

 

그러면 hack 컴퓨터의 기계어를 다른 컴퓨터의 프로세서에다가 동일하게 쓸수 있을까?

당연히 안된다. 16비트 길이의 기계어 00000000 11001010를

32비트 컴퓨터에는 길이가 다르니 당연히 쓸수 없고

 

동일한 16비트 컴퓨터라 하더라도 이전에 만든 alu에서 상태 출력으로 zr과 ng가 있었는데,

상태 출력을 없애거나 다른 상태 결과도 출력하도록 혹은 어떻게든간에 수정한다면 기존의 논리 회로가 달라질것이고

(동일한 16비트 기계어에서 동작가능하도록 변경이야 할수는 있겠지만, 기존의 기계어 규칙으로 동작못하게 바꾸면)

 

우리가 이미 만든 alu로 and 연산을 하든 +1 연산을 하는든 뭔 연산을 할때의 기계어와

수정한 프로세서로하는 and 연산, +1 연산을 하도록 하는 기계어가 다를 것이다.

 

그래서 한 프로세서의 기계어는 다른 프로세서에서 사용할수 없으며

이를 하드웨어에 의존적이다. 호환되지 않는다는 식으로 표현한다.

 

x86이니 x64니 arm이니 코어텍스가 뭐니 하는 내용까지는 제대로 정리해본적이 없어서 설명할수는 없지만

이런 이유로 저급언어는 각 프로세스마다 다르기 때문에 하드웨어에 의존적이라고 한다.

 

 

하지만 고급언어는 어떨까?

처음 C언어나 Java, 파이썬 같은 언어를 공부할 때 이식성이 좋다,

하나의 코드로 여러 플랫폼에서 사용가능하다는 식으로 설명한다.

 

나도 처음에 수업 들을때 아무도 이거에 대해서 제대로 설명해주질 않아서

이식성이 좋다는게 무슨 의미인지는 잘 모르지만, 그냥 그러려니 하고 넘어갔었다.

 

이걸 공부하면서 조금 더 와닿게 이해할수 있는데,

여기서 이식성이 좋다는 말은 어느 환경에서 코드를 작성하던 간에

 

이 프로그램을 리눅스면 리눅스, 윈도우면 윈도우, x86, atmega이든지

각 프로세서가 뭔지 동작 할 환경을 선택해서 컴파일하기 때문에

 

하나의 코드로도 사용하는 컴파일러나 가상머신에 따라

해당 프로세서에 적합하게 기계어/바이너리를 만들어내어

똑같은 코드지만 안드로이드에서도 돌아가고 윈도우에서 돌아가고, 다른 MCU 등

다양한 환경에서 실행가능하게 된다는 의미로 이식성, 호환성이 좋다고 하는 거더라

 

하지만 이런 고급언어와 컴파일러같은걸 생각안하고

직접 어셈블리어든 기계어를 바로 작성 한다면 프로세서 마다 다르게 작성해줘야 할거다.

 

 

 

 

 

 

기계어와 구성 요소

그러면 기계어에는 어떤 것들이 있을까?

이미 알겠지만 이진수로 된 바이너리와 알아볼수 있는 문자로 된(=심볼릭한) 어셈블리어가 있다.

 

저자가 말하기를 기계어로 직접 코딩하는 사람은 많진 않지만 이런 저수준 프로그래밍을 할 수 있다면

우리가 일반적으로 쓰는 고급 언어의 동작을 더 잘 이해하고 효율적으로 작성할수 있게 된다고 한다.

 

이미 앞에서 말한거지만 기계어는 단순한 0과 1의 조합에서 조금 더 정확하게 이야기하면

프로세서가 레지스터와 메모리 사이에 값을 읽고 쓰도록 제어하도록 만든 동작 과정인데

 

메모리는 명령어와 데이터들을 저장하는 장치로, 지난 장에서 메모리를 만들때 봤다시피 한 레지스터마다 고유의 주소를 가지고 있어 우리는 이 주소값을 이용하여 해당 메모리에 접근해 값을 읽거나 덮어씌운다.

 

프로세서(CPU)의 경우 구성요소 중 하나인 ALU를 구현했을때 봤다시피 산술, 논리 연산뿐만이아니라 기억장치에 접근하고, 분기 연산을 수행가능하도록 만든 장치라 할수 있다. 분기 연산이 어떻게 되는지는 아직 보지는 않았지만 2장 과제를 하면서 이 게이트, 저게이트 합쳐서 만든 ALU가 실제로 산술논리 연산하는것을 시뮬레이션을 돌리면서 봤었다.

 

프로세서는 레지스터라고 하는 작은 기억장치를 가지고 있는데, 레지스터의 경우 프로세서가 메모리를 접근해서 값을 읽고 쓴다고 했었지만 프로세서와 메모리 사이의 거리는 사람 기준으로 가깝다 해도 컴퓨터의 동작 클럭을 생각하면 너무 먼 거리고 시간도 오래걸릴것이다. 그래서 다양한 이유로 CPU 내부에 사용할수 있도록 만든 작지만 고속의 기억장치를 레지스터라 한다.

 

잠깐 딴 소리지만 레지스터에 대해서 정리하다가 생각난게 그 동안 이전에 컴공 공부하거나 자격증 준비하면서 SRAM은 정적인 비활성메모리다, DRAM은 동적인 비활성메모리다. 정도로만 이해하고 있다가. 전기 자격증도 준비하고, 폴리텍에서 공부하면서 SRAM과 DRAM에 대해서 조금 더 자세히 알게 되었었는데,

 

SRAM의 경우 플립플롭을 이용해서 만든 저용량이지만 고속의 기억장치로 케시메모리로 사용되고,

DRAM의 경우 캐패시터를 이용해서 만든 기억장치로 케시메모리에 비해 느리지만 큰 용량이라 메인 메모리로 사용된다고한다.

 

 

전처럼 아예 책 번역하는게 아니라 생각 정리하는건데도 생각보다 오래걸린다..

 

 

아무튼 다시 레지스터로 돌아와서 다른 cpu도 비슷하겟지만 hack의 레지스터로는 값을 저장하는 데이터 레지스터와 데이터 값 또는 메모리 주소를 저장하는 어드레스 레지스터 두 종류가 있고, 어드레스 레지스터가 가진 주소값에 접근하여 값을 읽고 쓰게 된다. 지난 렘 시뮬레이션때는 어드레스 레지스터의 값은 아니지만 .cmp 파일에서 지정한 주소에 접근해서 값을 읽고 쓰는고 원하는 결과가 나왔는지 비교하는 시뮬레이션을 돌렸었다.

 

 

 

 

기계어 : 바이너리와 심볼릭 

아까 기계어는 2진수 바이너리와 사람이 알아볼 수 있는 심볼릭(=어셈블리어 같은거) 두 가지가 있다고 했다.

R1 + R2한 결과를 R1에다가 저장하라는 연산을 바이너리랑 심볼릭으로 적어본다고 하자.

 

기계어로 쓸때 덧셈 연산을 6비트로 101011, R1과 R2 레지스터는 각각 5비트로 00001, 00010이라 할때 이들을 합쳐 16비트 바이너리 코드로 만들면 1010110001000001 -> 101011(add)/00010(R2)/00001(R1)이 된다.

심볼릭 하게 쓴다면(어셈블리어는 아니지만, 뭔소린지 알수있게) set R1 = R1 + R2 같은 식으로 될거같다.

 

심볼릭을 보면 R, 1, 2, + 같은 심볼(상징, 부호, 기호라고 하는데 부호 정도로 해야지)이 여러 차례 나오게 될텐데 이들을 어셈블리어라고 하며, 이런 심볼들을 기계어로 바꾸는 번역기가 어셈블리어가 된다.

 

아까도 말한거지만 어떤 하드웨어에 상관없이 사용가능한 고급언어와는 다르게 저급 언어는 ALU나 메모리, 레지스터 등에 따라서 다르다지만 비슷한 CPU 제품군 끼리는 동일한 기계어를 쓸수 있도록 되어있다고 한다.

 

 

 

심볼릭 명령어의 예시

1. 산술 논리 연산

load R1, 17        //17을 R1에 넣어라

add R1, R1, R2    //R1, R2를 더해서 R1에 넣어라

load R1, true      //R1에다가 true를 넣어라

and R1, R1, R2    //R1에다가 R1, R2 and 연산 결과를 넣어라

 

2. 메모리 접근 방법

다른 컴퓨터는 어떤지는 모르지만 hack 컴퓨터의 경우 어드레스 레지스터 A를 이용해서 메모리 접근을 한다.

메모리의 15번지에다가 1을 넣고 싶다면,

 

(1) load A, 15 명령으로 어드레스 레지스터 A에다가 1을 넣고

(어드레스 레지스터 A는 값을 임시로 저장하거나, A에 저장된 값은 메모리 레지스터 M의 주소를 의미한다. A에다가 값을 저장한건지 M의 주소를 담는 건지는 기계어에 따라서 구분하니 뒤에 참고하면 된다.)

 

(2) load M, 1 명령으로 메모리 레지스터 M에다가 1을 대입하면 된다.

왜 M에 저장이 안되고 15번지에 저장이 되었는지는 어드레스 레지스터 A에 입력된 값이 곧 메모리 레지스터 M의 주소이기 때문이다. (꼭 A에 저장된 값이 M의 주소값으로만 사용되는 건 아니다.)

 

3. 흐름 제어와 심볼

우리가 C언어를 공부할 때 goto나 어떤 언어를 공부하던간에 반복문을 배웠을거고 정말 자주 사용한다.

goto나 반복문에서는 어떻게든 간에 goto를 만나거나 반복문의 끝에 도달하면 지정한 곳이나

반복문의 앞으로 돌아갔지만 그러면 어셈블리어에서는 어떻게 사용할까?

 

물리적인 주소를 사용하는 경우와 심볼릭 주소를 사용하는 두가지가 있다.

 

일단 물리적인 주소를 사용하는 경우는 돌아갈 장소의 줄을 직접 써서 가고

9: add R2, R2, 1

...

13: goto 9

 

심볼릭을 사용하는 경우는 알아볼 수 있는 심볼을 써서 해당 심볼로 돌아가게 된다.

add r3, r3, 1

...

(LOOP)

...

goto LOOP

 

그러면 물리적인 주소와 심볼릭한 주소를 사용한 경우의 차이점은 무엇일까? 우리가 프로그래밍 언어를 배우면서 상수보다는 변수 쓰는것과 동일하다고 생각할수 있을거같은데, 

 

간단한 출력 예시를 든다면

print(3)

print(a)

 

위의 경우는 상수 3을 쓴 만큼 동작 중에는 바뀔수가 없고, 3대신 2를 출력하고 싶을때 기존의 3을 모두 2를 고쳐야 하면서 코드 작성할때 이해하거나 나중에 디버깅하는데 불편하다.

 

하지만 아래의 심볼릭으로 a를 썻을때는 a 에 3이 들어있더라도 2를 출력하길 원하면 a에 2를 넣으면 전체 a에 반영이 되기도 하고, 프로그램 실행 중에 a를 변경 할 수 이를 재할당 가능하다고 한다. 그래서 이런 재할당 가능한 심볼릭을 쓰면 물리적인 상수 주소를 썻을때보다 조금더 동적으로 변화하는 상황에 알맞게 동작하며 다루기도 좋다.

 

 

 

 

 

 

 

 

HACK 컴퓨터 구조

앞에 기계어가 뭐고, 고급언어가 뭐고, 심볼릭이니 어셈블리어니 하느라 말이 많았지만 이번에는 HACK 컴퓨터의 기계어에 대해서 정리하려고 하는데 HACK 컴퓨터에 대해서 잠깐 말하자면 앞서 구현한대로 HACK은 16비트 단위로 처리하는 폰 노이만 구조의 16비트 컴퓨터이다. 렘 파트에서 마지막에 16K 구현하길래 HACK의 메모리가 16K인줄 알았지만 32K였다.. 16K까지만 구현한 이유가 있는 곧 나오니도 하고 컴퓨터 구조를 간단하게 복습하면

* 이책 어딘가에 봤었는데 어딘질 모르겟다.

 

대표적인 컴퓨터 구조로는 폰 노이만 구조와 하버드 구조가 있다.

두 구조의 가장 큰 차이점이라면 폰 노이만 구조는 데이터메모리와 명령어 메모리가 붙어있어 동일한 통로(버스)로 되어있고, 하버드 구조는 데이터 메모리와 명령어 메모리가 분리되 서로 다른 버스로 읽고 쓴다고 한다.

 

 

폰 노이만 구조에서는 이전에 하드웨어로 직접 프로그래밍을 했던 것과는 달리 메모리에 저장하여 사용하게 되었지만(내장 프로그래밍) 통로가 하나 뿐이라 명령어와 데이터를 한번에 가져 올수없어 상대적으로 느리며(병목현상),

하버드 구조에서는 명령어 메모리와 데이터 메모리를 분리하여 한번에 두개를 가지고 올수 있어 상대적으로 빠른 처리가 가능하나 더 많은 메모리와 버스로 많은 공간을 차지한다고 한다. 

 

 

 

 

 

 

 

 

 

 

HACK의 메모리와 레지스터 그리고 주소

 

 아무튼 HACK 컴퓨터는 32K의 메모리를 가지고 있는데 폰 노이만 구조로 데이터 메모리와 명령어 메모리가 각각 16K씩 공간을 차지하고 있다. 그래서 지난번에 16K까지만 구현했나 보다. 데이터 값들을 담는 데이터 메모리는 값을 읽고 쓰니 RAM, 명령어 메모리는 여기에 저장된 명령어들을 읽기만 하니 ROM이라고 한다. 근데 이거 RAM 16K로 구현하였으니 휘발성 이니 이것도 RAM이라 하는게 맞지 않나? 아니면 명령어를 읽기만 해서 ROM이라 한건지는 좀더 봐야알거같다.

 

메모리와 레지스터 : 위 그림을 보면 데이터 메모리와 명령어 메모리, 아까 레지스터를 설명할 때 이야기한 어드레스 레지스터 A와 데이터 레지스터 D가 있다. 다시 반복하면 어드레스 레지스터 A는 데이터 레지스터와 동일하게 어느 값을 저장하고 있거나 데이터 메모리와 명령어 메모리의 주소를 저장하는 역활을 하는데, A가 주소를 담고 있을때, M = 0을 하면 RAM의 해당 주소에다가 0을 대입하게 된다. 어드레스 레지스터로 지정된 RAM 상의 레지스터를 데이타 메모리 레지스터 M이라 한다. ROM은 이름 그대로 읽기 전용 기억 장치로 차후에 자세히 설명하고, ROM의 레지스터 값은 현재 명령어를 나타낸다.

 

 어드레스 레지스터 A에다가 18을 넣고자 한다면 어셈블리어로 @18(현재 A는 주소가 아닌 값을 저장하는 역할을한다)

이 값을 데이터 레지스터 D에 담고자 한다면 D = A를 하면 된다.

 

 잠깐 뒷 내용을 당겨와서 하면 hack의 심볼릭과 기계어에는 주소와 값을 지정하기 위한 A명령어, 제어 명령을 나타내는 C명령어 두 종류가 있고, 이 hack 컴퓨터는 폰 노이만 구조라 데이터와 명령어 메모리가 하나의 버스를 공유하고 있다보니 hack 컴퓨터의 기계어는 값을 지정하기위해 A 명령어 먼저, 지정한 값을 사용하기 위해 C명령어가 따라와 같이 사용된다.

 

* hack의 심볼릭 예시 - 데이터 레지스터에다가 18 넣기(A는 값 저장 역할)

@18    // A 명령어

D = A  // C 명령어

 

* 주소 처리 : 어드레스 레지스터를 이용하여 RAM 200번지에다가 값 18을 넣기 예시

@18     // A명령어 - 어드레스 레지스터에다가 데이터 레지스터에 임시로 담을 값을 지정  

D=A     // C명령어 - 어드레스 레지스터 값을 데이터 레지스터에 대입

@200   // A명령어 - 어드레스 레지스터에 200 대입 

M=D    // C명령어 - RAM 200번지에다가 데이터레지스터의 값(18) 대입

 

 위의 예시를 보면 A 명령어 다음에 C 명령어에 데이터 메모리(RAM) 레지스터M이 사용되는 경우 A 명령어는 어드레스 역활을 하고, M이 없는 경우 A는 값을 보관하는 역활을 하고 있다.

 

분기 : hack 심볼릭 언어에서 조건 혹은 무조건적으로 특정 위치로 넘어가기 위해 goto를 하는 JMP 명령이 있으며, 무조건적으로 분기/넘어가고자 한다면 C명렁어로 0;JMP를 사용한다. 만약 무조건 56번지에 간다면 A명령어와 같이사용하여 다음 처럼 쓰면 된다.

 

@56

0;JMP

 

하지만 조건에 따라(0과 같거나 JEQ, 크거나 JGT, 작거나JLT) 분기하고자 하는 경우도 가능하며, 조건이 주어질때의 분기는 다음과 같이 할수있다.

 

@56

D;JEQ //D가 0과 같은 경우 56번지로 점프한다.

 

 

변수 이용하기 : 앞서 A 명령어를 사용할때마다 @{숫자}를 넣어서 사용해왔는데, @뒤에는 상수 뿐만이 아니라 심볼도 올수 있다. 예를들어 심볼/변수 x를 26로 지정하여 사용하고자 한다면

 

@26

D=A

@x

M=D

 

이렇게 하면 심볼 x에다가 값으로 26을 넣게 된다. 그런데 이 변수 x의 주소가 어디인지는 좀 더 봐야 알수있을거같고 일단 변수에다가 이런 식으로 값을 넣을수있다 정도로만 알고 넘어가자.

 

빌트인 심볼 : 그리고 hack 어셈블리에는 기본적으로 미리 지정된 빌트인 심볼 16개가 있는데 R0, R1, ..., R15로 R0은 데이터 메모리 0번지, R1는 1번지 ..., R15는 15번지라 할수 있고, 다음 명령어를 수행하면

 

@14

D=A

@R6

M=D

 

RAM[6]에다가 14를 넣은것과 같다고 생각하면 된다. 이 빌트인 레지스터들을 가상 레지스터라 한다.

 

 

 

 

 

 

A명렁어와 C명령어

 조금 전에 hack 어셈블리어를 설명하기 위해 간단하게 A 명령어와 C 명령어 그리고 두 명령어의 심볼릭에 대해서 설명을 했었는데 어셈블리어는 기계어와 1:1 매칭되는 만큼 심볼릭을 아래와 같이 기계어 바이너리로 바꿀수 있다.

 

 A명령어와 C명령어 구분 : hack은 16비트 컴퓨터라 명령어 길이가 16비트인데, 앞에 시작하는 바이너리 수를 보고 A 명령어인지 C명령어인지 구분할 수 있다. 아 잠깐 놓친건데 기계어 바이너리는 2부분으로 나눌수 있는데 opcode와 오퍼랜드이다.  opcode는 이 명령어가 할 동작을 나타내고, 오퍼랜드는 이 명령어의 값 혹은 주소를 나타낸다.

 

 hack 기계어의 MSB, 즉 가장 좌측 비트가 0인 경우 A 명령어로 보고 나머지 15비트는 오퍼랜드가 되겠다. 만약 MSB가 1인 경우에는 C 명령어가 되는데, 뒤의 따라오는 11은 칸을 채워주기 위함이며 나머지 13개 비트가 오퍼랜드가 된다.

 

 

A 명령어 : A 명령어는 1비트를 이미 사용하고 있으므로 표현 가능한 숫자는 15비트, 부호없이 0 ~ 32767까지 표현이 가능하며 2^15만큼의 주소에 접근할수 있게 되겠다. RAM32K를 구현했지만 RAM과 ROM은 각각 16K이고, 16K = 2^10 * 2^4인데다가 실제로는 ROM 전체를 다 사용하지 않아 A 명령어를 통해 모든 메모리 주소에 접근 가능하다.

 

A 명령어의 역할은 다음 3가지로 정리 할 수 있겠다.

(1) 상수를 넣을 떄          ex) @15    D=A

(2) 주소를 지정할 때       ex) @15    D=A       @56      M=D

(3) 점프 목적지 지정할때  ex) @15 0;JMP

 

 

 

아아아 차라리 시뮬레이션 돌릴 때가 나았지 그냥 안짚고 넘어갈수도 없고 너무 많다ㅠㅠ

 

+ Recent posts