가상 머신 개발 1

HandyPost는 한 도영(HDNua)이 작성하는 포스트 문서입니다.


소스: 

08_Runner1.zip


문서: 

08. 가상 머신 개발 1.pdf



1. 개요

JSCC, 우리의 JavaScript C Compiler를 위한 가상 머신을 개발한다.

 

2. Runner: 작성 시작

Runner는 실행기다따라서 Runner는 7장의 4.2절에서 보인 코드를 실행할 수 있어야 한다그런데 코드를 실행하려면일단 코드가 어떤 내용인지 분석해야 하는 것이 당연하다이 코드를 어떻게 분석해야 할까이 절에서는 코드를 분석하는 방법을 다루고 분석한 코드를 실행하는 코드까지 작성할 것이다. (당연히 HelloHASM.hda는 이 문서에 있으니그냥 읽으면 된다)


2.1) 중요: 규칙은 우리 마음대로 정해도 되는 거에요!

더 이야기를 진행하기 전에 한 번 정리해야겠다. 우리가 지금 어셈블리 언어를 해석하는 방법을 공부하려고 하지만, 실제로 실행기가 분석할 코드의 문법은 우리가 마음대로 정해도 되는 것이다. 여기서 어셈블리 언어와 비슷한 HASM을 선택한 것은, HASM이 고급 언어에 비해 분석이 상대적으로 쉽다는 점, 문법을 우리 마음대로 정의해보면서 정말 컴파일러를 만드는 느낌을 얻을 수 있다는 점, 실제로 어셈블리로 대치하기 쉽다는 점 때문이지, 그렇게 해야 하는 것이 전혀 아니다. 예를 들면, 분석할 수만 있다면 우리는 다음처럼 코드를 작성해도 전혀 상관없다.

++++++++++

[>+++++++>++++++++++>+++>+<<<<-]

>++.

>+.

+++++++.

.

+++.

>++.

<<+++++++++++++++.

>.

+++.

------.

--------.

>+.

>.

참고로 이거 실제로 존재하는 코드다! 관심이 있는 사람이라면 브레인퍽(Brainfuck) 프로그래밍 언어에 대해 찾아보면 재미있을 것이다. (참고로 이 코드는 무려 “Hello World!”를 출력하는 코드라고 한다)

 

2.2) 전략

분석에 앞서, 보기 불편하니 이전에 보인 HASM 뼈대 파일을 가져와서 보자.

HelloHASM.hda

; 데이터 세그먼트의 시작을 나타냅니다.

; nasm과 다르게 segment 지시어를 기록하지 않습니다.

.data

 

; 코드 세그먼트의 시작을 나타냅니다.

.code

_main:

push ebp

mov ebp, esp

 

; put your code here

 

mov esp, ebp

pop ebp

ret

일단 HASM이 어셈블리를 모태로 만들었다니 어셈블리처럼 분석한다고 치자. 이때 고려해야 하는 내용을 나열해보자.

- 세미콜론(;) 기호는 주석을 의미한다.

> 세미콜론 기호는 HASM에서도 마찬가지로 주석을 의미하는 것으로 하자. 그런데 이 코드를 보면 주석은 모두 앞에 있고 코드 뒤에 주석이 붙지는 않았다. 잠깐 생각해보니 일단 주석이야 어느 위치에 써도 보기 좀 불편할 뿐이고 크게 상관이 없을 것 같다. 그러면 규칙을 단순하게 하기 위해, 첫 글자가 세미콜론으로 시작하는 줄은 주석으로 간주하자. (정말 규칙을 우리 맘대로 정하고 있는 것이다)

- ‘.data’, ‘.code’는 세그먼트의 시작을 나타낸다.

> 이것도 공통점이 있다. (.)으로 시작한다는 거다. 그러니 점으로 시작하는 줄은 세그먼트로 간주하자.

- ‘_main:’는 레이블을 나타낸다.

> 이 경우는 맨 뒤에 콜론(:)이 있어서 레이블임을 알 수 있다. 원래 NASM에서는 레이블 뒤에 코드가 올 수 있지만, 안 그래도 딱히 상관없지 않은가? 줄을 나누면 되니 말이다. 그러니 여기서도 생각을 간단하게 하자. 줄의 마지막이 콜론으로 끝나면 레이블로 간주하자.

- push ebp와 같은 명령들이 있다.

> 명령 분석 규칙은 여기서 단정 짓기 힘들어 보인다. 일단 위에서 결정한 세 가지를 먼저 구현해보고, 그 다음에 생각하자.

일단 이 정도로 전략을 세웠다. 바로 작성해보자.

 

2.3) 코드 텍스트 얻기

2.3.1) 준비 코드

먼저 Runner 싱글톤 객체를 위한 준비 코드를 작성하는 것으로 시작하자.

Runner.js

/**

어셈블리 실행기 모듈 Runner를 초기화합니다.

*/

function initRunner() {

var runner = {}; // 싱글톤 객체 생성

// 속성 정의 이전에 정의해야 하는 내용 작성

/**

Runner 모듈의 메서드를 수행할 때 발생하는 예외를 정의합니다.

@param {string} msg

*/

function RunnerException(msg, data) {

this.description = msg;

this.data = data;

}

RunnerException.prototype = new Exception();

RunnerException.prototype.toString = function() {

return 'Runner' + Exception.prototype.toString.call(this);

};

// 필드 및 메서드 정의

/**

지정한 파일을 실행합니다.

@param {string} filename

*/

function run(filename) {

// 파일로부터 텍스트를 획득합니다.

var code = HandyFileSystem.load(filename);

if (code == null) // 텍스트를 획득하지 못한 경우 예외를 발생합니다.

throw new RunnerException("Cannot load file", filename);

// 코드를 더 작성하기 전에 일단 동작하는지 확인합니다.

log(code);

}

// 속성 등록

runner.run = run;

// 전역에 등록

window.RunnerException = RunnerException;

window.Runner = runner;

}

main.html <html.head>

<!-- Handy 프로젝트의 시작점입니다. -->

<script src="handy.js"></script>

 

<!-- 공용 모듈 목록입니다. -->

<script src="common.js"></script>

<script src="StringBuffer.js"></script>

 

<!-- 컴파일러를 위한 모듈 목록입니다. -->

<script src="Runner.js"></script>

 

<script>

function main() {

try {

// 초기화한 Runner 모듈을 이용하여 코드를 실행합니다.

Runner.run('HelloHASM.hda');

} catch (ex) {

if (ex instanceof RunnerException) {

// RunnerException에 대한 처리를 작성합니다.

log('Runner module throws an error');

log(ex);

}

else {

// RunnerException이 아니라면 여기서 처리하지 않습니다.

throw ex;

}

}

}

function init() {

var logStream = document.getElementById('HandyLogStream');

logStream.style.width = '100%';

logStream.style.height = '100%';

initHandyFileSystem();

 

initRunner(); // Runner 모듈 초기화 함수가 추가되었습니다!

}

</script>

실행 결과 (HelloHASM.hda 파일이 경로에 없는 경우)

Runner module throws an error

RunnerException: Cannot load file [HandyHASM.hda]

실행 결과 (HelloHASM.hda 파일이 경로에 있는 경우)

; 데이터 세그먼트의 시작을 나타냅니다.

; nasm과 다르게 segment 지시어를 기록하지 않습니다.

.data

 

; 코드 세그먼트의 시작을 나타냅니다.

.code

_main:

push ebp

mov ebp, esp

 

; put your code here

 

mov esp, ebp

pop ebp

ret

이렇게 일단 파일 읽기가 잘 되는 것을 확인할 수 있다. 이제 코드를 분석할 차례인데, 명령을 분석하기 전에 파악한 세 가지 특성부터 해결해보자. 어렵지 않다.

 

2.3.2) 주석, 빈 줄, 세그먼트, 레이블 처리

다음은 각각의 라인을 획득하고 구문을 분석하는 예제다.

Runner.js (run)

function run(filename) {

// 파일로부터 텍스트를 획득합니다.

var code = HandyFileSystem.load(filename);

if (code == null) // 텍스트를 획득하지 못한 경우 예외를 발생합니다.

throw new RunnerException("Cannot load file", filename);

 

// 획득한 텍스트를 줄 단위로 나눈 배열을 획득합니다.

var lines = String(code).split(NEWLINE);

 

// 얻은 텍스트 줄 배열을 분석합니다.

var segment = null;

for (var i=0, len=lines.length; i<len; ++i) {

// 줄 텍스트를 참조합니다.

var line = lines[i];

 

// 줄의 마지막 문자가 개행 문자라면 제거합니다.

if (line.charAt(line.length - 1) == '\r')

line = line.substr(0, line.length - 1);

 

// 빈 줄이라면 무시합니다.

if (line == '')

continue;

// 첫 문자가 세미콜론(;)이라면 주석이므로 무시합니다.

else if (line.charAt(0) == ';')

continue;

// 첫 문자가 점(.)이라면 세그먼트로 간주합니다.

else if (line.charAt(0) == '.') {

log('segment found: [' + line + ']');

segment = line; // 세그먼트를 보관합니다.

}

// 끝 문자가 콜론(:)이라면 레이블로 간주합니다.

else if (line.charAt(line.length-1) == ':') {

// 세그먼트가 정의되지 않았는데 텍스트가 먼저 발견되었다면 예외 처리합니다.

if (segment == null)

throw new RunnerException("segment is null", {

lineNumber: i,

lineString: line

});

log('label: ' + line);

}

// 나머지는 "세그먼트: 텍스트"의 형식으로 출력합니다.

else {

if (segment == null)

throw new RunnerException("segment is null", {

lineNumber: i,

lineString:line

});

log(segment + ': ' + line);

}

}

log('analyzing complete'); // 분석에 성공한 경우 출력되는 문장입니다.

}

실행 결과

segment found: [.data]

segment found: [.code]

label: _main:

.code: push ebp

.code: mov ebp, esp

.code: mov esp, ebp

.code: pop ebp

.code: ret

analyzing complete

코드가 길고 복잡해보이지만 실제로 보면 정말 별 거 없다. 주석을 자세히 달아놓았으니 읽으면서 분석할 수 있을 것이라고 생각한다. 실행 결과는 우리가 코드를 아주 잘 분석해냈음을 보여준다. 이 정도면 코드 분석이 별 거 아니라는 생각에 뿌듯해 해도 된다.

 

2.3.3) 명령 처리

그럼 이제 코드 영역의 문장을 분석해보자. 코드를 분석하려면 코드가 무엇인지 알아야 한다. 우리는 5장에서 어셈블리 명령어에 대해 공부한 적이 있다. 다시 한 번 보자.

[label:] [mnemonic [operands]] [; comment]

- [label:]

> 명령어의 주소를 획득하고 싶다면 레이블을 등록하여 가져온다.

- mnemonic

> 기계어의 명령 코드(operation code, opcode)에 일대일 대응하는 연상 기호이다.

- [operands]

> 연상 기호의 인자로 넘어가는 피연산자를 말한다.

- [; comment]

> 주석을 제공하여 가독성을 높일 수 있다.

그럼 이를 바탕으로 명령어를 분석할 때 어떤 것을 고려해야 하는지를 우선 나열해보자.

- 주석은 고려할 필요 없다.

> 우리는 이미 가정에서, 첫 줄에 세미콜론이 나타나는 경우만 주석으로 간주하기로 했다.

- 레이블 또한 고려할 필요 없다.

> 이유는 주석의 경우와 같다.

- 그러면 문장에 남는 건 니모닉(mnemonic)과 피연산자(operands).

> 따라서 첫 단어는 반드시 니모닉이다.

> 첫 단어로 끝난다면 피연산자가 없다.

> 두 단어라면 피연산자가 하나다.

> 세 단어라면 피연산자가 두 개이고 가운데에 반점이 들어간다.

이를 바탕으로 코드를 분석해보자. 코드 분석 또한 전략에 따라 단계별로 나눈다. 먼저 첫 단어는 반드시 니모닉이므로, 일단 니모닉으로만 구성된 문장부터 획득해보자.

Runner.js (run.else)

// StringBuffer 객체를 생성합니다.

var buffer = new StringBuffer(line);

 

// 맨 처음 획득하는 토큰은 무조건 니모닉입니다.

// (거기다 무조건 식별자여야 합니다.)

var mne = buffer.get_identifier();

 

// 식별자를 획득하지 못했다면 예외를 발생합니다.

if (mne == null)

// 예외에 대한 정보를 담는 객체를 생성하고 같이 전달합니다.

throw new RunnerException('first word is not mnemonic', {

lineNumber: i, // 오류가 발생한 줄 번호

lineString: line, // 오류가 발생한 줄 텍스트

});

 

