2009년 12월 24일 목요일

2장 - 환경 구축

2장 - 환경 구축

우리는 지난 시간에 what is assembly ? 라는 질문에 어떻게 답변할 지를 배웠다. 또한 "단순한 마인드"의 필요성을 실감했다. 이외에도 어셈블리
언어를 배우려면 툴이 있어야겠다. 가장 좋은 툴은 역시 자신의 "머리"일 것이다. 부수적으로 필요한 툴을 몇개 나열하면 이렇다:

1. 컴퓨터 & 운영체제 (따로 언급할 필요가 없겠다.)
2. CPU 매뉴얼 (인텔 홈피 참조)
3. 텍스트 편집기 (메모장이나 텍스트 에디터)
4. 어셈블러 & 링커 (www.movsd.com 참조)
5. 리소스 (책, 인터넷 사이트)는 많을수록 좋고, 절판된 책에 나온 코드도 살짝 뜯어고쳐 쓸 수 있다. 우측 링크 목록에 나름대로 중요하다싶은
   링크를 걸어뒀으니 참조하라.

masm32 홈피에서 다운받아 설치하면 C:\masm32 폴더가 아래 <그림 1>처럼 생길 것이다. 




                                    <그림1>

이제 약간의 환경 변수를 만들어주자. 물론 배치 파일로 만들어도 되지만, 요즘 배치 파일 명령어 기억하는 사람도 없으니 패스하고 바탕화면에
폴더를 단축아이콘 만들어주면 편할 것이다. 이외에도 약간의 더 옵션을 손봐야한다. 고급 언어와는 다르게 어셈블리어에서는 프로젝트라는
개념이 없다. 만약 프로젝트 마인드에 익숙해있으면, 따로 IDE (Integrated Development Envirinment)를 구해보는 것도 좋다. 대표적인 IDE로

    * Visual Studio
    * RadAsm
    * WinAsm Studio
    * EasyCode

등이 있으며 이중 EasyCode를 IDE로 사용한 스샷은 <그림2>에 나와있다. 





                                  <그림2>

나는 IDE를 아예 쓰지 않을 것이므로, 빠른 세팅을 위해 아래에 <동영상>으로 보여주려한다. 화면이 작아서 잘 안보이면 링크를 클릭하라.


  <동영상> http://www.youtube.com/watch?v=BJA99a9bzro
 



                        

우리가 주로 작업할 폴더는 C:\masm32\bin 폴더이다. 이 폴더에 .asm/.obj/.exe 파일이 모두 저장된다. 그러므로 다른 어셈블러 보조 실행 파일과
섞일 수 있으므로 이 폴더 안에 작업용 하위 폴더를 하나 만들어두면 좋을 것이다 (예로, C:\masm32\bin\MyProg). 그러면 나중에 보관할 파일만
따로 여기에 담아두면 된다. 프로그래머라면 소스 파일을 보관하고 유저라면 실행 파일을 필요할 것이다. 우리는 이를 모두 작업 폴더에 저장하면
된다. 참고로 텍스트 에디터로 만들어진 소스 파일은 보관하기 전엔 C:\masm32\bin에서 어셈블 & 링크를 해야한다.

추가로 이 폴더에 작업 폴더 (나는 MyProg으로 해뒀다)를 만들어두고, 메모장과, 명령 프롬프트의 바로가기 아이콘을 만들어두면 좋을 것이다.
이 폴더에 빈곳을 우클릭하여 팝업 > 새로 만들기 > 폴더 / 바로가기로 생성. 바로가기가 만들어지면, 이제 이 폴더의 디폴트 시작 위치를 지정해준다.
팝업 > 속성 > 시작 위치 : C:\masm32\bin. 이제 우리는 텍스트 에디터 (메모장)로 소스를 작성한다. 메모장을 더블클릭하여 다음처럼 소스를 입력한다.

.model small
.stack 100h
.data
hello db "Hello, World!", 0Dh, 0Ah, '$'
prompt db "CopyLeft (CL) by E. T. from the outer-space", 0Dh, 0Ah, '$'

.code
main proc
    mov ax, @data
    mov ds, ax

    mov ah, 09h
    lea dx, hello
    int 21h

L1:
    mov ah, 01h
    int 21h

    or al, al
    jz L1

L2:
    mov ah, 09h
    lea dx, prompt
    int 21h

    mov ax, 4C00h
    int 21h
main endp
end main

