링커 개발

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


소스: 

10_Linker.zip


문서: 

10. 링커 개발.pdf

 

1. 개요

파일을 분리하여 코드를 작성할 수 있도록 링커를 개발한다.

 

2. 전략

이 문서의 목표는 다음과 같은 두 HASM 소스 코드가 존재하면,

puts.hda

.code

;===========================

; proc puts

;===========================

_puts:

push ebp

mov ebp, esp

handy puts, [ebp+0x8]

mov esp, ebp

pop ebp

ret

main.hda

.data

_sHelloWorld db 'Hello, world!', 0

 

.code

;===========================

; proc main

;===========================

_main:

push ebp

mov ebp, esp

 

push _sHelloWorld

call _puts

add esp, 4

 

_end:

mov esp, ebp

pop ebp

exit

이를 다음과 같은 하나의 코드로 합치는 것이다.

a.out

.data

_sHelloWorld db 'Hello, world!', 0

 

.code

;===========================

; proc puts

;===========================

_puts:

push ebp

mov ebp, esp

handy puts, [ebp+0x8]

mov esp, ebp

pop ebp

ret

 

;===========================

; proc main

;===========================

_main:

push ebp

mov ebp, esp

 

push _sHelloWorld

call _puts

add esp, 4

 

_end:

mov esp, ebp

pop ebp

exit

그러면 자연히 Runner가 프로그램을 잘 해석할 수 있게 될 것이다. 이것을 어떻게 구현할지 먼저 생각하는 시간을 가져보자.

 

2.1) 기본

각각의 HASM 소스 코드는 다음과 같은 형태를 띠고 있을 것이다.



 



이들을 하나의 소스에 묶는다면, 결국 이런 식으로 묶어야지 않겠는가?



우리는 정확히 이대로 프로그램을 구현할 것이다.

 

2.2) 순서 문제

좀만 더 깊이 생각해보자. 코드가 다음과 같이 구현되어있는 경우를 생각한다.

src1.hda

.data

_sHello db 'hello', 0

 

.code

_sayHello:

handy puts, _sHello

ret

src2.hda

.data

_sHi db 'hi', 0

 

.code

_sayHi:

handy puts, _sHi

ret

 

_main:

exit

이들을 링크한 결과를 그림으로 표현하면 다음과 같이 될 것이다.





그런데 사실, 파일을 링크한 결과는 다음과도 같아야 한다.





중요한 내용이다. 목적 파일을 링크한 순서가 프로그램 실행 결과에 영향을 미쳐서는 안 된다. 더 일반적인 예를 들어보자. 다음과 같은 식으로 소스 코드가 작성되어있다면,



이들을 바탕으로 생성된 다음의 링크 결과물은 모두 실행 결과가 완전히 같아야 한다.





이 문제는 아주 혼란스럽다. 프로그램을 구현하면서 생각하자.

 

2.3) 진입점 문제

우리가 지금까지 작성한 코드는 모두 main 프로시저가 코드 영역의 시작점에 있었다. 따라서 프로그램의 시작점이 곧 코드 영역의 시작점이었기 때문에, 진입점에 대해 별도로 처리를 할 필요가 없었다. 하지만 방금 링크 순서에 상관없이 프로그램이 동작해야 한다고 했다. 이는 main 프로시저 또한 해당한다. main 프로시저의 위치가 고정되어있지 않기 때문에, 우리는 프로그램을 링크할 때 main 프로시저를 찾아내야 한다. 이 문제 또한 구현하면서 생각하자.

 

3. 리팩터링

3.1) 점검

3장에서 우리는 링커와 컴파일러의 역할에 대해 배웠다.

- 소스 코드를 작성하고 파일로 저장한다.

- 저장한 소스 파일을 컴파일러를 이용하여 컴파일 한다. 목적 파일이 생성된다.

- 컴파일러가 생성한 목적 파일들을 링커를 이용하여 링크 한다. 실행 가능한 목적 파일이 생성된다.

이를 구현의 관점에서 다시 설명하면 이렇게 된다.

- C 프로그래밍 언어를 이용해 *.c 파일을 작성한다.

- Compiler 모듈이 *.c 파일을 컴파일 하여 *.hdo 목적 파일을 생성한다.

- Linker 모듈이 *.hdo 파일을 링크 하여 *.hdx 프로그램 파일을 생성한다.

- Runner 모듈이 *.hdx 프로그램 파일을 메모리에 불러온 다음 실행한다.

그런데 생각해보자. 우리가 만든 Runner 모듈은 *.hda 소스 파일을 바로 실행하는 도구였다. 하지만 위에서 보인 과정은 hda 소스 파일이 등장하지 않는다. 그럼 지금까지 만든 것은 무엇일까? 어셈블리 언어를 해석하는 도구를 만들면, 작성한 코드를 컴파일러를 이용하여 어셈블리 코드를 만들고 프로그램을 실행하려고 했던 것인데?

여기서 필자는 지금까지 작성하던 모듈이 근본적으로 문제가 있다는 생각을 한다. 이것이 문제가 있다고 생각한 이유는 필자가 RunnerLinker, Compiler를 번갈아 작업하면서 깨달은 것인데, 아무래도 여기에 압축해서 설명하는 것은 무리가 있다고 생각하여 연재가 끝난 다음에 기회가 있다면 포스트를 작성하려고 한다. 일단 jscc는 다음과 같은 식으로 작성할 것이다,

- C 프로그래밍 언어를 이용해 *.c 파일을 작성한다.

- Compiler 모듈이 *.c 파일을 컴파일 하여 *.hdo 목적 파일을 생성한다.

> *.c 파일은 HASM 언어로 번역된다.

- Linker 모듈이 *.hdo 파일을 링크 하여 *.hdx 프로그램 파일을 생성한다.

> *.hdx 파일은 Low Level Assembly 언어로 번역된다.

- Runner 모듈이 *.hdx 프로그램 파일을 메모리에 불러온 다음 실행한다.

> Runner 모듈은 Low Level Assembly에 대한 인터프리터가 된다.

따라서 우리는 작성한 모듈을 리팩터링 하는 과정을 먼저 거칠 것이다.

 

3.2) 설계

JSCC는 최종적으로 이러한 형태로 구현한다.



이 그림을 말로 풀어서 설명하겠다.

- 모듈을 크게 두 부분으로 나눈다. 하나는 컴파일’, ‘링크’, ‘실행 요청을 담당하는 JSCC 모듈이다. 하나는 프로그램을 실행하기 위해 준비하는 가상 머신이다.

- Memory 모듈은 Machine의 소속이 된다는 점을 제외하면 변하지 않는다.

- Register, Operate 모듈은 Processor라는 새 모듈의 소속이 된다. ProcessorLow Level Assembly를 직접 해석하여 실행하는 인터프리터다. 이 과정에서 각 모듈의 멤버 중 일부가 Processor 모듈로 이동할 수 있다.

- System 모듈은 실행 환경에 대한 설정 정보를 보관한다. 개행 문자와 같은 정의를 여기에 넣는다.

- Compiler 모듈은 작성한 *.c 파일을 컴파일 하여 *.hdo 목적 파일로 변환한다. *.hdo 목적 파일은 HASM 언어로 기록되어있다.

- Linker 모듈은 HASM 언어로 작성된 *.hdo 파일을 분석하고 묶어서, Machine이 이해할 수 있는 Low Level Assembly 언어로 변환한다. 원래는 CompilerLinker 사이에, HASM으로 번역된 코드를 Low Level Assembly 언어로 번역하는 Assembler 모듈이 있었지만, 여기서는 이를 생략하고 Linker가 이 번역도 같이 수행하는 것으로 하겠다.

- Runner 모듈은 어셈블리 언어를 직접 해석하는 인터프리터에서, Machine.Processor 모듈에 프로그램 실행을 요청하는 메신저의 역할을 하는 것으로 바뀐다. 이 과정에서 Runner 모듈의 멤버 중 일부가 Machine.Processor와 같은 모듈로 이동할 수 있다.

그럼 이제 링커를 개발하기 위한 토대를 마련해보자.

 

3.3) 반영

먼저 Machine, Program의 두 모듈을 위한 초기화 메서드를 만든다.

Machine.js

/**

JSCC의 실행을 담당하는 가상 기계를 생성합니다.

*/

function initMachine() {

var machine = {};

initMachineSystem(machine);

initMachineMemory(machine);

initMachineProcessor(machine);

initMachineOperation(machine);

window.Machine = machine;

}

Program.js

/**

JSCC 및 그 테스트 모듈을 관리하는 Program을 생성합니다.

*/

function initProgram() {

var program = {};

initProgramStream(program);

initProgramRunner(program);

window.Program = program;

}

Machine에 대해 먼저 처리하자. Machine의 부속 모듈에 대한 초기화 함수를 작성한다.

MachineSystem.js

/**

가상 머신의 공통적 요소를 관리합니다.

*/

function initMachineSystem(machine) {

}

같은 방식으로 다음 메서드를 모두 준비하면 된다.

function initMachineMemory(machine) { } // MachineMemory.js

function initMachineProcessor(machine) { } // MachineProcessor.js

function initMachineOperation(machine) { } // MachineOperation.js

Program에 대해서도 최소한의 준비만 해두자.

function initProgramRunner(program) { } // ProgramRunner.js

이제 작성했던 Memory 모듈의 initMemory() 메서드 내부에 있는 모든 코드를 Machine.Memory 모듈의 initMachineMemory() 메서드로 옮긴 다음 마지막 줄을 수정한다.

MachineMemory.js

function initMachineMemory(machine) {

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

...

// 전역에 싱글톤 등록

window.MemoryException = MemoryException;

machine.Memory = memory; // machine 모듈의 멤버로 넣습니다.

}

다만, Memory 모듈이 전역에서 Machine 모듈의 내부로 옮겨갔기 때문에 Memory를 직접 참조하는 부분을 다음과 같이 수정해야 한다.

var addr_end = Memory.MaxSize() - 1; -> var addr_end = Machine.Memory.MaxSize() - 1;

var byte = Memory.get_byte(addr); -> var byte = Machine.Memory.get_byte(addr);

Memory.get_dword(addr) -> Machine.Memory.get_dword(addr);

이는 Operate 모듈도 다르지 않다.

MachineOperation.js

function initMachineOperation(machine) {

var operate = {};

...

machine.Operate = operate; // machine 모듈의 멤버로 넣습니다.

}

하지만 Runner 모듈의 경우는, 그 내용이 바뀌기 때문에 그대로 적용할 수 없다. 다음과 같이 빈 메서드만 준비해놓자.

ProgramRunner.js

/**

Machine에 프로그램 실행을 요청합니다.

*/

function initProgramRunner(program) {

var runner = {};

function load(filename) { }

function run() { }

runner.load = load;

runner.run = run;

program.Runner = runner;

}

Stream 또한 Program의 소속이므로 모듈을 추가한다.

ProgramStream.js

function initProgramStream(program) {

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

...

program.Stream = _stream;

}

이때 StreamProgram의 속성으로 옮겨가면서 Stream을 참조하는 코드 또한 변경된다.

Stream.mem.writeln(mem_str); -> Program.Stream.mem.writeln(mem_str);

그리고 main.html 파일을 다음과 같이 수정한다.

main.html <html.head>

...

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

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

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

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

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

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

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

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

...

main.html <html.head.script>

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

function main() {

Program.Runner.load('main.hda');

Program.Runner.run();

Machine.Memory.show();

}

function init() {

initHandyFileSystem();

initStyle();

initMachine();

initProgram();

}

...

그리고 이를 테스트를 위해 실행하면 이런 결과가 나온다.

실행 결과 (memory)

0x0000 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................

...

이렇게 링커를 개발하기 위한 토대는 일단 마련이 된다.

 

3.4) Low Level Assembly

위에서 Machine.ProcessorLow Level Assembly라는 어셈블리 언어에 대한 인터프리터라고 말했다. 또한 Linker 모듈은 HASM 언어로 작성된 파일을 Low Level Assembly로 변환한다고 되어있다. 이것을 하려면 우리가 HASMLow Level Assembly로 변환하는 방법은 알고 있어야 한다.

사실 이건 말이 어렵지 실제로는 전혀 어렵지 않다. 예제를 통해 설명하겠다. 다음과 같은 HASM 코드가 있다면,

putn.hdo

.data

_sHelloWorld db 'hello, world!', 0

 

.code

; proc main

_main:

push ebp

mov ebp, esp

handy puts, _sHelloWorld

mov esp, ebp

pop ebp

exit

Linker는 이를 다음과 같이 변환하는 것이 전부인 모듈이다.

Program.hdx

.data

db 68, 65, 6, 6, 6, 2, 20, 77, 6, 72, 6, 64, 21, 0

 

.code

push ebp

mov ebp, esp

handy puts, 0x0004

mov esp, ebp

pop ebp

exit

문자열과 레이블이 즉시 값으로 바뀐다. 그거 말고도, 비슷한 방식으로 해석하기 쉽게 변환하는 것이 끝이다. 사실 이 예제에서는 이미 구현한 어셈블리 언어 해석기를 거의 그대로 사용할 것이기 때문에, 이 부분에서 그렇게 심각하게 고민할 필요는 없다.

 

4. 실행기 재작성

4.1) Machine.Processor.Register

실행기의 개발은 이미 해봤다. 몇 가지 바뀌는 건 있지만 기본은 같으므로 코드를 이해하는 것은 어렵지 않다. 먼저 이전에 Runnner 역할을 하던 모듈이 Processor 모듈로 옮겨갔기 때문에, Processor 모듈을 먼저 완성해야 한다. 이때 Register 모듈이 Processor 모듈의 부속 모듈이 된다.

MachineProcessor.js (initMachineProcessorRegister)