// 다음 토큰을 획득합니다.

var left = buffer.get_token();

 

// 다음 토큰이 없다면 단어가 한 개 존재하는 것이므로 획득이 끝납니다.

if (left == null) {

log('mnemonic: [' + mne + ']');

continue;

}

 

// 다음 토큰이 있는 경우의 처리입니다.

...

실행 결과

segment found: [.data]

segment found: [.code]

label: _main:

mnemonic: [ret]

analyzing complete

코드에는 니모닉만 존재하는 명령어는 ret이 유일하다. 실행 결과를 통해 우리가 ret을 잘 획득한 것을 볼 수 있다. 이 코드에서는 ret이 잘 획득된다는 것과, 예외 발생 시에 예외에 대한 정보를 객체를 생성하고 전달한다는 사실만 눈여겨보면 된다.

다음은 피연산자가 한 개인 구문을 분석하는 일인데, 이 또한 어렵지 않다.

Runner.js (run.else)

// 획득한 다음 토큰이 식별자인 경우의 처리입니다.

var operand_type = null;

if (is_fnamch(String(left).charAt(0)) == true) {

operand_type = 'identifier';

}

// 획득한 토큰이 숫자인 경우의 처리입니다.

else if (is_digit(String(left).charAt(0)) == true) {

operand_type = 'number';

}

// 이외의 경우 일단 예외 처리합니다.

else {

throw new RunnerException('invalid left operand', {

lineNumber: i,

lineString: line,

operand: left

});

}

 

// 다음 토큰을 획득합니다. 문법적으로 다음 토큰은 반점(,)이어야 합니다.

var right = buffer.get_token();

 

// 다음 토큰이 없다면 분석이 끝납니다.

if (right == null) {

log('mnemonic ' + operand_type + ': [' + mne + ' ' + left + ']');

continue;

}

 

// 다음 토큰이 있는 경우의 처리입니다.

...

실행 결과

segment found: [.data]

segment found: [.code]

label: _main:

mnemonic identifier: [push ebp]

mnemonic identifier: [mov ebp]

mnemonic identifier: [mov esp]

mnemonic identifier: [pop ebp]

mnemonic: [ret]

analyzing complete

그런데 실행 결과를 보니 우리가 원하는 결과가 나오지 않는다. 피연산자 한 개만 제대로 획득한다면 실행 결과는 다음과 같아야 하는데 말이다. 어떻게 된 일일까?

예상 결과

segment found: [.data]

segment found: [.code]

label: _main:

mnemonic identifier: [push ebp]

mnemonic identifier: [pop ebp]

mnemonic: [ret]

analyzing complete

이런 문제는 아주 골치 아픈데, 어디서 오류가 발생하는지를 찾아내는 것이 매우 어렵기 때문이다. 정말 끔찍하지만 어찌되었든 오류는 발생했고 우리는 이것을 해결해야 한다. 그럼 어떻게 오류를 찾아야 할까?

필자는 이를 원시 디버깅으로 해결했다. 원시 디버깅이란 별 거 없고, 오류가 있을 법한 지점에 일일이 출력문을 삽입해서 결과를 확인하는 일을 말한다(필자가 군대에서 한 일이다). 여기에 한 번 적용해보자. 실행 결과를 보니 피연산자가 2개인 명령어도 포함이 되었는데, 그렇다면 이는 피연산자가 두 개인 상황과 피연산자가 한 개인 상황을 서로 구분하지 못해서 나온 결과일 것이라고 예상해볼 수 있다. 그러니 아마도 left 피연산자의 다음 토큰을 획득하는 구문에서 오류가 발생했을 가능성이 높다. 우리는 다음 구문에서 토큰이 없으면 null, 토큰이 있으면 획득하기를 원하고 있다.

var right = buffer.get_token();

그럼 실제로 그런지 확인해보자. 이 문장 밑에 다음 코드를 추가한다.

log(right);

그리고 그 실행 결과는 다음과 같다.

실행 결과

segment found: [.data]

segment found: [.code]

label: _main:

null

mnemonic identifier: [push ebp]

null

mnemonic identifier: [mov ebp]

null

mnemonic identifier: [mov esp]

null

mnemonic identifier: [pop ebp]

mnemonic: [ret]

analyzing complete

우리의 예상대로라면 결과는 대충 다음과 같아야 한다.

예상 결과

segment found: [.data]

segment found: [.code]

label: _main:

null

mnemonic identifier: [push ebp]

null

mnemonic identifier: [mov ebp]

,

mnemonic identifier: [mov esp]

,

mnemonic identifier: [pop ebp]

mnemonic: [ret]

analyzing complete

즉 피연산자가 두 개인 지점의 다음 토큰은 반드시 반점이어야 한다. 그런데도 불구하고 우리 프로그램은 어째선지 null을 출력하고 있다! 그렇다면 우리는 오류가 buffer.get_token 메서드에 있을 것이라고 짐작해볼 수 있다. StringBuffer.js에서 해당 부분을 보자.

StringBuffer.js (get_token)

StringBuffer.prototype.get_token = function() {

try {

this.trim();

if (this.is_empty())

throw new StringBufferException("Buffer is empty", this);

var ch = this.str[this.idx];

var ss = ''; // 문자열 스트림 생성

if (is_digit(ch)) { // 정수를 발견했다면 정수 획득

ss += this.get_number(); // cout 출력 스트림처럼 사용하면 된다

}

else if (is_fnamch(ch)) { // 식별자 문자를 발견했다면 식별자 획득

ss += this.get_identifier();

}

else { // 이외의 경우 일단 획득

ss += this.get_operator();

}

return ss; // 획득한 문자열을 반환한다

} catch (ex) {

// 토큰 획득에 실패한 경우 null을 반환합니다.

return null;

}

}

반점은 식별자가 아니고, 숫자도 아니다. 따라서 이 경우 else 블록으로 이동하고 this.get_operator 함수를 호출하게 될 것이다. 그렇지? 그럼 결국 우리의 get_operator가 문제가 있다는 거다. 해당 메서드의 정의로 이동하자.

StringBuffer.js (get_operator)

StringBuffer.prototype.get_operator = function() {

this.trim();

if (this.is_empty())

throw new StringBufferException("Buffer is empty", this);

var ch = this.str[this.idx++]; // 현재 문자를 획득하고 포인터를 이동한다

var op = '';

switch (ch) {

case '+': op = ch; break;

case '-': op = ch; break;

case '*': op = ch; break;

case '/': op = ch; break;

default: throw new StringBufferException("invalid operator", this);

}

return op;

}

그리고 이 코드를 자세히 보면 우리의 문제가 어디서 발생했는지 파악할 수 있다. 바로 get_operator 메서드 내에 반점에 대한 처리가 없다는 것이다. switch-case 구문에서는 사칙연산 기호 네 개를 제외한 문자는 모두 예외 처리하도록 구현되어있다! 따라서 반점을 발견하면 get_operaotr는 이를 토큰으로 인지하지 못하고 예외를 발생한 것이다.

따라서 우리는 이 문제를 개선해야 한다. 일단 throw 구문을 없애고 다음과 같이 수정하자.

default: op = ch;

이를 저장하고 프로그램을 실행하면 우리가 원하는 결과를 얻을 수 있다.

실행 결과

segment found: [.data]

segment found: [.code]

label: _main:

null

mnemonic identifier: [push ebp]

,

,

null

mnemonic identifier: [pop ebp]

mnemonic: [ret]

analyzing complete

처음에 예상한 결과와 약간 다르지만, 이는 프로그램이 어떻게 실행되는지를 추적하면 금방 옳다는 결론을 얻을 수 있다.

이제 마지막 단계인 피연산자가 두 개인 경우만 남았다. 지금까지의 내용을 모두 이해했다면 이는 스스로의 힘으로도 작성할 수 있다.

Runner.js (run.else)

// 다음 토큰이 반점이 아니라면 예외 처리합니다.

if (right != ',')

throw new RunnerException(

'syntax error; right operand must be after comma(,)', {

lineNumber: i,

lineString: line,

operand: right

});

 

// 먼저 획득한 토큰이 반점이므로 토큰을 한 번 더 획득하여

// 오른쪽 피연산자를 획득합니다.

right = buffer.get_token();

 

// 반점이 있는데 피연산자 획득에 실패한 경우 예외 처리합니다.

if (right == null)

throw new RunnerException(

'syntax error; cannot find right operand', {

lineNumber: i,

lineString: line,

});

 

// mnemonic operand, operand 코드를 획득했으므로 처리합니다.

log('mnemonic operand, operand: ['

+ mne + '] [' + left + '], [' + right + ']');

실행 결과

segment found: [.data]

segment found: [.code]

label: _main:

mnemonic identifier: [push ebp]

mnemonic operand, operand: [mov] [ebp], [esp]

mnemonic operand, operand: [mov] [esp], [ebp]

mnemonic identifier: [pop ebp]

mnemonic: [ret]

analyzing complete

실행 결과는 너무나도 깔끔하다.

 

2.4) 니모닉 구현 시도

코드를 각각의 부분으로 뜯어내는 것은 완벽하게 성공했다. 그럼 이제 작성한 코드를 의미 있게 만들어야 할 것이다. 코드를 의미 있게 만드는 것은 니모닉이고, 우리는 명령어를 니모닉과 피연산자로 나누는 방법까지 알았다. 나머지는 획득한 니모닉에 맞는 처리를 하면 되는 것 밖에 없다.

HelloHASM.hda 소스 코드에는 총 네 개의 니모닉 push, mov, pop, ret이 있다. 그러니 이들 각각에 대해 처리를 해보자. 획득한 니모닉은 문자열이니 다음과 같은 if-else를 구성하면 될 것이라고 예상해볼 수 있다.

if (mne == 'push') { /* push routine */ }

else if (mne == 'mov') { /* mov routine */ }

else if (mne == 'pop') { /* pop routine */ }

else if (mne == 'ret') { /* ret routine */ }

else { throw new RunnerException('invalid mnemonic'); }

그런데 이를 실제 코드에 적용하면 생각보다 못생긴 코드 덩어리가 나온다. 코드가 너무 못생겨서 여기에 온전히 싣기 싫으므로 어떻게 되는지를 부분부분 잘라 보여주겠다. 전체 코드는 첨부 파일을 참조하라.

Runner.js (analysis_try)

...

if (left == null) {

// 니모닉만으로 구성된 명령에 대한 조건 분기 (피연산자 0)

if (mne == 'ret') {

log('ret routine');

}

else {

throw new RunnerException('invalid mnemonic', {

lineNumber: i, lineString: line

});

}

continue;

}

 

...

if (right == null) {

// 니모닉, 피연산자 구성의 명령에 대한 조건 분기 (피연산자 1)

if (mne == 'push') {

log('push routine');

}

else if (mne == 'pop') {

log('pop routine');

}

else {

throw new RunnerException('invalid mnemonic', {

lineNumber: i, lineString: line

});

}

continue;

}

 

...

// 니모닉, 피연산자 구성의 명령에 대한 조건 분기 (피연산자 2)

if (mne == 'mov') {

log('mov routine');

}

else {

throw new RunnerException('invalid mnemonic', {

lineNumber: i, lineString: line

});

}

실행 결과

segment found: [.data]

segment found: [.code]

label: _main:

push routine

mov routine

mov routine

pop routine

ret routine

analyzing complete

! 결과는 잘 나오지만 정말 너무 못생겼다! 같은 위치에 있어야 할 코드가 떨어져있고 같은 예외를 세 군데에 나누어 처리한 데다, run 메서드의 길이마저 끔찍하게 길어졌다! 이 총체적 난국을 어떻게 해결해야 하는가?

문제는 이것만이 아니다. 이것을 해결하기 전에 push, mov, pop, ret 각각의 모듈을 어떻게 구현하는지에 대해 감이 오는가? 아니, 여기서 가장 기본 명령인 mov를 구현하는 방법은 떠오르는가?

이는 사실 책을 보던지 고민을 해야 하는 부분이다. 이쯤에서 절을 나누어, 어떤 것을 배워야 더 진행할 수 있는지를 알아보고자 한다.

 

3. 고려되지 않은 것

이 절의 내용은 4장의 어셈블리 튜토리얼 1”과 같이 공부하면 효과가 좋다.

 

3.1) 레지스터

첫 줄의 push 명령을 보면 ebp 레지스터를 인자로 넘긴다. 여기서 우리가 레지스터를 아직 코드로 정의하지 않았다는 사실을 알 수 있다. 이전 문서에서 레지스터를, 단순히 CPU가 사용하는 변수라고 설명한 바 있다. 여기서도 같은 접근 방법을 쓴다. ebp, eax와 같은 레지스터를 모두 변수로 정의하고, 이 레지스터를 사용하는 구문은 이렇게 정의된 변수를 사용할 것이다.