이제 소스를 "test.asm" 이라는 이름으로 C:\masm32\bin 폴더에 파일로 저장한다. 파일로 저장되었으면 명령 프롬프트에서 어셈블러로 어셈블한다.
어셈블러 스위치는 "ml test.asm"이다 (masm test.asm이 아니다). 어셈블러는 성공적으로 "test.obj" 파일을 생성할 것이다. 이제 링커로 이를 실행
파일로 만든다. 링커 스위치는 "link16 test.obj"이다 (link test.obj가 아니다). 링커는 성공적으로 "test.exe" 파일을 생성할 것이다. 이 파일을
실행시키면 된다. 컴파일 스위치에 대한 자세한 설명은 "ml /?" "link /?"로 참조하라. 결국 텍스트 에디터 > 어셈블러 > 링커의 경로로 완성되는
셈이다. 만약, 32비트 코드를 작성한다면, 어셈블러와 링커 스위치를 아래처럼 바꿔줘야한다:

32비트 어셈블링 : ml  /c  /coff  /Cp 소스파일.asm
32비트 링킹 : link /SUBSYSTEM:WINDOWS  /LIBPATH:c:\masm32\lib  옵젝트파일.obj

중간에 생성된 *.obj 파일은 라이브러리로 만들때 사용하며 그 외의 경우는 삭제시켜 하드디스크의 용량을 확보하자. 우리는 당분간은 16비트 코딩을
할 것이다. 세상이 온통 64비트로 떠들석하지만, 우리는 이를 무시할 것이다. 64비트는 32비트에 토대를 두고, 32비트는 16비트에 토대를 둔다. 즉,
16비트 코딩으로 작동 방식을 빠르게 배우기위함이지 상용 제품을 만들려는 것은 아니다. 16비트 코딩으로 배운 어셈블리 배경 지식은 32비트 코딩에도
대부분 적용된다. 32비트 코드란 단순히 16비트 코드의 레지스터 크기와 어드레스 공간을 넓혀준 것에 불과하다. 64비트 코딩도 마찬가지다.

분석

이제 우리는 조금전 작성한 코드를 분석해볼 차례다. 이 코드는 사실 대부분이 뼈대 (template) 코드에 해당하는 수준이다. 이중 형광색으로
표시한 부분은 16비트 코딩 (앞으로는 특별히 16비트 코딩이라고 언급하지 않겠다.)시 거의 매번 사용하는 외워쓰는 코드다. 공개석상에서
"신사숙녀 여러분"하듯이 상투적으로 붙여준다고 생각하면 된다.

메모리 모델 (memory model)

.model 지시어는 메모리 모델을 정의하는 어셈블리 지시어이며, 다른 언어와 결합시킬 때를 위해서 존재하며, 코드와 데이터를 어떻게 조직화할
것인지를 결정한다. 대부분의 .exe 실행 파일은 도스에서는 small 모델이며, 이를 윈도우즈 버전으로 확장한 것이 flat 모델이다. exe의 선임이라고
할 수 있는 .com 실행 파일은 대부분 .tiny 모델이었다. small은 독립된 64K 세그먼트내에서 코드 섹션과 데이터 섹션이 존재한다. 반면 tiny 모델은
64K 세그먼트 안에 코드와 데이터가 공존한다. 이외에도 compact/huge/large/medium 등의 모델이 있으나 쓸 일이 거의 없으므로 무시하기로하자.

섹션

모든 어셈블리 프로그램은 코드와 데이터로 구성되며 이를 섹션이라 한다. 코드는 실행 코드이며 .code라는 섹션 아래에 위치한다. 데이터는 그 실행
코드가 참조하는 데이터 (예로, 값이나 스트링)이며, .data 섹션 아래에 위치한다. 이는 디버거로 열어보면 .code 섹션은 .text라는 이름으로 바뀌고,
.data 섹션은 그대로 .data섹션으로 보일 것이다. 이 두개의 섹션은 반드시 필요하며 없으면 실행조차 되지 않는다. 모든 .exe 파일은 스택이 필수로
요구된다. .stack 명령이 스택 섹션의 크기를 정해주며, 우리는 100h (256 바이트)를 정해주었다. 각 섹션은 ".섹션명"이 나오면 그 섹션이 끝난다.
앞으로 .model, .stack, .data, .code가 무엇인지는 다시는 언급하지 않겠다. 자세한 정보는 Masm Programmer's Guide를 참조하라.

변수