function initMachineProcessorRegister(processor) {

// Register 모듈을 정의합니다.

var register = {};

...

processor.Register = register;

}

그리고 다음과 같은 식으로 is_register를 정의하자.

MachineProcessor.js (is_register)

/**

주어진 인자가 레지스터라면 true, 아니면 false를 반환합니다.

@param {string} param

@return {boolean}

*/

function is_register(param) {

return (is_reg32(param) || is_reg16(param) || is_reg8(param));

}

레지스터는 32비트 레지스터도 있고, 16비트, 8비트 레지스터도 있다. is_register는 이 세 함수를 각각 호출해본 다음, 어느 것 하나라도 true를 반환하면 레지스터로 간주하도록 작성했다. 그럼 이제 이들의 구현을 볼 차례다. 32비트 레지스터인지 판정하는 is_reg32 메서드는 이렇게 생겼다.

MachineProcessor.js (is_reg32)

/**

주어진 인자가 32비트 레지스터라면 true, 아니면 false를 반환합니다.

@param {string} param

@return {boolean}

*/

function is_reg32(param) {

return Machine.Processor.Register._reg32[param] ? true : false;

}

이 구문만 보고는 이것이 어떻게 동작하는지 파악하기 어려울 수 있다. 일단 여기선 _reg32라는 객체에 접근하는데, 이 객체가 어떻게 생겼는지부터 확인해보자.

MachineProcessor.js (_reg32)

// 32비트 레지스터 목록입니다.

var _reg32 = {

eax: true, ebx: true, ecx: true, edx: true,

ebp: true, esp: true, eip: true, eflags: true

};

...

register._reg32 = _reg32;

_reg32 객체는 아주 단순하게 생겼다. 그냥 멤버로 레지스터의 이름이 있을 뿐이다. 그런데 여기에, 객체의 멤버에 접근하기 위해 문자열을 사용할 수 있다는 점을 이용하면, 다음과 같이 객체에 접근한 경우,

_reg32[param]; // param is string

_reg32 객체의 멤버로 param이 존재하는지 확인해본 다음, 이것이 있다면 정의된 값인 true, 정의되어있지 않다면 undefined가 반환되고, 이것이 삼항 연산자의 조건에 걸려서 true 또는 false를 반환하게 된다. 결국 이는 Machine.Processor.Register 모듈의 _reg32 객체에 접근한 다음, 그 필드로 param이 있다면 true, 아니면 false임을 나타는 것이다. 나머지도 모두 같은 식으로 구현되어있으니, 자세한 내용은 코드를 참조하기 바란다.

넘어가기 전에 16비트 레지스터와 8비트 레지스터에 대해 한 번 더 짚고 넘어가겠다.

MachineProcessor.js (_reg16, _reg8)

// 16비트 레지스터 목록입니다.

var _reg16 = {

ax: true, bx: true, cx: true, dx: true,

ds: true, ss: true, cs: true, es: true

};

// 8비트 레지스터 목록입니다.

var _reg8 = {

ah: true, al: true, bh: true, bl: true,

ch: true, cl: true, dh: true, dl: true

};

이에 대해서는 5장에서 그림을 통해 설명한 적이 있다. 그림을 다시 가져오겠다.



이 그림만 보고도 왜 이러한 멤버들이 추가되었는지 이해할 수 있을 것이다. dscs 등은 세그먼트에 대한 값을 보관하는 레지스터다.

 

4.2) Program.Runner

Program.Runner 모듈은 Processor가 메모리에 올린 코드를 실행하도록 요청하는 모듈이라고 했다. 이때 이 작업을 준비하는 과정을, 그러니까 메모리에 올린 코드를 실행하는 작업을 위한 준비 과정은 Runner가 담당한다. 그래서 이전에 initRunner에서 정의한 메서드 중 load를 포함한 여러 메서드와 필드를 여기로 옮긴다.

runner.imageBase = 4;

runner.baseOfData = 0;

runner.sizeOfData = 0;

runner.baseOfCode = 0;

runner.sizeOfCode = 0;

...

function RunnerException(...) { ... }

function decode(...) { ... }

function execute(...) { ... }

function LabelInfo(...) { ... }

function is_label_name(...) { ... }

function getDataTypeSize(...) { ... }

function load(...) { ... }

그리고 load 메서드의 맨 위에 다음 한 문장을 추가한다.

ProgramRunner.js (load)

function load(filename) {

var Memory = Machine.Memory;

...

아주 영리한 방법이다. 이전에는 메모리 모듈에 접근하기 위해 그냥 Memory를 썼다. 그런데 이는 사실 window.Memory에 접근하는 것과 같다. 그런데 우리는 Memory 모듈을 Machine의 소속으로 옮겼다. 따라서 이 한 문장을 추가하여 load 메서드 내에서 변수 MemoryMachine.Memory를 가리키게 만들면 이후의 Memory에 대한 접근은 모두 Machine.Memory가 되는 것이다. 이 방법은 리팩토링을 할 때 아주 유용하니 잘 기억하고 있기 바란다.

 

4.3) Machine.Processor

이제 이를 실행하는 run 메서드를 Processor 모듈로 옮기자. 다음을 복사하여 Processor로 옮긴다.

function RunnerException(...) { ... }

function fetch(...) { ... }

function decode(...) { ... }

function execute(...) { ... }

function run(...) { ... }

그리고 RunnerException의 이름을 ProcessorException으로 바꾼다.

MachineProcessor.js (ProcessorException)

...

ProcessorException.prototype.toString = function() {

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

};

...

리팩토링으로 발생하는 모든 오류에 대해 다음 코드를 추가하여 해결한다.

var Memory = Machine.Memory;

var Runner = Program.Runner;

var Stream = Program.Stream;

var Register = Machine.Processor.Register;

한편, 새로 작성한 Register 모듈은 현재 eax, ebx와 같은 레지스터를 필드로 포함하고 있지 않다. 따라서 이들에 대한 변수를 추가로 정의해야 하는데, 여기서는 Register 모듈의 필드로 직접 넣는 대신 실제 레지스터 변수를 보관하는 객체를 따로 만들자.

MachineProcessor.js (Register.reg)

// 실제 레지스터를 보관하는 reg 객체입니다.

var _reg = {

eax: 0, ebx: 0, ecx: 0, edx: 0,

ebp: 0, esp: 0, eip: 0, eflags: 0,

ds: 0, cs: 0, ss: 0, es: 0

}

...

register._reg = _reg;

그리고 레지스터에 접근하기 위한 메서드를 Register 모듈에 추가한다.

MachineProcessor.js (Register.get_and_set)

/**

레지스터의 값을 획득하여 4바이트 정수로 반환합니다.

@param {string} regName

@return {number}

*/

function get(regName) {

return Machine.Processor.Register._reg[regName];

}

/**

레지스터의 값을 4바이트 정수로 설정합니다.

@param {string} regName

@param {number} value

*/

function set(regName, value) {

Machine.Processor.Register._reg[regName] = value;

}

...

register.get = get;

register.set = set;

이는 당장 쓸모가 있어 보이지는 않지만, 후에 ax, ah와 같은 레지스터에 접근할 때 유용하다.

이전에는 레지스터의 값을 수정할 때 Register 모듈의 멤버에 직접 접근하였지만, 이제는 getset 메서드를 사용해야 한다. 이전에 이를 사용하고 있던 메서드를 모두 수정하자. mov의 경우를 예로 들면 다음과 같이 코드가 바뀌는 것이다.

MachineOperation.js (mov)

function mov(left, right) {

var Register = Machine.Processor.Register;

var Memory = Machine.Memory;

 

if (Register.is_register(right) == true) { // 레지스터라면

// 해당 레지스터의 값을 대입합니다.

Register.set(left, Register.get(right));

}

else if (Memory.is_memory(right)) { // 메모리라면

// 메모리의 값을 대입합니다.

Register.set(left, Memory.get_memory_value(right));

}

else { // 레지스터가 아니라면

// 정수로 간주하고, 정수로 변환하여 대입합니다.

Register.set(left, parseInt(right));

}

}

이와 같이 모든 메서드에 대해 처리해주면 된다. 첨부한 코드를 참조하라. 구현이 거의 같으니 스스로 작성할 수 있지만, eflags에 대한 처리만 여기서 설명하겠다. jnz, cmp와 같은 니모닉은 eflags의 정보를 얻거나 설정하기 위해 getZF, setZF 메서드를 사용하기 때문에, 이 메서드 또한 Register 모듈로 모두 옮겨줘야 한다.

MachineProcessor.js (Register)

// 플래그 레지스터에 대한 처리 메서드입니다.

const BIT_CF = 1;

function getCF() { return (this.eflags & BIT_CF) ? 1 : 0 };

function setCF(value) {

if (value == 0) { // 0으로 설정

this.eflags &= (~BIT_CF);

} else { // 1로 설정

this.eflags |= BIT_CF;

}

}

...

또한 MachineProcessor.js에 작성했던 fetch 메서드에 다음 줄이 추가된다.

MachineProcessor.js (fetch)

...

// 현재 메모리 바이트 포인터의 값을 eip 레지스터에 복사합니다.

Register.set('eip', Memory.bytePtr);

...

이 과정은 이전에 사용하지 않던 eip 레지스터를 실제로 사용하기 위한 것이다. Operate 모듈을 작성할 때 메모리의 바이트 포인터 대신 앞으로는 eip 레지스터를 이용할 것이다.

이제 출력을 보다 편리하게 하기 위해 다음 두 메서드를 추가한다.

MachineProcessor.js (exp_writeln, reg_writeln)

...

/**

식 스트림에 문자열을 출력하고 개행합니다.

@param {string} param

*/

function exp_writeln(param) {

Program.Stream.exp.writeln(param);

}

/**

레지스터 스트림에 현재 레지스터의 상태를 출력합니다.

*/

function reg_writeln() {

var Stream = Program.Stream;

var Reg = Machine.Processor.Register;

Stream.reg.writeln('eax = %08x, ebx = %08x, ecx = %08x, edx = %08x',

Reg.get('eax'), Reg.get('ebx'), Reg.get('ecx'), Reg.get('edx'));

Stream.reg.writeln('ebp = %04x, esp = %04x, eip = %08x, eflags = %08b',

Reg.get('ebp'), Reg.get('esp'), Reg.get('eip'), Reg.get('eflags'));

}

...

마지막으로 run 메서드를 다음과 같이 수정한다.

MachineProcessor.js (MachineProcessor.run)

/**

명령 포인터를 주소로 옮깁니다.

@param {number} addr

*/

function setip(addr) {

Machine.Processor.Register.set('eip', addr);

Machine.Memory.bytePtr = addr;

}

 

/**

진입점을 기준으로 프로그램을 실행합니다.

*/

function run() {

var Memory = Machine.Memory;

var Runner = Program.Runner;

var Stream = Program.Stream;

var Register = Machine.Processor.Register;

 

// 코드 영역의 시작 지점으로 바이트 포인터를 옮깁니다.

var MEMORY_END = Memory.MaxSize();

Register.set('ebp', MEMORY_END);

Register.set('esp', MEMORY_END);

setip(Runner.baseOfCode);

 

while (true) {

// 메모리로부터 명령 코드를 획득합니다.

var opcode = fetch();

if (opcode == 'exit') // 명령 코드가 exit이면 프로그램을 종료합니다.

break;

 

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

var info = decode(opcode);

 

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

execute(info);

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

exp_writeln(opcode);

reg_writeln();

}

log('run complete');

}

그러면 프로그램을 실행하여 다음 결과를 얻는다.

main.hda

.data

_sHello db 'hello', 0

_sWorld db 'world', 0

 

.code

_main:

push ebp

mov ebp, esp

 

_cond1_if:

cmp 1, 1

jnz _cond1_else

 

handy puts, _sHello

jmp _cond1_endif

 

_cond1_else:

handy puts, _sWorld

 

_cond1_endif:

mov esp, ebp

pop ebp

exit

실행 결과 (register)

eax = 00000000, ebx = 00000000, ecx = 00000000, edx = 00000000

ebp = 0400, esp = 03fc, eip = 00000019, eflags = 00000000

eax = 00000000, ebx = 00000000, ecx = 00000000, edx = 00000000

ebp = 03fc, esp = 03fc, eip = 00000025, eflags = 00000000

eax = 00000000, ebx = 00000000, ecx = 00000000, edx = 00000000

ebp = 03fc, esp = 03fc, eip = 0000002d, eflags = 01000000

eax = 00000000, ebx = 00000000, ecx = 00000000, edx = 00000000

ebp = 03fc, esp = 03fc, eip = 00000038, eflags = 01000000

eax = 00000000, ebx = 00000000, ecx = 00000000, edx = 00000000

ebp = 03fc, esp = 03fc, eip = 0000004a, eflags = 01000000

eax = 00000000, ebx = 00000000, ecx = 00000000, edx = 00000000

ebp = 03fc, esp = 03fc, eip = 00000055, eflags = 01000000

eax = 00000000, ebx = 00000000, ecx = 00000000, edx = 00000000

ebp = 03fc, esp = 03fc, eip = 00000073, eflags = 01000000

eax = 00000000, ebx = 00000000, ecx = 00000000, edx = 00000000

ebp = 0400, esp = 0400, eip = 0000007b, eflags = 01000000

이로써 리팩터링은 마무리가 된다.

 

5. 준비

Runner를 리팩터링하는 데 한참 시간을 들였다. 링커 개발에 앞서, 그림을 다시 보자.





핵심은, 각각의 파일을 분석하여 메모리에 올리면 레이블이 올바르게 대치되어야 한다는 것이다. 그럼 코드를 작성하기 전에 고려해야 될 것을 다시 한 번 생각해보자.

 

5.1) 고려해야 할 것