예를 들어보자. 우리가 다음 구문을 분석했다고 치자.

mov edx, 4 ; edx = 4

mov eax, 3 ; eax = 3

add eax, edx ; eax += edx

edx4, eax3을 대입한 후 eaxedx를 더한다. 그런데 마지막 문장이 정상적으로 실행되려면 적어도 eax의 값인 3edx의 값인 4는 당연히 저장하고 있어야 한다. 바로 이것이 목표다. 우리는 각각의 니모닉을 만났을 때 다음과 연산되도록 코드를 작성할 것이다.

if (mne == 'mov') { // 니모닉 mov에 대해

if (left == 'eax') { // lefteax 레지스터라면

if (right == 'edx') { // rightedx 레지스터라면

// 레지스터 left에 레지스터 right의 값을 대입합니다.

Register.eax = Register.edx;

 

}

...

}

...

}

else if (mne == 'add') { // 니모닉 add에 대해

if (left == 'eax') { // lefteax 레지스터라면

if (right == 'edx') { // rightedx 레지스터라면

// 레지스터 left에 레지스터 right의 값을 더합니다.

Register.eax += Register.edx;

 

}

...

}

...

}

좀 더 일반적인 형식으로 작성하면 다음과 같이 된다.

if (mne == 'mov') { // 니모닉 mov에 대해

if (is_register(left)) { // left가 레지스터라면

if (is_register(right)) { // right 또한 레지스터라면

 

// 레지스터 left에 레지스터 right의 값을 대입합니다.

Register[left] = Register[right];

 

}

...

}

...

}

else if (mne == 'add') { // 니모닉 add에 대해

if (is_register(left)) { // left가 레지스터라면

if (is_register(right)) { // right 또한 레지스터라면

 

// 레지스터 left에 레지스터 right의 값을 더합니다.

Register[left] += Register[right];

 

}

...

}

...

}

이에 대한 논의는 4절에서 하는 것으로 하자.

 

3.2) 메모리

push와 같은 명령은 값을 메모리에 푸시 하는 명령이다. 즉 값을 푸시 할 메모리가 있어야 한다. 더 정확히 얘기하면, Memory 모듈은 가상 메모리를 표현하는 모듈이기 때문에 메모리에 접근하여 값을 가져오거나 특정 메모리에 값을 기록하는 메서드가 반드시 필요하다. 그런데 다음에 보일 메서드의 정의를 이해하려면 우리가 메모리를 어떻게 접근할지를 먼저 이해해야 한다. 그림을 통해 이를 알아보자.

 

3.2.1) 기본

메모리는 바이트 배열이다. 이를 그림으로 표현하면 다음과 같다.



우리가 생각하는 바이트가 그대로 구현된 것이다. 정말 단순하다! 그럼 이제 메모리를 조작해보자. 메모리 조작이라고 해서 별 게 아니다. 그냥 임의의 번지에 값을 기록하면 그것이 메모리를 조작한 것이다. 예를 들어 8번지의 메모리부터 4바이트만큼을 0으로 채운다면 이렇게 될 것이다.



여기서 5번지의 메모리부터 2바이트를 0으로 채운다면 이렇게 될 것이다.



한 번만 더 해보자. 우리는 기본적으로 16진법을 이용하여 수를 표시한다. 이진수 111116진수로 F와 같으므로, 바이트 하나가 모두 1로 차 있다면 16진수로는 FF가 된다(1바이트 = 8비트, 11111111 = FF). 따라서 6번지부터 3바이트의 모든 비트를 1로 초기화하면 다음 결과를 얻는다.



이제 메모리에 대한 접근 방법을 알 수 있을 것이므로, 두 가지 개념에 대해서 추가로 설명한 후에 이에 대한 구현을 보이기로 하겠다.

 

3.2.2) 메모리 포인터

여기서는 메모리 모듈이, 내부적으로 자신의 위치를 기록하고 있다고 가정한다. bytePtr라는 필드가 메모리 내에 있어서 다음의 형태로 그림이 그려진다고 생각하면 된다.



만일 현재 메모리에서부터 1바이트를 0으로 기록한다고 하면, bytePtr가 가리키는 메모리의 값이 0으로 바뀌고 bytePtr1만큼 이동한다.



여기서 4바이트를 모두 FF로 초기화하면 bytePtr가 가리키는 메모리의 값이 모두 FF가 되고 bytePtr가 이동한다.



개념은 단순하다. 다만 0번지를 사용 불가능한 번지로 만들기 위해 bytePtr의 초기 값은 0은 아니라고 가정하자.


이 정도로 메모리 포인터는 이해할 수 있다.

 

3.2.3) 엔디안

다음과 같이 빈 메모리가 있다고 하자.



이 메모리에 16진수 정수 0x1234를 기록하고 싶다. 어떻게 해야 할까?

먼저 메모리를 어느 위치에 기록해야 할지를 결정해야 한다. 여기서는 4번지에 기록한다고 하자. 또한, 정수를 몇 바이트에 기록할 것인지 결정해야 한다. 0x12341바이트가 저장할 수 있는 정수의 최댓값인 255를 넘어서므로, 1바이트만으로 기록할 수 없다. 0x12341바이트에 담기에는 크지만 2바이트 이상의 모든 메모리에 충분히 담을 수 있다. 이 경우 2바이트만큼의 메모리에 값을 쓴다면 그 결과는 다음과 같을 것이다.



그런데 컴퓨터 과학자들은 메모리에 값을 쓰는 방법을 두 가지로 나눠서 생각했다. 예를 들어보자. 주어진 정수 0x1234는 이렇게 기록하는 것이 상식적이다.



그런데 컴퓨터 과학자들은 수를 이렇게 거꾸로 기록하는 방식도 생각해냈다.



그리고 이 두 방식 중 전자를 빅 엔디안(Big endian), 후자를 리틀 엔디안(Little endian)이라고 한다. 엔디안(endian)은 컴퓨터에 값을 기록하는 순서를 말한다. 빅 엔디안은 큰 자릿수부터, 리틀 엔디안은 작은 자릿수부터 메모리에 채우기 때문에 이러한 이름이 붙었다. 실제로 인텔 계열의 CPU들은 수를 기록할 때 리틀 엔디안 방식을 사용한다. 이는 이렇게 수를 저장할 때 연산이 쉬워진다는 장점이 있기 때문인데 이에 대해서는 후에 다루겠다. 그렇다고 세상 모든 컴퓨터가 리틀 엔디안으로 수를 기록하는 것도 아니다. 빅 엔디안도 물론 장점이 있으며 소켓 통신의 경우는 모든 데이터를 빅 엔디안 방식으로 주고받도록 표준으로 정해져있다. 이에 대해서는 엔디안을 키워드로 검색해보기 바란다.

 

4. Runner: 빠진 것을 고려하고

4.1) fetch, decode, execute

무엇이 고려되지 않았는지는 3절에서 다루었다. 하지만 각각의 구현에 고려해야 하는 것을 알았다고 쳐도 아까 본 코드는 너무 난잡하다. 우선 이걸 정리하자.

run의 몸체가 아주 길어졌으므로, 이를 함수로 나누어 관리하면 편할 것이라는 생각이 든다. 바로 나눠보자. 각 명령어의 실행은 다음 세 단계를 거친다.

1. fetch: 텍스트 덩어리로부터 우리가 분석할 문장을 가져온다.

2. decode: 가져온 문장을 분석하여 실행 가능한 형태의 정보로 변환한다.

3. execute: 분석이 완료된 정보를 바탕으로 명령을 실행한다.

즉 우리의 run 메서드는 반복문이 있고, 반복문 안에 fetch, decode, execute가 순서대로 호출되는 구조를 가지게 된다. 이를 반영하여 코드를 작성하면 run 메서드는 다음과 같이 정리가 된다.

Runner.js (run_fix.run)

function run(filename) {

// 파일로부터 텍스트를 획득합니다.

var code = HandyFileSystem.load(filename);

if (code == null) // 텍스트를 획득하지 못한 경우 예외를 발생합니다.

throw new RunnerException("Cannot load file", filename);

 

// 획득한 텍스트를 줄 단위로 나눈 배열을 획득합니다.

var segment = null;

var lines = String(code).split(NEWLINE);

for (var i=0, len=lines.length; i<len; ++i) {

try {

// 분석할 i번째 줄을 가져옵니다.

var line = fetch(lines, i);

 

// Runner에 보내는 지시어(directive)를 처리합니다.

if (line == '') continue; // 빈 줄이라면 무시합니다.

else if (line.charAt(0) == ';') continue; // 주석을 무시합니다.

else if (line.charAt(0) == '.') { // 세그먼트를 처리합니다.

segment = line; // 세그먼트를 보관합니다.

log('segment found: [' + line + ']');

continue;

}

 

// 지시어가 아닌 일반 명령으로 확인되면 분석합니다.

if (line.charAt(line.length-1) == ':') { // 레이블을 처리합니다.

// 세그먼트가 정의되지 않았는데 텍스트가 먼저 발견되었다면 예외 처리합니다.

if (segment == null) // 같이 넘기던 인자가 사라졌습니다!

throw new RunnerException("segment is null");

log('label: ' + line);

continue; // 레이블은 처리가 끝나면 지나갑니다.

}

 

// fetch를 통해 가져온 정보를 분석합니다.

var info = decode(line);

 

// decode를 통해 분석된 정보를 바탕으로 명령을 실행합니다.

execute(info);

}

catch (ex) {

if (ex instanceof RunnerException) {

log('RunnerException occurred');

log(i + ': [' + lines[i] + '] ' + ex.description);

}

else {

throw ex;

}

}

}

log('analyzing complete'); // 분석에 성공한 경우 출력되는 문장입니다.

}

이전의 run 메서드보다 아주 예쁜 코드가 되었다. 어느 부분이 코드를 획득하고, 어느 부분이 코드를 분석하는지, 어느 부분이 코드를 실행하며 또 어느 부분이 예외 처리인지에 대한 내용이 한 눈에 보인다.

추가로 여기에는 개선된 사항도 있는데, 바로 RunnerException의 인자로 줄 번호와 줄 텍스트를 넘기지 않아도 된다는 사실이다. 줄 번호와 텍스트는 사실 줄을 분석할 때 반복문에서 가지고 있던 정보이므로, 필요하다면 여기서 참조하면 되기 때문에 반복문 내의 catch 영역에서 이를 사용하는 식으로 개선했다.

그럼 이제 추가한 fetch, decode, execute를 볼 차례다. 먼저 fetch를 보자.

Runner.js (fetch)

/**

텍스트를 가져옵니다.

@param {Array} lines

@param {Number} lineNumber

@return {string}

*/

function fetch(lines, lineNumber) {

var line = lines[lineNumber]; // 줄 번호에 해당하는 줄을 획득합니다.

return line.trim(); // 양 끝에 공백을 없애 정리한 줄을 반환합니다.

}

fetchtrim이라는 메서드가 있다는 사실만 알면 볼 것이 없다. decode를 보자.

Runner.js (decode)

/**

코드를 분석합니다.

@param {string} line

@return {InstructionInfo}

*/

function decode(line) {

// StringBuffer 객체를 생성하고 line으로 초기화합니다.

var buffer = new StringBuffer(line);

 

// 가장 처음에 획득하는 단어는 반드시 니모닉입니다.

var mne = buffer.get_token();

// 니모닉 획득에 실패한 경우 예외 처리합니다.

if (is_fnamch(mne) == false)

throw new RunnerException('invalid mnemonic');

 

// 다음 토큰 획득을 시도합니다.

var left = buffer.get_token();

var right = null;

if (left != null) { // 다음 토큰이 있는 경우의 처리

 

// 피연산자가 두 개인지 확인하기 위해 토큰 획득을 시도합니다.

right = buffer.get_token();

if (right != null) { // 다음 토큰이 있는 경우

if (right != ',') // 반점이 아니라면 HASM 문법 위반입니다.

throw new RunnerException

('syntax error; right operand must be after comma(,)');

 

// 오른쪽 피연산자 획득

right = buffer.get_token();

if (right == null) // 획득 실패 시 문법을 위반한 것으로 간주합니다.

throw new RunnerException

('syntax error; cannot find right operand');

}

 

// 다음 토큰이 없다면 rightnull이고

// 그렇지 않으면 다음 토큰 문자열이 된다

}

 

// 획득한 코드 정보를 담는 객체를 생성하고 반환합니다.

var info = { mnemonic: mne, left: left, right: right };

return info;

}

decode의 경우는 내용이 길지만, 아까 다루었던 내용이라 주석만으로 이해할 수 있을 것이다. 여기서 중요한 건 마지막 부분에 있는, 코드 정보 객체를 생성하는 부분이다.