고급 언어에서 변수 (variable)라는 것은 어셈블리 레벨로 살펴보면 메모리의 특정 영역에 붙여준 상징적 이름에 불과하다. 변수의 타입 (type)은
그 변수의 크기를 정해주는 것이며, db (define byte)라는 지시어를 사용하여 바이트 타입의 변수를 정해줄 수 있다. 즉, 위의 hello/prompt 라는
두 변수는 바이트 단위의 배열을 이룬 변수이다. 이를 고급 언어에서는 스트링이라고 한다. 변수의 이름은 적을수록 좋다. 변수의 타입을 지정하는
지시어는 db (Byte), dw (Word), dd (DWord), dq (Qword), real4 (Single FP), real8 (Double FP), real10 (Extended Double FP)등이 있다.
스트링 변수 뒤에 0Dh, 0Ah는 엔터 키의 아스키 코드이다. 캐리지 리턴 + 라인 피드 두 16진수가 합쳐서 엔터 키를 출력하는 방식이다. 눈에 보이지
않는 이런 특수한 아스키 문자를 제어 문자 (Control character)라고 한다. 스트링의 끝에 붙은 '$'는 해당 인터럽트 함수 (int 21h, ah = 09h)가
요구하는 인자 (argument)이다. 이는 스트링 하나를 끝맺는 역할이다. 여기서는 각자가 독립된 스트링이므로 hello, prompt 뒤에 각자 붙여줬다.
만약, 이를 붙여서 하나의 변수로 출력한다면 앞에 있는 '$'는 빼줘야하고 변수명인 prompt를 삭제해줘야 한다. db 지시어는 여러 바이트 데이터를
컴마 (,)로 구분해서 뭉쳐줄 수 있기 때문이다. 나중에 다시 다루겠다.

프로시저

모든 코드는 .code 섹션에서 시작되며, 여기서는 main proc이 첫 코드이다. 이는 역시 CPU 명령이 아닌 어셈블러 지시어이다. 즉, 기계어를 생성하지
않고,그냥 이 메모리 영역을 어셈블러가 프로시저의 시작점이라고 인식하는 상징적인 심벌이다. 그리고 end main은 그 심벌로 시작되는 코드를 더 이상
어셈블하지 말라고 어셈블러에게 알려주는 지시어이다. 그 안의 proc / endp는 프로시저 (또는 함수, 서브루틴)의 시작과 끝을 정하는 지시어이다.

    mov ax, @data
    mov ds, ax

디폴트 세그먼트

이 명령은 디폴트 데이터 세그먼트를 정해주는 명령이다. 특별한 이유가 없는한 모든 .exe 실행 파일은 디폴트 데이터 세그먼트를 ds에 정해줘야 한다.
이는 masm의 귀찮은 것 중 하나이며, ds에 mov 명령으로 직접 상수 주소를 정해줄 수 없으므로, 범용 레지스터인 ax에 임시로 data라는 심벌의 주소
(@은 심벌의 주소를 의미한다)를 복사하여 이를 다시 ds 세그먼트 레지스터에 복사해주는 방식이다. 인텔 CPU의 mov 명령은 직접 세그먼트 레지스터
(여기서는 ds)에 상수를 할당할 수 없다. 자세한 내용은 CPU 매뉴얼에서 mov 명령을 참조하라.

    mov ah, 09h
    lea dx, hello
    int 21h

인터럽트 호출

이는 단순한 도스 인터럽트 호출문이다. 인터럽트는 도스 시절 대부분의 입출력 등의 보조 루틴을 OS 레벨에서 정의한 함수와 유사한 것이다.
인터럽트의 모든 단위는 16진수이다 (16진수는 어셈블리 프로그래밍에서 자연스런 단위이다.) 인터럽트는 인자를 mov, lead 등의 명령으로 레지스터에
정해주고, int 명령을 써서 인터럽트를 호출한다. 간단히 "인자를 주고 호출하면 무언가를 리턴한다"는 정도로 이해하면 된다. 인터럽트에 대해서는
나중에 다시 다루겠다. 당장 궁금하면 랄프 브라운의 인터럽트 리스트를 참조하라.

int 21h / ah = 09h 인터럽트는 스트링 출력 함수이다. 이는 ds:dx (세그먼트:옵셋)에 해당 스트링의 주소를 정해준다. 우리는 @data로 ds를 세팅
해줬으므로 dx에 해당 스트링의 주소를 lea (load effective address)로 로드해주면 된다. 이는 옛날 masm 프로그래밍에선 "mov dx, offset hello"
라고 해준 것과 같은 명령이다. 둘다 같은 역할이며 특정 심벌의 옵셋 주소를 얻어낸다. 그럼에도 불구하고 lea 명령이 훨씬 간략하고 우아한
명령이다. 하지만 둘 다 특정 심벌의 옵셋 (offset) 주소를 얻어냄을 잊진말자.