사실 링커를 만드는 것이 어려운 이유는 레이블의 범위가 하나가 아니라는 점 때문이다. 예를 들어보자. C에서는 다른 파일에서 특정 함수를 참조할 수 없도록 static이라는 키워드가 제공된다.

static void func() { ... } // 이 함수가 정의된 파일 외부에서는 이 함수를 찾을 수 없다

이는 어셈블리 언어에서는 정반대로 일어난다. C에서 이런 일이 일어나는 것은 기본적으로 C의 함수가 외부 선언을 가지기 때문인데, 어셈블리 언어는 특별히 레이블의 범위를 지정하지 않으면 기본적으로 local이 된다. 다시 말해, 레이블은 전역 특성을 정해주지 않으면 외부에서 참조할 수 없다는 것이다.

NASM 어셈블리에서는 이를 위해 global이라는 지시어(directive)를 제공한다. 이는 HASM에도 반영되었다. HASM에서는 기본적으로 레이블은 local이지만, global 지시어를 통해 레이블을 외부에서 참조 가능하도록 만들 수 있다.

여기서 우리가 무엇을 고려해야 하는지에 대한 답이 나온다. 우리는 레이블이 지역에서만 참조 가능한 것인가, 아니면 전역에서 참조 가능한 것인가를 결정해야 한다. 이것은 아주 중요한 문제다. 예를 들어 sum이라는 프로시저가 src1src2에 다음과 같이 정의되어있다고 하자.

src1.hda

.code

_sum:

push ebp

mov ebp, esp

mov edx, [ebp+0x8]

mov eax, [ebp+0xc]

add eax, edx

mov esp, ebp

pop ebp

ret

src2.hda

.code

_sum:

push ebp

mov ebp, esp

mov eax, ecx

add eax, edx

mov esp, ebp

pop ebp

ret

이 둘을 링크한 결과가 이것이면 안 된다.

out.hdx

.code

_sum:

push ebp

mov ebp, esp

mov edx, [ebp+0x8]

mov eax, [ebp+0xc]

add eax, edx

mov esp, ebp

pop ebp

ret

_sum:

push ebp

mov ebp, esp

mov eax, ecx

add eax, edx

mov esp, ebp

pop ebp

ret

한 눈에 그 이유를 알 수 있다. 레이블을 중복 정의하고 있기 때문이다. _sum의 정의가 중복이 되면 레이블을 대치할 때 둘 중 어느 주소 값으로 대치해야 할 지 판정할 수 없게 된다. 그래서 우리는 지역에 정의한 레이블을, 링크 이전에 따로 처리하는 등의 방법을 써야 한다. 여기서는 이 문제를 해결하기 위해 이런 방법을 쓴다.

1. 목적 파일의 정보를 보관하는 리스트를 생성한다.

2. 목적 파일 정보 객체는 데이터/코드 세그먼트의 정보, 지역 레이블 정보를 갖는다.

3. 모든 목적 파일이 공통으로 접근할 수 있는 전역 레이블 관리 객체를 정의한다.

4. 목적 파일을 Linker 모듈이 불러와서 분석한 후 리스트에 보관한다.

5. 불러오기가 완료되면 링크 메서드를 실행하여 프로그램을 생성한다.

다음 절에서 이에 대해 알아볼 것이다.

 

5.2) StringStream

앞으로는 파일에 텍스트를 기록하는데, 아무래도 이런 식으로 문자열을 만드는 것은 읽기도 어렵고 번거롭다.

var ss = '';

ss += Handy.format('.code\n');

ss += Handy.format('push ebp\n');

ss += Handy.format('mov ebp, esp\n');

ss += Handy.format('mov esp, ebp\n');

ss += Handy.format('pop ebp\n');

ss += Handy.format('exit\n');

HandyFileSystem.save(filename, ss);

따라서 우리는 BaseStream 형식을 모방한 StringStream을 다음과 같이 작성하여 사용한다.

ProgramStream.js (StringStream)

/**

문자열을 스트림처럼 사용하기 위해 형식을 정의합니다.

*/

function StringStream() {

this.str = '';

}

...

StringStream.prototype.write = ss_write;

StringStream.prototype.writeln = ss_writeln;

StringStream.prototype.clear = ss_clear;

...

_stream.StringStream = StringStream;

그러면 이렇게 문자열 스트림을 사용할 수 있다.

var ss = new Program.Stream.StringStream();

ss.writeln('.code');

ss.writeln('push ebp');

ss.writeln('mov ebp, esp');

ss.writeln('mov esp, ebp');

ss.writeln('pop ebp');

ss.writeln('exit');

HandyFileSystem.save(filename, ss.str);

그럼 이제 링커를 개발해보자.

 

6. Linker

먼저 Linker 모듈을 준비한다.

ProgramLinker.js

/**

실행 가능한 목적 파일을 생성합니다.

*/

function initProgramLinker(program) {

var linker = {};

... // LinkerException 정의를 포함합니다.

program.Linker = linker;

}

그 후 다음 두 메서드를 준비한다.

ProgramLinker.js (load, link)

// 메서드 정의

/**

파일 시스템으로부터 목적 파일을 불러옵니다.

@param {string} filename

*/

function load(filename) {

/* ... */

}

/**

불러온 목적 파일을 링크합니다.

@param {string} filename

*/

function link(filename) {

/* ... */

}

그리고 목적 파일 정보를 보관하기 위한 객체 형식을 정의한다.

ProgramLinker.js (ObjectInfo)

/**

목적 파일의 정보를 보관하는 객체 형식을 정의합니다.

*/

function ObjectInfo(segment, labelInfoDict, sizeOfData, sizeOfCode) {

this.segment = segment;

this.labelInfoDict = labelInfoDict;

this.sizeOfData = sizeOfData;

this.sizeOfCode = sizeOfCode;

}

이제 이를 바탕으로 목적 파일로부터 정보를 획득하는 load 메서드를 작성하자.

 

6.1) load

사실 Linker, 여기서는 어셈블리를 쓰기 때문에 decode를 그대로 사용할 수 있다(fetchexecute야 실행기가 쓰는 거니까 필요가 없지만). 그러니 이 메서드를 포함하여, 기존의 load 메서드가 사용하던 코드를 일부 복사한다.

ProgramLinker.js (load_ready)

function LabelInfo(...) { ... }

function is_label_name(...) { ... }

function getDataTypeSize(...) { ... }

function decode(...) { ... }

이제 필드를 정의한 다음,

ProgramLinker.js (fields)

// 필드 정의

linker.entrypoint = null;

linker.baseOfData = 0;

linker.sizeOfData = 0;

linker.baseOfCode = 0;

linker.sizeOfCode = 0;

linker.ObjectInfoList = []; // ObjectInfo 객체에 대한 리스트입니다.

linker.GlobalLabelDict = {}; // 전역 레이블 객체에 대한 딕셔너리입니다.

Program.Runner에 정의된 load 메서드를 Linker로 모두 복사한다.

ProgramLinker.js (load)

function load(...) { ... }

바뀌는 건 레이블에 대한 처리뿐이다. Runner는 모든 목적 파일을 하나로 모은 목적 파일이므로 외부 레이블을 참조할 필요가 없다. 반면, Linker는 따로 떨어진 목적 파일을 하나로 합치는 것이 목적이므로 지역 레이블과 전역 레이블을 구분해야 한다.

여기서 Runnerload와 차이가 발생한다. NASM에서는 어떤 레이블이 전역임을 알리기 위해 global 지시어를 사용한다고 했다. HASM에서도 이를 반영한다. 만약 분석을 진행하다가 다음과 같은 구문을 만나면,

global _main

우리는 _main이라는 레이블을 전역 레이블로 간주해야 한다. 이를 위해 링커의 멤버로 전역 레이블을 보관하는 GlobalLabelDict를 추가한 것이다.

global에 대해 처리하기 위해 세그먼트 처리 구문 이전에 다음 코드가 추가된다.

ProgramLinker.js (load.global)

...

// 행을 분석하기 위해 문자열 버퍼를 생성합니다.

var lineBuffer = new StringBuffer(line);

var lineToken = lineBuffer.get_token(); // 첫 번째 토큰을 획득합니다.

if (lineToken == 'global') { // 전역 레이블을 지정하는 구문이라면

label = lineBuffer.get_token(); // 레이블을 획득합니다.

 

// 레이블 획득에 실패하면 예외 처리합니다.

if (label == null)

throw new LinkerException('directive global got empty param');

else if (is_label_name(label) == false)

throw new LinkerException('directive global got invalid param', label);

 

// 전역 레이블 딕셔너리에 레이블 정보를 추가합니다.

if (labelInfoDict[label] != null) {

// 지역 레이블 딕셔너리에 이미 정보가 있다면

// 전역 레이블 딕셔너리로 옮깁니다.

Linker.GlobalLabelDict[label] = labelInfoDict[label];

 

// 더 이상 지역 레이블이 아니므로 딕셔너리에서 제거합니다.

labelInfoDict[label] = null;

}

else if (is_global(label) == false) {

// 딕셔너리에 이미 정보가 있다면 새로 생성할 필요가 없습니다.

Linker.GlobalLabelDict[label] = new LabelInfo(segment_target, label);

}

continue; // 전역 레이블 지정자에 대한 처리를 종료합니다.

}

 

// 데이터 세그먼트를 처리합니다.

if (segment_target == 'data') {

...

토큰을 획득한 다음, 이것이 전역 지정자인지 확인하는 구문이 추가되었다. 이 코드는 레이블을 획득한 후, 전역 레이블 딕셔너리에 레이블 정보를 추가한다. 만약 지역 레이블 딕셔너리에 이미 정보가 있다면, 전역 레이블 딕셔너리에 레이블 정보를 옮기고 지역 딕셔너리에서 레이블 정보를 빼낸다. 여기에는 load 메서드 이전에, 어떤 레이블이, 전역 레이블인지 확인하는 is_global 메서드가 정의되어있다.

ProgramLinker.js (is_global)

/**

전역 레이블이라면 true, 아니면 false입니다.

@param {string} label

@return {boolean}

*/

function is_global(label) {

return (Program.Linker.GlobalLabelDict[label] != undefined);

}

그리고 load 메서드의 마지막에 다음을 추가하여, 목적 파일 불러오기가 완료되었음을 알린다.

ProgramLinker.js (load.last)

...

// 목적 파일 정보를 보관하는 새 객체를 생성합니다.

var objectInfo = new ObjectInfo(segment, labelInfoDict, sizeOfData, sizeOfCode);

Linker.ObjectInfoList.push(objectInfo);

}

일단 꽤 많이 처리한 것 같으니 중간 점검을 한 번 하자.

 

6.2) 중간 점검

link는 아직 구현하지 않았지만, 일단 점검을 위한 용도로 사용하자. 다음과 같이 코드를 작성한다.

ProgramLinker.js (link)

/**

불러온 목적 파일을 링크합니다.

@param {string} filename

*/

function link(filename) {

var Linker = Program.Linker;

var objectList = Linker.ObjectInfoList;

 

for (var j=0, objectCount=objectList.length; j<objectCount; ++j) {

var objectInfo = objectList[j]; // 목적 파일 정보를 획득합니다.

log('objectInfo %d: ', j);

 

// ObjectInfo 객체의 정보를 문자열로 반환하기 위해

// 문자열 스트림을 생성합니다.

var ss = new Program.Stream.StringStream();

// 지역 레이블 정보를 출력합니다.

ss.writeln('> local labels');

for (label in objectInfo.labelInfoDict) {

var info = objectInfo.labelInfoDict[label];

ss.write('%s %s: %04x [ ', info.segmentName, info.name, info.offset);

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

ss.write('%04x ', info.refered[i]);

}

ss.writeln(']');

}

// 데이터 세그먼트의 정보를 출력합니다.

var sizeOfData = objectInfo.sizeOfData,

sizeOfCode = objectInfo.sizeOfCode;

ss.writeln('> data segment [%d(%04x)]', sizeOfData, sizeOfData);

for (var i=0, len=objectInfo.segment.data.length; i<len; ++i) {

var data = objectInfo.segment.data[i];

ss.writeln('%d: %d, [%04x]', i, data.typeSize, data.value);

}

// 코드 세그먼트의 정보를 출력합니다.

ss.writeln('> code segment [%d(%04x)]', sizeOfCode, sizeOfCode);

for (var i=0, len=objectInfo.segment.code.length; i<len; ++i) {

var code = objectInfo.segment.code[i];

ss.writeln('%s', code);

}

// 목적 파일 정보 문자열을 로그 스트림에 출력합니다.

log(ss.str);

}

 

// 전역 레이블 정보를 출력합니다.

log('===== global labels =====');

var globalList = Linker.GlobalLabelDict;

for (label in globalList) {

var info = globalList[label];

// 문자열 스트림을 생성합니다.

var ss = new Program.Stream.StringStream();

ss.write('%s %s: %04x [ ', info.segmentName, info.name, info.offset);

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

ss.write('%04x ', info.refered[i]);

}

ss.write(']');

log(ss.str);

}

 

log('link complete');

}

코드가 길지만 전혀 어려운 코드가 아니다. 그냥 정보가 어떻게 들어갔는지를 확인하기 위해 문자열 스트림을 생성하고 그 스트림에 정보를 출력한 것이 전부다.

출력이 어떤 식으로 이루어지는지 정리를 하고 가자. 먼저 목적 파일의 인덱스가 출력된다.

objectInfo 0: // 목적 파일의 인덱스는 0번입니다.

그리고 현재 목적 파일에서 획득한 지역 레이블 딕셔너리를 출력한다.

> local labels

// 기본 구문: <segment> <label_name>: <definition_offset_hex> [ <reference_offset_hex> ]