이 코드에서는 니모닉을 먼저 획득한 다음 left에 다음 토큰을 획득한다. 그런데 다음 토큰이 있는 경우의 처리는 되었지만 없는 경우에 대해 따로 처리가 없다. 왜 그런가? 그 이유는 필자가 코드 정보를 객체에 담을 때, 피연산자가 없다면 기본적으로 null을 대입하기로 결정했기 때문이다. 예를 들어보자. push 니모닉은 하나의 피연산자를 가지므로 leftpush의 인자고 rightnull이 된다. mov 니모닉은 두 개의 피연산자를 가지므로 leftright 모두 null이 아니다. 마지막의 ret 니모닉은 피연산자가 하나도 없기 때문에 leftright가 모두 null이 된다. 이는 get_token 메서드가, 토큰을 획득할 수 없는 경우 기본적으로 null을 반환하도록 정의된 것에서 아이디어를 찾을 수 있다.

마지막으로, 분석된 명령을 실행하는 execute 메서드다.

Runner.js (execute)

/**

분석된 코드를 실행합니다.

@param {InstructionInfo}

*/

function execute(info) {

var mne = info.mnemonic; var left = info.left; var right = info.right;

log('instruction: [' + mne + '][' + left + '][' + right + ']');

}

아직 각각의 명령을 구현하지 않았으므로, 일단 코드 분석이 정상적으로 진행되었는지를 확인하는 코드를 삽입했다. 그리고 이렇게 작성한 네 함수를 모두 종합하여 실행한 결과는 다음과 같다.

실행 결과

segment found: [.data]

segment found: [.code]

label: _main:

instruction: [push][ebp][null]

instruction: [mov][ebp][esp]

instruction: [mov][esp][ebp]

instruction: [pop][ebp][null]

instruction: [ret][null][null]

analyzing complete

결과가 아주 잘 출력되었다. 시간이 된다면, 이렇게 분석한 코드를 실제 어셈블리 코드처럼 다시 출력하도록 코드를 변경해보기 바란다. 꽤 재미있을 것이다.

 

4.2) 메모리 반영

3절에서는 고려되지 않은 것에서 레지스터를 먼저 다룬 다음 메모리를 이야기했지만, 여기서는 아무래도 메모리를 먼저 구현하는 것이 이해적 측면에서 도움이 될 것 같다. Runner에서 했던 것처럼 모듈을 작성하기 위한 코드를 먼저 준비하자.

Memory.js

/**

메모리 관리 모듈 Memory를 초기화합니다.

*/

function initMemory() {

var memory = {}; // 싱글톤 객체 생성

 

// 상수 정의

var MAX_MEMORY_SIZ = 100;

 

// 속성 정의 이전에 정의해야 하는 내용 작성

/**

Memory 모듈의 메서드를 수행할 때 발생하는 예외를 정의합니다.

@param {string} msg

*/

function MemoryException(msg, data) {

this.description = msg;

this.data = data;

}

MemoryException.prototype = new Exception();

MemoryException.prototype.toString = function() {

return 'Memory' + Exception.prototype.toString.call(this);

};

 

// 필드 정의

memory.byteArr = new Array(MAX_MEMORY_SIZ); // 메모리 바이트 배열 정의

memory.bytePtr = 0; // 메모리를 가리키는 바이트 포인터 정의

/* 구현 */

// 전역에 싱글톤 등록

window.MemoryException = MemoryException;

window.Memory = memory;

}

이전과 똑같다. 빈 싱글톤 객체를 생성하고 예외 형식을 정의한다. 다만 구현 이전에 필드의 정의가 있고, 그 멤버로 3절에서 설명한 bytePtr가 있음에만 주목하면 된다.

이어서 메서드를 작성하겠다. 특정 메모리에 값을 기록하는 set_byte 메서드를 보자.

Memory.js (set_byte)

/**

value를 바이트로 변환한 값을 index 주소에 기록합니다.

@param {number} value

@param {number} index

*/

function set_byte(value, index) {

this.byteArr[index] = (value & 0xFF);

}

바이트 배열 byteArr의 주소 indexvalue를 바이트로 변환한 값을 기록하고 있다. & 연산자를 이용하여 16진수 FF와 비트 & 연산을 함으로써 value를 바이트로 변환할 수 있다. FF11111111임을 생각하면 이해할 수 있을 것이다.

나머지는 해당 인덱스에서 값을 가져오는 get_byte 메서드를 구현한 것인데 어렵지 않다.

Memory.js (set_byte)

/**

바이트 배열에서 index 주소에 있는 바이트를 획득합니다.

@param {number} index

@return {byte}

*/

function get_byte(index) {

return this.byteArr[index];

}

다음에는 read_byte, write_byte 메서드의 정의인데, bytePtr를 이용한다는 점을 빼면 get, set 메서드와 전혀 다르지 않다.

Memory.js (read_byte, write_byte)

/**

바이트 배열에서 바이트를 획득합니다.

@return {number}

*/

function read_byte() {

return this.byteArr[this.bytePtr++];

}

/**

value를 바이트로 변환한 값을 기록합니다.

@param {number} value

*/

function write_byte(value) {

this.byteArr[this.bytePtr++] = (value & 0xFF);

}

그리고 이를 테스트하는 코드는 다음과 같다.

main.html <html.head>

...

<!-- 컴파일러를 위한 모듈 목록입니다. -->

<script src="Memory.js"></script>

<script src="Runner.js"></script>

<script>

// Memory 테스트 예제입니다.

function main() {

try {

var value;

Memory.bytePtr = 4; // bytePtr를 초기화합니다.

 

// 메모리에 값을 기록합니다.

Memory.write_byte(10); Memory.write_byte(20);

 

Memory.bytePtr = 4; // bytePtr를 다시 초기화합니다.

 

// 메모리에서 값을 가져옵니다.

value = Memory.read_byte(); log(value);

value = Memory.read_byte(); log(value);

 

// 메모리에서 주소를 이용하여 직접 값을 가져옵니다.

value = Memory.get_byte(4); log(value);

 

// 현재 바이트 포인터가 가리키는 위치에 값을 기록합니다.

Memory.set_byte(30, Memory.bytePtr);

 

// 바이트 포인터가 가리키는 값을 읽습니다.

value = Memory.read_byte(); log(value);

} catch (ex) {

if (ex instanceof MemoryException) {

// MemoryException에 대한 처리를 작성합니다.

log('Memory module throws an error');

log(ex);

}

else {

// MemoryException이 아니라면 여기서 처리하지 않습니다.

throw ex;

}

}

}

function init() {

...

initMemory();

initRunner();

}

</script>

실행 결과

10

20

10

30

사실 이 실행 결과는 단번에 받아들이기 힘들 수 있다. 여기서 한 번만 더 정리를 해주면 앞으로 이런 문제가 닥쳤을 때 이 글을 읽는 본인이 스스로 문제를 해결할 수 있으리라 생각하므로 귀찮음을 이겨내고 마지막으로 한 번만 정리하겠다.

Memory.bytePtr = 4; 구문을 통해 바이트 포인터를 초기화하면 다음 상태가 된다.



write_byte 메서드를 통해 메모리에 값을 두 번 기록한다.





그러면 값 104번지에, 205번지에 저장되게 된다. 맞지?

여기서 다시 한 번 Memory.bytePtr = 4;를 실행해서 바이트 포인터를 옮긴다.



그리고 거기서 read_byte를 통해 바이트 하나를 읽으면 다음과 같이 된다. 여기서 10을 출력한다.



다시 한 번 read_byte를 호출하면 이렇게 된다. 여기서 20을 출력한다.



get_byte는 바이트 포인터를 조작하지 않는다. 따라서 실행한다고 메모리에 변화가 생기지 않는다. 여기서 10을 출력한다.

set_byte를 이용하여 현재 바이트 포인터가 가리키는 위치에 30을 기록한다.



그리고 다시 read_byte를 호출하여 값을 읽는다. 여기서 30을 출력한다.



이로써 이 예제 코드는 끝이 난다.

많은 내용을 했지만 아직 메모리에 내용 하나가 남아있다. 3절에서 엔디안에 대해 다루었는데, 이것만 처리를 끝내면 메모리 모듈의 작성이 완료된다. 이 예제에서는 리틀 엔디안 방식으로 수를 저장할 것인데, 크게 어렵지 않으므로 빠르게 보고 다음으로 넘어가자.

3절에서 말했지만, 정수를 기록하려면 기록 위치와 기록할 값, 사용할 메모리의 크기를 알아야 한다. 바이트의 경우는 기록 위치와 값은 인자로 넘겼고, 사용할 메모리의 크기는 1바이트로 정해져있어서 이것이 해결되었다. 워드(2바이트)와 더블 워드(4바이트)의 경우도 다르지 않지만 코드를 볼 필요는 있다. 다음은 특정 주소에서 워드 값을 읽고 쓰는 read_word, write_word 메서드를 작성한 것이다.

Memory.js (read_word)

/**

바이트 배열에서 워드를 획득합니다.

@return {number}

*/

function read_word() {

var byte1 = this.read_byte(); // 첫 번째 바이트 XY을 획득합니다.

var byte2 = this.read_byte(); // 두 번째 바이트 ZW를 획득합니다.

var word = ((byte2 << 8) | byte1); // 결과 값은 ZWXY입니다.

return word;

}

/**

value를 워드로 변환한 값을 기록합니다.

@param {number} value

*/

function write_word(value) {

this.write_byte(value & 0xFF); // 첫 번째 바이트를 기록합니다.

value >>= 8; // 다음에 기록할 바이트로 value 값을 옮깁니다.

this.write_byte(value & 0xFF); // 두 번째 바이트를 기록합니다.

}

처음에는 스스로 이에 대해 연구하게끔 하려고 했으나, 한 도영은 친절하므로 이를 그냥 두고 볼 수 없다. 따라서 이것도 설명하고자 한다.

write_word(0x8F4B) 메서드가 호출된 경우를 먼저 보자. 이진수 10001111 01001011(16진수 8F4B)이 변수 value에 저장되어있는 상황을 가정하자. 그림으로는 다음과 같다.



여기서 write_byte(value & 0xFF)가 수행되면서 먼저 value & 0xFF를 계산하여 다음을 얻는다.



그리고 write_byte(0x4B)가 수행되면 다음과 같이 된다.



그 다음 value >>= 8; 문장을 수행하여, 다음에 획득할 바이트를 첫 번째 바이트로 맞춘다.



이 과정을 반복한다.





이렇게 0x8F4B를 리틀 엔디언으로 기록하는 과정은 완료가 된다. 그럼 이제 read_word를 통해 이렇게 기록한 정수를 읽어보자. 4번지로 바이트 포인터를 옮기면 이렇게 될 것이다.



현재 위치에서 바이트 하나를 획득하고 byte1에 저장한다.



똑같이 바이트 하나를 획득하고 byte2에 저장한다.



마지막으로 수를 모두 합치면 된다.



크게 어렵지 않으므로, 그림을 보면서 공부하면 이해할 수 있을 것이다. 나머지는 이전 예제를 조합하여 스스로 작성할 수 있으므로, 여기에 재차 코드를 싣지 않겠다. 혹 코드가 궁금하다면 첨부한 소스 파일을 참조하라. 그리고 사실 이 예제에서 MemoryException 클래스는 정의만 하고 사용하지 않았는데, 이렇게 필요할 것처럼 보이는 정의가 실제 구현에서는 쓸모없어지는 일도 은근히 생기니 지금은 신경 쓰지 않아도 좋다. 다음은 Memory 모듈의 메서드를 정리한 것이다.

Memory

// 필드

Array byteArr; // 메모리를 표현하는 바이트 배열을 정의합니다.

Number bytePtr; // 메모리를 가리키는 바이트 포인터를 정의합니다.

// 메서드

byte read_byte(); // 바이트를 획득합니다.

void write_byte(value); // value의 첫 바이트를 기록합니다.

byte get_byte(index); // index 주소에 있는 바이트를 획득합니다.

void set_byte(value, index); // value의 첫 바이트를 index 주소에 기록합니다.

word read_word(); // 워드를 획득합니다.

void write_word(value); // value의 첫 워드를 기록합니다.

word get_word(index); // index 주소에 있는 워드를 획득합니다.

void set_word(value, index); // value의 첫 워드를 index 주소에 기록합니다.

dword read_dword(); // 더블 워드를 획득합니다.

void write_dword(value); // value의 첫 더블 워드를 기록합니다.

dword get_dword(index); // index 주소에 있는 더블 워드를 획득합니다.

void set_dword(value, index); // value의 첫 더블 워드를 index 주소에 기록합니다.