16 진수

16진수는 0,1,2,3,...9,A,B,C,D,E,F로 구성되며 9보다 1큰 수는 A이고, 2+E = 10이다. 16진수는 2진수 4개를 묶어서 사람이 읽기 쉽게 추상화시킨
것이다. 16진수를 표현할 때, 9h, 0Ah를 예로 들면, 알파벳으로 시작하는 16진수는 0 (leading zero)을 붙여주어 10진수와 혼동을 피하게끔한다.
만약 알파벳으로 시작하는 16진수에 0을 빼고 작성하여 컴파일하면 에러를 일으킨다. 관습적으로 b = 2진수, d = 10진수, h = 16진수 접미어를
사용한다 (10진수는 진수 단위를 생략하기도 하며, t를 붙여주기도 한다).

L1:
    mov ah, 01h
    int 21h

    or al, al
    jz L1

루프

이는 일종의 루프 (loop)이다. 루프가 무엇인지는 잘 알 것이다. 루프를 사용한 대표적인 예가 윈도우즈 프로그램이다. 이들은 모두 메시지 루프라는
것을 사용한다. 이 메시지 루프는 무한 루프이다. 루프를 구성할 때 보통 CPU loop 명령보다 jz/jne/jmp 등의 점프 명령을 더 많이 사용한다. 왜인가 ?
loop 명령은 (E)CX를 꼭 함께 제어해야 하는 귀찮은 명령이기 때문이다. loop 명령이 한번 실행될 때마다 (E)CX는 자동으로 1감소한다. 이 자동화 기능
때문에 대부분의 어셈블리 프로그래머는 점프 명령을 더 많이 사용하여 루프를 구현한다.

int 21h / ah = 01h는 한 문자를 입력받는 인터럽트이다. 이는 al에 문자의 아스키 값을 저장한다. 그러므로 or 명령은 이 키 값을 검사하는 역할을 하고,
이어지는 jz 명령은 그 검사 결과에 따라 분기하는 명령이다. 그런데 jz의 분기 타겟 (jump target)이 바로 앞의 L1이므로 루프가 되는 것이다. L1:은
콜론 (:)을 뒤에 붙여 레이블 (label)을 구성한다. 레이블은 분기 타겟 (jump target)과 위치 마커 (location marker) 역할을 한다. 대부분의 코드는
비교-분기 코드가 잇으며, 분기하려는 대상 주소를 분기 타겟이라고 한다. GW-BASIC 같은 옛 언어에서 라인 넘버와 비슷한 역할이다. 나중에 코드에서
분기 타겟을 참조하려면, jz L1처럼 콜론을 붙이지 않고 그 주소로 점프한다. 간혹 코딩하다보면 주의를 요하는 부분이 있기 마련이다. 주의를 요하는
코드에도 위치 마커 역할을 하는 레이블을 만들어 줄 수 있다. L2가 바로 그런 역할이다.

    mov ax, 4C00h
    int 21h

종료 코드
 
int 21h / ah = 4Ch는 도스 표준 종료 함수이다. 이 함수의 인자로는 al에는 종료 코드를 넣어준다. 여기서는 정상 종료를 의미하는 00을 al에 넣어줘
ah + al = ax가 되고 결국 ax = 4C00h이 되었다. 이 함수가 호출되면 프로그램은 메모리를 반납하고 도스로 제어를 넘긴다. 어셈에서 세미콜론 (;)으로
적어주면 문장 끝까지 모두 주석처리된다.

메모리 주소


메모리 주소는 레지스터의 어드레싱이 가능한 주소이다. 이 주소는 레지스터의 크기와 같은 말이다. ah와 al은 ax의 각각 상/하위 반토막 8비트
레지스터이고, ax는 16비트 레지스터이다. 32비트 레지스터인 EAX의 하위 반토막은 AX이고, 64비트 레지스터인 RAX의 하위 반토막은 EAX이다. 다른
범용 레지스터도 마찬가지다. 다만 64비트 일지라도 세그먼트 레지스터는 여전히 16비트이다. 왜 16비트 세그먼트를 고수하는지는 그저 인텔의
전통이라고 표현해야겠다.