data _sHelloWorld: 0000 [ 0077 ] // data 세그먼트 소속 _sHelloWorld 레이블입니다.

// 아직 정의가 발견되지 않았으면 0x0000입니다.

// 레이블을 참조한 위치가 출력됩니다. 이 경우 0x0077입니다.

code _sum: 0000 [ 00a7 ] // code 세그먼트 소속 _sum 레이블입니다.

// 0x0000이므로, 아직 정의가 발견되지 않았음을 알 수 있습니다.

// 0x00a7 메모리에서 이 레이블을 참조합니다.

code _main: 005d [ 00f3 ] // code 세그먼트 소속 _main 레이블입니다.

// 정의가 0x005d 오프셋에 존재합니다.

// 0x00f3 메모리에서 이 레이블을 참조합니다.

모든 지역 레이블을 출력하고 나면, 데이터 세그먼트, 코드 세그먼트의 레이블을 차례로 출력한다.

> data segment [14(000e)] // 이 목적 파일의 데이터의 총 크기는 14입니다.

0: 1, ['hello, world!',0] // 0번 데이터는 요소의 크기가 1입니다. 나머지는 데이터입니다.

 

> code segment [250(00fa)] // 이 목적 파일의 코드의 총 크기는 250입니다.

push ebp // 이하로는 모두 코드입니다.

mov ebp,esp

...

결국 입력 파일이 다음과 같이 들어가면,

main.hdo

.data

_sHelloWorld db 'hello, world!', 0

.code

; ===========

_sum:

push ebp

...

ret

; ===========

_main:

push ebp

...

call _sum

...

ret

; 프로그램의 시작 지점이 _main 레이블임을 알립니다.

end _main

다음과 같이 출력이 된다.

예상 결과

objectInfo 0:

> local labels

data _sHelloWorld: 0000 [ 0077 ]

code _sum: 0000 [ 00a7 ]

code _main: 005d [ 00f3 ]

> data segment [14(000e)]

0: 1, ['hello, world!',0]

> code segment [250(00fa)]

push ebp

mov ebp,esp

...

이제 테스트를 위해 main.html 파일을 다음과 같이 수정한다.

main.html <html.head>

...

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

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

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

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

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

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

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

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

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

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

...

main.html <html.head.script>

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

function main() {

Program.Linker.load('main.hdo');

Program.Linker.load('print.hdo');

Program.Linker.link('program.hdx');

}

...

그러면 프로그램을 실행하여 다음 결과를 얻는다.

main.hdo

print.hdo

.data

_sHelloWorld db 'hello, world!', 0

 

.code

;====================================

; proc sum

; receives: 2 integers

; returns: sum of 2 parameters

;====================================

_sum:

push ebp

mov ebp, esp

mov edx, [ebp+0x8]

mov eax, [ebp+0xc]

add eax, edx

mov esp, ebp

pop ebp

ret

 

;====================================

; proc main

;====================================

_main:

push ebp

mov ebp, esp

; hello, world!를 출력합니다.

push _sHelloWorld

call _prints

add esp, 4

; 3+4의 계산 결과를 출력합니다.

push 4

push 3

call _sum

add esp, 8

push eax

call _printn

add esp, 4

mov esp, ebp

pop ebp

ret

; 프로그램 시작 지점은 _main 레이블입니다.

end _main

.data

_sEnteredString db 'Entered String: ', 0

_sEnteredNumber db 'Entered Number: ', 0

 

.code

;====================================

; proc prints

; receives: start address of string

; returns: none

;====================================

global _prints

_prints:

push ebp

mov ebp, esp

handy puts, _sEnteredString

handy puts, [ebp+0x8]

mov esp, ebp

pop ebp

ret

 

;====================================

; proc prints

; receives: number to print

; returns: none

;====================================

global _printn

_printn:

push ebp

mov ebp, esp

handy puts, _sEnteredNumber

handy putn, [ebp+0x8]

mov esp, ebp

pop ebp

ret

실행 결과 (log)

main.hdo 분석 결과

print.hdo 분석 결과

objectInfo 0:

> local labels

data _sHelloWorld: 0000 [ 0077 ]

code _sum: 0000 [ 00a7 ]

code _main: 005d [ 00f3 ]

code _prints: 0000 [ 0083 ]

code _printn: 0000 [ 00c6 ]

> data segment [14(000e)]

0: 1, ['hello, world!',0]

> code segment [250(00fa)]

push ebp

mov ebp,esp

mov edx,[ebp+0x8]

mov eax,[ebp+0xc]

add eax,edx

mov esp,ebp

pop ebp

ret

push ebp

mov ebp,esp

push 0x0000

call 0x0000

add esp,4

push 4

push 3

call 0x0000

add esp,8

push eax

call 0x0000

add esp,4

mov esp,ebp

pop ebp

ret

end 0x0000

objectInfo 1:

> local labels

data _sEnteredStringIs: 0000 [ 0020 ]

data _sEnteredNumberIs: 0014 [ 0074 ]

code _prints: 0000 [ ]

code _printn: 0054 [ ]

> data segment [40(0028)]

0: 1, ['Entered String is: ',0]

1: 1, ['Entered Number is: ',0]

> code segment [168(00a8)]

push ebp

mov ebp,esp

handy puts,0x0000

handy puts,[ebp+0x8]

mov esp,ebp

pop ebp

ret

push ebp

mov ebp,esp

handy puts,0x0000

handy putn,[ebp+0x8]

mov esp,ebp

pop ebp

ret

===== global labels =====

code _prints: 0000 [ ]

code _printn: 0000 [ ]

link complete

일단 코드 영역은, 레이블이었던 위치가 성공적으로 0x0000으로 대치되었음을 알 수 있다. 각각의 목적 파일에서 찾아낸 레이블의 정의나 참조 위치가 타당한지는, 직접 위치를 세거나 이전에 작성한 모듈을 응용하여 확인할 수 있다. 이것이 올바른지 확인하자. 다음은 main.hdo 파일을 메모리에 올린 상태를 표현한 것이다. 주황색은 데이터 세그먼트, 빨간색은 _sum 프로시저, 파란색은 _main 프로시저를 표현한 것이다.

실행 결과 (log)

0x0000 | 00 00 00 00 68 65 6c 6c 6f 2c 20 77 6f 72 6c 64 | ....hello, world

0x0010 | 21 00 70 75 73 68 20 65 62 70 00 6d 6f 76 20 65 | !.push ebp.mov e

0x0020 | 62 70 2c 65 73 70 00 6d 6f 76 20 65 64 78 2c 5b | bp,esp.mov edx,[

0x0030 | 65 62 70 2b 30 78 38 5d 00 6d 6f 76 20 65 61 78 | ebp+0x8].mov eax

0x0040 | 2c 5b 65 62 70 2b 30 78 63 5d 00 61 64 64 20 65 | ,[ebp+0xc].add e

0x0050 | 61 78 2c 65 64 78 00 6d 6f 76 20 65 73 70 2c 65 | ax,edx.mov esp,e

0x0060 | 62 70 00 70 6f 70 20 65 62 70 00 72 65 74 00 70 | bp.pop ebp.ret.p

0x0070 | 75 73 68 20 65 62 70 00 6d 6f 76 20 65 62 70 2c | ush ebp.mov ebp,

0x0080 | 65 73 70 00 70 75 73 68 20 30 78 30 30 30 34 00 | esp.push 0x0000.

0x0090 | 63 61 6c 6c 20 30 78 30 30 31 32 00 61 64 64 20 | call 0x0000.add

0x00a0 | 65 73 70 2c 34 00 70 75 73 68 20 34 00 70 75 73 | esp,4.push 4.pus

0x00b0 | 68 20 33 00 63 61 6c 6c 20 30 78 30 30 31 32 00 | h 3.call 0x0000.

0x00c0 | 61 64 64 20 65 73 70 2c 38 00 70 75 73 68 20 65 | add esp,8.push e

0x00d0 | 61 78 00 63 61 6c 6c 20 30 78 30 30 31 32 00 61 | ax.call 0x0000.a

0x00e0 | 64 64 20 65 73 70 2c 34 00 6d 6f 76 20 65 73 70 | dd esp,4.mov esp

0x00f0 | 2c 65 62 70 00 70 6f 70 20 65 62 70 00 72 65 74 | ,ebp.pop ebp.ret

0x0100 | 00 65 6e 64 20 30 78 30 30 36 66 00 00 00 00 00 | .end 0x0000.....

...

main.hdo의 레이블 상태를 다시 가져오면 다음과 같다.

> labels

data _sHelloWorld: 0000 [ 0077 ]

code _sum: 0000 [ 00a7 ]

code _main: 005d [ 00f3 ]

code _prints: 0000 [ 0083 ]

code _printn: 0000 [ 00c6 ]

> data segment [14(000e)]

0: 1, ['hello, world!',0]

> code segment [250(00fa)]

push ebp

...

그럼 이것이 맞는지 검증해보자. _main 프로시저의 시작은 이 결과대로라면 0x005d가 되어야 한다. 그런데 실제 _main 프로시저의 시작은 파란색이 처음 나타나는 0x006f. 결국 0x12만큼, 18만큼의 차이가 발생한 것이다. 왜 이러한 차이가 나타나는지 알겠는가?

답은 imageBasesizeOfData 때문이다. 각각의 레이블은, 세그먼트의 시작점을 기준으로 오프셋으로 설정되어있다. 우리는 imageBase를 처음에 4로 정의했고, sizeOfData14임을 다음을 통해 알 수 있다.

> data segment [14(000e)] // data 세그먼트의 크기는 14입니다. 16진수로는 0x000e입니다.

그래서 코드 세그먼트의 시작점이 18이 되었기 때문에 18 + 0x005d = 0x006f라는 결과가 나온 것이다. 바로 이러한 점이 링커를 구현하기 어렵게 만든다.

사실 문제는 이것만이 아니다. print.hdo 파일에는 _prints, _printn 프로시저는 분명하기 전역으로 정의되어있다. 하지만 어째선지 지역 레이블을 출력해야 하는 부분에서 전역 레이블까지 출력하고 있다. 이는 우리가 레이블의 참조를 찾아냈을 때 지역 레이블 딕셔너리에만 추가하는 Runnerload 메서드를 그대로 사용하고 있기 때문이다. 즉 이 부분 또한 수정해야 한다.

하나만 더 생각하자. end는 여기서 새롭게 추가된 지시어(directive). end _main과 같이 쓰면 우리의 프로그램이 _main 레이블부터 시작함을 알리는 것이다. 이전에도 말했지만, _main의 위치는 프로그램을 링크하는 순서에 따라 바뀔 수 있다. 전역 레이블을 대치할 때는 이 점을 잘 생각해야 한다.

이들을 모두 고려하여 프로그램 작성을 다시 시작하자. 일단 load가 잘못되어있으니 이것을 먼저 수정한 다음, link 메서드를 수정하도록 한다.

 

6.3) load 수정

지금 레이블의 정의와 참조가 문제가 되는 부분은 코드 세그먼트다. 데이터 세그먼트의 경우는 코드 영역에서 참조만 하고, 데이터 세그먼트 내에서 서로를 참조하는 일이 없으니 우리는 코드 세그먼트만 고려하면 된다. 레이블의 정의를 발견한 경우에 대한 처리를 수정하자.

ProgramLinker.js (load.codeseg)

...

// 레이블에 대해 처리합니다.

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

// 레이블 이름을 획득합니다.

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

// 전역 레이블에 등록되어있다면

if (is_global(label)) {

/* 전역 레이블에 대해 처리합니다. */

}

...

이제 전역 레이블에 대해 처리해야 하는데, 여기서 전역 레이블 정보 형식을 새롭게 정의한다.

ProgramLinker.js (GlobalLabelInfo)

/**

전역 레이블 정보를 표현하는 형식 GlobalLabelInfo를 정의합니다.

@param {number} objectIndex

@param {string} segmentName

@param {string} name

*/

function GlobalLabelInfo(objectIndex, segmentName, name) {

this.segmentName = segmentName;

this.name = name;

this.index = objectIndex; // 레이블이 정의된 목적 파일의 인덱스를 보관합니다.

this.offset = 0; // 목적 파일의 인덱스로부터 세그먼트의 시작 위치를 계산합니다.

this.refered = []; // 이 레이블을 참조하는 목적 파일의 인덱스도 보관해야 합니다.

}

그리고 이 형식을 이용하여 이전의 GlobalLabelDict 구문을 모두 수정한 다음, 코드 세그먼트에서의 전역 레이블 처리 코드를 작성해야 한다.

ProgramLinker.js (load.global)