이와 같이 Memory 모듈을 작성할 수 있었다.

 

4.3) 레지스터 반영

이제 레지스터를 생각해보자. 레지스터! 여러 차례 CPU가 사용하는 변수라고 설명했다. 그러니까 우리도 변수처럼 쓰면 된다. 어떻게? 사실 전역에 레지스터 변수를 var로 몽땅 선언하고 필요할 때 이걸 써도 우리는 레지스터를 사용하는 흉내를 낼 수 있다. 하지만 그거보다는 더 멋지게 만들어놓는 것이 후에 관리하기 편할 것 같다.

그럼 레지스터는 어떻게 표현해야 하는가? 이를 생각하기에 앞서, 레지스터의 위치는 어디로 결정하는 것이 옳은가? 메모리와 관계가 있으므로 Memory 모듈에 넣어야 하는가? 명령을 분석하는 실행기인 Runner 모듈에 넣어야 하는가? 아니면 두 모듈과 다른 별개의 Register 모듈을 만드는 것이 좋을까?

사실 이 경우는 지금 당장 어느 하나가 정답이라고 분명히 결정하기 어렵다. 이유는 간단하다. 아직 안 해봤으니까. 그러면 일단 마음이 가는 모듈에 한 번 구현을 해본 다음, 후에 수정하고 싶을 때 다른 위치로 옮기면 그만이다. 여기서는 이전에 그래왔던 것처럼 Register라는 모듈을 새롭게 만들고 이를 통해 레지스터를 관리하도록 하는 것이 좋겠다(이 절을 읽기 전에 먼저 Register.js라는 파일을 만든 사람이 있을까 하여 배려하는 것이다). Register.js 모듈을 만들고 준비해놓자.

Register.js

/**

레지스터 관리 모듈 Register를 초기화합니다.

*/

function initRegister() {

var register = {}; // 싱글톤 객체 생성

/* ...? */

// 전역에 등록

window.Register = register;

}

그래, 일단 Register 모듈은 만들었다. 그런데 이제 뭐 해야 할까?

이에 대해서는 3절에서 밝힌 바 있다. 우리는 레지스터가 CPU가 사용하는 변수라고 생각하고 있고, 그래서 여기서도 레지스터를 변수처럼 쓰고 싶어한다고 계속 말하고 있다. 그리고 Register.eax와 같이 레지스터에 직접 접근하는 코드를 예제로 보여 우리가 무엇을 개발해야 하는지에 대한 힌트도 제공했다.

그럼 남은 건 이들을 조합하는 것뿐이다. Register.eax, Register.ebx와 같은 식으로 레지스터에 접근하려면 이들은 모두 Register 모듈의 필드여야 한다. 코드로 작성하자.

Register.js

function initRegister() {

var register = {}; // 싱글톤 객체 생성

// 필드를 정의합니다.

register.eax = 0;

register.ebx = 0;

register.ecx = 0;

register.edx = 0;

register.ebp = 0;

register.esp = 0;

// 전역에 등록

window.Register = register;

}

? 이게 끝인가? eflagseip 같은 용도 모를 레지스터 같은 것이 나왔던 것 같기는 한데 나중에 추가하면 그만일 것 같다. 코드에도 ebp, esp밖에 안 나왔고, eax 같은 레지스터는 뭐 당연히 필요할 것 같으니까 일단 넣어봤다. 그런데 지금 이 상태로도 별 부족한 느낌이 당장 들지 않는다!

그러면 일단 이것만으로 구현이 가능한지를 알아보는 것이 우선이다. push, mov, pop, ret의 네 니모닉을 구현하는 데 성공하면 이것으로 HelloHASM을 분석하는 데 무리가 없는지 알아보자. HelloHASM의 코드 영역을 다시 보이겠다. 주석은 분석할 필요가 없으니 뺐다.

HelloHASM.hda (code)

...

_main:

push ebp

mov ebp, esp

mov esp, ebp

pop ebp

ret

그럼 우리에게는 레이블을 제외하고 5개의 명령이 남는다. 분석해보자.

명령 획득은 fetch, 명령 분석은 decode가 한다. 따라서 우리는 execute 메서드만 수정하면 된다. push에 대한 처리를 추가해보자.

Runner.js (execute)

function execute(info) {

var mne = info.mnemonic, left = info.left, right = info.right;

log('instruction: [' + mne + '][' + left + '][' + right + ']');

 

if (mne == 'push') { // push 니모닉에 대한 처리

if (left == null) // 피연산자가 없으면 예외 처리합니다.

throw new RunnerException('invalid operand');

 

/* ...? */

}

}

코드를 보면 알겠지만, push는 완전히 구현되지 않았다. 이를 직접 구현해가면서 다른 것들을 어떻게 구현할 지에 대한 감각을 키워보자.

 

4.3.1) push의 행동

push ebp는 무엇을 하는 명령인지 그림으로 그려볼 수 있는가? 이를 그릴 수 있다면 4장을 아주 열심히 공부한 것이며, 더 깊게 생각하면 필자의 도움 없이 이 문제를 해결할 수 있다. 이것을 직접 한 사람이라면 자신의 생각을 검증하는 용도로, 아니라면 그냥 필자의 구현을 보고 이해하면 된다.

4장의 CIL 코드를 다시 가져오자. 내용 이해를 위해 필요 없는 부분은 제외했다.

ProcNaked.c (modified)

#include "CIL.h"

STRING sHelloWorld = "Hello, world!\n";

 

PROC(main)

// naked_proc 프로시저를 호출합니다.

CALL(naked_proc)

ENDP

 

// NAKED 프로시저를 정의합니다.

PROC_NAKED(naked_proc)

PUSH(bp) // 이전 스택 시작 주소를 푸시하여 보관합니다.

MOVL(bp, sp) // 스택 시작 주소를 현재 스택 포인터로 맞춥니다.

 

MOVL(sp, bp) // 현재 스택 포인터를 스택 시작 주소로 맞춥니다.

POP(bp) // 보관했던 이전 스택 시작 주소를 불러옵니다.

RET() // 복귀 지점으로 돌아갑니다.

ENDP_NAKED

그리고 각각의 상태를 그림으로 표현하면 다음과 같이 진행이 된다고 설명했었다.





그리고 호출한 함수를 정리하는 코드가 있지만, 당장 중요한 부분은 PUSH(bp)MOVL(bp, sp)로 이루어진, 호출한 함수의 스택을 형성하는 부분이다. CALL에서 PUSH로 넘어갈 때 어떤 일이 일어났다고 생각하는가? 이를 추론할 수 있으면 위에서 제시했던 문제는 해결이 된다.

답을 바로 말하겠다. 이 사이에서는 다음의 명령들이 일어났다.

1. sp의 값이 4만큼 줄었다.

2. sp가 가리키는 위치의 메모리에서 4바이트만큼을 bp로 채웠다.

이 두 가지가 끝이다. 그런데 그 전에 또 고려된 것이 있다. 바로 bpsp가 초기 값을 가지고 있었다는 사실이다. PROC(main) 부분의 코드를 보라. spbp는 기본적으로 메모리의 끝을 가리키고 있었다. 이 별것 아닌 것처럼 보이는 사실이 고려되지 않으면 우리는 프로젝트를 진행할 수 없게 된다!

 

4.3.2) Memory와의 결합

그럼 이를 모두 반영하여 push를 작성하겠다. 이를 위해 Memory의 코드가 바뀐다.

Memory.js

...

/**

메모리의 전체 크기를 반환합니다.

@return {number}

*/

function GetMaxMemorySize() {

return MAX_MEMORY_SIZ;

}

 

// 싱글톤에 속성 등록

...

memory.MaxSize = GetMaxMemorySize;

...

그리고 컴파일러를 위한 모듈 목록에 방금 작성한 Register를 추가한다.

main.html <html.head>

<!-- 컴파일러를 위한 모듈 목록입니다. -->

<script src="Memory.js"></script>

<script src="Register.js"></script>

<script src="Runner.js"></script>

그리고 ebp, esp 레지스터를 방금 말했듯 메모리의 마지막으로 설정한다.

Register.js

...

register.ebp = Memory.MaxSize();

register.esp = Memory.MaxSize();

...

이제 Runner, 방금 그림을 통해 보인 내용을 코드로 작성하면 된다. 출력문은 주석 처리했다.

Runner.js

/**

분석된 코드를 실행합니다.

@param {InstructionInfo}

*/

function execute(info) {

var mne = info.mnemonic, left = info.left, right = info.right;

// log('instruction: [' + mne + '][' + left + '][' + right + ']');

 

if (mne == 'push') { // push 니모닉에 대한 처리

if (left == null) // 피연산자가 없으면 예외 처리합니다.

throw new RunnerException('invalid operand');

 

// esp의 값이 4만큼 줄었다.

Register.esp -= 4;

 

// esp가 가리키는 위치의 메모리에서 4바이트만큼을 ebp로 채웠다.

Memory.set_dword(Register.ebp, Register.esp);

}

}

다음은 Memory에 해당 값이 제대로 들어갔는지를 확인하기 위해 테스트 코드를 작성한 것이다.

main.html <html.head.script>

// Memory 테스트 예제입니다.

function main() {

try {

// run 이전 고려사항:

// 1. Runner가 프로그램을 실행합니다.

// 2. Memory 모듈의 bytePtr0으로 초기화되어있었습니다.

// 3. ebp, espMemory.MAX_MEMORY_SIZ였습니다.

Runner.run('HelloHASM.hda');

 

// run 이후 고려사항:

// 1. push만 작성했으므로 push만 메모리에 반영되어있습니다.

// 2. ebp는 변하지 않았습니다.

// 3. esp4만큼 줄었습니다.

// 4. esp가 가리키는 위치의 메모리는 ebp의 값과 같습니다.

var dword = Memory.get_dword(Register.esp);

log(dword);

 

} catch (ex) {

if (ex instanceof RunnerException) {

// RunnerException에 대한 처리를 작성합니다.

log('Runner module throws an error');

log(ex);

}

else {

// RunnerException이 아니라면 여기서 처리하지 않습니다.

throw ex;

}

}

}

function init() {

...

initMemory();

initRegister();

initRunner();

}

실행 결과

segment found: [.data]

segment found: [.code]

label: _main:

analyzing complete

100

마지막 줄에 ebp의 값인 100이 제대로 찍혔음을 확인할 수 있다. 이렇게 push는 완성이 된다.

 

5. 모듈 개선

5.1) 메모리가 제대로 바뀐 게 맞나요?

그런데 지금 우리가 제대로 하고 있는 걸까? 100이야 초기화가 안 된 메모리에서 아무 것이나 읽어내서 나온 숫자일 수도 있지 않은가? 우리가 얻어낸 100이 정말로 우리가 원하는 위치에서 읽어낸 결과가 맞다고 보장할 수 있는가? 이 문제를 해결하기 위해 이 절에서는 현재 메모리의 상태를 보여주는 show 메서드를 작성할 것이다.

C++ 소스 코드를 하나 작성하자. 9번 줄에 적당히 중단점을 설정한다.

#include <iostream>

int main(void) {

int arr[] = {

8, 9, 10, 11, // 10진수 표기

0x0A, 0x0b, 0x12, 0x34, // 16진수 표기

'A', 'B', 'C', 'D', // 아스키 코드 값 표기

65, 66, 67, 68 // 아스키 코드 값(정수) 표기

};

for (int i = 0; i < 10; ++i) {

std::cout << arr[i] << std::endl;

}

return 0;

}

Microsoft의 훌륭한 도구인 Visual Studio로 프로그램을 디버깅하면 이런 창을 볼 수 있다.



디버깅 모드로 프로그램을 실행하고 [디버그>>메모리>메모리 1] 메뉴를 통해 현재 메모리 상태를 확인할 수 있도록 되어있다. 이 창을 처음 봤다면 많이 당황했겠지만, 배열 arr가 메모리에 표현되어있는 게 전부다. 나머지는 대부분 지금 상황과 관련 없는 쓰레기 메모리 값이다. 이것이 arr 배열이라는 사실을 보다 자세히 보여주겠다.



int의 크기인 4바이트만큼, 리틀 엔디안 방식으로 수가 기록되어있는 것을 볼 수 있다. 이렇듯 이 창은 아주 단순한 사실을 보여준다. 왼쪽은 메모리의 주소값, 오른쪽은 해당 값의 아스키 코드 값을 보여준다. 우리도 이런 식으로 메모리를 보여준다면 한층 프로그램을 작성하기 쉬워질 것이다.

그럼 이런 식으로 메모리를 보여주는 코드를 작성해보자. 그러니까 결과가 이렇게 나오면 좋겠다.

예상 결과

0x00000000 | 0000000000000000 | ................