세그먼트와 옵셋이 결합하여 논리상의 주소가 만들어진다. 그런데 16비트 세그먼트는 단지 한자리 숫자를 더한 주소에 불과하다. 그러므로, 옵셋
주소만 언급할 땐 0 ~ FFFFh까지의 16비트 영역이며, 세그먼트까지 고려한다면 0 ~ FFFFFh 영역의 20비트 주소가 된다. 인텔 초창기에 세그먼트
모델이 구현되었으며, 그래서 FFFF라는 옵셋을 모두 쓸 능력이 안돼, 단지 F라는 옵셋 한자리만 더 더해주었다. 만약 초기 설계시에 4:16이 아닌
16:16으로 주소를 쓸 수 있었으면 좋았겠지만, 그 당시 기술력이 걸림돌이 되었다.

그 이후로 계속 메모리 주소가 확장되었지만, 여전히 세그먼트 레지스터의 크기는 16비트를 유지했으며, 32비트로 넘어오더라도 32:32 주소가 아닌
0:32 플랫 메모리 모델이 되었다. 만약 32:32 주소로 되었다면 64비트 (18,446,744,073,709,551,615)라는 어마어마한 주소를 가질 수 있었을 것이다.
하지만, 32비트 메모리는 0:32 (0~FFFF FFFFh)까지의 4GB로 한정되었다. 또한 요즘 화두가 된 64비트 프로그래밍도 현재의 기술력으로 0:64의 주소를
모두 액세스하지 못한다.

이제 우리는 16비트 .exe 파일의 뼈대 (skeleton, shell)를 구성해보면 이렇다.

;================== skeleton ====================
.model small
.stack 100h

;================== include =====================
;================== EQU =========================
.data
;================== define data =================
;================== Extern ======================
.code
main proc
  mov ax, @data
  mov ds, ax
 
;================== instructions ================

  mov ax, 4C00h
  int 21h
main endp
end main
;================================================

hello, world를 16비트 .com 버전으로 구성해보면 이렇다.

;=================================================
.model tiny
;================== include =====================
;================== EQU =========================
;================== define data =================
.data
hello db "hello, world!", 0Dh, 0Ah, '$'

.code
org 100h        ; com entry point

main proc
;================= instructions =================
  mov ah, 09h
  lea dx, hello
  int 21h
 
  mov ah, 01h
  int 21h
 
  or al, al
  jz @F
 
@@:
  mov ax, 4C00h
  int 21h
main endp
end main
;================================================

이를 .com 실행 파일로 만드는 방법은 이렇다. 먼저 소스를 "test.asm"으로 저장한 후, "ml /AT test.asm"으로 tiny 모델로 컴파일하여
링커로 "link16 test.obj"라고 .exe 파일을 생성한다. 이제 C:\windows\system32 폴더에 있는 exe2bin을 masm 폴더로 복사하거나 test.exe
파일을 C:\windows\system 폴더로 복사하여 두 파일 (exe2bin/test.exe)이 같은 폴더에 있게 해준 후 "exe2bin test.exe test.com"을
입력하면 test.com 파일이 생성된다.

결론

시작부터 너무 어렵게 설명한 것 같은 기분이 든다. 하지만 형광색 표시를 제외하면 별것 없음을 알 것이다. 더구나 두 출력 인터럽트가 중복되니
더더욱 별것 없다. CPU 명령 모두를 예로 들 수 없고, 랄프 브라운의 인터럽트 또한 모두 사용하는 프로그램은 없다. 여기서는 필수(?)라고 생각되는
명령 5개 (mov, lea, int, or, jz)와 인터럽트 3개 (int 21h, ah = 09h, 01h, 4ch)만 살짝 언급했을 뿐이다. 이 명령과 인터럽트는 여기서 설명해봤자
시간 낭비일 뿐이니 궁금하면 CPU 매뉴얼과 랄프 브라운이 정리한 인터럽트 리스트를 참조하라. 세상 모든 것이 그렇듯이 알기 전엔 어렵지만,
알고나면 왜 그걸 어려워했는지 이해를 못하게 될지도 모른다. 어셈블리 프로그래밍도 마찬가지다. 레지스터가 도대체 뭔가 ? 어쩌면 우리는 CPU를
직접 만든 사람이 아니기에 평생 모를지도 모른다. 하지만, 그럼에도 불구하고 우리는 레지스터는 무엇이다고 "아는척"하며 사람들에게 쇼를 할 뿐이다.
우리는 이제 발을 담궜으니 한 걸음 더 나아가자.

댓글 없음:

댓글 쓰기

블로그 보관함