function load(filename) {

var Linker = Program.Linker;

// 현재 목적 파일의 위치를 획득합니다.

// 리스트의 가장 마지막에 추가할 원소이므로

// 아직 원소가 추가되지 않았을 때의 리스트의 길이와 같습니다.

var objectIndex = Linker.ObjectInfoList.length;

...

if (lineToken == 'global') { // 전역 레이블을 지정하는 구문이라면

label = lineBuffer.get_token(); // 레이블을 획득합니다.

// 레이블 획득에 실패하면 예외 처리합니다.

if (label == null)

throw new LinkerException('directive global got empty param');

else if (is_label_name(label) == false)

throw new LinkerException('directive global got invalid param', label);

 

// 전역 레이블 딕셔너리에 레이블 정보를 추가합니다.

// 지역 레이블 딕셔너리에 이미 정보가 있다면

if (labelInfoDict[label] != null) {

// 전역 레이블 딕셔너리에 추가할 객체를 생성하고 기존 정보를 복사합니다.

var gLabelInfo = new GlobalLabelInfo

(objectIndex, segment_target, label);

gLabelInfo.offset = labelInfoDict[label].offset;

 

// 기존에 존재하던 참조 위치를 GlobalLabelInfo에 맞게 변환합니다.

// 참조 위치에 목적 파일의 인덱스를 멤버로 추가하는 식으로 변환합니다.

var ref = labelInfoDict[label].refered;

for (var j=0, refCount=ref.length; j<refCount; ++j)

gLabelInfo.refered.push({ objectIndex:objectIndex, offset:ref[j]});

 

// 전역 레이블 딕셔너리로 옮깁니다.

Linker.GlobalLabelDict[label] = gLabelInfo;

// 더 이상 지역 레이블이 아니므로 딕셔너리에서 제거합니다.

labelInfoDict[label] = null;

}

else if (is_global(label) == false) {

// 딕셔너리에 이미 정보가 있다면 새로 생성할 필요가 없습니다.

Linker.GlobalLabelDict[label] = new GlobalLabelInfo

(objectIndex, segment_target, label);

}

continue; // 전역 레이블 지정자에 대한 처리를 종료합니다.

}

...

코드 세그먼트에서 레이블의 정의를 찾은 경우를 보자.

ProgramLinker.js (load.codeseg.global_label)

...

// 레이블의 정의를 발견했다면

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

// 레이블 이름을 획득합니다.

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

 

// 전역 레이블에 등록되어있다면

if (is_global(label)) {

// 전역 레이블 정보 객체를 획득합니다.

var gLabelInfo = Linker.GlobalLabelDict[label];

 

// 현재 목적 파일의 인덱스와 발견한 레이블 정의를 기록합니다.

gLabelInfo.index = objectIndex;

gLabelInfo.offset = sizeOfCode;

 

}

// 레이블이 딕셔너리에 없는 경우 생성합니다.

else if (labelInfoDict[label] == undefined) {

labelInfoDict[label] = new LabelInfo(segment_target, label);

 

// 레이블 딕셔너리에 정보를 등록합니다.

labelInfoDict[label].offset = sizeOfCode;

}

continue;

}

...

정의에서 중요한 것은, 현재 목적 파일의 인덱스를 반드시 넘겨야 한다는 것이다. 이제 레이블의 참조 문제를 해결하자.

ProgramLinker.js (load.codeseg.label_reference)

...

// 레이블이라면 (맨 앞이 밑줄 기호라면)

if (info.left.charAt(0) == '_') {

// 참조 위치는 (현재 바이트 포인터 위치 + 니모닉의 길이 + 공백 한 칸)입니다.

var refered_addr = sizeOfCode + s.length;

s += '0x0000'; // 일단 0으로 대치합니다.

 

var label = info.left; // 레이블 이름을 획득합니다.

 

// 전역 레이블이라면

if (is_global(label)) {

// 전역 레이블 정보 객체를 획득합니다.

var gLabelInfo = Linker.GlobalLabelDict[label];

 

// 참조하고 있는 위치를 보관합니다.

labelInfoDict[label].refered.push({

objectIndex:objectIndex, offset:refered_addr

});

}

// 레이블이 딕셔너리에 등록되지 않았다면 등록합니다.

else if (labelInfoDict[label] == undefined) {

labelInfoDict[label] = new LabelInfo(segment_target, label);

 

// 참조하고 있는 위치를 보관합니다.

labelInfoDict[label].refered.push(refered_addr);

}

else {

// 참조하고 있는 위치를 보관합니다.

labelInfoDict[label].refered.push(refered_addr);

}

}

...

이제 테스트를 위해 link 메서드를 다음과 같이 변경한다.

ProgramLinker.js (link.global_labels)

...

// 전역 레이블 정보를 출력합니다.

log('===== global labels =====');

var globalList = Linker.GlobalLabelDict;

for (label in globalList) {

var info = globalList[label];

// 문자열 스트림을 생성합니다.

var ss = new Program.Stream.StringStream();

ss.write('%s %s: [%d] %04x [ ',

info.segmentName, info.name, info.index, info.offset);

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

ss.write('%04x ', info.refered[i]);

}

ss.write(']');

log(ss.str);

}

...

그리고 프로그램을 실행하여 다음 결과를 얻는다.

실행 결과 (log)

main.hdo 분석 결과

print.hdo 분석 결과

objectInfo 0:

> local labels

data _sHelloWorld: 0000 [ 0077 ]

code _sum: 0000 [ 00a7 ]

code _main: 005d [ 00f3 ]

code _prints: 0000 [ 0083 ]

code _printn: 0000 [ 00c6 ]

> data segment [14(000e)]

0: 1, ['hello, world!',0]

> code segment [250(00fa)]

push ebp

mov ebp,esp

mov edx,[ebp+0x8]

mov eax,[ebp+0xc]

add eax,edx

mov esp,ebp

pop ebp

ret

push ebp

mov ebp,esp

push 0x0000

call 0x0000

add esp,4

push 4

push 3

call 0x0000

add esp,8

push eax

call 0x0000

add esp,4

mov esp,ebp

pop ebp

ret

end 0x0000

objectInfo 1:

> local labels

data _sEnteredString: 0000 [ 0020 ]

data _sEnteredNumber: 0011 [ 0074 ]

> data segment [34(0022)]

0: 1, ['Entered String: ',0]

1: 1, ['Entered Number: ',0]

> code segment [168(00a8)]

push ebp

mov ebp,esp

handy puts,0x0000

handy puts,[ebp+0x8]

mov esp,ebp

pop ebp

ret

push ebp

mov ebp,esp

handy puts,0x0000

handy putn,[ebp+0x8]

mov esp,ebp

pop ebp

ret

===== global labels =====

code _prints: [1] 0000 [ ]

code _printn: [1] 0054 [ ]

link complete

이 결과는 아주 중요하다. print.hdo 파일의 경우는, 지역 레이블에 _printn, _prints 레이블이 뜨지 않는다. 이는 우리가 정확히 원하는 결과다. 다만 문제는 main.hdo와 전역 레이블의 출력에 있다. main 오브젝트 파일에서 _prints, _printn을 참조하고 있는데도 전역 레이블 출력 부분에서 참조가 리스트에 들어가지 않았다. 그리고 main.hdo의 지역 레이블에 이들의 참조가 들어갔다. 왜 이런 일이 발생했을까?

문제는 목적 파일을 링크한 순서에 있다. _prints, _printn은 모두 전역 레이블이지만, main.hdo에는 이들이 전역 레이블임을 알 수 있는 장치가 없다. global _prints, global _printn과 같은 구문이 없다는 뜻이다. 만약 우리가 main.hdoprint.hdo의 순서를 바꿔서 load 메서드를 호출한다면, print.hdo를 먼저 load한 다음 main.hdo를 로드한다면 위와 다른 결과를 얻는다.

실행 결과 (log)

print.hdo 분석 결과

main.hdo 분석 결과

objectInfo 0:

> local labels

data _sEnteredString: 0000 [ 0020 ]

data _sEnteredNumber: 0011 [ 0074 ]

code puts: 0000 [ 0020 ]

> data segment [34(0022)]

0: 1, ['Entered String: ',0]

1: 1, ['Entered Number: ',0]

> code segment [168(00a8)]

push ebp

mov ebp,esp

...

objectInfo 1:

> local labels

data _sHelloWorld: 0000 [ 0077 ]

code _sum: 0000 [ 00a7 ]

code _main: 005d [ 00f3 ]

> data segment [14(000e)]

0: 1, ['hello, world!',0]

> code segment [250(00fa)]

push ebp

mov ebp,esp

mov edx,[ebp+0x8]

...

===== global labels =====

code _prints: [0] 0000 [ [object Object] ]

code _printn: [0] 0054 [ [object Object] ]

link complete

main.hdo에서 지역 레이블의 목록에 _prints, _printn이 없어지고, 전역 레이블에 이상한 객체가 들어갔다. 사실 이 객체는 우리가 전역 레이블에 대한 참조 위치를 넘길 때 생성한 객체다. toString 메서드를 오버라이딩 하지 않았기 때문에 기본 문자열이 출력된 것뿐이다.

gLabelInfo.refered.push({ objectIndex:objectIndex, offset:refered_addr });

따라서 이를 확인하기 위해 link 메서드를 다음과 같이 수정하면,

ProgramLinker.js (link.show_global_labels)

...

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

var refered = info.refered[i];

ss.write('[%d:%04x] ', refered.objectIndex, refered.offset);

}

...

프로그램을 실행했을 때 올바른 결과를 얻을 수 있다.

실행 결과 (log.global_labels)

===== global labels =====

code _prints: [0] 0000 [ [1:0083] ]

code _printn: [0] 0054 [ [1:00c6] ]

link complete

이 결과는 이렇게 해석하면 된다.

_prints

> _prints는 코드 영역에 정의되어있다.

> _prints0번째로 load한 목적 파일의 시작점에서 0만큼 떨어진 곳에 정의되어있다.

> _prints1번째로 load한 목적 파일이 0x0083 오프셋에서 참조한다.

_printn

> _printn은 코드 영역에 정의되어있다.> _printn0번째로 load한 목적 파일의 시작점에서 0x0054만큼 떨어진 곳에 정의되어있다.

> _printn1번째로 load한 목적 파일이 0x00c6 오프셋에서 참조한다.

결국 문제는 링크 순서에 따라 어떤 레이블이 전역 레이블임을 인지하지 못할 수도 있다는 것이다. 이 문제를 어떻게 해결해야 할까? 필자는 그 답으로 extern 지시어(directive)를 제안한다.

 

6.4) extern

extern은 특정 레이블이 파일의 외부에 정의되어있음을 링커에게 알리는 지시어다. 따라서 레이블을 사용하기 전에, extern 지시어로 해당 레이블이 전역에 정의되어있음을 알리면 된다. 다만 extern 키워드는 파일 내에서만 유효하기 때문에, extern 키워드로 지정된 레이블을 그냥 Linker.GlobalLabelDict에 넣으면 원래 내부 레이블로만 사용하려고 했던 것을 전역 레이블로 오인할 수 있다. 따라서 externLabelDict라는 딕셔너리를 새로 생성하고, load가 끝날 때 Linker.GlobalLabelDict에 참조 위치를 추가하는 식으로 구현한다. 여기서 global 또한 globalLabelDict라는 지역 변수를 만들고, 그 다음 Linker 모듈의 GlobalLabelDict에 추가하는 식으로 구현하도록 코드가 바뀐다.

코딩을 시작하자. 어떤 레이블이 전역 레이블인지 판정하는 is_global 메서드의 이름을 바꾼다.

ProgramLinker.js (load.is_global_label)

...

function is_public(label) { // is_global -> is_public으로 이름을 바꿉니다.

return (Program.Linker.GlobalLabelDict[label] != undefined);

}

...

디버깅을 편하게 하기 위해 전역 레이블 정보 형식 GlobalLabelInfo를 약간 바꾼다.

ProgramLinker.js (GlobalLabelInfo)

...

this.offset = -1; // 목적 파일의 인덱스로부터 세그먼트의 시작 위치를 계산합니다.

...

extern 레이블 정보, global 레이블 정보를 보관하는 딕셔너리를 생성한다.

ProgramLinker.js (load.prepare_externDict)

...

// extern 레이블에 대한 정보를 보관합니다.

var externLabelDict = {}; // 목적 파일 별로 다르기 때문에 매번 새로 생성합니다.

function is_external(label) {

return (externLabelDict[label] != undefined) ? true : false;

}

// global 레이블에 대한 정보를 보관합니다.

var globalLabelDict = {};

function is_global(label) {

return (globalLabelDict[label] != undefined) ? true : false;

}

...

그리고 global의 처리 밑에 extern에 대한 처리를 다음과 같이 추가한다.

ProgramLinker.js (load.extern)

...

else if (lineToken == 'extern') { // 레이블이 외부에 있음을 알리는 구문이라면

label = lineBuffer.get_token(); // 레이블을 획득합니다.

 

// 레이블 획득에 실패하면 예외 처리합니다.

if (label == null)

throw new LinkerException('directive extern got empty param');

else if (is_label_name(label) == false)

throw new LinkerException('directive extern got invalid param', label);

 

// 등록된 외부 레이블이 아니라면 딕셔너리에 새로 생성합니다.

// 이미 등록되어있다면 무시합니다.

if (is_external(label) == false) {

externLabelDict[label] = new GlobalLabelInfo

(objectIndex, segment_target, label);

}

continue; // 전역 레이블 지정자에 대한 처리를 종료합니다.

}

...

이제 외부 레이블을 참조하는 코드 세그먼트의 처리를 수정한다. 정의는 예외 처리하면 된다.

ProgramLinker.js (load.codeseg.define_label)

...

// 외부 레이블 딕셔너리에 등록되어있다면

if (is_external(label)) {

// 레이블 중복 정의로 간주하고 예외 처리합니다.

throw new LinkerException('label name is same with included external label');

}

// 전역 레이블 딕셔너리에 등록되어있다면

else if (is_global(label)) {

// 전역 레이블 정보 객체를 획득합니다.

var gLabelInfo = globalLabelDict[label];

 

// 현재 목적 파일의 인덱스와 발견한 레이블 정의를 기록합니다.

gLabelInfo.index = objectIndex;

gLabelInfo.offset = sizeOfCode;

}

...

여기서, 전역 레이블 정보를 Linker.Global에 바로 넣지 않고 globalLabelDict 지역 딕셔너리에 넣었다는 점을 눈여겨봐야 한다.

레이블의 참조는 다음과 같이 작성한다.

ProgramLinker.js (load.codeseg.ref_label)

...

// 외부 레이블이라면

if (is_external(label)) {

// 외부 레이블 정보 객체를 획득합니다.

var eLabelInfo = externLabelDict[label];

 

// 참조하고 있는 위치를 보관합니다.

eLabelInfo.refered.push({

objectIndex:objectIndex, offset:refered_addr

});

}