0x00000010 | 0000000000000000 | ................

0x00000020 | 0000000000000000 | ................

0x00000030 | 0000000000000000 | ................

0x00000040 | 0000000000000000 | ................

0x00000050 | 0000000000000000 | ................

0x00000060 | 0000 | ................

그러면 다음의 값은 필요하다.

var address_begin; // 보여주기 시작하려는 메모리의 시작 주소

var address; // 현재 보여주려는 메모리의 주소

var byte = Memory.get_byte(address); // 해당 메모리의 바이트 값

var letter = String.fromCharCode(byte); // 코드 값으로부터 문자를 획득

그리고 다음은 이를 바탕으로 메모리의 바이트를 보여주는 show 메서드를 작성한 것이다.

Memory.js (show)

/**

메모리를 출력합니다.

*/

function show() {

var mem_str = ''; // 모든 메모리 텍스트를 보관하는 변수

var mem_addr = ''; // 메모리 텍스트 줄을 보관하는 변수

var mem_byte = ''; // 바이트 텍스트를 보관하는 변수

var mem_char = ''; // 아스키 문자 텍스트를 보관하는 변수

 

var addr_beg = 0; // 보여주기 시작하려는 메모리의 시작 주소

var addr_end = Memory.MaxSize() - 1; // 메모리의 끝 주소

 

// 모든 메모리의 바이트를 탐색합니다.

for (var i=0; (addr_beg+i <= addr_end); ++i) {

 

// 주소를 얻습니다.

var addr = addr_beg + i;

 

if (i % 16 == 0) { // 출력 중인 바이트의 인덱스가 16의 배수라면

// 개행합니다.

mem_str += (mem_addr + ' | ' + mem_byte + ' | ' + mem_char + NEWLINE);

 

// 표현하려는 주소 값을 16진수로 표시합니다.

var addr_str = Number(addr).toString(16);

 

// 다른 값을 초기화합니다.

mem_addr = addr_str;

mem_byte = '';

mem_char = '';

}

 

// 획득한 주소의 바이트 값을 얻습니다.

var byte = Memory.get_byte(addr);

 

// 초기화되지 않은 메모리라면 0을 대입합니다.

if (byte == undefined)

byte = 0;

 

// 바이트 값으로부터 문자를 획득합니다.

var ascii = String.fromCharCode(byte);

 

// 널 문자인 경우 점으로 대체합니다.

if (ascii == '\0')

ascii = '.';

 

// 각각의 문자열에 추가합니다.

mem_byte += Number(byte).toString(16);

mem_char += ascii;

}

 

// 마지막 줄이 출력되지 않았다면 출력합니다.

if (addr_beg + i == addr_end + 1) {

mem_str += (mem_addr + ' | ' + mem_byte + ' | ' + mem_char);

}

log(mem_str);

}

main.html <html.head.script> (main.try)

Memory.show();

실행 결과

| |

0 | 0000000000000000 | ................

10 | 0000000000000000 | ................

20 | 0000000000000000 | ................

30 | 0000000000000000 | ................

40 | 0000000000000000 | ................

50 | 0000000000000000 | ................

60 | 0000 | ....

결과가 나오기는 했지만 그렇게 이쁘게 출력되지 않았다. 이 문제를 어떻게 해결해야 하는가? 일단 위에 빈 세로 줄이 나온 것은 그렇다 치더라도 작성한 줄을 정렬하기는 해야 할 것 같다. 사실 지금 작성한 코드는 주석을 달긴 했지만 읽으라고 만든 코드는 아니기 때문에 읽는 데 시간을 들일 필요는 없다. 그저 이대로 그냥 show를 구현하는 것은 문제가 있다는 사실을 알면 된다. 형식을 맞추려면 어떻게 해야겠는가?

필자는 그 답으로 Handy.format 메서드를 제안한다.

 

5.2) Handy 모듈 개선

Handy.format? 일단 Handyformat 사이에 점이 들어갔으니 formatHandy라는 객체의 멤버로 보인다. 정확한 판단이다! 우리는 Handy라는 객체를 만들고 여기에 format이라는 메서드를 넣을 것이다. 그리고 여기서 log 메서드도 개선하여 보다 사용하기 편하게 만들고자 한다.

그럼 바로 시작하자. 일단 Handy는 싱글톤 객체로 구현할 것이다. 따라서 여기에도 다음과 같은 코드가 들어가게 된다.

handy.js (initHandy)

/**

Handy 라이브러리 기본 객체인 Handy 싱글톤 객체를 초기화합니다.

*/

function initHandy() {

var handy = {}; // 빈 객체 생성

/* ... */

window.Handy = handy;

}

그리고 여기서 format이라는 메서드를 구현할 것이다. 다만 오류를 줄이기 위해 일단 바깥에서, main.html 파일에서 구현해본 다음 initHandy 메서드 안으로 옮기는 식으로 구현하자.

 

5.2.1) Handy.format

C에는 printf 함수가 있어서 형식화된 문자열을 매우 편리하게 출력할 수 있다. 만약 우리가 작성한 show 메서드를 printf로 작성할 수 있었다면 출력하기 편했을 것이다. Handy.format은 바로 이를 위한 메서드다. 이 메서드는 printf와 비슷한 방법으로 형식화된 문자열을 반환해준다. 한 번 사용해보고 나면 Handy.format의 매력에 빠져 헤어 나오지 못할 것이라고 장담한다.

그래, 이제 format이 무엇인지 알았으니 이를 구현할 차례다. Handy.format은 이렇게 사용한다.

var str = Handy.format('Hello, world!');

log(str); // Hello, world!

str = Handy.format('Hello, %s, %d and %c!', 'your_name', 100, 65);

log(str); // Hello, your_name, 100 and A!

log('Hello! %04b * %-4d is %04x', 3, 6, 3 * 6); // Hello, 0011 + 6___ is __12

맨 마지막을 보면 log 메서드도 printf처럼 사용하고 있음을 알 수 있다. 더 자세히 보자.

1. %d는 정수, %s는 문자열이다. %c는 문자 코드 값에 해당하는 문자를 출력한다.

2. Handy.format은 문자열을 반환한다. 인자의 수는 제한이 없고 첫 문자열은 형식 문자열이다.

3. %b는 이진수, %x16진수로 수를 출력한다.

4. % 다음에 -가 붙으면 왼쪽 정렬, 0이 붙으면 빈 칸에 0이 붙는다. 기본적으로 오른쪽 정렬이다.

이 정도면 Handy.format을 구현하기 위해 어떤 것이 고려되어야 하는지 알 수 있을 것이다. printf를 구현해본 경험이 있다면 편하게 구현할 수 있겠지만, 없어도 별 상관없다. 여기서 설명하는 내용은 모두 Runner를 구현하는 것과 직접적으로 관련이 있기 때문에 반드시 이해하고 넘어가야 한다(Memory 모듈의 show를 구현하는 데 그치지 않는다).

먼저 format을 가장 단순하게 구현하자. 다음과 같을 것이다.

main.html <html.head.script> (format)

/**

형식화된 문자열을 반환합니다.

@param {string} fmt

*/

function format(fmt) {

// fmt를 그대로 반환합니다.

return fmt;

}

이것만으로는 format은 무의미하다. 우리는 printf처럼 가변 인자를 이용하여 출력을 하고 싶다. 이를 위해 우리는 JavaScript의 함수가 기본적으로 가지는 필드인 argument를 이용해야 한다.

예제를 보자. 다음은 넘겨받은 모든 정수의 합을 반환하는 sum을 구현한 것이다.

main.html <html.head.script> (sum)

/**

넘겨받은 모든 정수의 합을 반환합니다.

@return {number}

*/

function sum() {

 

// 인자의 정보를 담는 리스트 객체를 획득합니다.

var args = sum.arguments;

 

// 인자의 갯수를 획득합니다.

var argc = args.length;

 

// 주어진 모든 인자의 합을 구합니다.

var s = 0;

for (var i=0; i<argc; ++i) {

s += args[i]; // i번째 인자에 접근합니다.

}

 

// 정수의 합을 반환합니다.

return s;

}

main.html <html.head.script> (main)

function main() {

log(sum(1,2,3));

log(sum(1,2,3,4,5));

log(sum(1,2,3,4,5,6,7,8,9,10));

}

실행 결과

6

15

55

함수인 sum을 마치 객체처럼 접근했다. 사실 6장에서 설명한 것처럼, JavaScript의 모든 것은 객체다. 지금 보인 바와 같이 함수조차도 말이다. 메서드는 각자의 인자에 대한 정보를 가지고 있으며, 이를 이용해 넘겨받은 인자를 일괄적으로 처리하는 것이 가능해진다.

이를 이용하면 우리가 코드를 어떻게 변경해야 하는지 감이 오게 된다. 일단 연습을 해보자. 인자로 넘긴 모든 문자열을 더하여 반환하는 cat 함수를 작성해보라. 즉 다음과 같이 코드를 작성하면,

main.html <html.head.script> (main)

...

function main() {

log(cat('hell', 'oven', 'us'));

}

...

실행 결과는 다음과 같아야 한다.

예상 결과

hellovenus

필자의 구현은 다음과 같다.

main.html <html.head.script> (cat)

/**

문자열을 모두 더하여 반환합니다.

@return {string}

*/

function cat() {

var args = cat.arguments;

var argc = args.length;

var s = ''; // 문자열을 반환하므로 초기값도 문자열입니다.

for (var i=0; i<argc; ++i) {

s += args[i]; // i번째 인자에 접근합니다.

}

return s;

}

코드를 보면 알 수 있는데, 실제로 sum과 다른 것이 하나도 없다! 이제 이를 이용해 format을 구현하는 방법을 생각해보자.

printf는 형식적으로 값을 출력할 때 언제나 앞에 퍼센트(%) 기호가 붙었다. 그리고 그렇지 않은 문자는 그냥 그대로 출력된다. 따라서 우리는 format을 만들 때 이 내용을 고려해야 한다.

1. %를 만났다면 그에 대해 처리합니다.

2. %가 아니라면 그냥 출력합니다.

바로 반영해보자. printf는 어려운 것이 없고, 그냥 % 기호가 나타나면 그에 대해 처리할 뿐이다.

main.html <html.head.script> (format)

function format(fmt) {

// 반환할 문자열입니다.

var result = '';

 

// fmt의 모든 문자에 대해 반복문을 구성합니다.

for (var i=0, len=fmt.length; i<len; ++i) {

 

// i번째 문자를 획득합니다.

var c = fmt.charAt(i);

 

// 퍼센트 기호라면 특별하게 처리합니다.

if (c == '%') {

result += '<value>'; // 퍼센트 기호라면 '<value>' 문자열을 붙입니다.

}

// 퍼센트 기호가 아니라면 그냥 붙입니다.

else {

result += c;

}

}

// 문자열을 반환합니다.

return result;

}

그리고 이렇게 작성된 format을 테스트해보면 다음과 같다.

main.html <html.head.script> (main)

function main() {

log(format('hello, %s!', 'test'));

}

실행 결과

hello, <value>s!

<value>를 붙이도록 했는데 뒤에 s가 그냥 붙었다. 왜 그런지 이해하겠는가? 이는 우리가 퍼센트 기호를 읽은 후에 그냥 다음 문자를 획득했기 때문이다. 즉 퍼센트 기호를 해석했다면 뒤따라오는 문자까지 해석해야 올바르게 문자열을 해석했다고 할 수 있다.

이에 대한 처리는 어렵지 않다. 일단 정수(d), 문자열(s)와 퍼센트 기호 자체를 출력하기 위한 %만을 고려하여 작성해보자. 이는 다음과 같다.

main.html <html.head.script> (format.case%)

...

// 퍼센트 기호라면 특별하게 처리합니다.

if (c == '%') {

 

// i번째 문자는 이미 해석했으므로 i를 한 칸 옮깁니다.

++i;

 

// % 기호 다음의 문자를 가져옵니다.

var nextChar = fmt.charAt(i);

 

// 가져온 문자를 기준으로 조건 분기합니다.

var value;

switch (nextChar) {

case 'd': // 정수를 출력합니다.

value = '<integer>'; // <integer> 문자열을 붙입니다.

break;

case 's': // 문자열을 출력합니다.

value = '<string>'; // <integer> 문자열을 붙입니다.

break;

case '%': // 퍼센트를 출력합니다.

value = '%'; // <integer> 문자열을 붙입니다.

break;

}

 

// 해석한 문자열을 붙입니다.

result += value;

}

...

main.html <html.head.script> (main)

function main() {

log(format('hello, %s, %d and %%!', 'test', 10));

}

실행 결과

hello, <string>, <integer> and %!