// 전역 레이블이라면

else if (is_global(label)) {

...

마지막으로, 획득한 외부 레이블 정보를 모두 전역 레이블 딕셔너리로 옮긴다.

ProgramLinker.js (load.end)

...

// 외부 레이블 정보를 모두 공용 전역 레이블 딕셔너리로 옮깁니다.

for (label in externLabelDict) {

// 외부 레이블 정보를 획득합니다.

var info = externLabelDict[label];

 

// 전역 레이블 딕셔너리에 정보가 있다면

if (is_public(label)) {

// 전역 레이블 정보를 획득합니다.

var gLabelInfo = Linker.GlobalLabelDict[label];

 

// 참조 위치 배열에 참조 위치를 추가합니다.

var ref = info.refered;

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

gLabelInfo.refered.push(ref[i]);

}

 

}

// 전역 레이블 딕셔너리에 정보가 없다면 바로 대치합니다.

else {

Linker.GlobalLabelDict[label] = info;

}

}

// 전역 레이블 정보를 모두 공용 전역 레이블 딕셔너리로 옮깁니다.

for (label in globalLabelDict) {

// 외부 레이블 정보를 획득합니다.

var info = globalLabelDict[label];

 

// 전역 레이블 딕셔너리에 정보가 있다면

if (is_public(label)) {

// 전역 레이블 정보를 획득합니다.

var gLabelInfo = Linker.GlobalLabelDict[label];

 

if (info.offset >= 0) {

gLabelInfo.index = info.index;

gLabelInfo.offset = info.offset;

}

 

// 참조 위치 배열에 참조 위치를 추가합니다.

var ref = info.refered;

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

gLabelInfo.refered.push(ref[i]);

}

 

}

// 전역 레이블 딕셔너리에 정보가 없다면 바로 대치합니다.

else {

Linker.GlobalLabelDict[label] = info;

}

}

 

// 목적 파일 정보를 보관하는 새 객체를 생성합니다.

...

그리고 테스트를 위해 main.hdo의 코드 영역 가장 위에 이 두 문장을 추가한다.

main.hdo (extern)

...

.code

extern _prints

extern _printn

;====================================

; proc sum

...

그러면 프로그램을 실행하여 다음 결과를 얻는다.

실행 결과 (log.show_global_labels)

===== global labels =====

code _prints: [1] 0000 [ [0:0083] ]

code _printn: [1] 0054 [ [0:00c6] ]

link complete

이제 모든 결과가 정상적으로 나온다. 우리에게 남은 건 이들을 실제로 묶는 과정뿐이다.

 

6.5) 레이블 위치 확인

링크 문제를 해결하기 위해 지금까지 획득한 모든 레이블 정보를 펼쳐보자.

실행 결과 (log)

main.hdo 분석 결과

print.hdo 분석 결과

objectInfo 0:

> local labels

data _sHelloWorld: 0000 [ 0077 ]

code _sum: 0000 [ 00a7 ]

code _main: 005d [ 00f3 ]

> data segment [14(000e)]

0: 1, ['hello, world!',0]

> code segment [250(00fa)]

push ebp

mov ebp,esp

mov edx,[ebp+0x8]

mov eax,[ebp+0xc]

add eax,edx

mov esp,ebp

pop ebp

ret

push ebp

mov ebp,esp

push 0x0000 ; _sHelloWorld

call 0x0000 ; _prints

add esp,4

push 4

push 3

call 0x0000 ; _sum

add esp,8

push eax

call 0x0000 ; _printn

add esp,4

mov esp,ebp

pop ebp

ret

end 0x0000 ; _main

objectInfo 1:

> local labels

data _sEnteredString: 0000 [ 0020 ]

data _sEnteredNumber: 0011 [ 0074 ]

code puts: 0000 [ 0020 ]

> data segment [34(0022)]

0: 1, ['Entered String: ',0]

1: 1, ['Entered Number: ',0]

> code segment [168(00a8)]

push ebp

mov ebp,esp

handy puts,0x0000 ; _sEnteredString

handy puts,[ebp+0x8]

mov esp,ebp

pop ebp

ret

push ebp

mov ebp,esp

handy puts,0x0000 ; _sEnteredNumber

handy putn,[ebp+0x8]

mov esp,ebp

pop ebp

ret

===== global labels =====

code _prints: [1] 0000 [ [0:0083] ]

code _printn: [1] 0054 [ [0:00c6] ]

link complete

우리가 값을 변경해야 하는 위치에 주석을 표시했다. 코드를 작성하여 레이블을 대치하기 전에 각각에 어떤 값이 들어가야 하는지를 계산해보는 것도 검증하기 좋을 것이다.

이제 값을 대치해보자. 전략은 이렇다.

1. 전역 레이블 딕셔너리의 목적 파일 인덱스와 오프셋을 이용하여, 전역 레이블의 정의를 계산한다.

우리는 각각의 목적 파일의 세그먼트 크기를 알고 있고, 세그먼트의 시작 지점부터 전역 레이블까지의 오프셋을 알고 있다. 따라서 데이터 세그먼트의 전체 크기를 획득하고, 현재 목적 파일까지의 코드 세그먼트의 크기를 모두 더한 다음 여기서 오프셋을 더하면 된다. 그림으로 보자. 다음과 같이 소스 코드 세 개가 나란히 있고, 전역 레이블 _labels2의 코드 영역에 정의되어있다고 하면,



이것을 링크했을 때 _label은 다음 위치에 있게 된다는 것이다.



일단 이 방식으로 모든 전역 레이블의 위치가 어디에 있는지를 계산해야 한다.

2. 목적 파일을 순서대로 해석하면서 지역 레이블과 전역 레이블을 대치한다.

3. 대치가 끝나면 파일에 출력한다.

load 메서드의 마지막에 다음 코드를 추가하는 것으로 시작하자.

ProgramLinker.js (load.end)

...

// 목적 파일 리스트의 데이터 세그먼트, 코드 세그먼트의 크기를 구합니다.

Linker.sizeOfData += sizeOfData;

Linker.sizeOfCode += sizeOfCode;

}

프로그램의 시작 지점을 설정하는 end 지시어에 대해 처리되지 않았으므로, 이에 대해 처리하자.

ProgramLinker.js (load.end_directive)

...

else if (lineToken == 'end') { // 프로그램 시작 지점이 발견되었다면

label = lineBuffer.get_token(); // 다음 토큰을 획득합니다.

Linker.entrypoint = label; // 시작 지점을 보관합니다.

continue;

}

...

한편, 링크를 목적으로 load한 파일을 사용하지 않게 될 경우를 위해 clear라는 메서드를 만든다.

ProgramLinker.js (clear)

/**

목적 파일 리스트를 비웁니다.

*/

function clear() {

var Linker = Program.Linker;

Linker.ObjectInfoList = [];

Linker.GlobalLabelDict = {};

Linker.sizeOfData = 0;

Linker.sizeOfCode = 0;

}

그리고 imageBase라는 필드가 Linker에 추가된다.

ProgramLinker.js (fields)

...

linker.imageBase = 4; // 메모리에 올라갈 바이트 배열의 시작점입니다.

...

우리는 Runner를 개발할 때 imageBase를 고려했기 때문에, Linker에서 이것이 고려되지 않은 상태로 레이블을 대치하면 imageBase만큼의 차이가 발생하게 된다.

이제 link 메서드를 수정할 차례다. 작성한 코드가 아까우니 이전 link의 이름을 test로 바꾼다.

ProgramLinker.js (load.end)

/**

테스트 메서드입니다.

*/

function test(filename) { // link -> test로 이름을 변경합니다.

...

그리고 위 전략을 토대로 link 메서드를 작성해보자.

ProgramLinker.js (link)

/**

불러온 목적 파일을 링크합니다.

@param {string} filename

*/

function link(filename) {

var Linker = Program.Linker;

var ObjectList = Linker.ObjectInfoList;

var GlobalDict = Linker.GlobalLabelDict;

 

/* ... */

}

link가 하는 일은 단순하다. load한 목적 파일을 모두 하나의 문자열 스트림에 기록한 다음, 이 스트림의 문자열을 파일로 출력한다.

이제 전역 레이블의 정의를 구하는 코드를 작성해야 하지만, 필자는 이들을 링크한 결과를 먼저 얻고자 한다. 레이블이 대치되지 않더라도 코드 자체는 0x0000으로 치환되어있어서, 이들을 메모리에 올린 다음 show 메서드를 실행하면 레이블이 어느 위치로 치환되어야 하는지 알기 쉽기 때문이다. 그러니 이에 대한 코드를 먼저 작성하자.

ProgramLinker.js (link)

function link(filename) {

var Linker = Program.Linker;

var ObjectList = Linker.ObjectInfoList;

var GlobalDict = Linker.GlobalLabelDict;

 

// 실행 가능한 목적 파일의 코드를 기록할 문자열 스트림입니다.

var dataStream = new Program.Stream.StringStream();

var codeStream = new Program.Stream.StringStream();

 

// 세그먼트 스트림에 맞게 정보를 출력합니다.

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

// 목적 파일 정보를 획득합니다.

var objectInfo = ObjectList[i];

// 데이터 세그먼트 스트림에 데이터를 출력합니다.

var segmentData = objectInfo.segment.data;

for (var j=0, size=segmentData.length; j<size; ++j) {

var data = segmentData[j];

 

var typeString = '';

switch (data.typeSize) {

case 1: typeString = 'db'; break;

case 2: typeString = 'dw'; break;

case 4: typeString = 'dd'; break;

}

 

dataStream.writeln('%s %s', typeString, data.value);

}

// 코드 세그먼트 스트림에 데이터를 출력합니다.

var segmentCode = objectInfo.segment.code;

for (var j=0, size=segmentCode.length; j<size; ++j) {

var code = segmentCode[j];

codeStream.writeln(code);

}

}

}

그리고 main.html 파일을 다음과 같이 바꾼다.

main.html <html.head.script>

...

function main() {

Program.Linker.load('main.hdo');

Program.Linker.load('print.hdo');

Program.Linker.link('program.hdx');

 

Program.Runner.load('program.hdx');

Machine.Memory.show();

}

...

그러면 프로그램을 실행하여 다음 결과를 얻는다.

실행 결과 (program.hdx)

.data

db 'hello, world!',0

db 'Entered String: ',0

db 'Entered Number: ',0

 

.code

push ebp

mov ebp,esp

mov edx,[ebp+0x8]

mov eax,[ebp+0xc]

add eax,edx

mov esp,ebp

pop ebp

ret

push ebp

mov ebp,esp

push 0x0000

call 0x0000

add esp,4

push 4

push 3

call 0x0000

add esp,8

push eax

call 0x0000

add esp,4

mov esp,ebp

pop ebp

ret

push ebp

mov ebp,esp

handy puts,0x0000

handy puts,[ebp+0x8]

mov esp,ebp

pop ebp

ret

push ebp

mov ebp,esp

handy puts,0x0000

handy putn,[ebp+0x8]

mov esp,ebp

pop ebp

ret

이제 Low Level Assembly의 의미가 이해되는가? 우리가 Linker로 생성한 hdx 코드는 분명 어셈블리 언어로 작성되어있지만 HASM보다는 읽는 것이 아주 불편하다. 레이블도 눈에 보이지 않고, 조건 분기나 레이블 참조는 모두 레이블의 이름을 사용하는 것이 아니라 즉시 값을 사용한다. 그래서 HASM보다 수준이 낮다는 뜻으로 Low Level Assembly라고 이를 이름 붙인 것이다.

이 파일을 분석한 결과가 메모리에 출력되도록 코드를 작성했으니, 이를 한 번 보자.

실행 결과 (memory)

0x0000 | 00 00 00 00 68 65 6c 6c 6f 2c 20 77 6f 72 6c 64 | ....hello, world

0x0010 | 21 00 45 6e 74 65 72 65 64 20 53 74 72 69 6e 67 | !.Entered String

0x0020 | 3a 20 00 45 6e 74 65 72 65 64 20 4e 75 6d 62 65 | : .Entered Numbe

0x0030 | 72 3a 20 00 70 75 73 68 20 65 62 70 00 6d 6f 76 | r: .push ebp.mov

0x0040 | 20 65 62 70 2c 65 73 70 00 6d 6f 76 20 65 64 78 | ebp,esp.mov edx

0x0050 | 2c 5b 65 62 70 2b 30 78 38 5d 00 6d 6f 76 20 65 | ,[ebp+0x8].mov e

0x0060 | 61 78 2c 5b 65 62 70 2b 30 78 63 5d 00 61 64 64 | ax,[ebp+0xc].add

0x0070 | 20 65 61 78 2c 65 64 78 00 6d 6f 76 20 65 73 70 | eax,edx.mov esp

0x0080 | 2c 65 62 70 00 70 6f 70 20 65 62 70 00 72 65 74 | ,ebp.pop ebp.ret

0x0090 | 00 70 75 73 68 20 65 62 70 00 6d 6f 76 20 65 62 | .push ebp.mov eb

0x00a0 | 70 2c 65 73 70 00 70 75 73 68 20 30 78 30 30 30 | p,esp.push 0x000

0x00b0 | 30 00 63 61 6c 6c 20 30 78 30 30 30 30 00 61 64 | 0.call 0x0000.ad

0x00c0 | 64 20 65 73 70 2c 34 00 70 75 73 68 20 34 00 70 | d esp,4.push 4.p

0x00d0 | 75 73 68 20 33 00 63 61 6c 6c 20 30 78 30 30 30 | ush 3.call 0x000

0x00e0 | 30 00 61 64 64 20 65 73 70 2c 38 00 70 75 73 68 | 0.add esp,8.push

0x00f0 | 20 65 61 78 00 63 61 6c 6c 20 30 78 30 30 30 30 | eax.call 0x0000

0x0100 | 00 61 64 64 20 65 73 70 2c 34 00 6d 6f 76 20 65 | .add esp,4.mov e

0x0110 | 73 70 2c 65 62 70 00 70 6f 70 20 65 62 70 00 72 | sp,ebp.pop ebp.r

0x0120 | 65 74 00 70 75 73 68 20 65 62 70 00 6d 6f 76 20 | et.push ebp.mov

0x0130 | 65 62 70 2c 65 73 70 00 68 61 6e 64 79 20 70 75 | ebp,esp.handy pu

0x0140 | 74 73 2c 30 78 30 30 30 30 00 68 61 6e 64 79 20 | ts,0x0000.handy

0x0150 | 70 75 74 73 2c 5b 65 62 70 2b 30 78 38 5d 00 6d | puts,[ebp+0x8].m

0x0160 | 6f 76 20 65 73 70 2c 65 62 70 00 70 6f 70 20 65 | ov esp,ebp.pop e

0x0170 | 62 70 00 72 65 74 00 70 75 73 68 20 65 62 70 00 | bp.ret.push ebp.

0x0180 | 6d 6f 76 20 65 62 70 2c 65 73 70 00 68 61 6e 64 | mov ebp,esp.hand

0x0190 | 79 20 70 75 74 73 2c 30 78 30 30 30 30 00 68 61 | y puts,0x0000.ha

0x01a0 | 6e 64 79 20 70 75 74 6e 2c 5b 65 62 70 2b 30 78 | ndy putn,[ebp+0x

0x01b0 | 38 5d 00 6d 6f 76 20 65 73 70 2c 65 62 70 00 70 | 8].mov esp,ebp.p

0x01c0 | 6f 70 20 65 62 70 00 72 65 74 00 00 00 00 00 00 | op ebp.ret......

...

굉장히 눈 아픈 결과지만 한 번 보자. 일단 알아보기 쉽게 프로시저 단위로 색을 구분했다. 코드를 자세히 보면, 주황색은 데이터 세그먼트, 파란색은 _sum이고, 빨간색, 옥색, 갈색은 _main, _prints, _printn임을 파악할 수 있다. 이 내용을 바탕으로 레이블의 위치를 파악하면 다음과 같다.

_sHelloWorld 0x0004, _sEnteredString 0x0012, _sEnteredNumber 0x0023,

_sum 0x0034, _main 0x0091, _prints 0x0123,

_printn 0x0177

따라서 이 내용에 맞게 레이블을 대치할 수 있으면 링크를 성공하는 것이다.

 

6.6) 전역 레이블 위치 계산

지금 우리가 작성한 코드를 다시 한 번 보자. 코드 세그먼트에서 가져온 코드를 별다른 변환 없이 바로 스트림에 기록을 하고 있다.

ProgramLinker.js (link.write_stream)

...

// 코드 세그먼트로부터 코드를 가져옵니다.

var code = segmentCode[j];

// 가져온 코드를 스트림에 출력합니다.

codeStream.writeln(code);

...

그런데 여기에 왼쪽 인자와 오른쪽 인자가 무엇이었는지를 별도로 보관하면, 레이블을 대치할 때 아주 편리할 것 같다. 그러니 코드를 보관하는 부분을 다음과 같이 변경하자.

ProgramLinker.js (load.codeseg.push)

...

// 획득한 줄 텍스트를 보관합니다.

segment[segment_target].push({

text:s, mnemonic:info.mnemonic, left:info.left, right:info.right

});

...

그리고 일단 dataStream, codeStream 이하는 무시하기로 하자. 이제 비로소 링크를 시작할 준비가 되었다. 먼저 전역 레이블의 정의를 계산해보자.

ProgramLinker.js (link.showGlobalLabelDef)

...

// 1. 전역 레이블의 정의를 계산합니다.

var imageBase = Linker.imageBase;

var dataBases = [];

var codeBases = [];

var baseOfData = 0;

var baseOfCode = 0;

var sizeOfData = Linker.sizeOfData;

// 목적 파일 정보를 순회하면서 세그먼트의 시작점을 찾아 보관합니다.

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

// 목적 파일 정보를 획득합니다.

var objectInfo = ObjectList[i];

 

// i번째 목적 파일의 시작점을 보관합니다.

dataBases.push(baseOfData);

codeBases.push(baseOfCode);

 

// i+1번째 목적 파일의 시작점을 획득합니다.

baseOfData += objectInfo.sizeOfData;

baseOfCode += objectInfo.sizeOfCode;

}

// 획득한 세그먼트의 시작점을 바탕으로 전역 레이블의 정의를 계산합니다.

for (gLabel in GlobalDict) {

// 전역 레이블 정보를 획득합니다.

var globalInfo = GlobalDict[gLabel];

 

// 전역 레이블의 위치는

// 1. 세그먼트의 시작점(imageBase)

// 2. 코드 세그먼트의 시작점(sizeOfData)

// 3. 코드 세그먼트에서 목적 파일의 코드 세그먼트 시작점(codeBases)

// 4. 세그먼트 시작점으로부터의 오프셋(offset)

// 의 합입니다.

globalInfo.offset +=

(imageBase + sizeOfData + codeBases[globalInfo.index]);

// 이후부터 목적 파일의 인덱스를 신경쓰지 않습니다.

globalInfo.index = -1;

}

 

test();

return;

...

반복문이 총 두 개가 있다. 프로그램을 실행하면 먼저 나오는 반복문에서, 목적 파일 리스트에서 요소들을 순회하면서 각각의 목적 파일의 세그먼트 시작 위치를 보관해놓는다. 그 다음 반복문이 바로 정의를 대치하는 구문이다. 전역 레이블의 위치는 imageBase + sizeOfData + codeBase + offset이 되는데, 이는 좀 오래 고민해야 이해할 수 있으니 생각해보기 바란다.

마지막으로 test를 호출하여 전역 레이블의 정의가 제대로 들어갔는지 확인할 필요가 있다.

실행 결과 (GlobalLabelOffset)

===== global labels =====

code _prints: [-1] 0123 [ [0:0083] ]

code _printn: [-1] 0177 [ [0:00c6] ]

link complete

load complete

이는 우리가 6.5절 마지막에서 구한 결과와 일치한다. 따라서 정의 문제는 해결되었다. 그런데 우리는 이미 각 세그먼트의 시작점과 전체 데이터 세그먼트의 크기를 구했기 때문에, 사실 참조 문제도 이 반복문에서 해결할 수가 있다. 이것도 진행하자.

ProgramLinker.js (link.GlobalLabelReferenceOffset)

...

// 전역 레이블의 참조 위치는

// 1. 세그먼트의 시작점(imageBase)

// 2. 코드 세그먼트의 시작점(sizeOfData)

// 3. 코드 세그먼트에서 참조 목적 파일의 코드 세그먼트 시작점(codeBases)

// 4. 세그먼트 시작점으로부터의 오프셋(offset)

// 의 합입니다.

var ref = globalInfo.refered;

for (var j=0, gCount=ref.length; j<gCount; ++j) {

ref[j].offset +=

(imageBase + sizeOfData + codeBases[ref[j].objectIndex]);

}

 

// 이후부터 목적 파일의 인덱스를 신경쓰지 않습니다.

globalInfo.index = -1;

...

이 과정까지 진행한 후 프로그램을 실행하면 다음 결과를 얻는다.

실행 결과 (GlobalLabelOffset)

===== global labels =====

code _prints: [-1] 0123 [ [0:00b7] ]

code _printn: [-1] 0177 [ [0:00fa] ]

link complete

load complete

이 결론이 타당한가는 다음 메모리 상태를 통해 확인할 수 있다.

실행 결과 (memory)

...

0x00b0 | 30 00 63 61 6c 6c 20 30 78 30 30 30 30 00 61 64 | 0.call 0x0000.ad

...

0x00f0 | 20 65 61 78 00 63 61 6c 6c 20 30 78 30 30 30 30 | eax.call 0x0000

...

이를 통해 참조 위치가 정확하계 계산되었음을 확인할 수 있다.

 

6.7) 지역 레이블 위치 계산

지역 레이블의 위치 계산은 전역의 경우보다 쉽다. 레이블이 언제나 목적 파일에 존재해서, 목적 파일 인덱스를 고려하지 않아도 되기 때문이다. 또한 이러한 특성을 이용하면, 지역 레이블의 위치를 계산하면서 코드 세그먼트 스트림에 문자열을 출력할 수도 있다. 바로 진행하자.

6.6절에서 segment.code에 푸시하는 것이 문자열에서 객체로 바뀌었기 때문에, 이를 반영하여 이전에 작성한 코드를 다음과 같이 수정해야 한다.

ProgramLinker.js (link.write_code)

...

for (var j=0, size=segmentCode.length; j<size; ++j) {

// 코드 세그먼트로부터 코드를 가져옵니다.

var code = segmentCode[j];

 

// 피연산자가 비어있지 않은 경우

if (code.left != undefined) {

var left = '', right = '';

var leftLabel = null, rightLabel = null;

 

// 피연산자가 레이블이라면 레이블 이름을 획득합니다.

if (is_label_name(code.left)) { // 레이블이라면

var leftLabel = code.left;

left = ' 0x0000';

}

else { // 레이블이 아니라면 그냥 추가합니다.

left = ' ' + code.left;

}

if (code.right != undefined) { // 오른쪽 피연산자가 있다면

if (is_label_name(code.right)) { // 레이블이라면

var rightLabel = code.right;

right = ',0x0000';

}

else { // 레이블이 아니라면 그냥 추가합니다.

right = ',' + code.right;

}

}

 

// 코드 세그먼트의 코드를 형식에 맞춰 출력합니다.

var text = Handy.format

('%s%s%s', code.mnemonic, left, right);

 

// 형식화된 문자열을 코드 세그먼트 스트림에 출력합니다.

codeStream.writeln(text);

}

else {

// 가져온 코드를 스트림에 출력합니다.

codeStream.writeln(code.text);

}

}

...

지역 레이블의 정의와 참조 문제가 아직 해결되지 않았기 때문에, 레이블에 대한 처리는 일단 0으로 채우기로 했다. 그럼 지역 레이블의 정의와 참조 문제를 해결해보자.

ProgramLinker.js (link.calc_local_label)

...

// 구하려는 레이블의 소속에 따라 시작점을 결정합니다.

var segmentBase = 0;

if (labelInfo.segmentName == 'data') {

segmentBase = dataBases[i];

// 지역 레이블의 위치는

// 1. 세그먼트의 시작점(imageBase)

// 2. 세그먼트에서 목적 파일의 세그먼트 시작점(segmentBase)

// 3. 세그먼트 시작점으로부터의 오프셋(offset)

// 의 합입니다.

labelInfo.offset += (imageBase + segmentBase);

// 지역 레이블의 참조 위치는

// 1. 세그먼트의 시작점(imageBase)

// 2. 세그먼트에서 목적 파일의 세그먼트 시작점(segmentBase)

// 3. 세그먼트 시작점으로부터의 오프셋(offset)

// 의 합입니다.

var ref = labelInfo.refered;

for (var j=0, gCount=ref.length; j<gCount; ++j) {

ref[j] += (imageBase + segmentBase);

}

}

else {

segmentBase = codeBases[i];

// 지역 레이블의 위치는

// 1. 세그먼트의 시작점(imageBase)

// 2. 코드 세그먼트의 시작점(sizeOfData)

// 3. 세그먼트에서 목적 파일의 세그먼트 시작점(segmentBase)

// 4. 세그먼트 시작점으로부터의 오프셋(offset)

// 의 합입니다.

labelInfo.offset += (imageBase + sizeOfData + segmentBase);

// 지역 레이블의 참조 위치는

// 1. 세그먼트의 시작점(imageBase)

// 2. 코드 세그먼트의 시작점(sizeOfData)

// 3. 세그먼트에서 목적 파일의 세그먼트 시작점(segmentBase)

// 4. 세그먼트 시작점으로부터의 오프셋(offset)

// 의 합입니다.

var ref = labelInfo.refered;

for (var j=0, gCount=ref.length; j<gCount; ++j) {

ref[j] += (imageBase + sizeOfData + segmentBase);

}

}

...

링커 문제에서 가장 헷갈리는 부분이 등장했다.

 

이것을 실행하면 다음 결과를 얻는다.

실행 결과 (GlobalLabelOffset)

main.hdo 분석 결과

print.hdo 분석 결과

objectInfo 0:

> local labels

data _sHelloWorld: 0004 [ 007b ]

code _sum: 0034 [ 00db ]

code _main: 0091 [ ]

> data segment [14(000e)]

0: 1, ['hello, world!',0]

> code segment [239(00ef)]

...

objectInfo 1:

> local labels

data _sEnteredString: 0012 [ 0032 ]

data _sEnteredNumber: 0023 [ 0086 ]

> data segment [34(0022)]

0: 1, ['Entered String: ',0]

1: 1, ['Entered Number: ',0]

> code segment [168(00a8)]

...

===== global labels =====

code _prints: [-1] 0123 [ [0:00b7] ]

code _printn: [-1] 0177 [ [0:00fa] ]

link complete

load complete

이 결과가 타당하다는 것은 다음을 보면서 비교하면 명백하다.

_sHelloWorld 0x0004, _sEnteredString 0x0012, _sEnteredNumber 0x0023,

_sum 0x0034, _main 0x0091, _prints 0x0123,

_printn 0x0177

이로써 데이터 세그먼트의 정의와 참조의 대치가 끝난다. 나머지는 이를 적용하는 과정뿐이다.

 

6.8) link

코드 세그먼트 스트림에 데이터를 출력하는 반복문을 다음과 같이 변경한다.