결과는 나름 만족스럽다. 하지만 문자열과 정수에 대해서는 잘 처리되지 않았다. 사실 우리는 formatarguments 속성에 접근한 적도 없으니 이는 당연한 것이다. 그럼 이제 이에 대해 처리해보자.

main.html <html.head.script> (format)

...

var params = format.arguments; // format 인수 목록입니다.

 

// fmt의 모든 문자에 대해 반복문을 구성합니다.

// pi는 인자의 인덱스를 의미합니다.

// 0번 인덱스는 fmt이므로 그 다음 인자부터 고려합니다.

for (var i=0, pi=1, len=fmt.length; i<len; ++i) {

var c = fmt.charAt(i); // i번째 문자를 획득합니다.

if (c == '%') { // 퍼센트 기호라면 특별하게 처리합니다.

// % 기호 다음의 문자를 가져옵니다.

var nextChar = fmt.charAt(++i);

 

// 가져온 문자를 기준으로 조건 분기합니다.

var value;

switch (nextChar) {

case 'd': // 정수를 출력합니다.

 

// pi번째 인수를 획득합니다.

value = params[pi];

 

// 인덱스 번호를 증가시킵니다.

++pi;

 

break;

case 's': // 문자열을 출력합니다.

 

// 한 문장으로 합친 것입니다.

value = params[pi++];

break;

 

case '%': // 퍼센트를 출력합니다.

value = '%';

break;

}

...

실행 결과

hello, test, 10 and %!

좋아, 이제 우리가 원하는 출력이 제대로 되고 있음을 알 수 있다. 그럼 이제 남은 건 이진수와 16진수, -0을 이용한 공백 및 간격에 대한 처리뿐이다. 사실 이 부분은 어렵다기보다는 그냥 헷갈리는 것이 전부이므로 코드만 보이고 설명은 더 하지 않겠다.

main.html <html.head.script> (format)

/**

형식화된 문자열을 반환합니다.

@param {string} fmt

@return {string}

*/

function format(fmt) {

var value;

var result = ''; // 반환할 문자열입니다.

var params = format.arguments; // format 인수 목록입니다.

 

// fmt의 모든 문자에 대해 반복문을 구성합니다.

// pi는 인자의 인덱스를 의미합니다.

// 0번 인덱스는 fmt이므로 그 다음 인자부터 고려합니다.

var i, pi, len;

for (i=0, pi=1, len=fmt.length; i<len; ++i) {

var c = fmt.charAt(i); // i번째 문자를 획득합니다.

 

if (c == '%') { // 퍼센트 기호라면 특별하게 처리합니다.

// % 기호 다음의 문자를 가져옵니다.

var nextChar = fmt.charAt(++i);

 

// 옵션을 먼저 획득합니다.

var showLeft = false; // 기본적으로 오른쪽 정렬합니다.

var fillZero = false; // 기본적으로 여백을 공백으로 채웁니다.

switch (nextChar) {

case '0': // 이 옵션이 적용되면 여백을 0으로 채웁니다.

fillZero = true;

// 다음 문자를 다시 얻습니다.

nextChar = fmt.charAt(++i);

break;

case '-': // 이 옵션이 적용되면 왼쪽 정렬합니다.

showLeft = true;

// 다음 문자를 다시 얻습니다.

nextChar = fmt.charAt(++i);

break;

}

 

// 여백의 크기를 획득합니다.

// nextChar가 숫자인 동안 크기를 획득합니다.

var width = 0;

while ('0123456789'.indexOf(nextChar) >= 0) {

// 길이를 획득합니다. get_number 구현을 참조하십시오.

width = width * 10 + (nextChar - '0');

// 다음 문자를 다시 얻습니다.

nextChar = fmt.charAt(++i);

}

 

// 가져온 문자를 기준으로 조건 분기합니다.

switch (nextChar) {

case 'b': // 2진수를 출력합니다.

value = params[pi++].toString(2);

break;

case 'x': // 16진수를 출력합니다.

value = params[pi++].toString(16);

break;

case 'd': // 정수를 출력합니다.

value = params[pi++].toString();

break;

case 's': // 문자열을 출력합니다.

value = params[pi++];

break;

case 'c': // 문자 코드에 해당하는 문자를 출력합니다.

var param = params[pi++];

// 문자열이라면 첫 번째 글자만 획득합니다.

if (typeof(param) == 'string') {

value = param.charAt(0);

}

else { // 그 외의 경우 정수로 간주합니다.

value = String.fromCharCode(param);

}

break;

case '%': // 퍼센트를 출력합니다.

value = '%';

break;

}

 

// 정상적으로 값을 획득하지 못한 경우 undefined를 출력합니다.

if (value == undefined) {

value = 'undefined';

}

 

// 옵션을 문자열에 반영합니다.

if (showLeft) { // 왼쪽 정렬 옵션입니다.

var space = ''; // 여백 문자열입니다.

// 주어진 크기만큼 여백을 확보합니다.

for (var si=0, slen=width - value.length; si<slen; ++si)

space += ' ';

// 옵션을 반영합니다.

value = value + space;

}

else if (fillZero) { // 공백을 0으로 채우는 옵션입니다.

var space = ''; // 여백 문자열입니다.

// 주어진 크기만큼 여백을 확보합니다.

for (var si=0, slen=width - value.length; si<slen; ++si)

space += '0';

// 옵션을 반영합니다.

value = space + value;

}

else if (width > 0) { // 두 옵션 없이 크기만 주어진 경우입니다.

var space = ''; // 여백 문자열입니다.

// 주어진 크기만큼 여백을 확보합니다.

for (var si=0, slen=width - value.length; si<slen; ++si)

space += ' ';

// 옵션을 반영합니다.

value = space + value;

}

 

// 해석한 문자열을 붙입니다.

result += value;

}

// 퍼센트 기호가 아니라면 그냥 붙입니다.

else {

result += c;

}

}

 

// 문자열을 반환합니다.

return result;

}

main.html <html.head.script> (main)

// format 테스트 예제입니다.

function main() {

log(format(' ----- score -----'));

log(format('%-5x|%5s|%5s|%5c', 1, 'A', 'B', 67));

log(format('%-5d|%5b|%5d|%5x', 2, 5, 5, 5));

log(format('%-5d|%05b|%05d|%05x', 4, 5, 5, 5));

}

실행 결과



이제 이렇게 만든 formatHandy 내부로 옮기면 Handy.format의 작성은 완료가 된다.

 

5.2.2) log 메서드 개선

바로 위에서 main 함수에 log(format(...));과 같은 식으로 코드를 작성했는데, 보기 안 좋고 불편하다. 그러니 기왕 할 거 log 메서드도 저렇게 사용할 수 있도록 해보자. 그런데 보통 log 메서드를 저렇게 바꾸자고 하면 코드를 통째로 복사해서 붙여 넣는 식을 생각하기 쉽다. 물론 그렇게 할 것이었다면 여기서 소개하지 않을 것이고, 우리는 좀 더 현명한 방법으로 log를 개선하면서 가변 인자에 대한 감각을 더 늘려볼 생각이다.

먼저 log 메서드의 정의로 돌아가보자.

handy.js (log)

function log(message) {

var logStream = document.getElementById('HandyLogStream');

logStream.value += (message + NEWLINE);

}

log를 가변 인자 메서드처럼 사용하려고 한다면, 당연히 logarguments 인자는 호출해야 한다. 그런데 log가 넘겨받은 인자 리스트를 그냥 format에 가져다주면 바로 끝나지 않을까? 그렇다면 log의 파라미터 목록을 format에 어떻게 넘겨야 하는가?

필자는 arguments 객체를 넘기기 위해 format 메서드를 vformat으로 분리하는 방법을 생각해냈다. 이를 이용하면 log 메서드와 format 메서드의 내부는 다음과 같이 바뀐다.

handy.js (log)

/**

로그 스트림에 문자열을 출력합니다.

@param {string} fmt

*/

function log(fmt) {

var result; // 로그 스트림에 출력할 문자열을 보관합니다.

if (log.arguments.length == 0) { // 인자의 수가 0이라면

result = ''; // 개행만 합니다.

}

else if (log.arguments.length > 1) { // 인자의 수가 1보다 크면 format을 호출합니다.

var params = Handy.toArray(log.arguments, 1); // 인자 목록 배열을 획득합니다.

result = Handy.vformat(fmt, params); // 형식 문자열과 인자 목록을 같이 넘깁니다.

}

else { // 인자의 수가 1이면 그냥 출력합니다.

result = fmt;

}

// 로그 스트림에 출력합니다.

var logStream = document.getElementById('HandyLogStream');

logStream.value += (result + NEWLINE);

}

handy.js (format)

function format(fmt) {

// 1번 인수부터 끝 인수까지의 인수들을 배열로 만들어서 vformat의 인자로 넘깁니다.

return Handy.vformat(fmt, toArray(format.arguments, 1));

}

그리고 vformat의 구현은, 다음과 같이 두 번째 인자로 인수의 목록을 받는 형식으로 바뀐다.

handy.js (vformat)

...

function vformat(fmt, params) {

var value;

var result = ''; // 반환할 문자열입니다.

 

// fmt의 모든 문자에 대해 반복문을 구성합니다.

// pi는 인자의 인덱스를 의미합니다.

var i, pi, len;

for (i=0, pi=0, len=fmt.length; i<len; ++i) { // pi0으로 바뀌었습니다!

...

마지막으로 toArray의 정의를 보면 다음과 같다.

handy.js (vformat)

/**

리스트 객체의 부분집합을 배열 객체로 변환하여 반환합니다.

@param {number} startIndex

@param {number} endIndex

@return {Array}

*/

function toArray(list, startIndex, endIndex) {

// 시작 인덱스와 끝 인덱스의 기본 값을 설정합니다.

startIndex = getValid(startIndex, 0);

endIndex = getValid(endIndex, list.length-1);

 

// 배열을 복사합니다.

var arr = new Array();

for (var i=startIndex; i<=endIndex; ++i) {

arr[arr.length] = (list[i]);

}

 

// 복사한 배열을 반환합니다.

return arr;

}

그리고 이를 이용해 Memory.show 메서드를 다음과 같이 수정한다(2군데다).

Memory.js (show)

...

// mem_str += (mem_addr + ' | ' + mem_byte + ' | ' + mem_char + NEWLINE);

mem_str += Handy.format

('0x%08x | %-32s | %-16s \n', mem_addr, mem_byte, mem_char);

...

mem_byte += Handy.format('%02x', byte);

...

그리고 코드를 실행하면 다음 결과를 얻는다.

실행 결과



이제야 우리는 메모리를 제대로 표현하게 되었다. 맨 위에 있는 줄이 거슬린다면 지금의 당신은 스스로 해결할 수 있을 것이다. 마지막으로 main.html 파일을 다음과 같이 수정하면 된다.

main.html <html.body.script>

try {

initHandy();

init();

main();

} catch (ex) {

if (ex instanceof Exception) {

log(ex);

} else {

// Exception 예외 객체가 아니라면 여기서 처리하지 않습니다.

throw ex;

}

}

이전 문서에서 얘기하지 않았지만, 사실 try-catch로 문장을 감싸지 않는 것이 디버깅엔 더 편할 것이다. Chrome의 특성 상 오류가 발생한 지점에 대한 정보가, try-catch가 없는 경우에 더 잘 나온다.

 

5.3) 출력 스트림 분리

우리는 메모리의 상태를 봐야 한다. 그런데 우리는 또한, 실행한 프로그램의 결과도 봐야 한다. 즉 우리는 한 화면에 두 개 이상의 결과를 봐야 한다. 그러므로 하나의 텍스트박스만 가지고서는 아무래도 내용이 섞여서 관리도, 알아보는 것도 힘들어진다. 그래서 이쯤에서 출력 스트림을 두 개 이상으로 분리하는 것이 좋겠다는 생각이 든다.

그럼 생각해보자. 메모리랑 실행 결과는 출력해야 한다. 또 뭐 출력해야 될 게 더 있을까? 일단 메모리와 실행 결과는 모두 로그 스트림에 출력하지 않을 것이므로, 로그 스트림도 별도로 필요하다. 그러면 일단 3개의 스트림은 반드시 필요한 셈이다. 필자가 스트림을 구현할 때는 총 6개의 스트림을 사용했는데, 지금 설명하면 납득하기 힘들 것이라고 생각하므로 후에 다루겠다.

main.html 파일을 열고 다음을 추가한다.

main.html <head.body>

<textarea id='outputStream'></textarea>

<textarea id='memoryStream'></textarea>

<textarea id='HandyLogStream'></textarea>

<script>

...

그리고 스타일을 변경하자. 이대로는 화면이 좁으므로 package.json 파일을 수정한다.

package.json

{

"name": "nw_demo",

"version": "0.0.1",

"main": "main.html",

"window": {

"width": 1280,

"height": 768,

"position": "center"

}

}

그리고 outputStreammemoryStream 요소의 크기를 적절히 맞춰준다.

main.html (init)

function init() {

var logStream = document.getElementById('HandyLogStream');

logStream.style.width = '100%';

logStream.style.height = '29%';

 

var outStream = document.getElementById('outputStream');

outStream.style.width = '49%';

outStream.style.height = '70%';

 

var memStream = document.getElementById('memoryStream');

memStream.style.width = '49%';

memStream.style.height = '70%';

 

initHandyFileSystem();

initMemory();

initRegister();

initRunner();

}

그런데 이렇게 놓고 보니 init 함수가 그 내용과 상관없이 지나치게 길어지는 것 같다. 따라서 화면의 스타일을 조정하는 함수를 새로 만들고 이를 호출하는 식으로 변경하자.

main.html (init)

function init() {

initHandyFileSystem();

initStyle();

 

initMemory();

initRegister();

initRunner();

}

function initStyle() {

...

이제 스타일에 대해서는 모두 정리가 되었다. 남은 것은 만든 textarea 요소를 실제 스트림처럼 동작하게 만드는 일이다. 별 게 있는 건 아니고 log 메서드를 복사해서 출력되는 위치만 바꿔주면 된다. 메모리를 출력하는 메서드를 mem_write, 출력 문자열을 출력하는 메서드를 out_write라고 하자.

하나만 더 생각하자. 이 두 메서드의 위치가 어디라고 생각하는가? Handy? Runner? 아니면 Memory인가? 일단 이 함수들은 모두 Runner만 사용하는 함수로 보인다. 그렇다면 Runner에 구현하고 후에 이것이 틀렸을 경우에 수정하기로 하자.

 

5.4) register 예제 확인

이제 Register 예제가 정말 올바른 결과를 냈는지 확인할 차례다. main에 다음 두 구문을 적는다.

main.html (init)

function main() {

Runner.run('HelloHASM.hda');

Memory.show();}

그리고 Memory.show가 메모리 스트림에 출력하도록 코드를 고친다.

Memory.js (show)

function show() {

...

// log(mem_str);

Runner.mem_write(mem_str);

}

그리고 코드를 실행하면 다음 결과를 얻는다.

실행 결과



메모리를 확대해서 보자.

실행 결과



메모리에서 맨 아랫줄만 64로 바뀌었는데, 제대로 된 게 맞는 것일까? 결론부터 말하자면, 아주 깔끔한 결과다! 6416진수로 표기된 값인데 이를 10진수로 바꾸면 100이 된다. 우리가 기록한 ebp의 값인 것이다! 맨 왼쪽에 있는 것은 리틀 엔디안 방식으로 표기했기 때문이고, 뒤에 06개 붙는 것은 우리가 esp4만큼 뺐기 때문이다. 이로써 show 함수를 통해 메모리를 관찰할 수 있게 되었다.

push 연산이 올바르게 동작한다는 사실을 확인했으니, 나머지 연산들이 제대로 동작하는지를 확인하면 된다. push 명령 다음은 다음의 mov 명령이다.

mov ebp, esp

그런데 mov 명령은 메모리가 변하는 명령이 아니라, 순수하게 레지스터의 값만 변하는 명령이다. 그러므로 이 명령이 제대로 동작했는지 확인하려면 우리는 레지스터의 값도 볼 수 있어야 한다. , 레지스터 값을 출력하는 스트림이 필요하다. 또한, 각 명령어 단위로 레지스터의 값이 어떻게 변하는지를 볼 수 있어야 한다. 이 내용을 바탕으로 스트림을 만들고 몇 가지 수정해보자.

 

5.5) 스트림 추가

일단 우리가 무엇을 만들려는지부터 다시 파악하자. 레지스터 값을 출력하는 스트림이 필요한 건 알겠는데, 각 명령어 단위로 레지스터의 값의 변화를 보는 스트림은 아직 잘 모르겠다.

이 스트림은 expressionStream이라고 하는데, 현재 실행한 명령을 보여준다. HelloHASM 예제를 이용해 각 스트림의 용도를 설명하겠다. Runner는 처음 프로그램을 실행하면 이 상태가 된다.

Runner

HelloHASM.hda

Run

Undo

Redo

output stream

memory stream

register stream

expression stream

log stream

가장 위의 줄은 메뉴 바다. 맨 왼쪽에 입력 요소가 있어서 실행할 hda 파일의 이름을 입력한다. 나머지 세 요소는 모두 버튼이다. Run은 실행 버튼이다. Undo/Redo는 후에 설명하겠다. 그림에는 각각의 스트림의 용도가 설명되어있는데, 이것이 정확히 어떤 뜻인지 예제를 보이면서 설명하겠다.

여기서 run 버튼을 누르자. 그러면 코드 영역 이전의 내용이 먼저 분석되어 로그 스트림에 출력된다.

Runner

HelloHASM.hda

Run

Undo

Redo

 

...

0x60 | 00000000 | ....

eax = 0, ebx = 0, ecx = 0, edx = 0,

ebp = 100, esp = 100

 

...

label: _main:

push ebp 명령을 만나면 다음과 같이 상황이 변한다.

Runner

HelloHASM.hda

Run

Undo

Redo

 

...

0x60 | 64000000 | d...

eax = 0, ebx = 0, ecx = 0, edx = 0,

ebp = 100, esp = 96

push ebp

...

label: _main:

한 번만 더 해보자. mov ebp, esp가 수행되면 다음과 같이 상황이 변한다.

Runner

HelloHASM.hda

Run

Undo

Redo

 

...

0x60 | 64000000 | d...

eax = 0, ebx = 0, ecx = 0, edx = 0,

ebp = 96, esp = 96

mov ebp, esp

...

label: _main:

각각의 스트림에 맞게 문자열이 들어갔음을 볼 수 있다. 이 내용을 Runner에 반영해보자.

요소를 새로 만드는 과정은 크게 어렵지 않다. 적당히 버튼 요소와 textarea 요소를 만든다.

main.html <html.body>

<input id='inputFileName'>

<input type="button" value='Run' onclick='run()'>

<input type="button" value='Undo' onclick='undo()'>

<input type="button" value='Redo' onclick='redo()'><br>

<textarea id='outputStream'></textarea>

<textarea id='memoryStream'></textarea><br>

<textarea id='registerStream'></textarea>

<textarea id='expressionStream'></textarea><br>

<textarea id='HandyLogStream'></textarea>

...

이 부분은 프로그램에 전혀 중요한 부분이 아니지만, HTMLJavaScript를 먼저 배우지 않아도 좋다고 시작 문서에서 말해놓은 만큼 설명을 하고 가겠다. input은 사용자로부터 입력을 받을 때 사용하는 요소다. 사용자로부터 입력을 받는 방법은 텍스트 상자도 있지만 버튼도 있고, 라디오 버튼도 있고 그 종류가 다양하다. 그래서 버튼을 만들 때는 typebutton으로 조정해주면 버튼이 된다. value는 만든 버튼에 표시할 텍스트의 이름이고, onclick은 버튼을 클릭했을 때 호출할 함수를 말한다. <br>은 줄을 바꿀 때 쓰는 요소다. 그리고 여기에 registerStream, expressionStream 요소가 추가되었음을 확인하면 된다.

추가한 요소들의 스타일을 맞춰서 깔끔하게 만들어준다.

main.html <html.head.script> (initStyle)

function initStyle() {

var logStream = document.getElementById('HandyLogStream');

logStream.style.width = '100%'; logStream.style.height = '25%';

var outStream = document.getElementById('outputStream');

outStream.style.width = '49%'; outStream.style.height = '34%';

var memStream = document.getElementById('memoryStream');

memStream.style.width = '49%'; memStream.style.height = '34%';

var regStream = document.getElementById('registerStream');

regStream.style.width = '49%'; regStream.style.height = '34%';

var expStream = document.getElementById('expressionStream');

expStream.style.width = '49%'; expStream.style.height = '34%';

}

그리고 각각의 스트림에 대해 출력 메서드를 만들어준다. 각각 reg_write, exp_write라 하자. 위치는 변하지 않는다. 이는 메모리, 출력 스트림을 만드는 것과 완전히 동일하므로 코드를 재차 싣지 않겠다.

 

6. 나머지 명령에 대한 처리

이제 mov ebp, esp를 처리해보자. 이를 위해 execute 메서드가 바뀐다.

Runner.js (execute.mov)

...

else if (mne == 'mov') { // mov 니모닉에 대한 처리

// right의 레지스터 값을 left에 대입합니다.

Register[left] = Register[right];

}

...

그리고 각 레지스터의 값을 출력하기 위해 run 메서드를 바꾼다. execute는 명령을 실행하는 구문이기 때문에 레지스터의 값을 출력하는 코드를 여기에 넣는 것은 바람직하지 않다.

Runner.js (run.after_execute)

// decode를 통해 분석된 정보를 바탕으로 명령을 실행합니다.

execute(info);

 

// 식을 분석하고 실행한 결과를 스트림에 출력합니다.

Runner.exp_write(line);

Runner.reg_write('eax = %08x, ebx = %08x, ecx = %08x, edx = %08x',

Register.eax, Register.ebx, Register.ecx, Register.edx);

Runner.reg_write('ebp = %02x, esp = %02x', Register.ebp, Register.esp);

 

// 각 명령을 단계적으로 보기 위해 중단점을 삽입합니다.

alert();

그리고 이를 실행해보면, 최종적으로 다음 결과를 얻는다.

실행 결과



각각의 명령이 단계적으로 보이고, 마지막에 최종 메모리 상태가 보인다. 어떤 명령을 분석했는지가 식 스트림에, 명령이 아닌 어떤 구문을 분석했는지가 로그 스트림에, 어떻게 레지스터가 변화했는지가 레지스터 스트림에 제대로 출력되었다. 구현한 pushmov가 제대로 반영되었음을 메모리 스트림과 레지스터 스트림을 통해 알 수 있다.

다음으로 넘어가자. 처리해야 하는 명령은 pop인데, 그 구현이 전혀 어렵지 않다.

Runner.js (execute.pop)

...

else if (mne == 'pop') { // pop 니모닉에 대한 처리

// esp가 가리키는 메모리의 dword 값을 획득합니다.

var popVal = Memory.get_dword(Register.esp);

 

// esp를 레지스터 크기만큼 증가시킵니다.

Register.esp += 4;

 

// 목적지 레지스터에 획득한 값을 대입합니다.

Register[left] = popVal;

}

...

4장의 그림을 보면서 공부하면, 정말 그림대로 코드가 구현되었음을 알 수 있다. 남은 건 ret이다. 그런데 이 문서에서 배운 내용만으로는 ret을 구현할 수 없기 때문에, ret은 일단 무시하고 다음 문서에서 다루려고 한다.

 

7. 아직 안 끝났어요

꾸준히 글을 읽어오던 독자라도 생각보다 긴 분량에 당황했을 것이라고 생각한다. 이번 문서에서 설명할 것이 많겠다고 생각은 했지만, 필자도 60쪽이 넘게 글을 썼는데 아직도 Runner를 모두 설명할 수 없었다는 사실에 놀라고 있다. 글을 계속 읽는 분이 있다면, 갑자기 두 배만큼 길어진 이 글을 정말 모두 읽을 수 있을까 하는 걱정이 앞선다.

얼마 전 Facebook 페이지에 운영자님께서 공지를 올려주셔서 용기를 얻었지만, 사실 이 긴 글을 정말 읽고 있는 사람이 있는지 모르겠다. 이제는 그냥 나를 위해, 후에 내 능력을 증명할 때 증거로 내놓기 위해 글을 쓴다고 생각하고 있다. 매번 댓글 달아주시는 분들은 진심으로 감사하게 생각하고 있다.

다음 문서 또한 Runner에 대한 것이다. 다음 문서로 끝날지 모르겠지만, 최대 3챕터까지만 투자하면 Runner 모듈을 모두 설명할 수 있을 것이라 생각한다(사실 Runner가 제일 복잡하고 어렵다).

'휴지통 > document' 카테고리의 다른 글

[JSCC] 10. 링커 개발  (0) 2015.07.10
[JSCC] 9. 가상 머신 개발 2  (0) 2015.07.03
[JSCC] 8. 가상 머신 개발 1  (2) 2015.06.26
[JSCC] 7. JSCC 준비  (0) 2015.06.19
[JSCC] 이번주 JSCC는 휴재합니다.  (0) 2015.06.08
[JSCC] 6. JavaScript 튜토리얼  (0) 2015.06.05
Posted by 누아니