ProgramLinker.js (link.codeseg.write)

...

// 코드 세그먼트 스트림에 데이터를 출력합니다.

var segmentCode = objectInfo.segment.code;

for (var j=0, size=segmentCode.length; j<size; ++j) {

// 코드 세그먼트로부터 코드를 가져옵니다.

var code = segmentCode[j];

 

// 피연산자가 비어있지 않은 경우

if (code.left != undefined) {

var left = '', right = '';

var leftLabel = null, rightLabel = null;

 

// 피연산자가 레이블이라면 레이블 이름을 획득합니다.

if (is_label_name(code.left)) { // 레이블이라면

var leftLabel = code.left;

 

var leftInfo = null;

if (is_public(leftLabel)) {

leftInfo = Linker.GlobalLabelDict[leftLabel];

}

else {

leftInfo = labelDict[leftLabel];

}

 

// 획득한 레이블의 정의로 대치합니다.

left = Handy.format(' 0x%04x', leftInfo.offset);

}

else { // 레이블이 아니라면 그냥 추가합니다.

left = ' ' + code.left;

}

if (code.right != undefined) { // 오른쪽 피연산자가 있다면

if (is_label_name(code.right)) { // 레이블이라면

var rightLabel = code.right;

 

var rightInfo = labelDict[rightLabel];

if (is_public(rightLabel)) {

rightInfo = Linker.GlobalLabelDict[rightLabel];

}

else {

rightInfo = labelDict[rightLabel];

}

 

// 획득한 레이블의 정의로 대치합니다.

right = Handy.format(',0x%04x', rightInfo.offset);

}

else { // 레이블이 아니라면 그냥 추가합니다.

right = ',' + code.right;

}

}

 

// 코드 세그먼트의 코드를 형식에 맞춰 출력합니다.

var text = Handy.format

('%s%s%s', code.mnemonic, left, right);

 

// 형식화된 문자열을 코드 세그먼트 스트림에 출력합니다.

codeStream.writeln(text);

}

else {

// 가져온 코드를 스트림에 출력합니다.

codeStream.writeln(code.text);

}

}

...

그리고 마지막에 파일을 출력하는 구문을 넣으면,

ProgramLinker.js (link.write_to_file)

...

// 파일에 출력합니다.

var sstream = new Program.Stream.StringStream();

sstream.writeln('.data');

sstream.writeln(dataStream.str);

sstream.writeln('.code');

sstream.writeln(codeStream.str);

HandyFileSystem.save(filename, sstream.str);

...

프로그램을 실행하여 다음 결과를 얻는다.

실행 결과 (program.hdx)

.data

db 'hello, world!',0

db 'Entered String: ',0

db 'Entered Number: ',0

 

.code

push ebp

mov ebp,esp

mov edx,[ebp+0x8]

mov eax,[ebp+0xc]

add eax,edx

mov esp,ebp

pop ebp

ret

push ebp

mov ebp,esp

push 0x0004

call 0x0123

add esp,4

push 4

push 3

call 0x0034

add esp,8

push eax

call 0x0177

add esp,4

mov esp,ebp

pop ebp

ret

push ebp

mov ebp,esp

handy puts,0x0012

handy puts,[ebp+0x8]

mov esp,ebp

pop ebp

ret

push ebp

mov ebp,esp

handy puts,0x0023

handy putn,[ebp+0x8]

mov esp,ebp

pop ebp

ret

실행 결과 (memory)

0x0000 | 00 00 00 00 68 65 6c 6c 6f 2c 20 77 6f 72 6c 64 | ....hello, world

0x0010 | 21 00 45 6e 74 65 72 65 64 20 53 74 72 69 6e 67 | !.Entered String

0x0020 | 3a 20 00 45 6e 74 65 72 65 64 20 4e 75 6d 62 65 | : .Entered Numbe

0x0030 | 72 3a 20 00 70 75 73 68 20 65 62 70 00 6d 6f 76 | r: .push ebp.mov

0x0040 | 20 65 62 70 2c 65 73 70 00 6d 6f 76 20 65 64 78 | ebp,esp.mov edx

0x0050 | 2c 5b 65 62 70 2b 30 78 38 5d 00 6d 6f 76 20 65 | ,[ebp+0x8].mov e

0x0060 | 61 78 2c 5b 65 62 70 2b 30 78 63 5d 00 61 64 64 | ax,[ebp+0xc].add

0x0070 | 20 65 61 78 2c 65 64 78 00 6d 6f 76 20 65 73 70 | eax,edx.mov esp

0x0080 | 2c 65 62 70 00 70 6f 70 20 65 62 70 00 72 65 74 | ,ebp.pop ebp.ret

0x0090 | 00 70 75 73 68 20 65 62 70 00 6d 6f 76 20 65 62 | .push ebp.mov eb

0x00a0 | 70 2c 65 73 70 00 70 75 73 68 20 30 78 30 30 30 | p,esp.push 0x000

0x00b0 | 34 00 63 61 6c 6c 20 30 78 30 31 32 33 00 61 64 | 4.call 0x0123.ad

0x00c0 | 64 20 65 73 70 2c 34 00 70 75 73 68 20 34 00 70 | d esp,4.push 4.p

0x00d0 | 75 73 68 20 33 00 63 61 6c 6c 20 30 78 30 30 33 | ush 3.call 0x003

0x00e0 | 34 00 61 64 64 20 65 73 70 2c 38 00 70 75 73 68 | 4.add esp,8.push

0x00f0 | 20 65 61 78 00 63 61 6c 6c 20 30 78 30 31 37 37 | eax.call 0x0177

0x0100 | 00 61 64 64 20 65 73 70 2c 34 00 6d 6f 76 20 65 | .add esp,4.mov e

0x0110 | 73 70 2c 65 62 70 00 70 6f 70 20 65 62 70 00 72 | sp,ebp.pop ebp.r

0x0120 | 65 74 00 70 75 73 68 20 65 62 70 00 6d 6f 76 20 | et.push ebp.mov

0x0130 | 65 62 70 2c 65 73 70 00 68 61 6e 64 79 20 70 75 | ebp,esp.handy pu

0x0140 | 74 73 2c 30 78 30 30 31 32 00 68 61 6e 64 79 20 | ts,0x0012.handy

0x0150 | 70 75 74 73 2c 5b 65 62 70 2b 30 78 38 5d 00 6d | puts,[ebp+0x8].m

0x0160 | 6f 76 20 65 73 70 2c 65 62 70 00 70 6f 70 20 65 | ov esp,ebp.pop e

0x0170 | 62 70 00 72 65 74 00 70 75 73 68 20 65 62 70 00 | bp.ret.push ebp.

0x0180 | 6d 6f 76 20 65 62 70 2c 65 73 70 00 68 61 6e 64 | mov ebp,esp.hand

0x0190 | 79 20 70 75 74 73 2c 30 78 30 30 32 33 00 68 61 | y puts,0x0023.ha

0x01a0 | 6e 64 79 20 70 75 74 6e 2c 5b 65 62 70 2b 30 78 | ndy putn,[ebp+0x

0x01b0 | 38 5d 00 6d 6f 76 20 65 73 70 2c 65 62 70 00 70 | 8].mov esp,ebp.p

0x01c0 | 6f 70 20 65 62 70 00 72 65 74 00 00 00 00 00 00 | op ebp.ret......

...

이제 진입점 문제만 남았는데, 이는 어렵지 않다. 다만 실행기 Runner의 행동이 약간 바뀐다.

먼저 end 지시어를 Linker에서 처리해보자. 어떤 프로시저가 프로그램의 시작점을 알리려면 이렇게 하는 것도 하나의 방법이다.

...

.code

call _main

exit

...

_main:

코드 영역의 시작점에 call _main을 등장시키는 것으로 프로시저의 시작점을 _main으로 맞출 수 있는 것이다. 다만 우리가 작성한 예제에서는 이것을 고려하려면 대규모의 오프셋 조정이 일어난다. 그래서 여기에서는 이런 방법을 쓴다.

...

.code

...

_main:

...

call 0x00ab ; _main = 0x00ab

exit

작성된 코드의 가장 마지막에 프로시저의 시작 지점을 알리는 코드를 넣는다. 이렇게 하면, 코드의 가장 마지막 지점에서 ‘call 0x00ab\nexit' 문자열의 길이만큼만 뺀 위치를 프로그램의 시작점으로 고정하는 것으로 문제를 해결할 수 있다. 그래서 우리 예제도 이런 방식으로 작성할 것이다.

먼저 end 지시어로 설정한 프로시저 시작 레이블의 위치를 획득하는 것으로 시작하자.

ProgramLinker.js (link.write.end)

...

// 파일에 출력합니다.

var sstream = new Program.Stream.StringStream();

sstream.writeln('.data');

sstream.writeln(dataStream.str);

sstream.writeln('.code');

sstream.writeln(codeStream.str);

 

// 진입점의 레이블을 획득합니다.

var entryLabel = Linker.entrypoint;

 

// 진입점의 절대 위치를 획득합니다.

var entrypoint = Linker.GlobalLabelDict[entryLabel].offset;

 

// 파일의 끝에 기록합니다.

sstream.writeln('call 0x%04x', entrypoint);

sstream.write('exit');

 

HandyFileSystem.save(filename, sstream.str);

...

그리고 여기서 진입점 프로시저가 반드시 global 형태로 정의되어야 함을 추론할 수 있다. main.hdo 파일을 다음과 같이 수정하자.

main.hdo

...

;====================================

; proc main

;====================================

global _main

_main:

...

Machine.Processor 모듈로 이동하여 run 메서드를 수정한다.

MachineProcessor.js (run)

...

setip(Runner.entrypoint);

...

이제 Program.Runner 모듈로 이동하여 load 메서드를 수정한다.

ProgramRunner.js (load)

...

setip(Runner.entrypoint);

...

Machine.Memory 모듈에 존재하는 오류를 하나 잡는다.

MachineMemory.js (get_memory_value)

function get_memory_value(param) {

var Register = Machine.Processor.Register; // 추가

...

// 획득한 레지스터가 보관하고 있는 값을 획득합니다.

var regval = Register.get(reg); // Register[reg]에서 수정

...

}

그리고 MachineOperate 모듈에 handy putn에 대한 처리를 추가하자.

MachineMemory.js (handy.putn)

...

else if (left == 'putn') {

var value = null;

 

// 레지스터라면 레지스터 값 획득

if (Register.is_register(right)) {

value = Register.get(right); // Register[right];

}

// 메모리라면 메모리 값 획득

else if (Memory.is_memory(right)) {

value = Memory.get_memory_value(right);

}

// 모두 아니라면 즉시 값 획득

else {

value = parseInt(right);

}

 

Stream.out.write(value);

}

...

마지막으로 main.html 파일에서 이 프로그램을 실행하는 구문을 추가하면,

main.html <html.head.script>

function main() {

Program.Linker.load('main.hdo');

Program.Linker.load('print.hdo');

Program.Linker.link('program.hdx');

Program.Runner.load('program.hdx');

Program.Runner.run();

Machine.Memory.show();

}

...

다음과 같이 성공적으로 동작하는 것을 확인할 수 있다.

실행 결과 (output)

Entered String: hello, world!Entered Number: 7

실행 결과 (expression)

call 0x0091

push ebp

mov ebp,esp

push 0x0004

call 0x0123

push ebp

mov ebp,esp

handy puts,0x0012

handy puts,[ebp+0x8]

mov esp,ebp

pop ebp

ret

add esp,4

push 4

push 3

call 0x0034

push ebp

mov ebp,esp

mov edx,[ebp+0x8]

mov eax,[ebp+0xc]

add eax,edx

mov esp,ebp

pop ebp

ret

add esp,8

push eax

call 0x0177

push ebp

mov ebp,esp

handy puts,0x0023

handy putn,[ebp+0x8]

mov esp,ebp

pop ebp

ret

add esp,4

mov esp,ebp

pop ebp

ret

이로써 길었던 링커 개발이 끝이 난다.

 

7. 단원 마무리

사실 이 프로젝트는 발전시키면 dll같은 동적 라이브러리도 동작할 수 있게 되지만, 이것까지는 내 연구에 포함되어있지 않았다. (이 경우는 Linker뿐만 아니라 Runner도 수정해야 할 것 같다)

링커를 만드는 것이 재미있는 건, 정말 사소한 이유로 위치가 틀려서 해당 위치로 값을 맞춰주어야 하고, 이것이 성공했을 때의 쾌감이 크기 때문이다. 만약 우리가 실제 주소 모드로 프로그래밍을 한다고 하면, 우리는 각각의 메모리 위치를 이런 식으로 직접 관리해야 한다. 가상 메모리를 통해 변수를 간단하게 선언할 수 있는 건, 생각보다 정말 굉장한 축복인 것이다.

다음은 최종장인 C 컴파일러를 다룬다. 단언컨대 이 이후가 JSCC 프로젝트에서 가장 재미있다. 우리는 RunnerCompiler를 오가면서 코딩을 할 것이고, 그렇게 하나하나 필요한 구문을 완성해나갈 것이다. 다음 문서를 읽고 나면 C언어가 무엇인지, 나아가서는 고급 프로그래밍 언어가 어떤 것인지에 대해 감을 잡을 수 있으리라 확신한다.

'알려주기' 카테고리의 다른 글

Git Usages  (0) 2015.07.18
[JSCC] 11. C 컴파일러 개발 (기본편)  (2) 2015.07.17
[JSCC] 9. 가상 머신 개발 2  (0) 2015.07.03
[JSCC] 8. 가상 머신 개발 1  (2) 2015.06.26
[JSCC] 7. JSCC 준비  (0) 2015.06.19
Posted by 누아니
,